Decoder Properties

This commit is contained in:
Mike Farah 2022-02-08 14:06:32 +11:00
parent 4d4bd5114d
commit a5ab9a8a0a
7 changed files with 214 additions and 16 deletions

View File

@ -54,7 +54,10 @@ func configureDecoder() (yqlib.Decoder, error) {
switch yqlibInputFormat { switch yqlibInputFormat {
case yqlib.XMLInputFormat: case yqlib.XMLInputFormat:
return yqlib.NewXMLDecoder(xmlAttributePrefix, xmlContentName), nil return yqlib.NewXMLDecoder(xmlAttributePrefix, xmlContentName), nil
case yqlib.PropertiesInputFormat:
return yqlib.NewPropertiesDecoder(), nil
} }
return yqlib.NewYamlDecoder(), nil return yqlib.NewYamlDecoder(), nil
} }

View File

@ -0,0 +1,6 @@
# comments on values appear
person.name = Mike
# comments on array values appear
person.pets.0 = cat
person.food.0 = pizza

View File

@ -0,0 +1,109 @@
package yqlib
import (
"bytes"
"io"
"strconv"
"strings"
"github.com/magiconair/properties"
"gopkg.in/yaml.v3"
)
type propertiesDecoder struct {
reader io.Reader
finished bool
d DataTreeNavigator
}
func NewPropertiesDecoder() Decoder {
return &propertiesDecoder{d: NewDataTreeNavigator(), finished: false}
}
func (dec *propertiesDecoder) Init(reader io.Reader) {
dec.reader = reader
dec.finished = false
}
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) applyProperty(properties *properties.Properties, context Context, key string) error {
value, _ := properties.Get(key)
path := parsePropKey(key)
rhsNode := &yaml.Node{
Value: value,
Tag: "!!str",
Kind: yaml.ScalarNode,
LineComment: properties.GetComment(key),
}
rhsNode.Tag = guessTagFromCustomType(rhsNode)
rhsCandidateNode := &CandidateNode{
Path: path,
Node: rhsNode,
}
assignmentOp := &Operation{OperationType: assignOpType, Preferences: assignPreferences{}}
rhsOp := &Operation{OperationType: valueOpType, CandidateNode: rhsCandidateNode}
assignmentOpNode := &ExpressionNode{
Operation: assignmentOp,
LHS: createTraversalTree(path, traversePreferences{}, false),
RHS: &ExpressionNode{Operation: rhsOp},
}
_, err := dec.d.GetMatchingNodes(context, assignmentOpNode)
return err
}
func (dec *propertiesDecoder) Decode(rootYamlNode *yaml.Node) error {
if dec.finished {
return io.EOF
}
buf := new(bytes.Buffer)
if _, err := buf.ReadFrom(dec.reader); err != nil {
return err
}
properties, err := properties.LoadString(buf.String())
if err != nil {
return err
}
rootMap := &CandidateNode{
Node: &yaml.Node{
Kind: yaml.MappingNode,
Tag: "!!map",
},
}
context := Context{}
context = context.SingleChildContext(rootMap)
for _, key := range properties.Keys() {
if err := dec.applyProperty(properties, context, key); err != nil {
return err
}
}
rootYamlNode.Kind = yaml.DocumentNode
rootYamlNode.Content = []*yaml.Node{rootMap.Node}
dec.finished = true
return nil
}

View File

