From ea75efee240191cbdb7f4595c9472f311d99e671 Mon Sep 17 00:00:00 2001 From: xrstf Date: Thu, 4 Jan 2024 23:46:54 +0100 Subject: [PATCH] add yamldocs as output format, automagically read multi-document YAML files as vectors --- cmd/rudi/cmd/script/command.go | 35 ++------------ cmd/rudi/encoding/decode.go | 22 ++++++++- cmd/rudi/encoding/encode.go | 84 ++++++++++++++++++++++++++++++++++ cmd/rudi/options/pflag.go | 13 +++++- cmd/rudi/types/const.go | 25 +++++++--- 5 files changed, 137 insertions(+), 42 deletions(-) create mode 100644 cmd/rudi/encoding/encode.go diff --git a/cmd/rudi/cmd/script/command.go b/cmd/rudi/cmd/script/command.go index f869d80..f76e95b 100644 --- a/cmd/rudi/cmd/script/command.go +++ b/cmd/rudi/cmd/script/command.go @@ -5,19 +5,15 @@ package script import ( "context" - "encoding/json" "errors" "fmt" "os" "strings" "go.xrstf.de/rudi" + "go.xrstf.de/rudi/cmd/rudi/encoding" "go.xrstf.de/rudi/cmd/rudi/options" - "go.xrstf.de/rudi/cmd/rudi/types" "go.xrstf.de/rudi/cmd/rudi/util" - - "github.com/BurntSushi/toml" - "gopkg.in/yaml.v3" ) func Run(handler *util.SignalHandler, opts *options.Options, args []string) error { @@ -86,34 +82,9 @@ func Run(handler *util.SignalHandler, opts *options.Options, args []string) erro } // print the output - var encoder interface { - Encode(v any) error - } - - switch opts.OutputFormat { - case types.JsonEncoding: - encoder = json.NewEncoder(os.Stdout) - encoder.(*json.Encoder).SetIndent("", " ") - case types.YamlEncoding: - encoder = yaml.NewEncoder(os.Stdout) - encoder.(*yaml.Encoder).SetIndent(2) - case types.TomlEncoding: - encoder = toml.NewEncoder(os.Stdout) - encoder.(*toml.Encoder).Indent = " " - default: - encoder = &rawEncoder{} - } - - if err := encoder.Encode(evaluated); err != nil { - return fmt.Errorf("failed to encode %v: %w", evaluated, err) + if err := encoding.Encode(evaluated, opts.OutputFormat, os.Stdout); err != nil { + return fmt.Errorf("failed to encode data: %w", err) } return nil } - -type rawEncoder struct{} - -func (e *rawEncoder) Encode(v any) error { - fmt.Println(v) - return nil -} diff --git a/cmd/rudi/encoding/decode.go b/cmd/rudi/encoding/decode.go index dd61187..8abc57d 100644 --- a/cmd/rudi/encoding/decode.go +++ b/cmd/rudi/encoding/decode.go @@ -5,6 +5,7 @@ package encoding import ( "encoding/json" + "errors" "fmt" "io" @@ -41,8 +42,25 @@ func Decode(input io.Reader, enc types.Encoding) (any, error) { case types.YamlEncoding: decoder := yaml.NewDecoder(input) - if err := decoder.Decode(&data); err != nil { - return nil, fmt.Errorf("failed to parse file as YAML: %w", err) + + documents := []any{} + for { + var doc any + if err := decoder.Decode(&doc); err != nil { + if errors.Is(err, io.EOF) { + break + } + + return nil, fmt.Errorf("failed to parse file as YAML: %w", err) + } + + documents = append(documents, doc) + } + + if len(documents) == 1 { + data = documents[0] + } else { + data = documents } case types.TomlEncoding: diff --git a/cmd/rudi/encoding/encode.go b/cmd/rudi/encoding/encode.go new file mode 100644 index 0000000..62dd088 --- /dev/null +++ b/cmd/rudi/encoding/encode.go @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2024 Christoph Mewes +// SPDX-License-Identifier: MIT + +package encoding + +import ( + "encoding/json" + "fmt" + "io" + "reflect" + + "go.xrstf.de/rudi/cmd/rudi/types" + + "github.com/BurntSushi/toml" + "gopkg.in/yaml.v3" +) + +func newYamlEncoder(out io.Writer) *yaml.Encoder { + encoder := yaml.NewEncoder(out) + encoder.SetIndent(2) + + return encoder +} + +func Encode(data any, enc types.Encoding, out io.Writer) error { + var encoder interface { + Encode(v any) error + } + + switch enc { + case types.JsonEncoding: + encoder = json.NewEncoder(out) + encoder.(*json.Encoder).SetIndent("", " ") + case types.YamlEncoding: + encoder = newYamlEncoder(out) + case types.YamlDocumentsEncoding: + encoder = &yamldocsEncoder{out: out} + case types.TomlEncoding: + encoder = toml.NewEncoder(out) + encoder.(*toml.Encoder).Indent = " " + default: + encoder = &rawEncoder{out: out} + } + + return encoder.Encode(data) +} + +type rawEncoder struct { + out io.Writer +} + +func (e *rawEncoder) Encode(v any) error { + _, err := fmt.Fprintln(e.out, v) + return err +} + +type yamldocsEncoder struct { + out io.Writer +} + +func (e *yamldocsEncoder) Encode(data any) error { + rValue := reflect.ValueOf(data) + rType := reflect.TypeOf(data) + if rType.Kind() == reflect.Pointer { + rValue = rValue.Elem() + rType = rValue.Type() + } + + encoder := newYamlEncoder(e.out) + + switch rType.Kind() { + case reflect.Slice, reflect.Array: + for i := 0; i < rValue.Len(); i++ { + value := rValue.Index(i).Interface() + if err := encoder.Encode(value); err != nil { + return err + } + } + + return nil + } + + return encoder.Encode(data) +} diff --git a/cmd/rudi/options/pflag.go b/cmd/rudi/options/pflag.go index de71be0..b55bab2 100644 --- a/cmd/rudi/options/pflag.go +++ b/cmd/rudi/options/pflag.go @@ -12,7 +12,6 @@ import ( type enumValue interface { fmt.Stringer - IsValid() bool } type enumFlag struct { @@ -40,7 +39,17 @@ var _ pflag.Value = &enumFlag{} func (f *enumFlag) Set(s string) error { newValue := f.stringToEnumValue(s) - if !newValue.IsValid() { + + // do not rely on a possible IsValid() on the enum type, as the flag might just be + // accepting a subset of all valid values + valid := false + for _, accepted := range f.values { + if newValue == accepted { + valid = true + break + } + } + if !valid { return fmt.Errorf("invalid value %q, must be one of %v", s, f.values) } diff --git a/cmd/rudi/types/const.go b/cmd/rudi/types/const.go index f6769ac..d38dfcd 100644 --- a/cmd/rudi/types/const.go +++ b/cmd/rudi/types/const.go @@ -23,15 +23,28 @@ func (e Encoding) IsValid() bool { } const ( - RawEncoding Encoding = "raw" - JsonEncoding Encoding = "json" - Json5Encoding Encoding = "json5" - YamlEncoding Encoding = "yaml" - TomlEncoding Encoding = "toml" + RawEncoding Encoding = "raw" + JsonEncoding Encoding = "json" + Json5Encoding Encoding = "json5" + YamlEncoding Encoding = "yaml" + YamlDocumentsEncoding Encoding = "yamldocs" + TomlEncoding Encoding = "toml" ) var ( AllEncodings = []Encoding{ + RawEncoding, + JsonEncoding, + Json5Encoding, + YamlEncoding, + YamlDocumentsEncoding, + TomlEncoding, + } + + // InputEncodings contains all valid encodings for reading data. + // Note that YAML is always read in multi-document mode, hence + // YamlDocumentsEncoding is not part of this list. + InputEncodings = []Encoding{ RawEncoding, JsonEncoding, Json5Encoding, @@ -39,11 +52,11 @@ var ( TomlEncoding, } - InputEncodings = AllEncodings OutputEncodings = []Encoding{ RawEncoding, JsonEncoding, YamlEncoding, + YamlDocumentsEncoding, TomlEncoding, } )