diff --git a/internal/cli/command.go b/internal/cli/command.go index 2b74f12..b7389eb 100644 --- a/internal/cli/command.go +++ b/internal/cli/command.go @@ -46,8 +46,8 @@ func Run(stdin io.Reader, stdout, stderr io.Writer) (*kong.Context, error) { "version": internal.Version, }, kong.Bind(&cli.Globals), - kong.TypeMapper(reflect.TypeFor[*[]variable](), &variableMapper{}), - kong.TypeMapper(reflect.TypeFor[*[]extReadWriteFlag](), &extReadWriteFlagMapper{}), + kong.TypeMapper(reflect.TypeFor[variables](), &variableMapper{}), + kong.TypeMapper(reflect.TypeFor[extReadWriteFlags](), &extReadWriteFlagMapper{}), kong.OptionFunc(func(k *kong.Kong) error { k.Stdout = cli.Stdout k.Stderr = cli.Stderr diff --git a/internal/cli/interactive.go b/internal/cli/interactive.go index 6eee0ec..5543e44 100644 --- a/internal/cli/interactive.go +++ b/internal/cli/interactive.go @@ -21,11 +21,11 @@ func NewInteractiveCmd(queryCmd *QueryCmd) *InteractiveCmd { } type InteractiveCmd struct { - Vars *[]variable `flag:"" name:"var" help:"Variables to pass to the query. E.g. --var foo=\"bar\" --var baz=json:file:./some/file.json"` - ExtReadFlags *[]extReadWriteFlag `flag:"" name:"read-flag" help:"Reader flag to customise parsing. E.g. --read-flag xml-mode=structured"` - ExtWriteFlags *[]extReadWriteFlag `flag:"" name:"write-flag" help:"Writer flag to customise output"` - InFormat string `flag:"" name:"in" short:"i" help:"The format of the input data."` - OutFormat string `flag:"" name:"out" short:"o" help:"The format of the output data."` + Vars variables `flag:"" name:"var" help:"Variables to pass to the query. E.g. --var foo=\"bar\" --var baz=json:file:./some/file.json"` + ExtReadFlags extReadWriteFlags `flag:"" name:"read-flag" help:"Reader flag to customise parsing. E.g. --read-flag xml-mode=structured"` + ExtWriteFlags extReadWriteFlags `flag:"" name:"write-flag" help:"Writer flag to customise output"` + InFormat string `flag:"" name:"in" short:"i" help:"The format of the input data."` + OutFormat string `flag:"" name:"out" short:"o" help:"The format of the output data."` Query string `arg:"" help:"The query to execute." optional:"" default:""` } diff --git a/model/value.go b/model/value.go index 19d0f8a..79402bc 100644 --- a/model/value.go +++ b/model/value.go @@ -272,3 +272,12 @@ func (v *Value) Len() (int, error) { return l, nil } + +func (v *Value) Copy() (*Value, error) { + switch v.Type() { + case TypeMap: + return v.MapCopy() + default: + return nil, fmt.Errorf("copy not supported for type: %s", v.Type()) + } +} diff --git a/model/value_map.go b/model/value_map.go index 527df88..77b92ad 100644 --- a/model/value_map.go +++ b/model/value_map.go @@ -1,6 +1,7 @@ package model import ( + "errors" "fmt" "reflect" @@ -58,6 +59,28 @@ func (v *Value) SetMapKey(key string, value *Value) error { } } +func (v *Value) MapCopy() (*Value, error) { + res := NewMapValue() + kvs, err := v.MapKeyValues() + if err != nil { + return nil, fmt.Errorf("error getting map key values: %w", err) + } + for _, kv := range kvs { + if err := res.SetMapKey(kv.Key, kv.Value); err != nil { + return nil, fmt.Errorf("error setting map key: %w", err) + } + } + return res, nil +} + +func (v *Value) MapKeyExists(key string) (bool, error) { + _, err := v.GetMapKey(key) + if err != nil && !errors.As(err, &MapKeyNotFound{}) { + return false, err + } + return err == nil, nil +} + // GetMapKey returns the value at the specified key in the map. func (v *Value) GetMapKey(key string) (*Value, error) { switch { diff --git a/parsing/hcl/hcl.go b/parsing/hcl/hcl.go index a1fddf3..9b14104 100644 --- a/parsing/hcl/hcl.go +++ b/parsing/hcl/hcl.go @@ -10,10 +10,10 @@ const ( ) var _ parsing.Reader = (*hclReader)(nil) - -//var _ parsing.Writer = (*hclWriter)(nil) +var _ parsing.Writer = (*hclWriter)(nil) func init() { parsing.RegisterReader(HCL, newHCLReader) - parsing.RegisterWriter(HCL, newHCLWriter) + // HCL writer is not implemented yet + //parsing.RegisterWriter(HCL, newHCLWriter) } diff --git a/parsing/hcl/reader.go b/parsing/hcl/reader.go index fc72917..b6fa445 100644 --- a/parsing/hcl/reader.go +++ b/parsing/hcl/reader.go @@ -2,6 +2,7 @@ package hcl import ( "fmt" + "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclsyntax" @@ -11,13 +12,19 @@ import ( ) func newHCLReader(options parsing.ReaderOptions) (parsing.Reader, error) { - return &hclReader{}, nil + return &hclReader{ + alwaysReadLabelsToSlice: options.Ext["hcl-block-format"] == "array", + }, nil } -type hclReader struct{} +type hclReader struct { + alwaysReadLabelsToSlice bool +} // Read reads a value from a byte slice. -func (j *hclReader) Read(data []byte) (*model.Value, error) { +// Reads the HCL data into a model that follows the HCL JSON spec. +// See https://github.com/hashicorp/hcl/blob/main/json%2Fspec.md +func (r *hclReader) Read(data []byte) (*model.Value, error) { f, _ := hclsyntax.ParseConfig(data, "input", hcl.InitialPos) body, ok := f.Body.(*hclsyntax.Body) @@ -25,14 +32,15 @@ func (j *hclReader) Read(data []byte) (*model.Value, error) { return nil, fmt.Errorf("failed to assert file body type") } - return decodeHCLBody(body) + return r.decodeHCLBody(body) } -func decodeHCLBody(body *hclsyntax.Body) (*model.Value, error) { +func (r *hclReader) decodeHCLBody(body *hclsyntax.Body) (*model.Value, error) { res := model.NewMapValue() + var err error for _, attr := range body.Attributes { - val, err := decodeHCLExpr(attr.Expr) + val, err := r.decodeHCLExpr(attr.Expr) if err != nil { return nil, fmt.Errorf("failed to decode attr %q: %w", attr.Name, err) } @@ -42,77 +50,116 @@ func decodeHCLBody(body *hclsyntax.Body) (*model.Value, error) { } } - blockTypeIndexes := make(map[string]int) - blockValues := make([][]*model.Value, 0) + res, err = r.decodeHCLBodyBlocks(body, res) + if err != nil { + return nil, err + } + + return res, nil +} + +func (r *hclReader) decodeHCLBodyBlocks(body *hclsyntax.Body, res *model.Value) (*model.Value, error) { for _, block := range body.Blocks { - if _, ok := blockTypeIndexes[block.Type]; !ok { - blockValues = append(blockValues, make([]*model.Value, 0)) - blockTypeIndexes[block.Type] = len(blockValues) - 1 + if err := r.decodeHCLBlock(block, res); err != nil { + return nil, err } - res, err := decodeHCLBlock(block) + } + return res, nil +} + +func (r *hclReader) decodeHCLBlock(block *hclsyntax.Block, res *model.Value) error { + key := block.Type + v := res + for _, label := range block.Labels { + exists, err := v.MapKeyExists(key) if err != nil { - return nil, fmt.Errorf("failed to decode block %q: %w", block.Type, err) + return err } - blockValues[blockTypeIndexes[block.Type]] = append(blockValues[blockTypeIndexes[block.Type]], res) - } - for t, index := range blockTypeIndexes { - blocks := blockValues[index] - switch len(blocks) { - case 0: - continue - case 1: - if err := res.SetMapKey(t, blocks[0]); err != nil { - return nil, err + if exists { + keyV, err := v.GetMapKey(key) + if err != nil { + return err } - default: - val := model.NewSliceValue() - for _, b := range blocks { - if err := val.Append(b); err != nil { - return nil, err - } - } - if err := res.SetMapKey(t, val); err != nil { - return nil, err + v = keyV + } else { + keyV := model.NewMapValue() + if err := v.SetMapKey(key, keyV); err != nil { + return err } + v = keyV } - } - - return res, nil -} -func decodeHCLBlock(block *hclsyntax.Block) (*model.Value, error) { - res, err := decodeHCLBody(block.Body) - if err != nil { - return nil, err + key = label } - labels := model.NewSliceValue() - for _, l := range block.Labels { - if err := labels.Append(model.NewStringValue(l)); err != nil { - return nil, err - } + body, err := r.decodeHCLBody(block.Body) + if err != nil { + return err } - if err := res.SetMapKey("labels", labels); err != nil { - return nil, err + exists, err := v.MapKeyExists(key) + if err != nil { + return err } + if exists { + keyV, err := v.GetMapKey(key) + if err != nil { + return err + } - if err := res.SetMapKey("type", model.NewStringValue(block.Type)); err != nil { - return nil, err + switch keyV.Type() { + case model.TypeSlice: + if err := keyV.Append(body); err != nil { + return err + } + case model.TypeMap: + // Previous value was a map. + // Create a new slice containing the previous map and the new map. + newKeyV := model.NewSliceValue() + previousKeyV, err := keyV.Copy() + if err != nil { + return err + } + if err := newKeyV.Append(previousKeyV); err != nil { + return err + } + if err := newKeyV.Append(body); err != nil { + return err + } + if err := keyV.Set(newKeyV); err != nil { + return err + } + default: + return fmt.Errorf("unexpected type: %s", keyV.Type()) + } + } else { + if r.alwaysReadLabelsToSlice { + slice := model.NewSliceValue() + if err := slice.Append(body); err != nil { + return err + } + if err := v.SetMapKey(key, slice); err != nil { + return err + } + } else { + if err := v.SetMapKey(key, body); err != nil { + return err + } + } } - return res, nil + return nil } -func decodeHCLExpr(expr hcl.Expression) (*model.Value, error) { +func (r *hclReader) decodeHCLExpr(expr hcl.Expression) (*model.Value, error) { source := cty.Value{} _ = gohcl.DecodeExpression(expr, nil, &source) - return decodeCtyValue(source) + return r.decodeCtyValue(source) } -func decodeCtyValue(source cty.Value) (res *model.Value, err error) { +func (r *hclReader) decodeCtyValue(source cty.Value) (res *model.Value, err error) { defer func() { r := recover() if r != nil { @@ -126,15 +173,7 @@ func decodeCtyValue(source cty.Value) (res *model.Value, err error) { sourceT := source.Type() switch { - case sourceT.IsMapType(): - return nil, fmt.Errorf("map type not implemented") - case sourceT.IsListType(): - return nil, fmt.Errorf("list type not implemented") - case sourceT.IsCollectionType(): - return nil, fmt.Errorf("collection type not implemented") - case sourceT.IsCapsuleType(): - return nil, fmt.Errorf("capsule type not implemented") - case sourceT.IsTupleType(): + case sourceT.IsListType(), sourceT.IsTupleType(): res = model.NewSliceValue() it := source.ElementIterator() for it.Next() { @@ -143,7 +182,7 @@ func decodeCtyValue(source cty.Value) (res *model.Value, err error) { // Just validates the key is correct. _, _ = k.AsBigFloat().Float64() - val, err := decodeCtyValue(v) + val, err := r.decodeCtyValue(v) if err != nil { return nil, fmt.Errorf("failed to decode tuple value: %w", err) } @@ -153,8 +192,26 @@ func decodeCtyValue(source cty.Value) (res *model.Value, err error) { } } return res, nil - case sourceT.IsObjectType(): - return nil, fmt.Errorf("object type not implemented") + case sourceT.IsMapType(), sourceT.IsObjectType(), sourceT.IsSetType(): + v := model.NewMapValue() + it := source.ElementIterator() + for it.Next() { + k, el := it.Element() + if k.Type() != cty.String { + return nil, fmt.Errorf("object key must be a string") + } + kStr := k.AsString() + + elVal, err := r.decodeCtyValue(el) + if err != nil { + return nil, fmt.Errorf("failed to decode object value: %w", err) + } + + if err := v.SetMapKey(kStr, elVal); err != nil { + return nil, err + } + } + return v, nil case sourceT.IsPrimitiveType(): switch sourceT { case cty.String: @@ -173,9 +230,7 @@ func decodeCtyValue(source cty.Value) (res *model.Value, err error) { default: return nil, fmt.Errorf("unhandled primitive type %q", source.Type()) } - case sourceT.IsSetType(): - return nil, fmt.Errorf("set type not implemented") default: - return nil, fmt.Errorf("unhandled type: %s", sourceT.FriendlyName()) + return nil, fmt.Errorf("unsupported type: %s", sourceT.FriendlyName()) } } diff --git a/parsing/hcl/reader_test.go b/parsing/hcl/reader_test.go index e97e71f..6b60a8f 100644 --- a/parsing/hcl/reader_test.go +++ b/parsing/hcl/reader_test.go @@ -2,9 +2,10 @@ package hcl_test import ( "fmt" + "testing" + "github.com/tomwright/dasel/v3/parsing" "github.com/tomwright/dasel/v3/parsing/hcl" - "testing" ) type readTestCase struct { @@ -62,4 +63,25 @@ service "http" "web_proxy" { } }`, }.run) + t.Run("document c", readTestCase{ + in: `image_id = "ami-123" +cluster_min_nodes = 2 +cluster_decimal_nodes = 2.2 +cluster_max_nodes = true +availability_zone_names = [ +"us-east-1a", +"us-west-1c", +] +docker_ports = [{ +internal = 8300 +external = 8300 +protocol = "tcp" +}, +{ +internal = 8301 +external = 8301 +protocol = "tcp" +} +]`, + }.run) }