mirror of
https://github.com/mikefarah/yq.git
synced 2026-09-06 10:04:43 +08:00
wip
This commit is contained in:
@@ -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
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user