github.com/arr-ai/arrai@v0.319.0/pkg/arrai/out.go (about) 1 package arrai 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "os" 8 "path" 9 "strings" 10 11 "github.com/go-errors/errors" 12 "github.com/spf13/afero" 13 14 "github.com/arr-ai/arrai/pkg/ctxfs" 15 "github.com/arr-ai/arrai/pkg/fu" 16 "github.com/arr-ai/arrai/rel" 17 ) 18 19 const ( 20 dirField = "dir" 21 fileField = "file" 22 ifExistsConfig = "ifExists" 23 ifExistsRemove = "remove" 24 ifExistsReplace = "replace" 25 ifExistsMerge = "merge" 26 ifExistsIgnore = "ignore" 27 ifExistsFail = "fail" 28 ) 29 30 var errFileAndDirMustNotExist = errors.Errorf("%s and %s must not exist", dirField, fileField) 31 var errFileOrDirMustExist = errors.Errorf("exactly one of %s or %s must exist", dirField, fileField) 32 33 // OutputValue handles output writing for evaluated values. 34 func OutputValue(ctx context.Context, value rel.Value, w io.Writer, out string) error { 35 if out != "" { 36 return outputValue(ctx, value, out) 37 } 38 39 var s string 40 switch v := value.(type) { 41 case rel.String: 42 s = v.String() 43 case rel.Bytes: 44 s = v.String() 45 case rel.Set: 46 if !v.IsTrue() { 47 s = "" 48 } else { 49 s = fu.Repr(v) 50 } 51 default: 52 s = fu.Repr(v) 53 } 54 fmt.Fprintf(w, "%s", s) 55 if s != "" && !strings.HasSuffix(s, "\n") { 56 if _, err := w.Write([]byte{'\n'}); err != nil { 57 return err 58 } 59 } 60 61 return nil 62 } 63 64 func outputValue(ctx context.Context, value rel.Value, out string) error { 65 parts := strings.SplitN(out, ":", 2) 66 if len(parts) == 1 { 67 parts = []string{"", parts[0]} 68 } 69 mode := parts[0] 70 arg := parts[1] 71 72 fs := ctxfs.RuntimeFsFrom(ctx) 73 switch mode { 74 case "file", "f", "": 75 return outputFile(value, arg, fs, false) 76 case "dir", "d": 77 if t, is := rel.AsDict(value); is { 78 if err := outputTupleDir(t, arg, fs, true); err != nil { 79 return err 80 } 81 return outputTupleDir(t, arg, fs, false) 82 } 83 return fmt.Errorf("result not a dict: %v", value) 84 } 85 return fmt.Errorf("invalid --out flag: %s", out) 86 } 87 88 func outputTupleDir(v rel.Value, dir string, fs afero.Fs, dryRun bool) error { 89 t, err := getDirField(v) 90 if err != nil { 91 return err 92 } 93 if _, err := fs.Stat(dir); os.IsNotExist(err) { 94 if err := fs.Mkdir(dir, 0755); err != nil { 95 return err 96 } 97 } 98 99 // this is to allow empty directory 100 if !t.IsTrue() { 101 return nil 102 } 103 104 for e := t.(rel.Dict).DictEnumerator(); e.MoveNext(); { 105 k, v := e.Current() 106 name, is := k.(rel.String) 107 if !is { 108 return fmt.Errorf("dir output dict key must be a non-empty string") 109 } 110 subpath := path.Join(dir, name.String()) 111 switch content := v.(type) { 112 case rel.Tuple: 113 if err := configureOutput(content, subpath, fs, dryRun); err != nil { 114 return err 115 } 116 case rel.Dict: 117 if err := outputTupleDir(content, subpath, fs, dryRun); err != nil { 118 return err 119 } 120 case rel.Bytes, rel.String: 121 if err := outputFile(content, subpath, fs, dryRun); err != nil { 122 return err 123 } 124 case rel.Set: 125 if content.IsTrue() { 126 return fmt.Errorf("dir output entry must be dict, string or byte array") 127 } 128 if err := outputFile(content, subpath, fs, dryRun); err != nil { 129 return err 130 } 131 } 132 } 133 return nil 134 } 135 136 func outputFile(content rel.Value, path string, fs afero.Fs, dryRun bool) error { 137 var bytes []byte 138 switch content := content.(type) { 139 case rel.Bytes: 140 bytes = content.Bytes() 141 case rel.String: 142 bytes = []byte(content.String()) 143 default: 144 if _, is := content.(rel.Set); !(is && !content.IsTrue()) { 145 return fmt.Errorf("file output not string or byte array: %v", content) 146 } 147 bytes = []byte{} 148 } 149 150 if dryRun { 151 return nil 152 } 153 154 f, err := fs.Create(path) 155 if err != nil { 156 return err 157 } 158 defer f.Close() 159 160 if _, err = f.Write(bytes); err != nil { 161 return err 162 } 163 return f.Sync() 164 } 165 166 func configureOutput(t rel.Tuple, dir string, fs afero.Fs, dryRun bool) error { 167 configNames := []string{ifExistsConfig} 168 169 for _, c := range configNames { 170 if t.HasName(c) { 171 for _, applier := range getConfigurators() { 172 if err := applier(t, dir, fs, dryRun); err != nil { 173 return err 174 } 175 } 176 return nil 177 } 178 } 179 return applyFilesFields(t, dir, fs, dryRun) 180 } 181 182 func getConfigurators() []func(rel.Tuple, string, afero.Fs, bool) error { 183 // mind the order when adding new configurations 184 return []func(rel.Tuple, string, afero.Fs, bool) error{ 185 applyIfExistsConfig, 186 } 187 } 188 189 func applyIfExistsConfig(t rel.Tuple, dir string, fs afero.Fs, dryRun bool) (err error) { 190 conf, has := t.Get(ifExistsConfig) 191 if !has { 192 return nil 193 } 194 errInvalidConfig := errors.Errorf( 195 "%s: value '%s' is not valid value. It has to be one of %s", 196 ifExistsConfig, conf, 197 strings.Join([]string{ifExistsMerge, ifExistsRemove, ifExistsReplace, ifExistsIgnore, ifExistsFail}, ", "), 198 ) 199 200 if _, isString := conf.(rel.String); !isString { 201 return errInvalidConfig 202 } 203 switch conf.String() { 204 case ifExistsIgnore, ifExistsRemove, ifExistsReplace, ifExistsFail: 205 case ifExistsMerge: 206 if t.HasName(fileField) { 207 return errors.Errorf("%s: '%s' config must not have '%s' field", ifExistsConfig, fileField, ifExistsMerge) 208 } 209 default: 210 return errInvalidConfig 211 } 212 213 if _, err := fs.Stat(dir); os.IsNotExist(err) { 214 if conf.String() != ifExistsRemove { 215 return applyFilesFields(t, dir, fs, dryRun) 216 } 217 } else if err != nil { 218 return err 219 } 220 221 switch conf.String() { 222 case ifExistsRemove: 223 if err := checkNotDirAndNotFileField(t); err != nil { 224 return err 225 } 226 if dryRun { 227 return nil 228 } 229 return fs.RemoveAll(dir) 230 case ifExistsReplace: 231 if err := checkDirXorFileField(t); err != nil { 232 return err 233 } 234 if dryRun { 235 return nil 236 } 237 if err := fs.RemoveAll(dir); err != nil { 238 return err 239 } 240 return applyFilesFields(t, dir, fs, dryRun) 241 case ifExistsMerge: 242 if v, has := t.Get(dirField); has { 243 d, err := getDirField(v) 244 if err != nil { 245 return err 246 } 247 return outputTupleDir(d, dir, fs, dryRun) 248 } 249 return errors.Errorf("%s: '%s' field must exist", ifExistsConfig, dirField) 250 case ifExistsIgnore: 251 return nil 252 case ifExistsFail: 253 return errors.Errorf("%s: '%s' exists", ifExistsConfig, dir) 254 } 255 panic("impossible") 256 } 257 258 func checkNotDirAndNotFileField(t rel.Tuple) error { 259 _, hasDirs := t.Get(dirField) 260 _, hasFiles := t.Get(fileField) 261 if hasDirs || hasFiles { 262 return errFileAndDirMustNotExist 263 } 264 return nil 265 } 266 267 func checkDirXorFileField(t rel.Tuple) error { 268 _, hasDirs := t.Get(dirField) 269 _, hasFiles := t.Get(fileField) 270 if hasDirs == hasFiles { 271 return errFileOrDirMustExist 272 } 273 return nil 274 } 275 276 func applyFilesFields(t rel.Tuple, path string, fs afero.Fs, dryRun bool) error { 277 if dir, has := t.Get(dirField); has { 278 d, err := getDirField(dir) 279 if err != nil { 280 return err 281 } 282 return outputTupleDir(d, path, fs, dryRun) 283 } 284 if file, has := t.Get(fileField); has { 285 return outputFile(file, path, fs, dryRun) 286 } 287 return errFileOrDirMustExist 288 } 289 290 func getDirField(v rel.Value) (rel.Set, error) { 291 switch k := v.(type) { 292 case rel.Dict, rel.EmptySet: 293 return k.(rel.Set), nil 294 } 295 return nil, errors.Errorf("%s must be of type Dictionary, not %T", dirField, v) 296 }