|
| 1 | +package editor |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | + "os" |
| 6 | + "os/exec" |
| 7 | + "strings" |
| 8 | + |
| 9 | + "github.com/scaleway/scaleway-cli/v2/internal/config" |
| 10 | +) |
| 11 | + |
| 12 | +var SkipEditor = false |
| 13 | +var marshalMode = MarshalModeYAML |
| 14 | + |
| 15 | +type GetResourceFunc func(interface{}) (interface{}, error) |
| 16 | +type Config struct { |
| 17 | + // PutRequest means that the request replace all fields |
| 18 | + // If false, fields that were not edited will not be sent |
| 19 | + // If true, all fields will be sent |
| 20 | + PutRequest bool |
| 21 | + |
| 22 | + MarshalMode MarshalMode |
| 23 | + |
| 24 | + // Template is a template that will be shown before marshaled data in edited file |
| 25 | + Template string |
| 26 | + |
| 27 | + // IgnoreFields is a list of json tags that will be removed from marshaled data |
| 28 | + // The content of these fields will be lost in edited data |
| 29 | + IgnoreFields []string |
| 30 | + |
| 31 | + // If not empty, this will replace edited text as if it was edited in the terminal |
| 32 | + // Should be paired with global SkipEditor as true, useful for tests |
| 33 | + editedResource string |
| 34 | +} |
| 35 | + |
| 36 | +func editorPathAndArgs(fileName string) (string, []string) { |
| 37 | + defaultEditor := config.GetDefaultEditor() |
| 38 | + editorAndArguments := strings.Fields(defaultEditor) |
| 39 | + args := []string{fileName} |
| 40 | + |
| 41 | + if len(editorAndArguments) > 1 { |
| 42 | + args = append(editorAndArguments[1:], args...) |
| 43 | + } |
| 44 | + |
| 45 | + return editorAndArguments[0], args |
| 46 | +} |
| 47 | + |
| 48 | +// edit create a temporary file with given content, start a text editor then return edited content |
| 49 | +// temporary file will be deleted on complete |
| 50 | +// temporary file is not deleted if edit fails |
| 51 | +func edit(content []byte) ([]byte, error) { |
| 52 | + if SkipEditor { |
| 53 | + return content, nil |
| 54 | + } |
| 55 | + |
| 56 | + tmpFileName, err := createTemporaryFile(content, marshalMode) |
| 57 | + if err != nil { |
| 58 | + return nil, fmt.Errorf("failed to create temporary file: %w", err) |
| 59 | + } |
| 60 | + defer os.Remove(tmpFileName) |
| 61 | + |
| 62 | + editorPath, args := editorPathAndArgs(tmpFileName) |
| 63 | + cmd := exec.Command(editorPath, args...) |
| 64 | + cmd.Stdin = os.Stdin |
| 65 | + cmd.Stdout = os.Stdout |
| 66 | + |
| 67 | + err = cmd.Run() |
| 68 | + if err != nil { |
| 69 | + return nil, fmt.Errorf("failed to edit temporary file %q: %w", tmpFileName, err) |
| 70 | + } |
| 71 | + |
| 72 | + editedContent, err := os.ReadFile(tmpFileName) |
| 73 | + if err != nil { |
| 74 | + return nil, fmt.Errorf("failed to read temporary file %q: %w", tmpFileName, err) |
| 75 | + } |
| 76 | + |
| 77 | + return editedContent, nil |
| 78 | +} |
| 79 | + |
| 80 | +// updateResourceEditor takes a complete resource and a partial updateRequest |
| 81 | +// will return a copy of updateRequest that has been edited |
| 82 | +func updateResourceEditor(resource interface{}, updateRequest interface{}, cfg *Config) (interface{}, error) { |
| 83 | + // Create a copy of updateRequest completed with resource content |
| 84 | + completeUpdateRequest := copyAndCompleteUpdateRequest(updateRequest, resource) |
| 85 | + |
| 86 | + // TODO: fields present in updateRequest should be removed from marshal |
| 87 | + // ex: namespace_id, region, zone |
| 88 | + // Currently not an issue as fields that should be removed are mostly path parameter /{zone}/namespace/{namespace_id} |
| 89 | + // Path parameter have "-" as json tag and are not marshaled |
| 90 | + |
| 91 | + updateRequestMarshaled, err := marshal(completeUpdateRequest, cfg.MarshalMode) |
| 92 | + if err != nil { |
| 93 | + return nil, fmt.Errorf("failed to marshal update request: %w", err) |
| 94 | + } |
| 95 | + |
| 96 | + if len(cfg.IgnoreFields) > 0 { |
| 97 | + updateRequestMarshaled, err = removeFields(updateRequestMarshaled, cfg.MarshalMode, cfg.IgnoreFields) |
| 98 | + if err != nil { |
| 99 | + return nil, fmt.Errorf("failed to remove ignored fields: %w", err) |
| 100 | + } |
| 101 | + } |
| 102 | + |
| 103 | + if cfg.Template != "" { |
| 104 | + updateRequestMarshaled = addTemplate(updateRequestMarshaled, cfg.Template, cfg.MarshalMode) |
| 105 | + } |
| 106 | + |
| 107 | + // Start text editor to edit marshaled request |
| 108 | + updateRequestMarshaled, err = edit(updateRequestMarshaled) |
| 109 | + if err != nil { |
| 110 | + return nil, fmt.Errorf("failed to edit marshalled data: %w", err) |
| 111 | + } |
| 112 | + |
| 113 | + // If editedResource is present, override edited resource |
| 114 | + // This is useful for testing purpose |
| 115 | + if cfg.editedResource != "" { |
| 116 | + updateRequestMarshaled = []byte(cfg.editedResource) |
| 117 | + } |
| 118 | + |
| 119 | + // Create a new updateRequest as destination for edited yaml/json |
| 120 | + // Must be a new one to avoid merge of maps content |
| 121 | + updateRequestEdited := newRequest(updateRequest) |
| 122 | + |
| 123 | + // TODO: if !putRequest |
| 124 | + // fill updateRequestEdited with only edited fields and fields present in updateRequest |
| 125 | + // fields should be compared with completeUpdateRequest to find edited ones |
| 126 | + |
| 127 | + // Add back required non-marshaled fields (zone, ID) |
| 128 | + copyRequestPathParameters(updateRequestEdited, updateRequest) |
| 129 | + |
| 130 | + err = unmarshal(updateRequestMarshaled, updateRequestEdited, cfg.MarshalMode) |
| 131 | + if err != nil { |
| 132 | + return nil, fmt.Errorf("failed to unmarshal edited data: %w", err) |
| 133 | + } |
| 134 | + |
| 135 | + return updateRequestEdited, nil |
| 136 | +} |
| 137 | + |
| 138 | +// UpdateResourceEditor takes a complete resource and a partial updateRequest |
| 139 | +// will return a copy of updateRequest that has been edited |
| 140 | +// Only edited fields will be present in returned updateRequest |
| 141 | +// If putRequest is true, all fields will be present, edited or not |
| 142 | +func UpdateResourceEditor(resource interface{}, updateRequest interface{}, cfg *Config) (interface{}, error) { |
| 143 | + return updateResourceEditor(resource, updateRequest, cfg) |
| 144 | +} |
0 commit comments