Skip to content

Commit

Permalink
feat: Add custom stringToString parser
Browse files Browse the repository at this point in the history
Fixes issues with the --values (-v) flag:

1. pflag's default parser doesn't support multi-line values. Conversely,
   the custom parser handles multiple values and single values spanning
   multiple lines based on the presence of `=`.

2. The default parser strips trailing `]` from values. I'll open a PR to
   fix this, but it may not be merged soon due to pflag's inactivity.
  • Loading branch information
gabe565 committed Jul 26, 2024
1 parent 330e194 commit 17e8f88
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 141 deletions.
13 changes: 10 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package config

import "github.com/rs/zerolog"
import (
"github.com/clevyr/yampl/internal/config/flag"
"github.com/rs/zerolog"
)

type Config struct {
Values Values
valuesStringToString *flag.StringToString
Values Values

Inplace bool
Recursive bool
Prefix string
Expand All @@ -23,7 +28,9 @@ type Config struct {

func New() *Config {
return &Config{
Values: make(Values),
valuesStringToString: &flag.StringToString{},
Values: make(Values),

Prefix: "#yampl",
LeftDelim: "{{",
RightDelim: "}}",
Expand Down
99 changes: 99 additions & 0 deletions internal/config/flag/string_to_string.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package flag

import (
"bytes"
"encoding/csv"
"errors"
"fmt"
"io"
"maps"
"strings"
)

var ErrStringToStringFormat = errors.New("must be formatted as key=value")

type StringToString struct {
value map[string]string
changed bool
}

// Set Format: a=1,b=2
func (s *StringToString) Set(val string) error {
val = strings.TrimSpace(val)
count := strings.Count(val, "=")
records := make([]string, 0, count)
switch count {
case 0:
return fmt.Errorf("%s %w", val, ErrStringToStringFormat)
case 1:
records = append(records, val)
default:
r := csv.NewReader(strings.NewReader(val))
r.TrimLeadingSpace = true
for {
line, err := r.Read()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return err
}

r.FieldsPerRecord = 0 // Prevent wrong number of fields error

for _, v := range line {
switch {
case strings.Contains(v, "="):
records = append(records, v)
case len(records) != 0:
records[len(records)-1] += "\n" + v
default:
return fmt.Errorf("%s %w", v, ErrStringToStringFormat)
}
}
}
}

result := make(map[string]string, len(records))
for _, pair := range records {
kv := strings.SplitN(pair, "=", 2)
if len(kv) != 2 {
return fmt.Errorf("%s %w", pair, ErrStringToStringFormat)
}
result[kv[0]] = kv[1]
}

if s.changed {
for k, v := range result {
s.value[k] = v
}
} else {
s.changed = true
s.value = result
}

return nil
}

func (s *StringToString) Type() string {
return "stringToString"
}

func (s *StringToString) String() string {
records := make([]string, 0, len(s.value))
for k, v := range s.value {
records = append(records, k+"="+v)
}

var buf bytes.Buffer
w := csv.NewWriter(&buf)
if err := w.Write(records); err != nil {
panic(err)
}
w.Flush()
return "[" + strings.TrimSpace(buf.String()) + "]"
}

func (s *StringToString) Values() map[string]string {
return maps.Clone(s.value)
}
111 changes: 111 additions & 0 deletions internal/config/flag/string_to_string_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package flag

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestStringToString_Set(t *testing.T) {
type args struct {
val string
}
tests := []struct {
name string
args args
want *StringToString
wantErr require.ErrorAssertionFunc
}{
{
"one value",
args{"a=b"},
&StringToString{value: map[string]string{"a": "b"}, changed: true},
require.NoError,
},
{
"two values",
args{"a=b,c=d"},
&StringToString{value: map[string]string{"a": "b", "c": "d"}, changed: true},
require.NoError,
},
{
"multiline value",
args{"a=b\nc,d=e"},
&StringToString{value: map[string]string{"a": "b\nc", "d": "e"}, changed: true},
require.NoError,
},
{
"multiline values",
args{"a=b\nc=d"},
&StringToString{value: map[string]string{"a": "b", "c": "d"}, changed: true},
require.NoError,
},
{
"multiple newlines",
args{"a=b\n\nc=d"},
&StringToString{value: map[string]string{"a": "b", "c": "d"}, changed: true},
require.NoError,
},
{
"trim spaces",
args{"a=b\n c=d"},
&StringToString{value: map[string]string{"a": "b", "c": "d"}, changed: true},
require.NoError,
},
{
"newline around values",
args{"\na=b\nc=d\n"},
&StringToString{value: map[string]string{"a": "b", "c": "d"}, changed: true},
require.NoError,
},
{
"json value",
args{"a=[1]"},
&StringToString{value: map[string]string{"a": "[1]"}, changed: true},
require.NoError,
},
{
"json values",
args{"a=[1, 2, 3]"},
&StringToString{value: map[string]string{"a": "[1, 2, 3]"}, changed: true},
require.NoError,
},
{"error empty", args{""}, &StringToString{}, require.Error},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := &StringToString{}
tt.wantErr(t, s.Set(tt.args.val))
assert.Equal(t, tt.want, s)
})
}

t.Run("consecutive", func(t *testing.T) {
s := &StringToString{}
require.NoError(t, s.Set("a=b"))
assert.True(t, s.changed)
assert.Equal(t, map[string]string{"a": "b"}, s.value)
require.NoError(t, s.Set("c=d"))
assert.True(t, s.changed)
assert.Equal(t, map[string]string{"a": "b", "c": "d"}, s.value)
})
}

func TestStringToString_String(t *testing.T) {
tests := []struct {
name string
value *StringToString
want string
}{
{"empty", &StringToString{}, "[]"},
{"simple value", &StringToString{value: map[string]string{"a": "b"}, changed: true}, "[a=b]"},
{"value with comma", &StringToString{value: map[string]string{"a": "b,c"}, changed: true}, `["a=b,c"]`},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.value.String()
assert.Equal(t, tt.want, got)
})
}
}
2 changes: 1 addition & 1 deletion internal/config/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const (

func (c *Config) RegisterFlags(cmd *cobra.Command) {
cmd.Flags().BoolVarP(&c.Inplace, InplaceFlag, "i", c.Inplace, "Edit files in place")
cmd.Flags().StringToStringP(ValueFlag, ValueFlagShort, map[string]string{}, "Define a template variable. Can be used more than once.")
cmd.Flags().VarP(c.valuesStringToString, ValueFlag, ValueFlagShort, "Define a template variable. Can be used more than once.")
cmd.Flags().BoolVarP(&c.Recursive, RecursiveFlag, "r", c.Recursive, "Recursively update yaml files in the given directory")
cmd.Flags().StringVarP(&c.Prefix, PrefixFlag, "p", c.Prefix, "Template comments must begin with this prefix. The beginning '#' is implied.")
cmd.Flags().StringVar(&c.LeftDelim, LeftDelimFlag, c.LeftDelim, "Override template left delimiter")
Expand Down
6 changes: 1 addition & 5 deletions internal/config/load.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,7 @@ func (c *Config) Load(cmd *cobra.Command) error {
c.Prefix = "#" + c.Prefix
}

rawValues, err := cmd.Flags().GetStringToString(ValueFlag)
if err != nil {
return err
}
c.Values.Fill(rawValues)
c.Values.Fill(c.valuesStringToString.Values())

if f := cmd.Flags().Lookup(FailFlag); f.Changed {
val, err := cmd.Flags().GetBool(FailFlag)
Expand Down
64 changes: 0 additions & 64 deletions internal/config/values_hack.go

This file was deleted.

64 changes: 0 additions & 64 deletions internal/config/values_hack_test.go

This file was deleted.

Loading

0 comments on commit 17e8f88

Please sign in to comment.