Skip to content

Commit 3951576

Browse files
authored
Merge branch 'main' into feat/add-toml-parser-146
2 parents f2c0348 + 4586d7b commit 3951576

4 files changed

Lines changed: 114 additions & 123 deletions

File tree

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ go 1.24.0
44

55
require (
66
emperror.dev/errors v0.8.1
7-
github.com/Jeffail/gabs/v2 v2.7.0
87
github.com/NYTimes/logrotate v1.0.0
98
github.com/acobaugh/osrelease v0.1.0
109
github.com/apex/log v1.9.0
@@ -41,6 +40,8 @@ require (
4140
github.com/shirou/gopsutil/v3 v3.24.5
4241
github.com/spf13/cobra v1.10.1
4342
github.com/stretchr/testify v1.11.1
43+
github.com/tidwall/gjson v1.18.0
44+
github.com/tidwall/sjson v1.2.5
4445
golang.org/x/crypto v0.41.0
4546
golang.org/x/sync v0.16.0
4647
golang.org/x/sys v0.35.0
@@ -72,6 +73,8 @@ require (
7273
github.com/muesli/cancelreader v0.2.2 // indirect
7374
github.com/muesli/termenv v0.16.0 // indirect
7475
github.com/rivo/uniseg v0.4.7 // indirect
76+
github.com/tidwall/match v1.1.1 // indirect
77+
github.com/tidwall/pretty v1.2.0 // indirect
7578
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
7679
)
7780

go.sum

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOEl
2121
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
2222
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
2323
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
24-
github.com/Jeffail/gabs/v2 v2.7.0 h1:Y2edYaTcE8ZpRsR2AtmPu5xQdFDIthFG0jYhu5PY8kg=
25-
github.com/Jeffail/gabs/v2 v2.7.0/go.mod h1:dp5ocw1FvBBQYssgHsG7I1WYsiLRtkUaB1FEtSwvNUw=
2624
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
2725
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
2826
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
@@ -407,6 +405,15 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl
407405
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
408406
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
409407
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
408+
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
409+
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
410+
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
411+
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
412+
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
413+
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
414+
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
415+
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
416+
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
410417
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
411418
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
412419
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=

parser/helpers.go

Lines changed: 84 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package parser
22

33
import (
4-
"bytes"
54
"regexp"
65
"strconv"
76
"strings"
87

98
"emperror.dev/errors"
10-
"github.com/Jeffail/gabs/v2"
119
"github.com/apex/log"
1210
"github.com/buger/jsonparser"
1311
"github.com/iancoleman/strcase"
12+
"github.com/tidwall/gjson"
13+
"github.com/tidwall/sjson"
1414
)
1515

1616
// Regex to match anything that has a value matching the format of {{ config.$1 }} which
@@ -62,12 +62,13 @@ func (cfr *ConfigurationFileReplacement) getKeyValue(value string) interface{} {
6262
// This does not currently support nested wildcard matches. For example, foo.*.bar
6363
// will work, however foo.*.bar.*.baz will not, since we'll only be splitting at the
6464
// first wildcard, and not subsequent ones.
65-
func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error) {
66-
parsed, err := gabs.ParseJSON(data)
67-
if err != nil {
68-
return nil, err
65+
func (f *ConfigurationFile) IterateOverJson(data []byte) ([]byte, error) {
66+
if !gjson.ValidBytes(data) {
67+
return nil, errors.New("invalid JSON data")
6968
}
7069

70+
jsonStr := string(data)
71+
7172
for _, v := range f.Replace {
7273
value, err := f.LookupConfigurationValue(v)
7374
if err != nil {
@@ -78,140 +79,109 @@ func (f *ConfigurationFile) IterateOverJson(data []byte) (*gabs.Container, error
7879
// begin doing a search and replace in the data.
7980
if strings.Contains(v.Match, ".*") {
8081
parts := strings.SplitN(v.Match, ".*", 2)
82+
basePath := strings.Trim(parts[0], ".")
83+
remainingPath := strings.Trim(parts[1], ".")
84+
85+
result := gjson.Get(jsonStr, basePath)
86+
if !result.Exists() {
87+
continue
88+
}
8189

82-
// Iterate over each matched child and set the remaining path to the value
83-
// that is passed through in the loop.
84-
//
85-
// If the child is a null value, nothing will happen. Seems reasonable as of the
86-
// time this code is being written.
87-
for _, child := range parsed.Path(strings.Trim(parts[0], ".")).Children() {
88-
if err := v.SetAtPathway(child, strings.Trim(parts[1], "."), value); err != nil {
89-
if errors.Is(err, gabs.ErrNotFound) {
90-
continue
90+
if result.IsArray() {
91+
result.ForEach(func(key, val gjson.Result) bool {
92+
fullPath := basePath + "." + key.String()
93+
if remainingPath != "" {
94+
fullPath += "." + remainingPath
9195
}
96+
var setErr error
97+
jsonStr, setErr = v.setValueWithSjson(jsonStr, fullPath, value)
98+
if setErr != nil {
99+
err = setErr
100+
return false
101+
}
102+
return true
103+
})
104+
if err != nil {
92105
return nil, errors.WithMessage(err, "failed to set config value of array child")
93106
}
107+
} else if result.IsObject() {
108+
result.ForEach(func(key, val gjson.Result) bool {
109+
fullPath := basePath + "." + key.String()
110+
if remainingPath != "" {
111+
fullPath += "." + remainingPath
112+
}
113+
var setErr error
114+
jsonStr, setErr = v.setValueWithSjson(jsonStr, fullPath, value)
115+
if setErr != nil {
116+
err = setErr
117+
return false
118+
}
119+
return true
120+
})
121+
if err != nil {
122+
return nil, errors.WithMessage(err, "failed to set config value of object child")
123+
}
94124
}
95125
continue
96126
}
97127

98-
if err := v.SetAtPathway(parsed, v.Match, value); err != nil {
99-
if errors.Is(err, gabs.ErrNotFound) {
128+
var setErr error
129+
jsonStr, setErr = v.setValueWithSjson(jsonStr, v.Match, value)
130+
if setErr != nil {
131+
if strings.Contains(setErr.Error(), "path not found") {
100132
continue
101133
}
102-
return nil, errors.WithMessage(err, "unable to set config value at pathway: "+v.Match)
134+
return nil, errors.WithMessage(setErr, "unable to set config value at pathway: "+v.Match)
103135
}
104136
}
105137

106-
return parsed, nil
138+
return []byte(jsonStr), nil
107139
}
108140

109-
// Regex used to check if there is an array element present in the given pathway by looking for something
110-
// along the lines of "something[1]" or "something[1].nestedvalue" as the path.
111-
var checkForArrayElement = regexp.MustCompile(`^([^\[\]]+)\[([\d]+)](\..+)?$`)
112-
113-
// Attempt to set the value of the path depending on if it is an array or not. Gabs cannot handle array
114-
// values as "something[1]" but can parse them just fine. This is basically just overly complex code
115-
// to handle that edge case and ensure the value gets set correctly.
116-
//
117-
// Bless thee who has to touch these most unholy waters.
118-
func setValueAtPath(c *gabs.Container, path string, value interface{}) error {
119-
var err error
120-
121-
matches := checkForArrayElement.FindStringSubmatch(path)
122-
123-
// Check if we are **NOT** updating an array element.
124-
if len(matches) < 3 {
125-
_, err = c.SetP(value, path)
126-
return err
127-
}
141+
func (cfr *ConfigurationFileReplacement) setValueWithSjson(jsonStr string, path string, value string) (string, error) {
142+
if cfr.IfValue != "" {
143+
// Check if we are replacing instead of overwriting.
144+
if strings.HasPrefix(cfr.IfValue, "regex:") {
145+
result := gjson.Get(jsonStr, path)
146+
if !result.Exists() {
147+
return jsonStr, nil
148+
}
128149

129-
i, _ := strconv.Atoi(matches[2])
130-
// Find the array element "i" or try to create it if "i" is equal to 0 and is not found
131-
// at the given path.
132-
ct, err := c.ArrayElementP(i, matches[1])
133-
if err != nil {
134-
if i != 0 || (!errors.Is(err, gabs.ErrNotArray) && !errors.Is(err, gabs.ErrNotFound)) {
135-
return errors.WithMessage(err, "error while parsing array element at path")
136-
}
150+
r, err := regexp.Compile(strings.TrimPrefix(cfr.IfValue, "regex:"))
151+
if err != nil {
152+
log.WithFields(log.Fields{"if_value": strings.TrimPrefix(cfr.IfValue, "regex:"), "error": err}).
153+
Warn("configuration if_value using invalid regexp, cannot perform replacement")
154+
return jsonStr, nil
155+
}
137156

138-
t := make([]interface{}, 1)
139-
// If the length of matches is 4 it means we're trying to access an object down in this array
140-
// key, so make sure we generate the array as an array of objects, and not just a generic nil
141-
// array.
142-
if len(matches) == 4 {
143-
t = []interface{}{map[string]interface{}{}}
157+
v := result.String()
158+
if r.MatchString(v) {
159+
newValue := r.ReplaceAllString(v, value)
160+
return sjson.Set(jsonStr, path, newValue)
161+
}
162+
return jsonStr, nil
144163
}
145164

146-
// If the error is because this isn't an array or isn't found go ahead and create the array with
147-
// an empty object if we have additional things to set on the array, or just an empty array type
148-
// if there is not an object structure detected (no matches[3] available).
149-
if _, err = c.SetP(t, matches[1]); err != nil {
150-
return errors.WithMessage(err, "failed to create empty array for missing element")
165+
result := gjson.Get(jsonStr, path)
166+
if !result.Exists() {
167+
return jsonStr, nil
151168
}
152-
153-
// Set our cursor to be the array element we expect, which in this case is just the first element
154-
// since we won't run this code unless the array element is 0. There is too much complexity in trying
155-
// to match additional elements. In those cases the server will just have to be rebooted or something.
156-
ct, err = c.ArrayElementP(0, matches[1])
157-
if err != nil {
158-
return errors.WithMessage(err, "failed to find array element at path")
169+
if result.String() != cfr.IfValue {
170+
return jsonStr, nil
159171
}
160172
}
161173

162-
// Try to set the value. If the path does not exist an error will be raised to the caller which will
163-
// then check if the error is because the path is missing. In those cases we just ignore the error since
164-
// we don't want to do anything specifically when that happens.
165-
//
166-
// If there are four matches in the regex it means that we managed to also match a trailing pathway
167-
// for the key, which should be found in the given array key item and modified further.
168-
if len(matches) == 4 {
169-
_, err = ct.SetP(value, strings.TrimPrefix(matches[3], "."))
174+
var setValue interface{}
175+
if cfr.ReplaceWith.Type() == jsonparser.Boolean {
176+
v, _ := strconv.ParseBool(value)
177+
setValue = v
178+
} else if v, err := strconv.Atoi(value); err == nil {
179+
setValue = v
170180
} else {
171-
_, err = ct.Set(value)
172-
}
173-
174-
if err != nil {
175-
return errors.WithMessage(err, "failed to set value at config path: "+path)
176-
}
177-
178-
return nil
179-
}
180-
181-
// Sets the value at a specific pathway, but checks if we were looking for a specific
182-
// value or not before doing it.
183-
func (cfr *ConfigurationFileReplacement) SetAtPathway(c *gabs.Container, path string, value string) error {
184-
if cfr.IfValue == "" {
185-
return setValueAtPath(c, path, cfr.getKeyValue(value))
186-
}
187-
188-
// Check if we are replacing instead of overwriting.
189-
if strings.HasPrefix(cfr.IfValue, "regex:") {
190-
// Doing a regex replacement requires an existing value.
191-
// TODO: Do we try passing an empty string to the regex?
192-
if c.ExistsP(path) {
193-
return gabs.ErrNotFound
194-
}
195-
196-
r, err := regexp.Compile(strings.TrimPrefix(cfr.IfValue, "regex:"))
197-
if err != nil {
198-
log.WithFields(log.Fields{"if_value": strings.TrimPrefix(cfr.IfValue, "regex:"), "error": err}).
199-
Warn("configuration if_value using invalid regexp, cannot perform replacement")
200-
return nil
201-
}
202-
203-
v := strings.Trim(c.Path(path).String(), "\"")
204-
if r.Match([]byte(v)) {
205-
return setValueAtPath(c, path, r.ReplaceAllString(v, value))
206-
}
207-
return nil
208-
}
209-
210-
if c.ExistsP(path) && !bytes.Equal(c.Bytes(), []byte(cfr.IfValue)) {
211-
return nil
181+
setValue = value
212182
}
213183

214-
return setValueAtPath(c, path, cfr.getKeyValue(value))
184+
return sjson.Set(jsonStr, path, setValue)
215185
}
216186

217187
// Looks up a configuration value on the Daemon given a dot-notated syntax.

parser/parser.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/icza/dyno"
1616
"github.com/magiconair/properties"
1717
"github.com/pelletier/go-toml/v2"
18+
"github.com/tidwall/pretty"
1819
"gopkg.in/ini.v1"
1920
"gopkg.in/yaml.v3"
2021

@@ -418,8 +419,14 @@ func (f *ConfigurationFile) parseJsonFile(file ufs.File) error {
418419
return err
419420
}
420421

421-
// Write the data to the file.
422-
if _, err := io.Copy(file, bytes.NewReader(data.BytesIndent("", " "))); err != nil {
422+
prettified := pretty.PrettyOptions(data, &pretty.Options{
423+
Width: 80,
424+
Prefix: "",
425+
Indent: " ",
426+
SortKeys: false,
427+
})
428+
429+
if _, err := io.Copy(file, bytes.NewReader(prettified)); err != nil {
423430
return errors.Wrap(err, "parser: failed to write properties file to disk")
424431
}
425432
return nil
@@ -439,8 +446,8 @@ func (f *ConfigurationFile) parseYamlFile(file ufs.File) error {
439446
}
440447

441448
// Unmarshal the yaml data into a JSON interface such that we can work with
442-
// any arbitrary data structure. If we don't do this, I can't use gabs which
443-
// makes working with unknown JSON significantly easier.
449+
// any arbitrary data structure. This allows us to use gjson/sjson for
450+
// working with unknown JSON significantly easier.
444451
jsonBytes, err := json.Marshal(dyno.ConvertMapI2MapS(i))
445452
if err != nil {
446453
return err
@@ -453,8 +460,12 @@ func (f *ConfigurationFile) parseYamlFile(file ufs.File) error {
453460
return err
454461
}
455462

456-
// Remarshal the JSON into YAML format before saving it back to the disk.
457-
marshaled, err := yaml.Marshal(data.Data())
463+
var jsonData interface{}
464+
if err := json.Unmarshal(data, &jsonData); err != nil {
465+
return err
466+
}
467+
468+
marshaled, err := yaml.Marshal(jsonData)
458469
if err != nil {
459470
return err
460471
}

0 commit comments

Comments
 (0)