@ -16,6 +16,7 @@ type InputFormat uint
const ( const (
YamlInputFormat = 1 << iota YamlInputFormat = 1 << iota
XMLInputFormat XMLInputFormat
PropertiesInputFormat
) )
func InputFormatFromString(format string) (InputFormat, error) { func InputFormatFromString(format string) (InputFormat, error) {
@ -24,6 +25,8 @@ func InputFormatFromString(format string) (InputFormat, error) {
return YamlInputFormat, nil return YamlInputFormat, nil
case "xml", "x": case "xml", "x":
return XMLInputFormat, nil return XMLInputFormat, nil
case "props", "p":
return PropertiesInputFormat, nil
default: default:
return 0, fmt.Errorf("unknown format '%v' please use [yaml|xml]", format) return 0, fmt.Errorf("unknown format '%v' please use [yaml|xml]", format)
} }

View File

@ -27,7 +27,7 @@ emptyMap: []
``` ```
then then
```bash ```bash
yq -o=props -I=0 '.' sample.yml yq -o=props sample.yml
``` ```
will output will output
```properties ```properties
@ -54,7 +54,7 @@ emptyMap: []
``` ```
then then
```bash ```bash
yq -o=props -I=0 '... comments = ""' sample.yml yq -o=props '... comments = ""' sample.yml
``` ```
will output will output
```properties ```properties
@ -80,7 +80,7 @@ emptyMap: []
``` ```
then then
```bash ```bash
yq -o=props -I=0 '(.. | select( (tag == "!!map" or tag =="!!seq") and length == 0)) = ""' sample.yml yq -o=props '(.. | select( (tag == "!!map" or tag =="!!seq") and length == 0)) = ""' sample.yml
``` ```
will output will output
```properties ```properties
@ -94,3 +94,28 @@ emptyArray =
emptyMap = emptyMap =
``` ```
## Decode properties
Given a sample.properties file of:
```properties
# comments on values appear
person.name = Mike
# comments on array values appear
person.pets.0 = cat
person.food.0 = pizza
```
then
```bash
yq -p=props sample.properties
```
will output
```yaml
person:
name: Mike # comments on values appear
pets:
- cat # comments on array values appear
food:
- pizza
```

View File

@ -98,19 +98,21 @@ func decodeJSON(t *testing.T, jsonString string) *CandidateNode {
func testJSONScenario(t *testing.T, s formatScenario) { func testJSONScenario(t *testing.T, s formatScenario) {
if s.scenarioType == "encode" || s.scenarioType == "roundtrip" { if s.scenarioType == "encode" || s.scenarioType == "roundtrip" {
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewJONEncoder(s.indent)), s.description) test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewJONEncoder(s.indent), NewYamlDecoder()), s.description)
} else { } else {
var actual = resultToString(t, decodeJSON(t, s.input)) var actual = resultToString(t, decodeJSON(t, s.input))
test.AssertResultWithContext(t, s.expected, actual, s.description) test.AssertResultWithContext(t, s.expected, actual, s.description)
} }
} }
func processFormatScenario(s formatScenario, encoder Encoder) string { func processFormatScenario(s formatScenario, encoder Encoder, decoder Decoder) string {
var output bytes.Buffer var output bytes.Buffer
writer := bufio.NewWriter(&output) writer := bufio.NewWriter(&output)
var decoder = NewYamlDecoder() if decoder == nil {
decoder = NewYamlDecoder()
}
inputs, err := readDocuments(strings.NewReader(s.input), "sample.yml", 0, decoder) inputs, err := readDocuments(strings.NewReader(s.input), "sample.yml", 0, decoder)
if err != nil { if err != nil {
@ -212,7 +214,7 @@ func documentJSONEncodeScenario(w *bufio.Writer, s formatScenario) {
} }
writeOrPanic(w, "will output\n") writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```json\n%v```\n\n", processFormatScenario(s, NewJONEncoder(s.indent)))) writeOrPanic(w, fmt.Sprintf("```json\n%v```\n\n", processFormatScenario(s, NewJONEncoder(s.indent), NewYamlDecoder())))
} }
func TestJSONScenarios(t *testing.T) { func TestJSONScenarios(t *testing.T) {

View File

@ -26,6 +26,14 @@ person.pets.0 = cat
person.food.0 = pizza person.food.0 = pizza
` `
var expectedDecodedYaml = `person:
name: Mike # comments on values appear
pets:
- cat # comments on array values appear
food:
- pizza
`
var expectedPropertiesNoComments = `person.name = Mike var expectedPropertiesNoComments = `person.name = Mike
person.pets.0 = cat person.pets.0 = cat
person.food.0 = pizza person.food.0 = pizza
@ -61,10 +69,16 @@ var propertyScenarios = []formatScenario{
input: samplePropertiesYaml, input: samplePropertiesYaml,
expected: expectedPropertiesWithEmptyMapsAndArrays, expected: expectedPropertiesWithEmptyMapsAndArrays,
}, },
{
skipDoc: true,
description: "Decode properties",
input: expectedProperties,
expected: expectedDecodedYaml,
scenarioType: "decode",
},
} }
func documentPropertyScenario(t *testing.T, w *bufio.Writer, i interface{}) { func documentEncodePropertyScenario(w *bufio.Writer, s formatScenario) {
s := i.(formatScenario)
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description)) writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
if s.subdescription != "" { if s.subdescription != "" {
@ -78,23 +92,59 @@ func documentPropertyScenario(t *testing.T, w *bufio.Writer, i interface{}) {
writeOrPanic(w, "then\n") writeOrPanic(w, "then\n")
expression := s.expression expression := s.expression
if expression == "" {
expression = "."
}
if s.indent == 2 { if expression != "" {
writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=props '%v' sample.yml\n```\n", expression)) writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=props '%v' sample.yml\n```\n", expression))
} else { } else {
writeOrPanic(w, fmt.Sprintf("```bash\nyq -o=props -I=%v '%v' sample.yml\n```\n", s.indent, expression)) writeOrPanic(w, "```bash\nyq -o=props sample.yml\n```\n")
} }
writeOrPanic(w, "will output\n") writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", processFormatScenario(s, NewPropertiesEncoder()))) writeOrPanic(w, fmt.Sprintf("```properties\n%v```\n\n", processFormatScenario(s, NewPropertiesEncoder(), NewYamlDecoder())))
}
func documentDecodePropertyScenario(w *bufio.Writer, s formatScenario) {
writeOrPanic(w, fmt.Sprintf("## %v\n", s.description))
if s.subdescription != "" {
writeOrPanic(w, s.subdescription)
writeOrPanic(w, "\n\n")
}
writeOrPanic(w, "Given a sample.properties file of:\n")
writeOrPanic(w, fmt.Sprintf("```properties\n%v\n```\n", s.input))
writeOrPanic(w, "then\n")
expression := s.expression
if expression != "" {
writeOrPanic(w, fmt.Sprintf("```bash\nyq -p=props '%v' sample.properties\n```\n", expression))
} else {
writeOrPanic(w, "```bash\nyq -p=props sample.properties\n```\n")
}
writeOrPanic(w, "will output\n")
writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", processFormatScenario(s, NewYamlEncoder(s.indent, false, true, true), NewPropertiesDecoder())))
}
func documentPropertyScenario(t *testing.T, w *bufio.Writer, i interface{}) {
s := i.(formatScenario)
if s.scenarioType == "decode" {
documentDecodePropertyScenario(w, s)
} else {
documentEncodePropertyScenario(w, s)
}
} }
func TestPropertyScenarios(t *testing.T) { func TestPropertyScenarios(t *testing.T) {
for _, s := range propertyScenarios { for _, s := range propertyScenarios {
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewPropertiesEncoder()), s.description) if s.scenarioType == "decode" {
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewYamlEncoder(2, false, true, true), NewPropertiesDecoder()), s.description)
} else {
test.AssertResultWithContext(t, s.expected, processFormatScenario(s, NewPropertiesEncoder(), NewYamlDecoder()), s.description)
}
} }
genericScenarios := make([]interface{}, len(propertyScenarios)) genericScenarios := make([]interface{}, len(propertyScenarios))
for i, s := range propertyScenarios { for i, s := range propertyScenarios {