-
Notifications
You must be signed in to change notification settings - Fork 156
feat: add editor #2878
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: add editor #2878
Changes from all commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
5044102
feat: add editor
Codelax 98abe5b
refactor and add custom marshaler
Codelax c9d353a
edit temporary file instead of using stdin and split arguments from e…
Codelax 385c431
improve text editors support and add file extension to temporary file
Codelax ad9aee2
refactor
Codelax c6a973a
disable broken test
Codelax fddba65
lint
Codelax 51e7aae
change yaml marshal to yamljson
Codelax ff7c3f5
handle slices and refactor valueMapper
Codelax af91d33
add putRequest bool and add TODO
Codelax a5e66fa
better support of pointers
Codelax b296cd7
delete interceptor and only expect resource to be given to the editor
Codelax 6477ff8
always delete temporary file
Codelax d8eab74
fix linter
Codelax 2b2bea7
take editor arguments as struct
Codelax 8da487b
add back path parameter to edited request
Codelax f453206
fix tests
Codelax 1e6774b
fix todo
Codelax d36b1c2
add security-group edit command
Codelax 2bd0579
add marshal-mode argument
Codelax 065644e
update goldens
Codelax 36fa633
update doc
Codelax 003dace
linter
Codelax 76202e5
add default editor for windows
Codelax eeab091
add examples when editing
Codelax dda30de
add rules in example
Codelax 22fee65
change reflect valueMapper arguments to options
Codelax c8e0dfa
add ignoredFields
Codelax 9259502
improve security-group edit output
Codelax 71525d9
fix editor doc with correct system default editor
Codelax ece5527
fix linter
Codelax d192c94
update golden
Codelax b94fdee
gen doc
Codelax 4d6f4d7
fix linter
Codelax b31c061
Merge branch 'master' into feat/editor
remyleone 246f825
fix edit test on windows
Codelax 041846b
revert changes on wrong test
Codelax 93f28af
change edit help
Codelax File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
21 changes: 21 additions & 0 deletions
21
cmd/scw/testdata/test-all-usage-instance-security-group-edit-usage.golden
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| 🎲🎲🎲 EXIT CODE: 0 🎲🎲🎲 | ||
| 🟥🟥🟥 STDERR️️ 🟥🟥🟥️ | ||
| This command starts your default editor to edit a marshaled version of your resource | ||
| Default editor will be taken from $VISUAL, then $EDITOR or an editor based on your system | ||
|
|
||
| USAGE: | ||
| scw instance security-group edit <security-group-id ...> [arg=value ...] | ||
|
|
||
| ARGS: | ||
| security-group-id ID of the security group to reset. | ||
| [mode=yaml] marshaling used when editing data (yaml | json) | ||
| [zone=fr-par-1] Zone to target. If none is passed will use default zone from the config | ||
|
|
||
| FLAGS: | ||
| -h, --help help for edit | ||
|
|
||
| GLOBAL FLAGS: | ||
| -c, --config string The path to the config file | ||
| -D, --debug Enable debug mode | ||
| -o, --output string Output format: json or human, see 'scw help output' for more info (default "human") | ||
| -p, --profile string The config profile to use |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package config | ||
|
|
||
| import ( | ||
| "os" | ||
| "runtime" | ||
| ) | ||
|
|
||
| var ( | ||
| // List of env variables where to find the editor to use | ||
| // Order in slice is override order, the latest will override the first ones | ||
| editorEnvVariables = []string{"EDITOR", "VISUAL"} | ||
| ) | ||
|
|
||
| func GetSystemDefaultEditor() string { | ||
| switch runtime.GOOS { | ||
| case "windows": | ||
| return "notepad" | ||
| default: | ||
| return "vi" | ||
| } | ||
| } | ||
|
|
||
| func GetDefaultEditor() string { | ||
| editor := "" | ||
| for _, envVar := range editorEnvVariables { | ||
| tmp := os.Getenv(envVar) | ||
| if tmp != "" { | ||
| editor = tmp | ||
| } | ||
| } | ||
|
|
||
| if editor == "" { | ||
| return GetSystemDefaultEditor() | ||
| } | ||
|
|
||
| return editor | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| package editor | ||
|
|
||
| import ( | ||
| "github.com/scaleway/scaleway-cli/v2/internal/core" | ||
| ) | ||
|
|
||
| var LongDescription = `This command starts your default editor to edit a marshaled version of your resource | ||
| Default editor will be taken from $VISUAL, then $EDITOR or an editor based on your system` | ||
|
|
||
| func MarshalModeArgSpec() *core.ArgSpec { | ||
| return &core.ArgSpec{ | ||
| Name: "mode", | ||
| Short: "marshaling used when editing data", | ||
| Required: false, | ||
| Default: core.DefaultValueSetter(MarshalModeDefault), | ||
| EnumValues: MarshalModeEnum, | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| package editor | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "strings" | ||
|
|
||
| "github.com/scaleway/scaleway-cli/v2/internal/config" | ||
| ) | ||
|
|
||
| var SkipEditor = false | ||
| var marshalMode = MarshalModeYAML | ||
|
|
||
| type GetResourceFunc func(interface{}) (interface{}, error) | ||
| type Config struct { | ||
| // PutRequest means that the request replace all fields | ||
| // If false, fields that were not edited will not be sent | ||
| // If true, all fields will be sent | ||
| PutRequest bool | ||
|
|
||
| MarshalMode MarshalMode | ||
|
|
||
| // Template is a template that will be shown before marshaled data in edited file | ||
| Template string | ||
|
|
||
| // IgnoreFields is a list of json tags that will be removed from marshaled data | ||
| // The content of these fields will be lost in edited data | ||
| IgnoreFields []string | ||
|
|
||
| // If not empty, this will replace edited text as if it was edited in the terminal | ||
| // Should be paired with global SkipEditor as true, useful for tests | ||
| editedResource string | ||
| } | ||
|
|
||
| func editorPathAndArgs(fileName string) (string, []string) { | ||
| defaultEditor := config.GetDefaultEditor() | ||
| editorAndArguments := strings.Fields(defaultEditor) | ||
| args := []string{fileName} | ||
|
|
||
| if len(editorAndArguments) > 1 { | ||
| args = append(editorAndArguments[1:], args...) | ||
| } | ||
|
|
||
| return editorAndArguments[0], args | ||
| } | ||
|
|
||
| // edit create a temporary file with given content, start a text editor then return edited content | ||
| // temporary file will be deleted on complete | ||
| // temporary file is not deleted if edit fails | ||
| func edit(content []byte) ([]byte, error) { | ||
| if SkipEditor { | ||
| return content, nil | ||
| } | ||
|
|
||
| tmpFileName, err := createTemporaryFile(content, marshalMode) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create temporary file: %w", err) | ||
| } | ||
| defer os.Remove(tmpFileName) | ||
|
|
||
| editorPath, args := editorPathAndArgs(tmpFileName) | ||
| cmd := exec.Command(editorPath, args...) | ||
| cmd.Stdin = os.Stdin | ||
| cmd.Stdout = os.Stdout | ||
|
|
||
| err = cmd.Run() | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to edit temporary file %q: %w", tmpFileName, err) | ||
| } | ||
|
|
||
| editedContent, err := os.ReadFile(tmpFileName) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to read temporary file %q: %w", tmpFileName, err) | ||
| } | ||
|
|
||
| return editedContent, nil | ||
| } | ||
|
|
||
| // updateResourceEditor takes a complete resource and a partial updateRequest | ||
| // will return a copy of updateRequest that has been edited | ||
| func updateResourceEditor(resource interface{}, updateRequest interface{}, cfg *Config) (interface{}, error) { | ||
| // Create a copy of updateRequest completed with resource content | ||
| completeUpdateRequest := copyAndCompleteUpdateRequest(updateRequest, resource) | ||
|
|
||
| // TODO: fields present in updateRequest should be removed from marshal | ||
| // ex: namespace_id, region, zone | ||
| // Currently not an issue as fields that should be removed are mostly path parameter /{zone}/namespace/{namespace_id} | ||
| // Path parameter have "-" as json tag and are not marshaled | ||
|
|
||
| updateRequestMarshaled, err := marshal(completeUpdateRequest, cfg.MarshalMode) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to marshal update request: %w", err) | ||
| } | ||
|
|
||
| if len(cfg.IgnoreFields) > 0 { | ||
| updateRequestMarshaled, err = removeFields(updateRequestMarshaled, cfg.MarshalMode, cfg.IgnoreFields) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to remove ignored fields: %w", err) | ||
| } | ||
| } | ||
|
|
||
| if cfg.Template != "" { | ||
| updateRequestMarshaled = addTemplate(updateRequestMarshaled, cfg.Template, cfg.MarshalMode) | ||
| } | ||
|
|
||
| // Start text editor to edit marshaled request | ||
| updateRequestMarshaled, err = edit(updateRequestMarshaled) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to edit marshalled data: %w", err) | ||
| } | ||
|
|
||
| // If editedResource is present, override edited resource | ||
| // This is useful for testing purpose | ||
| if cfg.editedResource != "" { | ||
| updateRequestMarshaled = []byte(cfg.editedResource) | ||
| } | ||
|
|
||
| // Create a new updateRequest as destination for edited yaml/json | ||
| // Must be a new one to avoid merge of maps content | ||
| updateRequestEdited := newRequest(updateRequest) | ||
|
|
||
| // TODO: if !putRequest | ||
| // fill updateRequestEdited with only edited fields and fields present in updateRequest | ||
| // fields should be compared with completeUpdateRequest to find edited ones | ||
|
|
||
| // Add back required non-marshaled fields (zone, ID) | ||
| copyRequestPathParameters(updateRequestEdited, updateRequest) | ||
|
|
||
| err = unmarshal(updateRequestMarshaled, updateRequestEdited, cfg.MarshalMode) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to unmarshal edited data: %w", err) | ||
| } | ||
|
|
||
| return updateRequestEdited, nil | ||
| } | ||
|
|
||
| // UpdateResourceEditor takes a complete resource and a partial updateRequest | ||
| // will return a copy of updateRequest that has been edited | ||
| // Only edited fields will be present in returned updateRequest | ||
| // If putRequest is true, all fields will be present, edited or not | ||
| func UpdateResourceEditor(resource interface{}, updateRequest interface{}, cfg *Config) (interface{}, error) { | ||
| return updateResourceEditor(resource, updateRequest, cfg) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| package editor | ||
|
|
||
| import ( | ||
| "testing" | ||
|
|
||
| "github.com/alecthomas/assert" | ||
| ) | ||
|
|
||
| func Test_updateResourceEditor(t *testing.T) { | ||
| SkipEditor = true | ||
|
|
||
| resource := &struct { | ||
| ID string | ||
| Name string | ||
| }{ | ||
| "uuid", | ||
| "name", | ||
| } | ||
| updateRequest := &struct { | ||
| ID string | ||
| Name string | ||
| }{ | ||
| "uuid", | ||
| "", | ||
| } | ||
|
|
||
| _, err := updateResourceEditor(resource, updateRequest, &Config{}) | ||
| assert.Nil(t, err) | ||
| } | ||
|
|
||
| func Test_updateResourceEditor_pointers(t *testing.T) { | ||
| SkipEditor = true | ||
|
|
||
| type UpdateRequest struct { | ||
| ID string | ||
| Name *string | ||
| } | ||
| resource := &struct { | ||
| ID string | ||
| Name string | ||
| }{ | ||
| "uuid", | ||
| "name", | ||
| } | ||
|
|
||
| updateRequest := &UpdateRequest{ | ||
| "uuid", | ||
| nil, | ||
| } | ||
|
|
||
| editedUpdateRequestI, err := updateResourceEditor(resource, updateRequest, &Config{}) | ||
| assert.Nil(t, err) | ||
| editedUpdateRequest := editedUpdateRequestI.(*UpdateRequest) | ||
|
|
||
| assert.NotNil(t, editedUpdateRequest.Name) | ||
| assert.Equal(t, resource.Name, *editedUpdateRequest.Name) | ||
| } | ||
|
|
||
| func Test_updateResourceEditor_map(t *testing.T) { | ||
| SkipEditor = true | ||
|
|
||
| type UpdateRequest struct { | ||
| ID string `json:"id"` | ||
| Env *map[string]string `json:"env"` | ||
| } | ||
| resource := &struct { | ||
| ID string `json:"id"` | ||
| Env map[string]string `json:"env"` | ||
| }{ | ||
| "uuid", | ||
| map[string]string{ | ||
| "foo": "bar", | ||
| }, | ||
| } | ||
|
|
||
| updateRequest := &UpdateRequest{ | ||
| "uuid", | ||
| nil, | ||
| } | ||
|
|
||
| editedUpdateRequestI, err := updateResourceEditor(resource, updateRequest, &Config{ | ||
| editedResource: ` | ||
| id: uuid | ||
| env: {} | ||
| `, | ||
| }) | ||
| assert.Nil(t, err) | ||
| editedUpdateRequest := editedUpdateRequestI.(*UpdateRequest) | ||
| assert.NotNil(t, editedUpdateRequest.Env) | ||
| assert.True(t, len(*editedUpdateRequest.Env) == 0) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.