Skip to content

Commit

Permalink
feat: Implement std.parseXmlJsonml
Browse files Browse the repository at this point in the history
  • Loading branch information
rohitjangid committed May 31, 2023
1 parent 44538a3 commit 3af9c4c
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 11 deletions.
1 change: 1 addition & 0 deletions BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ go_library(
"error_formatter.go",
"imports.go",
"interpreter.go",
"jsonml.go",
"runtime_error.go",
"thunks.go",
"util.go",
Expand Down
47 changes: 47 additions & 0 deletions builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,52 @@ func builtinParseYAML(i *interpreter, str value) (value, error) {
return jsonToValue(i, elems[0])
}

func builtinParseXmlJsonml(i *interpreter, str value) (value, error) {
sval, err := i.getString(str)
if err != nil {
return nil, err
}
s := sval.getGoString()

json, err := BuildJsonmlFromString(s)
if err != nil {
return nil, i.Error(fmt.Sprintf("failed to parse XML: %v", err.Error()))
}

arr, err := arrayToValue(i, json)
if err != nil {
return nil, err
}
return arr, nil
}

func arrayToValue(i *interpreter, json []interface{}) (*valueArray, error) {
var elements []*cachedThunk
var err error
for _, e := range json {
var val value
switch e := e.(type) {
case string:
val = makeValueString(e)
case map[string]interface{}:
val, err = jsonToValue(i, e)
if err != nil {
return nil, err
}
case []interface{}:
val, err = arrayToValue(i, e)
if err != nil {
return nil, err
}
default:
return nil, i.Error(fmt.Sprintf("invalid type for section: %v", reflect.TypeOf(e)))
}
elements = append(elements, readyThunk(val))
}

return makeValueArray(elements), nil
}

func jsonEncode(v interface{}) (string, error) {
buf := new(bytes.Buffer)
enc := json.NewEncoder(buf)
Expand Down Expand Up @@ -2412,6 +2458,7 @@ var funcBuiltins = buildBuiltinMap([]builtin{
&unaryBuiltin{name: "parseInt", function: builtinParseInt, params: ast.Identifiers{"str"}},
&unaryBuiltin{name: "parseJson", function: builtinParseJSON, params: ast.Identifiers{"str"}},
&unaryBuiltin{name: "parseYaml", function: builtinParseYAML, params: ast.Identifiers{"str"}},
&unaryBuiltin{name: "parseXmlJsonml", function: builtinParseXmlJsonml, params: ast.Identifiers{"str"}},
&generalBuiltin{name: "manifestJsonEx", function: builtinManifestJSONEx, params: []generalBuiltinParameter{{name: "value"}, {name: "indent"},
{name: "newline", defaultValue: &valueFlatString{value: []rune("\n")}},
{name: "key_val_sep", defaultValue: &valueFlatString{value: []rune(": ")}}}},
Expand Down
141 changes: 141 additions & 0 deletions jsonml.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
Copyright 2019 Google Inc. All rights reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package jsonnet

import (
"encoding/xml"
"errors"
"fmt"
"io"
"strings"
)

type stack struct {
elements []interface{}
}

func (s *stack) Push(e interface{}) {
s.elements = append(s.elements, e)
}

func (s *stack) Pop() (interface{}, error) {
if len(s.elements) == 0 {
return nil, errors.New("cannot pop from empty stack")
}
l := len(s.elements)
e := s.elements[l-1]
s.elements = s.elements[:l-1]
return e, nil
}

func (s *stack) Size() int {
return len(s.elements)
}

type jsonMLBuilder struct {
stack *stack
currDepth int
}

// BuildJsonmlFromString returns a jsomML form of given xml string.
func BuildJsonmlFromString(s string) ([]interface{}, error) {
b := newBuilder()
d := xml.NewDecoder(strings.NewReader(s))

for {
token, err := d.Token()
if err == io.EOF {
break
}

if err != nil {
return nil, err
}

if err := b.addToken(token); err != nil {
return nil, err
}
}

if b.stack.Size() == 0 {
// No nodes has been identified
return nil, fmt.Errorf("%s is not a valid XML", s)
}

return b.build(), nil
}

func newBuilder() *jsonMLBuilder {
return &jsonMLBuilder{
stack: &stack{},
}
}

func (b *jsonMLBuilder) addToken(token xml.Token) error {
switch token.(type) {
case xml.StartElement:
// check for multiple roots
if b.currDepth == 0 && b.stack.Size() > 0 {
// There are multiple root elements
return errors.New("XML cannot have multiple root elements")
}

t := token.(xml.StartElement)
node := []interface{}{t.Name.Local}
// Add Attributes
if len(t.Attr) > 0 {
attr := make(map[string]interface{})
for _, a := range t.Attr {
attr[a.Name.Local] = a.Value
}
node = append(node, attr)
}
b.stack.Push(node)
b.currDepth++
case xml.CharData:
t := token.(xml.CharData)
b.appendToLastNode(string(t))
case xml.EndElement:
b.squashLastNode()
b.currDepth--
}

return nil
}

func (b *jsonMLBuilder) build() []interface{} {
root, _ := b.stack.Pop()
return root.([]interface{})
}

func (b *jsonMLBuilder) appendToLastNode(e interface{}) {
if b.stack.Size() == 0 {
return
}
node, _ := b.stack.Pop()
n := node.([]interface{})
n = append(n, e)
b.stack.Push(n)
}

func (b *jsonMLBuilder) squashLastNode() {
if b.stack.Size() < 2 {
return
}
n, _ := b.stack.Pop()
b.appendToLastNode(n)
}
23 changes: 12 additions & 11 deletions linter/internal/types/stdlib.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,14 @@ func prepareStdlib(g *typeGraph) {

// Parsing

"parseInt": g.newSimpleFuncType(numberType, "str"),
"parseOctal": g.newSimpleFuncType(numberType, "str"),
"parseHex": g.newSimpleFuncType(numberType, "str"),
"parseJson": g.newSimpleFuncType(jsonType, "str"),
"parseYaml": g.newSimpleFuncType(jsonType, "str"),
"encodeUTF8": g.newSimpleFuncType(numberArrayType, "str"),
"decodeUTF8": g.newSimpleFuncType(stringType, "arr"),
"parseInt": g.newSimpleFuncType(numberType, "str"),
"parseOctal": g.newSimpleFuncType(numberType, "str"),
"parseHex": g.newSimpleFuncType(numberType, "str"),
"parseJson": g.newSimpleFuncType(jsonType, "str"),
"parseYaml": g.newSimpleFuncType(jsonType, "str"),
"parseXmlJsonml": g.newSimpleFuncType(jsonType, "str"),
"encodeUTF8": g.newSimpleFuncType(numberArrayType, "str"),
"decodeUTF8": g.newSimpleFuncType(stringType, "arr"),

// Manifestation

Expand Down Expand Up @@ -145,10 +146,10 @@ func prepareStdlib(g *typeGraph) {
"maxArray": g.newFuncType(anyArrayType, []ast.Parameter{required("arr"), optional("keyF")}),
"contains": g.newSimpleFuncType(boolType, "arr", "elem"),
// TODO these need test cases written by someone who understands how to make them
"all": g.newSimpleFuncType(boolArrayType, "arr"),
"any": g.newSimpleFuncType(boolArrayType, "arr"),
"remove": g.newSimpleFuncType(anyArrayType, "arr", "elem"),
"removeAt": g.newSimpleFuncType(anyArrayType, "arr", "i"),
"all": g.newSimpleFuncType(boolArrayType, "arr"),
"any": g.newSimpleFuncType(boolArrayType, "arr"),
"remove": g.newSimpleFuncType(anyArrayType, "arr", "elem"),
"removeAt": g.newSimpleFuncType(anyArrayType, "arr", "i"),

// Sets

Expand Down
18 changes: 18 additions & 0 deletions testdata/builtinParseXmlJsonml.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
"svg",
{
"height": "100",
"width": "100"
},
[
"circle",
{
"cx": "50",
"cy": "50",
"fill": "red",
"r": "40",
"stroke": "black",
"stroke-width": "3"
}
]
]
1 change: 1 addition & 0 deletions testdata/builtinParseXmlJsonml.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
std.parseXmlJsonml('<svg height="100" width="100"><circle cx="50" cy="50" fill="red" r="40" stroke="black" stroke-width="3"></circle></svg>')
Empty file.

0 comments on commit 3af9c4c

Please sign in to comment.