This commit is contained in:
Mike Farah
2024-03-12 15:45:08 +11:00
parent 2b2c285415
commit fc5595d0e9
39 changed files with 237 additions and 199 deletions
+130
View File
@@ -0,0 +1,130 @@
package properties
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
"github.com/magiconair/properties"
"github.com/mikefarah/yq/v4/pkg/yqlib"
)
type propertiesDecoder struct {
reader io.Reader
finished bool
d yqlib.DataTreeNavigator
}
func NewPropertiesDecoder() yqlib.Decoder {
return &propertiesDecoder{d: yqlib.NewDataTreeNavigator(), finished: false}
}
func (dec *propertiesDecoder) Init(reader io.Reader) error {
dec.reader = reader
dec.finished = false
return nil
}
func parsePropKey(key string) []interface{} {
pathStrArray := strings.Split(key, ".")
path := make([]interface{}, len(pathStrArray))
for i, pathStr := range pathStrArray {
num, err := strconv.ParseInt(pathStr, 10, 32)
if err == nil {
path[i] = num
} else {
path[i] = pathStr
}
}
return path
}
func (dec *propertiesDecoder) processComment(c string) string {
if c == "" {
return ""
}
return "# " + c
}
func (dec *propertiesDecoder) applyPropertyComments(context yqlib.Context, path []interface{}, comments []string) error {
assignmentOp := &yqlib.Operation{OperationType: assignOpType, Preferences: assignPreferences{}}
rhsCandidateNode := &yqlib.CandidateNode{
Tag: "!!str",
Value: fmt.Sprintf("%v", path[len(path)-1]),
HeadComment: dec.processComment(strings.Join(comments, "\n")),
Kind: yqlib.ScalarNode,
}
rhsCandidateNode.Tag = rhsCandidateNode.GuessTagFromCustomType()
rhsOp := &yqlib.Operation{OperationType: referenceOpType, CandidateNode: rhsCandidateNode}
assignmentOpNode := &yqlib.ExpressionNode{
Operation: assignmentOp,
LHS: createTraversalTree(path, traversePreferences{}, true),
RHS: &yqlib.ExpressionNode{Operation: rhsOp},
}
_, err := dec.d.GetMatchingNodes(context, assignmentOpNode)
return err
}
func (dec *propertiesDecoder) applyProperty(context yqlib.Context, properties *properties.Properties, key string) error {
value, _ := properties.Get(key)
path := parsePropKey(key)
propertyComments := properties.GetComments(key)
if len(propertyComments) > 0 {
err := dec.applyPropertyComments(context, path, propertyComments)
if err != nil {
return nil
}
}
rhsNode := createStringScalarNode(value)
rhsNode.Tag = rhsNode.GuessTagFromCustomType()
return dec.d.DeeplyAssign(context, path, rhsNode)
}
func (dec *propertiesDecoder) Decode() (*yqlib.CandidateNode, error) {
if dec.finished {
return nil, io.EOF
}
buf := new(bytes.Buffer)
if _, err := buf.ReadFrom(dec.reader); err != nil {
return nil, err
}
if buf.Len() == 0 {
dec.finished = true
return nil, io.EOF
}
properties, err := properties.LoadString(buf.String())
if err != nil {
return nil, err
}
properties.DisableExpansion = true
rootMap := &yqlib.CandidateNode{
Kind: yqlib.MappingNode,
Tag: "!!map",
}
context := yqlib.Context{}
context = context.SingleChildContext(rootMap)
for _, key := range properties.Keys() {
if err := dec.applyProperty(context, properties, key); err != nil {
return nil, err
}
}
dec.finished = true
return rootMap, nil
}
+149
View File
@@ -0,0 +1,149 @@
package properties
import (
"bufio"
"errors"
"fmt"
"io"
"strings"
"github.com/magiconair/properties"
"github.com/mikefarah/yq/v4/pkg/yqlib"
)
type propertiesEncoder struct {
prefs PropertiesPreferences
}
func NewPropertiesEncoder(prefs PropertiesPreferences) yqlib.Encoder {
return &propertiesEncoder{
prefs: prefs,
}
}
func (pe *propertiesEncoder) CanHandleAliases() bool {
return false
}
func (pe *propertiesEncoder) PrintDocumentSeparator(_ io.Writer) error {
return nil
}
func (pe *propertiesEncoder) PrintLeadingContent(writer io.Writer, content string) error {
reader := bufio.NewReader(strings.NewReader(content))
for {
readline, errReading := reader.ReadString('\n')
if errReading != nil && !errors.Is(errReading, io.EOF) {
return errReading
}
if strings.Contains(readline, "$yqDocSeparator$") {
if err := pe.PrintDocumentSeparator(writer); err != nil {
return err
}
} else {
if err := writeString(writer, readline); err != nil {
return err
}
}
if errors.Is(errReading, io.EOF) {
if readline != "" {
// the last comment we read didn't have a newline, put one in
if err := writeString(writer, "\n"); err != nil {
return err
}
}
break
}
}
return nil
}
func (pe *propertiesEncoder) Encode(writer io.Writer, node *yqlib.CandidateNode) error {
if node.Kind == yqlib.ScalarNode {
return writeString(writer, node.Value+"\n")
}
mapKeysToStrings(node)
p := properties.NewProperties()
p.WriteSeparator = pe.prefs.KeyValueSeparator
err := pe.doEncode(p, node, "", nil)
if err != nil {
return err
}
_, err = p.WriteComment(writer, "#", properties.UTF8)
return err
}
func (pe *propertiesEncoder) doEncode(p *properties.Properties, node *yqlib.CandidateNode, path string, keyNode *CandidateNode) error {
comments := ""
if keyNode != nil {
// include the key node comments if present
comments = headAndLineComment(keyNode)
}
comments = comments + headAndLineComment(node)
commentsWithSpaces := strings.ReplaceAll(comments, "\n", "\n ")
p.SetComments(path, strings.Split(commentsWithSpaces, "\n"))
switch node.Kind {
case yqlib.ScalarNode:
var nodeValue string
if pe.prefs.UnwrapScalar || !strings.Contains(node.Value, " ") {
nodeValue = node.Value
} else {
nodeValue = fmt.Sprintf("%q", node.Value)
}
_, _, err := p.Set(path, nodeValue)
return err
case yqlib.SequenceNode:
return pe.encodeArray(p, node.Content, path)
case yqlib.MappingNode:
return pe.encodeMap(p, node.Content, path)
case yqlib.AliasNode:
return pe.doEncode(p, node.Alias, path, nil)
default:
return fmt.Errorf("Unsupported node %v", node.Tag)
}
}
func (pe *propertiesEncoder) appendPath(path string, key interface{}) string {
if path == "" {
return fmt.Sprintf("%v", key)
}
switch key.(type) {
case int:
if pe.prefs.UseArrayBrackets {
return fmt.Sprintf("%v[%v]", path, key)
}
}
return fmt.Sprintf("%v.%v", path, key)
}
func (pe *propertiesEncoder) encodeArray(p *properties.Properties, kids []*yqlib.CandidateNode, path string) error {
for index, child := range kids {
err := pe.doEncode(p, child, pe.appendPath(path, index), nil)
if err != nil {
return err
}
}
return nil
}
func (pe *propertiesEncoder) encodeMap(p *properties.Properties, kids []*yqlib.CandidateNode, path string) error {
for index := 0; index < len(kids); index = index + 2 {
key := kids[index]
value := kids[index+1]
err := pe.doEncode(p, value, pe.appendPath(path, key.Value), key)
if err != nil {
return err
}
}
return nil
}
+208
View File
@@ -0,0 +1,208 @@
package yqlib
import (
"bufio"
"bytes"
"strings"
"testing"
"github.com/mikefarah/yq/v4/test"
)
type keyValuePair struct {
key string
value string
comment string
}
func (kv *keyValuePair) String(unwrap bool, sep string) string {
builder := strings.Builder{}
if kv.comment != "" {
builder.WriteString(kv.comment)
builder.WriteString("\n")
}
builder.WriteString(kv.key)
builder.WriteString(sep)
if unwrap {
builder.WriteString(kv.value)
} else {
builder.WriteString("\"")
builder.WriteString(kv.value)
builder.WriteString("\"")
}
return builder.String()
}
type testProperties struct {
pairs []keyValuePair
}
func (tp *testProperties) String(unwrap bool, sep string) string {
kvs := []string{}
for _, kv := range tp.pairs {
kvs = append(kvs, kv.String(unwrap, sep))
}
return strings.Join(kvs, "\n")
}
func yamlToProps(sampleYaml string, unwrapScalar bool, separator string) string {
var output bytes.Buffer
writer := bufio.NewWriter(&output)
var propsEncoder = NewPropertiesEncoder(PropertiesPreferences{KeyValueSeparator: separator, UnwrapScalar: unwrapScalar})
inputs, err := readDocuments(strings.NewReader(sampleYaml), "sample.yml", 0, NewYamlDecoder(ConfiguredYamlPreferences))
if err != nil {
panic(err)
}
node := inputs.Front().Value.(*CandidateNode)
err = propsEncoder.Encode(writer, node)
if err != nil {
panic(err)
}
writer.Flush()
return strings.TrimSuffix(output.String(), "\n")
}
func doTest(t *testing.T, sampleYaml string, props testProperties, testUnwrapped, testWrapped bool) {
wraps := []bool{}
if testUnwrapped {
wraps = append(wraps, true)
}
if testWrapped {
wraps = append(wraps, false)
}
for _, unwrap := range wraps {
for _, sep := range []string{" = ", ";", "=", " "} {
var actualProps = yamlToProps(sampleYaml, unwrap, sep)
test.AssertResult(t, props.String(unwrap, sep), actualProps)
}
}
}
func TestPropertiesEncoderSimple(t *testing.T) {
var sampleYaml = `a: 'bob cool'`
doTest(
t, sampleYaml,
testProperties{
pairs: []keyValuePair{
{
key: "a",
value: "bob cool",
},
},
},
true, true,
)
}
func TestPropertiesEncoderSimpleWithComments(t *testing.T) {
var sampleYaml = `a: 'bob cool' # line`
doTest(
t, sampleYaml,
testProperties{
pairs: []keyValuePair{
{
key: "a",
value: "bob cool",
comment: "# line",
},
},
},
true, true,
)
}
func TestPropertiesEncoderDeep(t *testing.T) {
var sampleYaml = `a:
b: "bob cool"
`
doTest(
t, sampleYaml,
testProperties{
pairs: []keyValuePair{
{
key: "a.b",
value: "bob cool",
},
},
},
true, true,
)
}
func TestPropertiesEncoderDeepWithComments(t *testing.T) {
var sampleYaml = `a: # a thing
b: "bob cool" # b thing
`
doTest(
t, sampleYaml,
testProperties{
pairs: []keyValuePair{
{
key: "a.b",
value: "bob cool",
comment: "# b thing",
},
},
},
true, true,
)
}
func TestPropertiesEncoderArray_Unwrapped(t *testing.T) {
var sampleYaml = `a:
b: [{c: dog}, {c: cat}]
`
doTest(
t, sampleYaml,
testProperties{
pairs: []keyValuePair{
{
key: "a.b.0.c",
value: "dog",
},
{
key: "a.b.1.c",
value: "cat",
},
},
},
true, false,
)
}
func TestPropertiesEncoderArray_Wrapped(t *testing.T) {
var sampleYaml = `a:
b: [{c: dog named jim}, {c: cat named jim}]
`
doTest(
t, sampleYaml,
testProperties{
pairs: []keyValuePair{
{
key: "a.b.0.c",
value: "dog named jim",
},
{
key: "a.b.1.c",
value: "cat named jim",
},
},
},
false, true,
)
}
+44
View File
@@ -0,0 +1,44 @@
package properties
import "github.com/mikefarah/yq/v4/pkg/yqlib"
type PropertiesPreferences struct {
UnwrapScalar bool
KeyValueSeparator string
UseArrayBrackets bool
}
func NewDefaultPropertiesPreferences() PropertiesPreferences {
return PropertiesPreferences{
UnwrapScalar: true,
KeyValueSeparator: " = ",
UseArrayBrackets: false,
}
}
func (p *PropertiesPreferences) Copy() PropertiesPreferences {
return PropertiesPreferences{
UnwrapScalar: p.UnwrapScalar,
KeyValueSeparator: p.KeyValueSeparator,
UseArrayBrackets: p.UseArrayBrackets,
}
}
var PropertiesFormat = &yqlib.Format{"props", []string{"p", "properties"},
func() yqlib.Encoder { return NewPropertiesEncoder(ConfiguredPropertiesPreferences) },
func() yqlib.Decoder { return NewPropertiesDecoder() },
}
var propertyYqRules = []*yqlib.ParticipleYqRule{
{"PropertiesDecode", `from_?props|@propsd`, decodeOp(PropertiesFormat), 0},
{"PropsEncode", `to_?props|@props`, encodeWithIndent(PropertiesFormat, 2), 0},
{"LoadProperties", `load_?props`, loadOp(NewPropertiesDecoder(), false), 0},
}
func RegisterPropertiesFormat() {
yqlib.RegisterFormat(PropertiesFormat)
yqlib.RegisterRules(propertyYqRules)
}
var ConfiguredPropertiesPreferences = NewDefaultPropertiesPreferences()