diff --git a/pkg/yqlib/encoder_xml.go b/pkg/yqlib/encoder_xml.go new file mode 100644 index 0000000..652d574 --- /dev/null +++ b/pkg/yqlib/encoder_xml.go @@ -0,0 +1,335 @@ +//go:build !yq_noxml + +package yqlib + +import ( + "encoding/xml" + "fmt" + "io" + "regexp" + "strings" +) + +type xmlEncoder struct { + indentString string + writer io.Writer + prefs XmlPreferences + leadingContent string +} + +func NewXMLEncoder(prefs XmlPreferences) Encoder { + var indentString = "" + + for index := 0; index < prefs.Indent; index++ { + indentString = indentString + " " + } + return &xmlEncoder{indentString, nil, prefs, ""} +} + +func (e *xmlEncoder) CanHandleAliases() bool { + return false +} + +func (e *xmlEncoder) PrintDocumentSeparator(_ io.Writer) error { + return nil +} + +func (e *xmlEncoder) PrintLeadingContent(_ io.Writer, content string) error { + e.leadingContent = content + return nil +} + +func (e *xmlEncoder) Encode(writer io.Writer, node *CandidateNode) error { + encoder := xml.NewEncoder(writer) + // hack so we can manually add newlines to procInst and directives + e.writer = writer + encoder.Indent("", e.indentString) + var newLine xml.CharData = []byte("\n") + + if node.Tag == "!!map" { + // make sure processing instructions are encoded first + for i := 0; i < len(node.Content); i += 2 { + key := node.Content[i] + value := node.Content[i+1] + + if key.Value == (e.prefs.ProcInstPrefix + "xml") { + name := strings.Replace(key.Value, e.prefs.ProcInstPrefix, "", 1) + procInst := xml.ProcInst{Target: name, Inst: []byte(value.Value)} + if err := encoder.EncodeToken(procInst); err != nil { + return err + } + if _, err := e.writer.Write([]byte("\n")); err != nil { + log.Warningf("Unable to write newline, skipping: %v", err) + } + } + } + } + + if e.leadingContent != "" { + + // remove first and last newlines if present + err := e.encodeComment(encoder, e.leadingContent) + if err != nil { + return err + } + err = encoder.EncodeToken(newLine) + if err != nil { + return err + } + } + + switch node.Kind { + case MappingNode: + err := e.encodeTopLevelMap(encoder, node) + if err != nil { + return err + } + case ScalarNode: + var charData xml.CharData = []byte(node.Value) + err := encoder.EncodeToken(charData) + if err != nil { + return err + } + return encoder.Flush() + default: + return fmt.Errorf("cannot encode %v to XML - only maps can be encoded", node.Tag) + } + + return encoder.EncodeToken(newLine) + +} + +func (e *xmlEncoder) encodeTopLevelMap(encoder *xml.Encoder, node *CandidateNode) error { + err := e.encodeComment(encoder, headAndLineComment(node)) + if err != nil { + return err + } + for i := 0; i < len(node.Content); i += 2 { + key := node.Content[i] + value := node.Content[i+1] + + start := xml.StartElement{Name: xml.Name{Local: key.Value}} + log.Debugf("comments of key %v", key.Value) + err := e.encodeComment(encoder, headAndLineComment(key)) + if err != nil { + return err + } + if headAndLineComment(key) != "" { + var newLine xml.CharData = []byte("\n") + err = encoder.EncodeToken(newLine) + if err != nil { + return err + } + } + + if key.Value == (e.prefs.ProcInstPrefix + "xml") { //nolint + // dont double process these. + } else if strings.HasPrefix(key.Value, e.prefs.ProcInstPrefix) { + name := strings.Replace(key.Value, e.prefs.ProcInstPrefix, "", 1) + procInst := xml.ProcInst{Target: name, Inst: []byte(value.Value)} + if err := encoder.EncodeToken(procInst); err != nil { + return err + } + if _, err := e.writer.Write([]byte("\n")); err != nil { + log.Warningf("Unable to write newline, skipping: %v", err) + } + } else if key.Value == e.prefs.DirectiveName { + var directive xml.Directive = []byte(value.Value) + if err := encoder.EncodeToken(directive); err != nil { + return err + } + if _, err := e.writer.Write([]byte("\n")); err != nil { + log.Warningf("Unable to write newline, skipping: %v", err) + } + } else { + + log.Debugf("recursing") + + err = e.doEncode(encoder, value, start) + if err != nil { + return err + } + } + err = e.encodeComment(encoder, footComment(key)) + if err != nil { + return err + } + } + return e.encodeComment(encoder, footComment(node)) +} + +func (e *xmlEncoder) encodeStart(encoder *xml.Encoder, node *CandidateNode, start xml.StartElement) error { + err := encoder.EncodeToken(start) + if err != nil { + return err + } + return e.encodeComment(encoder, headComment(node)) +} + +func (e *xmlEncoder) encodeEnd(encoder *xml.Encoder, node *CandidateNode, start xml.StartElement) error { + err := encoder.EncodeToken(start.End()) + if err != nil { + return err + } + return e.encodeComment(encoder, footComment(node)) +} + +func (e *xmlEncoder) doEncode(encoder *xml.Encoder, node *CandidateNode, start xml.StartElement) error { + switch node.Kind { + case MappingNode: + return e.encodeMap(encoder, node, start) + case SequenceNode: + return e.encodeArray(encoder, node, start) + case ScalarNode: + err := e.encodeStart(encoder, node, start) + if err != nil { + return err + } + + var charData xml.CharData = []byte(node.Value) + err = encoder.EncodeToken(charData) + if err != nil { + return err + } + + if err = e.encodeComment(encoder, lineComment(node)); err != nil { + return err + } + + return e.encodeEnd(encoder, node, start) + } + return fmt.Errorf("unsupported type %v", node.Tag) +} + +var xmlEncodeMultilineCommentRegex = regexp.MustCompile(`(^|\n) *# ?(.*)`) +var xmlEncodeSingleLineCommentRegex = regexp.MustCompile(`^\s*#(.*)\n?`) +var chompRegexp = regexp.MustCompile(`\n$`) + +func (e *xmlEncoder) encodeComment(encoder *xml.Encoder, commentStr string) error { + if commentStr != "" { + log.Debugf("got comment [%v]", commentStr) + // multi line string + if len(commentStr) > 2 && strings.Contains(commentStr[1:len(commentStr)-1], "\n") { + commentStr = chompRegexp.ReplaceAllString(commentStr, "") + log.Debugf("chompRegexp [%v]", commentStr) + commentStr = xmlEncodeMultilineCommentRegex.ReplaceAllString(commentStr, "$1$2") + log.Debugf("processed multiline [%v]", commentStr) + // if the first line is non blank, add a space + if commentStr[0] != '\n' && commentStr[0] != ' ' { + commentStr = " " + commentStr + } + + } else { + commentStr = xmlEncodeSingleLineCommentRegex.ReplaceAllString(commentStr, "$1") + } + + if !strings.HasSuffix(commentStr, " ") && !strings.HasSuffix(commentStr, "\n") { + commentStr = commentStr + " " + log.Debugf("added suffix [%v]", commentStr) + } + log.Debugf("encoding comment [%v]", commentStr) + + var comment xml.Comment = []byte(commentStr) + err := encoder.EncodeToken(comment) + if err != nil { + return err + } + } + return nil +} + +func (e *xmlEncoder) encodeArray(encoder *xml.Encoder, node *CandidateNode, start xml.StartElement) error { + + if err := e.encodeComment(encoder, headAndLineComment(node)); err != nil { + return err + } + + for i := 0; i < len(node.Content); i++ { + value := node.Content[i] + if err := e.doEncode(encoder, value, start.Copy()); err != nil { + return err + } + } + return e.encodeComment(encoder, footComment(node)) +} + +func (e *xmlEncoder) isAttribute(name string) bool { + return strings.HasPrefix(name, e.prefs.AttributePrefix) && + name != e.prefs.ContentName && + name != e.prefs.DirectiveName && + !strings.HasPrefix(name, e.prefs.ProcInstPrefix) +} + +func (e *xmlEncoder) encodeMap(encoder *xml.Encoder, node *CandidateNode, start xml.StartElement) error { + log.Debug("its a map") + + //first find all the attributes and put them on the start token + for i := 0; i < len(node.Content); i += 2 { + key := node.Content[i] + value := node.Content[i+1] + + if e.isAttribute(key.Value) { + if value.Kind == ScalarNode { + attributeName := strings.Replace(key.Value, e.prefs.AttributePrefix, "", 1) + start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: attributeName}, Value: value.Value}) + } else { + return fmt.Errorf("cannot use %v as attribute, only scalars are supported", value.Tag) + } + } + } + + err := e.encodeStart(encoder, node, start) + if err != nil { + return err + } + + //now we encode non attribute tokens + for i := 0; i < len(node.Content); i += 2 { + key := node.Content[i] + value := node.Content[i+1] + + err := e.encodeComment(encoder, headAndLineComment(key)) + if err != nil { + return err + } + if strings.HasPrefix(key.Value, e.prefs.ProcInstPrefix) { + name := strings.Replace(key.Value, e.prefs.ProcInstPrefix, "", 1) + procInst := xml.ProcInst{Target: name, Inst: []byte(value.Value)} + if err := encoder.EncodeToken(procInst); err != nil { + return err + } + } else if key.Value == e.prefs.DirectiveName { + var directive xml.Directive = []byte(value.Value) + if err := encoder.EncodeToken(directive); err != nil { + return err + } + } else if key.Value == e.prefs.ContentName { + // directly encode the contents + err = e.encodeComment(encoder, headAndLineComment(value)) + if err != nil { + return err + } + var charData xml.CharData = []byte(value.Value) + err = encoder.EncodeToken(charData) + if err != nil { + return err + } + err = e.encodeComment(encoder, footComment(value)) + if err != nil { + return err + } + } else if !e.isAttribute(key.Value) { + start := xml.StartElement{Name: xml.Name{Local: key.Value}} + err := e.doEncode(encoder, value, start) + if err != nil { + return err + } + } + err = e.encodeComment(encoder, footComment(key)) + if err != nil { + return err + } + } + + return e.encodeEnd(encoder, node, start) +} diff --git a/pkg/yqlib/xml_test.go b/pkg/yqlib/xml_test.go new file mode 100644 index 0000000..7628ae1 --- /dev/null +++ b/pkg/yqlib/xml_test.go @@ -0,0 +1,841 @@ +//go:build !yq_noxml + +package yqlib + +import ( + "bufio" + "fmt" + "testing" + + "github.com/mikefarah/yq/v4/test" +) + +const yamlInputWithProcInstAndHeadComment = `# cats ++p_xml: version="1.0" +this: is some xml` + +const expectedXmlProcInstAndHeadComment = ` + +is some xml +` + +const xmlProcInstAndHeadCommentBlock = ` + +is some xml +` + +const expectedYamlProcInstAndHeadCommentBlock = `# +# cats +# ++p_xml: version="1.0" +this: is some xml +` + +const inputXMLWithComments = ` + + + + 3 + + + + z + + + + + + +` +const inputXMLWithCommentsWithSubChild = ` + + + + 3 + + + + + + + + + + +` + +const expectedDecodeYamlWithSubChild = `# before cat +cat: + # in cat before + x: "3" # multi + # line comment + # for x + # before y + + y: + # in y before + d: + # in d before + z: + +@sweet: cool + # in d after + # in y after + # in_cat_after +# after cat +` + +const inputXMLWithCommentsWithArray = ` + + + + 3 + + + + + + + + + + + +` + +const expectedDecodeYamlWithArray = `# before cat +cat: + # in cat before + x: "3" # multi + # line comment + # for x + # before y + + y: + # in y before + d: + - # in d before + z: + +@sweet: cool + # in d after + - # in d2 before + z: + +@sweet: cool2 + # in d2 after + # in y after + # in_cat_after +# after cat +` + +const expectedDecodeYamlWithComments = `# before cat +cat: + # in cat before + x: "3" # multi + # line comment + # for x + # before y + + y: + # in y before + # in d before + d: z # in d after + # in y after + # in_cat_after +# after cat +` + +const expectedRoundtripXMLWithComments = ` + + 3 + + z + + +` + +const yamlWithComments = `# +# header comment +# above_cat +# +cat: # inline_cat + # above_array + array: # inline_array + - val1 # inline_val1 + # above_val2 + - val2 # inline_val2 +# below_cat +` + +const expectedXMLWithComments = ` + + + val1 + val2 + +` + +const inputXMLWithNamespacedAttr = ` + + baz + foobar + +` + +const expectedYAMLWithNamespacedAttr = `+p_xml: version="1.0" +map: + +@xmlns: some-namespace + +@xmlns:xsi: some-instance + +@xsi:schemaLocation: some-url + item: + +content: baz + +@foo: bar + xsi:item: foobar +` + +const expectedYAMLWithRawNamespacedAttr = `+p_xml: version="1.0" +map: + +@xmlns: some-namespace + +@xmlns:xsi: some-instance + +@xsi:schemaLocation: some-url + item: + +content: baz + +@foo: bar + xsi:item: foobar +` + +const expectedYAMLWithoutRawNamespacedAttr = `+p_xml: version="1.0" +some-namespace:map: + +@xmlns: some-namespace + +@xmlns:xsi: some-instance + +@some-instance:schemaLocation: some-url + some-namespace:item: + +content: baz + +@foo: bar + some-instance:item: foobar +` + +const xmlWithCustomDtd = ` + + + +]> + + &writer;©right; +` + +const expectedDtd = ` + + +]> + + &writer;&copyright; + +` + +const expectedSkippedDtd = ` + + &writer;&copyright; + +` + +const xmlWithProcInstAndDirectives = ` + + + + + things + +` + +const yamlWithProcInstAndDirectives = `+p_xml: version="1.0" ++directive: 'DOCTYPE config SYSTEM "/etc/iwatch/iwatch.dtd" ' +apple: + +p_coolioo: version="1.0" + +directive: 'CATYPE meow purr puss ' + b: things +` + +const expectedXmlWithProcInstAndDirectives = ` + + + things + +` + +var xmlScenarios = []formatScenario{ + { + skipDoc: true, + description: "bad xml", + input: ``, + expected: "+p_xml: version=\"1.0\" encoding=\"UTF-8\"\n", + }, + { + skipDoc: true, + input: " value ", + expected: "root: value # comment\n", + }, + { + skipDoc: true, + input: "valuevalue", + expectedError: "bad file 'sample.yml': invalid XML: Encountered chardata [value] outside of XML node", + scenarioType: "decode-error", + }, + { + skipDoc: true, + input: "value", + expected: "# comment\nroot: value\n", + }, + { + skipDoc: true, + input: " ", + expected: "root: # comment\n", + }, + { + skipDoc: true, + input: "valueanotherValue ", + expected: "root:\n # comment\n - value\n - anotherValue\n", + }, + { + skipDoc: true, + input: "quicksoftsquishy", + expected: "root:\n cats:\n cat:\n - quick\n - soft\n # kitty_comment\n\n - squishy\n", + }, + { + description: "ProcInst with head comment", + skipDoc: true, + input: yamlInputWithProcInstAndHeadComment, + expected: expectedXmlProcInstAndHeadComment, + scenarioType: "encode", + }, + { + description: "Scalar roundtrip", + skipDoc: true, + input: "cat", + expression: ".mike", + expected: "cat", + scenarioType: "roundtrip", + }, + { + description: "ProcInst with head comment round trip", + skipDoc: true, + input: expectedXmlProcInstAndHeadComment, + expected: expectedXmlProcInstAndHeadComment, + scenarioType: "roundtrip", + }, + { + description: "ProcInst with block head comment to yaml", + skipDoc: true, + input: xmlProcInstAndHeadCommentBlock, + expected: expectedYamlProcInstAndHeadCommentBlock, + scenarioType: "decode", + }, + { + description: "ProcInst with block head comment from yaml", + skipDoc: true, + input: expectedYamlProcInstAndHeadCommentBlock, + expected: xmlProcInstAndHeadCommentBlock, + scenarioType: "encode", + }, + { + description: "ProcInst with head comment round trip block", + skipDoc: true, + input: xmlProcInstAndHeadCommentBlock, + expected: xmlProcInstAndHeadCommentBlock, + scenarioType: "roundtrip", + }, + { + description: "Parse xml: simple", + subdescription: "Notice how all the values are strings, see the next example on how you can fix that.", + input: "\n\n meow\n 4\n true\n", + expected: "+p_xml: version=\"1.0\" encoding=\"UTF-8\"\ncat:\n says: meow\n legs: \"4\"\n cute: \"true\"\n", + }, + { + description: "Parse xml: number", + subdescription: "All values are assumed to be strings when parsing XML, but you can use the `from_yaml` operator on all the strings values to autoparse into the correct type.", + input: "\n\n meow\n 4\n true\n", + expression: " (.. | select(tag == \"!!str\")) |= from_yaml", + expected: "+p_xml: version=\"1.0\" encoding=\"UTF-8\"\ncat:\n says: meow\n legs: 4\n cute: true\n", + }, + { + description: "Parse xml: array", + subdescription: "Consecutive nodes with identical xml names are assumed to be arrays.", + input: "\ncat\ngoat", + expected: "+p_xml: version=\"1.0\" encoding=\"UTF-8\"\nanimal:\n - cat\n - goat\n", + }, + { + description: "Parse xml: force as an array", + subdescription: "In XML, if your array has a single item, then yq doesn't know its an array. This is how you can consistently force it to be an array. This handles the 3 scenarios of having nothing in the array, having a single item and having multiple.", + input: "cat", + expression: ".zoo.animal |= ([] + .)", + expected: "zoo:\n animal:\n - cat\n", + }, + { + description: "Parse xml: force all as an array", + input: "boing", + expression: ".. |= [] + .", + expected: "- zoo:\n - thing:\n - frog:\n - boing\n", + }, + { + description: "Parse xml: attributes", + subdescription: "Attributes are converted to fields, with the default attribute prefix '+'. Use '--xml-attribute-prefix` to set your own.", + input: "\n\n 7\n", + expected: "+p_xml: version=\"1.0\" encoding=\"UTF-8\"\ncat:\n +@legs: \"4\"\n legs: \"7\"\n", + }, + { + description: "Parse xml: attributes with content", + subdescription: "Content is added as a field, using the default content name of `+content`. Use `--xml-content-name` to set your own.", + input: "\nmeow", + expected: "+p_xml: version=\"1.0\" encoding=\"UTF-8\"\ncat:\n +content: meow\n +@legs: \"4\"\n", + }, + { + description: "Parse xml: content split between comments/children", + subdescription: "Multiple content texts are collected into a sequence.", + input: " value anotherValue frog cool!", + expected: "root:\n +content: # comment\n - value\n - anotherValue\n - cool!\n a: frog\n", + }, + { + description: "Parse xml: custom dtd", + subdescription: "DTD entities are processed as directives.", + input: xmlWithCustomDtd, + expected: expectedDtd, + scenarioType: "roundtrip", + }, + { + description: "Roundtrip with name spaced attributes", + skipDoc: true, + input: inputXMLWithNamespacedAttr, + expected: inputXMLWithNamespacedAttr, + scenarioType: "roundtrip", + }, + { + description: "Parse xml: skip custom dtd", + subdescription: "DTDs are directives, skip over directives to skip DTDs.", + input: xmlWithCustomDtd, + expected: expectedSkippedDtd, + scenarioType: "roundtrip-skip-directives", + }, + { + description: "Parse xml: with comments", + subdescription: "A best attempt is made to preserve comments.", + input: inputXMLWithComments, + expected: expectedDecodeYamlWithComments, + scenarioType: "decode", + }, + { + description: "Empty doc", + skipDoc: true, + input: "", + expected: "\n", + scenarioType: "decode", + }, + { + description: "Empty single node", + skipDoc: true, + input: "", + expected: "a:\n", + scenarioType: "decode", + }, + { + description: "Empty close node", + skipDoc: true, + input: "", + expected: "a:\n", + scenarioType: "decode", + }, + { + description: "Nested empty", + skipDoc: true, + input: "", + expected: "a:\n b:\n", + scenarioType: "decode", + }, + { + description: "Parse xml: with comments subchild", + skipDoc: true, + input: inputXMLWithCommentsWithSubChild, + expected: expectedDecodeYamlWithSubChild, + scenarioType: "decode", + }, + { + description: "Parse xml: with comments array", + skipDoc: true, + input: inputXMLWithCommentsWithArray, + expected: expectedDecodeYamlWithArray, + scenarioType: "decode", + }, + { + description: "Parse xml: keep attribute namespace", + subdescription: fmt.Sprintf(`Defaults to %v`, ConfiguredXMLPreferences.KeepNamespace), + skipDoc: false, + input: inputXMLWithNamespacedAttr, + expected: expectedYAMLWithNamespacedAttr, + scenarioType: "decode-keep-ns", + }, + { + description: "Parse xml: keep raw attribute namespace", + skipDoc: true, + input: inputXMLWithNamespacedAttr, + expected: expectedYAMLWithRawNamespacedAttr, + scenarioType: "decode-raw-token", + }, + { + description: "Parse xml: keep raw attribute namespace", + subdescription: fmt.Sprintf(`Defaults to %v`, ConfiguredXMLPreferences.UseRawToken), + skipDoc: false, + input: inputXMLWithNamespacedAttr, + expected: expectedYAMLWithoutRawNamespacedAttr, + scenarioType: "decode-raw-token-off", + }, + { + description: "Encode xml: simple", + input: "cat: purrs", + expected: "purrs\n", + scenarioType: "encode", + }, + { + description: "includes map tags", + skipDoc: true, + input: "purrs\n", + expression: `tag`, + expected: "!!map\n", + scenarioType: "decode", + }, + { + description: "includes array tags", + skipDoc: true, + input: "purrspurrs\n", + expression: `.cat | tag`, + expected: "!!seq\n", + scenarioType: "decode", + }, + { + description: "Encode xml: array", + input: "pets:\n cat:\n - purrs\n - meows", + expected: "\n purrs\n meows\n\n", + scenarioType: "encode", + }, + { + description: "Encode xml: attributes", + subdescription: "Fields with the matching xml-attribute-prefix are assumed to be attributes.", + input: "cat:\n +@name: tiger\n meows: true\n", + expected: "\n true\n\n", + scenarioType: "encode", + }, + { + description: "double prefix", + skipDoc: true, + input: "cat:\n +@+@name: tiger\n meows: true\n", + expected: "\n true\n\n", + scenarioType: "encode", + }, + { + description: "arrays cannot be encoded", + skipDoc: true, + input: "[cat, dog, fish]", + expectedError: "cannot encode !!seq to XML - only maps can be encoded", + scenarioType: "encode-error", + }, + { + description: "arrays cannot be encoded - 2", + skipDoc: true, + input: "[cat, dog]", + expectedError: "cannot encode !!seq to XML - only maps can be encoded", + scenarioType: "encode-error", + }, + { + description: "Encode xml: attributes with content", + subdescription: "Fields with the matching xml-content-name is assumed to be content.", + input: "cat:\n +@name: tiger\n +content: cool\n", + expected: "cool\n", + scenarioType: "encode", + }, + { + description: "round trip multiline 1", + skipDoc: true, + input: "\n", + expected: "\n", + scenarioType: "roundtrip", + }, + { + description: "round trip multiline 2", + skipDoc: true, + input: "\n", + expected: "\n", + scenarioType: "roundtrip", + }, + { + description: "round trip multiline 3", + skipDoc: true, + input: "\n", + expected: "\n", + scenarioType: "roundtrip", + }, + { + description: "round trip multiline 4", + skipDoc: true, + input: "\n", + expected: "\n", + scenarioType: "roundtrip", + }, + { + description: "round trip multiline 5", + skipDoc: true, // pity spaces aren't kept atm. + input: "\n", + expected: "\n", + scenarioType: "roundtrip", + }, + { + description: "Encode xml: comments", + subdescription: "A best attempt is made to copy comments to xml.", + input: yamlWithComments, + expected: expectedXMLWithComments, + scenarioType: "encode", + }, + { + description: "Encode: doctype and xml declaration", + subdescription: "Use the special xml names to add/modify proc instructions and directives.", + input: yamlWithProcInstAndDirectives, + expected: expectedXmlWithProcInstAndDirectives, + scenarioType: "encode", + }, + { + description: "Round trip: with comments", + subdescription: "A best effort is made, but comment positions and white space are not preserved perfectly.", + input: inputXMLWithComments, + expected: expectedRoundtripXMLWithComments, + scenarioType: "roundtrip", + }, + { + description: "Roundtrip: with doctype and declaration", + subdescription: "yq parses XML proc instructions and directives into nodes.\nUnfortunately the underlying XML parser loses whitespace information.", + input: xmlWithProcInstAndDirectives, + expected: expectedXmlWithProcInstAndDirectives, + scenarioType: "roundtrip", + }, +} + +func testXMLScenario(t *testing.T, s formatScenario) { + switch s.scenarioType { + case "", "decode": + yamlPrefs := ConfiguredYamlPreferences.Copy() + yamlPrefs.Indent = 4 + test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewXMLDecoder(ConfiguredXMLPreferences), NewYamlEncoder(yamlPrefs)), s.description) + case "encode": + test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewXMLEncoder(ConfiguredXMLPreferences)), s.description) + case "roundtrip": + test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewXMLDecoder(ConfiguredXMLPreferences), NewXMLEncoder(ConfiguredXMLPreferences)), s.description) + case "decode-keep-ns": + prefs := NewDefaultXmlPreferences() + prefs.KeepNamespace = true + test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewXMLDecoder(prefs), NewYamlEncoder(ConfiguredYamlPreferences)), s.description) + case "decode-raw-token": + prefs := NewDefaultXmlPreferences() + prefs.UseRawToken = true + test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewXMLDecoder(prefs), NewYamlEncoder(ConfiguredYamlPreferences)), s.description) + case "decode-raw-token-off": + prefs := NewDefaultXmlPreferences() + prefs.UseRawToken = false + test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewXMLDecoder(prefs), NewYamlEncoder(ConfiguredYamlPreferences)), s.description) + case "roundtrip-skip-directives": + prefs := NewDefaultXmlPreferences() + prefs.SkipDirectives = true + test.AssertResultWithContext(t, s.expected, mustProcessFormatScenario(s, NewXMLDecoder(prefs), NewXMLEncoder(prefs)), s.description) + case "decode-error": + result, err := processFormatScenario(s, NewXMLDecoder(NewDefaultXmlPreferences()), NewYamlEncoder(ConfiguredYamlPreferences)) + if err == nil { + t.Errorf("Expected error '%v' but it worked: %v", s.expectedError, result) + } else { + test.AssertResultComplexWithContext(t, s.expectedError, err.Error(), s.description) + } + case "encode-error": + result, err := processFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewXMLEncoder(NewDefaultXmlPreferences())) + if err == nil { + t.Errorf("Expected error '%v' but it worked: %v", s.expectedError, result) + } else { + test.AssertResultComplexWithContext(t, s.expectedError, err.Error(), s.description) + } + + default: + panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType)) + } +} + +func documentXMLScenario(_ *testing.T, w *bufio.Writer, i interface{}) { + s := i.(formatScenario) + + if s.skipDoc { + return + } + switch s.scenarioType { + case "", "decode": + documentXMLDecodeScenario(w, s) + case "encode": + documentXMLEncodeScenario(w, s) + case "roundtrip": + documentXMLRoundTripScenario(w, s) + case "decode-keep-ns": + documentXMLDecodeKeepNsScenario(w, s) + case "decode-raw-token-off": + documentXMLDecodeKeepNsRawTokenScenario(w, s) + case "roundtrip-skip-directives": + documentXMLSkipDirectivesScenario(w, s) + + default: + panic(fmt.Sprintf("unhandled scenario type %q", s.scenarioType)) + } +} + +func documentXMLDecodeScenario(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.xml file of:\n") + writeOrPanic(w, fmt.Sprintf("```xml\n%v\n```\n", s.input)) + + writeOrPanic(w, "then\n") + expression := s.expression + if s.expression != "" { + expression = fmt.Sprintf(" '%v'", s.expression) + } + writeOrPanic(w, fmt.Sprintf("```bash\nyq -oy%v sample.xml\n```\n", expression)) + writeOrPanic(w, "will output\n") + + writeOrPanic(w, fmt.Sprintf("```yaml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(ConfiguredXMLPreferences), NewYamlEncoder(ConfiguredYamlPreferences)))) +} + +func documentXMLDecodeKeepNsScenario(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.xml file of:\n") + writeOrPanic(w, fmt.Sprintf("```xml\n%v\n```\n", s.input)) + + writeOrPanic(w, "then\n") + writeOrPanic(w, "```bash\nyq --xml-keep-namespace=false sample.xml\n```\n") + writeOrPanic(w, "will output\n") + prefs := NewDefaultXmlPreferences() + prefs.KeepNamespace = false + writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(prefs), NewXMLEncoder(prefs)))) + + prefsWithout := NewDefaultXmlPreferences() + prefs.KeepNamespace = true + writeOrPanic(w, "instead of\n") + writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(prefsWithout), NewXMLEncoder(prefsWithout)))) +} + +func documentXMLDecodeKeepNsRawTokenScenario(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.xml file of:\n") + writeOrPanic(w, fmt.Sprintf("```xml\n%v\n```\n", s.input)) + + writeOrPanic(w, "then\n") + writeOrPanic(w, "```bash\nyq --xml-raw-token=false sample.xml\n```\n") + writeOrPanic(w, "will output\n") + + prefs := NewDefaultXmlPreferences() + prefs.UseRawToken = false + + writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(prefs), NewXMLEncoder(prefs)))) + + prefsWithout := NewDefaultXmlPreferences() + prefsWithout.UseRawToken = true + + writeOrPanic(w, "instead of\n") + writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(prefsWithout), NewXMLEncoder(prefsWithout)))) +} + +func documentXMLEncodeScenario(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.yml file of:\n") + writeOrPanic(w, fmt.Sprintf("```yaml\n%v\n```\n", s.input)) + + writeOrPanic(w, "then\n") + writeOrPanic(w, "```bash\nyq -o=xml sample.yml\n```\n") + writeOrPanic(w, "will output\n") + + writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewYamlDecoder(ConfiguredYamlPreferences), NewXMLEncoder(ConfiguredXMLPreferences)))) +} + +func documentXMLRoundTripScenario(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.xml file of:\n") + writeOrPanic(w, fmt.Sprintf("```xml\n%v\n```\n", s.input)) + + writeOrPanic(w, "then\n") + writeOrPanic(w, "```bash\nyq sample.xml\n```\n") + writeOrPanic(w, "will output\n") + + writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(ConfiguredXMLPreferences), NewXMLEncoder(ConfiguredXMLPreferences)))) +} + +func documentXMLSkipDirectivesScenario(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.xml file of:\n") + writeOrPanic(w, fmt.Sprintf("```xml\n%v\n```\n", s.input)) + + writeOrPanic(w, "then\n") + writeOrPanic(w, "```bash\nyq --xml-skip-directives sample.xml\n```\n") + writeOrPanic(w, "will output\n") + prefs := NewDefaultXmlPreferences() + prefs.SkipDirectives = true + + writeOrPanic(w, fmt.Sprintf("```xml\n%v```\n\n", mustProcessFormatScenario(s, NewXMLDecoder(prefs), NewXMLEncoder(prefs)))) +} + +func TestXMLScenarios(t *testing.T) { + for _, tt := range xmlScenarios { + testXMLScenario(t, tt) + } + genericScenarios := make([]interface{}, len(xmlScenarios)) + for i, s := range xmlScenarios { + genericScenarios[i] = s + } + documentScenarios(t, "usage", "xml", genericScenarios, documentXMLScenario) +}