Skip to content

Commit a2105c2

Browse files
committed
feat: Implement std.parseXmlJsonml
1 parent fed90cd commit a2105c2

File tree

7 files changed

+221
-8
lines changed

7 files changed

+221
-8
lines changed

BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ go_library(
1717
"error_formatter.go",
1818
"imports.go",
1919
"interpreter.go",
20+
"jsonml.go",
2021
"runtime_error.go",
2122
"thunks.go",
2223
"util.go",

builtins.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1512,6 +1512,52 @@ func builtinParseYAML(i *interpreter, str value) (value, error) {
15121512
return jsonToValue(i, elems[0])
15131513
}
15141514

1515+
func builtinParseXmlJsonml(i *interpreter, str value) (value, error) {
1516+
sval, err := i.getString(str)
1517+
if err != nil {
1518+
return nil, err
1519+
}
1520+
s := sval.getGoString()
1521+
1522+
json, err := BuildJsonmlFromString(s)
1523+
if err != nil {
1524+
return nil, i.Error(fmt.Sprintf("failed to parse XML: %v", err.Error()))
1525+
}
1526+
1527+
arr, err := arrayToValue(i, json)
1528+
if err != nil {
1529+
return nil, err
1530+
}
1531+
return arr, nil
1532+
}
1533+
1534+
func arrayToValue(i *interpreter, json []interface{}) (*valueArray, error) {
1535+
var elements []*cachedThunk
1536+
var err error
1537+
for _, e := range json {
1538+
var val value
1539+
switch e := e.(type) {
1540+
case string:
1541+
val = makeValueString(e)
1542+
case map[string]interface{}:
1543+
val, err = jsonToValue(i, e)
1544+
if err != nil {
1545+
return nil, err
1546+
}
1547+
case []interface{}:
1548+
val, err = arrayToValue(i, e)
1549+
if err != nil {
1550+
return nil, err
1551+
}
1552+
default:
1553+
return nil, i.Error(fmt.Sprintf("invalid type for section: %v", reflect.TypeOf(e)))
1554+
}
1555+
elements = append(elements, readyThunk(val))
1556+
}
1557+
1558+
return makeValueArray(elements), nil
1559+
}
1560+
15151561
func jsonEncode(v interface{}) (string, error) {
15161562
buf := new(bytes.Buffer)
15171563
enc := json.NewEncoder(buf)
@@ -2520,6 +2566,7 @@ var funcBuiltins = buildBuiltinMap([]builtin{
25202566
&unaryBuiltin{name: "parseInt", function: builtinParseInt, params: ast.Identifiers{"str"}},
25212567
&unaryBuiltin{name: "parseJson", function: builtinParseJSON, params: ast.Identifiers{"str"}},
25222568
&unaryBuiltin{name: "parseYaml", function: builtinParseYAML, params: ast.Identifiers{"str"}},
2569+
&unaryBuiltin{name: "parseXmlJsonml", function: builtinParseXmlJsonml, params: ast.Identifiers{"str"}},
25232570
&generalBuiltin{name: "manifestJsonEx", function: builtinManifestJSONEx, params: []generalBuiltinParameter{{name: "value"}, {name: "indent"},
25242571
{name: "newline", defaultValue: &valueFlatString{value: []rune("\n")}},
25252572
{name: "key_val_sep", defaultValue: &valueFlatString{value: []rune(": ")}}}},

jsonml.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
Copyright 2019 Google Inc. All rights reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package jsonnet
18+
19+
import (
20+
"encoding/xml"
21+
"errors"
22+
"fmt"
23+
"io"
24+
"strings"
25+
)
26+
27+
type stack struct {
28+
elements []interface{}
29+
}
30+
31+
func (s *stack) Push(e interface{}) {
32+
s.elements = append(s.elements, e)
33+
}
34+
35+
func (s *stack) Pop() (interface{}, error) {
36+
if len(s.elements) == 0 {
37+
return nil, errors.New("cannot pop from empty stack")
38+
}
39+
l := len(s.elements)
40+
e := s.elements[l-1]
41+
s.elements = s.elements[:l-1]
42+
return e, nil
43+
}
44+
45+
func (s *stack) Size() int {
46+
return len(s.elements)
47+
}
48+
49+
type jsonMLBuilder struct {
50+
stack *stack
51+
currDepth int
52+
}
53+
54+
// BuildJsonmlFromString returns a jsomML form of given xml string.
55+
func BuildJsonmlFromString(s string) ([]interface{}, error) {
56+
b := newBuilder()
57+
d := xml.NewDecoder(strings.NewReader(s))
58+
59+
for {
60+
token, err := d.Token()
61+
if err == io.EOF {
62+
break
63+
}
64+
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
if err := b.addToken(token); err != nil {
70+
return nil, err
71+
}
72+
}
73+
74+
if b.stack.Size() == 0 {
75+
// No nodes has been identified
76+
return nil, fmt.Errorf("%s is not a valid XML", s)
77+
}
78+
79+
return b.build(), nil
80+
}
81+
82+
func newBuilder() *jsonMLBuilder {
83+
return &jsonMLBuilder{
84+
stack: &stack{},
85+
}
86+
}
87+
88+
func (b *jsonMLBuilder) addToken(token xml.Token) error {
89+
switch token.(type) {
90+
case xml.StartElement:
91+
// check for multiple roots
92+
if b.currDepth == 0 && b.stack.Size() > 0 {
93+
// There are multiple root elements
94+
return errors.New("XML cannot have multiple root elements")
95+
}
96+
97+
t := token.(xml.StartElement)
98+
node := []interface{}{t.Name.Local}
99+
// Add Attributes
100+
if len(t.Attr) > 0 {
101+
attr := make(map[string]interface{})
102+
for _, a := range t.Attr {
103+
attr[a.Name.Local] = a.Value
104+
}
105+
node = append(node, attr)
106+
}
107+
b.stack.Push(node)
108+
b.currDepth++
109+
case xml.CharData:
110+
t := token.(xml.CharData)
111+
s := strings.TrimSpace(string(t))
112+
if len(s) > 0 {
113+
// Skip whitespace only string
114+
b.appendToLastNode(string(t))
115+
}
116+
case xml.EndElement:
117+
b.squashLastNode()
118+
b.currDepth--
119+
}
120+
121+
return nil
122+
}
123+
124+
func (b *jsonMLBuilder) build() []interface{} {
125+
root, _ := b.stack.Pop()
126+
return root.([]interface{})
127+
}
128+
129+
func (b *jsonMLBuilder) appendToLastNode(e interface{}) {
130+
if b.stack.Size() == 0 {
131+
return
132+
}
133+
node, _ := b.stack.Pop()
134+
n := node.([]interface{})
135+
n = append(n, e)
136+
b.stack.Push(n)
137+
}
138+
139+
func (b *jsonMLBuilder) squashLastNode() {
140+
if b.stack.Size() < 2 {
141+
return
142+
}
143+
n, _ := b.stack.Pop()
144+
b.appendToLastNode(n)
145+
}

linter/internal/types/stdlib.go

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,13 +106,14 @@ func prepareStdlib(g *typeGraph) {
106106

107107
// Parsing
108108

109-
"parseInt": g.newSimpleFuncType(numberType, "str"),
110-
"parseOctal": g.newSimpleFuncType(numberType, "str"),
111-
"parseHex": g.newSimpleFuncType(numberType, "str"),
112-
"parseJson": g.newSimpleFuncType(jsonType, "str"),
113-
"parseYaml": g.newSimpleFuncType(jsonType, "str"),
114-
"encodeUTF8": g.newSimpleFuncType(numberArrayType, "str"),
115-
"decodeUTF8": g.newSimpleFuncType(stringType, "arr"),
109+
"parseInt": g.newSimpleFuncType(numberType, "str"),
110+
"parseOctal": g.newSimpleFuncType(numberType, "str"),
111+
"parseHex": g.newSimpleFuncType(numberType, "str"),
112+
"parseJson": g.newSimpleFuncType(jsonType, "str"),
113+
"parseYaml": g.newSimpleFuncType(jsonType, "str"),
114+
"parseXmlJsonml": g.newSimpleFuncType(jsonType, "str"),
115+
"encodeUTF8": g.newSimpleFuncType(numberArrayType, "str"),
116+
"decodeUTF8": g.newSimpleFuncType(stringType, "arr"),
116117

117118
// Manifestation
118119

@@ -152,7 +153,7 @@ func prepareStdlib(g *typeGraph) {
152153
"minArray": g.newFuncType(anyArrayType, []ast.Parameter{required("arr"), optional("keyF")}),
153154
"maxArray": g.newFuncType(anyArrayType, []ast.Parameter{required("arr"), optional("keyF")}),
154155
"contains": g.newSimpleFuncType(boolType, "arr", "elem"),
155-
"avg": g.newSimpleFuncType(numberType, "arr"),
156+
"avg": g.newSimpleFuncType(numberType, "arr"),
156157
"all": g.newSimpleFuncType(boolArrayType, "arr"),
157158
"any": g.newSimpleFuncType(boolArrayType, "arr"),
158159
"remove": g.newSimpleFuncType(anyArrayType, "arr", "elem"),

testdata/builtinParseXmlJsonml.golden

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[
2+
"svg",
3+
{
4+
"height": "100",
5+
"width": "100"
6+
},
7+
[
8+
"circle",
9+
{
10+
"cx": "50",
11+
"cy": "50",
12+
"fill": "red",
13+
"r": "40",
14+
"stroke": "black",
15+
"stroke-width": "3"
16+
}
17+
]
18+
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
std.parseXmlJsonml('<svg height="100" width="100"><circle cx="50" cy="50" fill="red" r="40" stroke="black" stroke-width="3"></circle></svg>')

testdata/builtinParseXmlJsonml.linter.golden

Whitespace-only changes.

0 commit comments

Comments
 (0)