Skip to content

Commit

Permalink
feat: Add std.parseCsv and std.manifestCsv
Browse files Browse the repository at this point in the history
  • Loading branch information
rohitjangid committed May 25, 2023
1 parent 868d9c6 commit d520a66
Show file tree
Hide file tree
Showing 11 changed files with 158 additions and 0 deletions.
143 changes: 143 additions & 0 deletions builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"bytes"
"crypto/md5"
"encoding/base64"
"encoding/csv"
"encoding/hex"
"encoding/json"
"fmt"
Expand Down Expand Up @@ -1425,6 +1426,146 @@ func builtinParseYAML(i *interpreter, str value) (value, error) {
return jsonToValue(i, elems[0])
}

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

json := make(map[string]interface{})
var keys []string
reader := csv.NewReader(strings.NewReader(s))
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
return nil, i.Error(fmt.Sprintf("failed to parse JSON: %s", err.Error()))
}

if len(keys) == 0 {
keys = record
for _, r := range record {
json[r] = []interface{}{}
}
} else {
for i, r := range record {
k := keys[i]
v := json[k].([]interface{})
json[k] = append(v, r)
}
}
}
return jsonToValue(i, json)
}

func builtinManifestCsv(i *interpreter, arguments []value) (value, error) {
objv := arguments[0]
hv := arguments[1]

obj, err := i.getObject(objv)
if err != nil {
return nil, err
}

var headers []string
if hv.getType() == nullType {
// default to all headers
simpleObj := obj.uncached.(*simpleObject)
for fieldName := range simpleObj.fields {
headers = append(headers, fieldName)
}
} else {
// headers are provided
ha, err := i.getArray(hv)
if err != nil {
return nil, err
}

for _, elem := range ha.elements {
header, err := i.evaluateString(elem)
if err != nil {
return nil, err
}
headers = append(headers, header.getGoString())
}
}

var buf bytes.Buffer
w := csv.NewWriter(&buf)
w.Write(headers)

for r := 0; ; r++ {
record := make([]string, len(headers))
elems := 0
for c, h := range headers {
arrv, err := obj.index(i, h)
if err != nil { // no corresponding column
// skip to next column
continue
}

v, err := i.getArray(arrv)
if err != nil {
return nil, i.Error("invalid JSON: not a valid json for CSV")
}

if r >= len(v.elements) { // if less elements in record
continue
}
val, err := v.elements[r].getValue(i)
if err != nil {
return nil, err
}

s, err := stringFromValue(i, val)
if err != nil {
return nil, err
}
record[c] = s
elems++
}
if elems == 0 { // No elements in record
break
}
w.Write(record)
}

w.Flush()

return makeValueString(buf.String()), nil
}

func stringFromValue(i *interpreter, v value) (string, error) {
switch v.getType() {
case stringType:
s, err := i.getString(v)
if err != nil {
return "", err
}
return s.getGoString(), nil
case numberType:
n, err := i.getNumber(v)
if err != nil {
return "", err
}
return fmt.Sprint(n.value), nil
case booleanType:
b, err := i.getBoolean(v)
if err != nil {
return "", err
}
return fmt.Sprint(b.value), nil
case nullType:
return "", nil
default:
// for functionType, objectType and arrayType
return "", i.Error("invalid string conversion")
}
}

func jsonEncode(v interface{}) (string, error) {
buf := new(bytes.Buffer)
enc := json.NewEncoder(buf)
Expand Down Expand Up @@ -2290,6 +2431,8 @@ 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: "parseCsv", function: builtinParseCSV, params: ast.Identifiers{"str"}},
&generalBuiltin{name: "manifestCsv", function: builtinManifestCsv, params: []generalBuiltinParameter{{name: "obj"}, {name: "arr", defaultValue: &nullValue}}},
&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
2 changes: 2 additions & 0 deletions linter/internal/types/stdlib.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ func prepareStdlib(g *typeGraph) {
"parseHex": g.newSimpleFuncType(numberType, "str"),
"parseJson": g.newSimpleFuncType(jsonType, "str"),
"parseYaml": g.newSimpleFuncType(jsonType, "str"),
"parseCsv": g.newSimpleFuncType(jsonType, "str"),
"encodeUTF8": g.newSimpleFuncType(numberArrayType, "str"),
"decodeUTF8": g.newSimpleFuncType(stringType, "arr"),

Expand All @@ -116,6 +117,7 @@ func prepareStdlib(g *typeGraph) {
"manifestJsonMinified": g.newSimpleFuncType(stringType, "value"),
"manifestYamlDoc": g.newSimpleFuncType(stringType, "value"),
"manifestYamlStream": g.newSimpleFuncType(stringType, "value"),
"manifestCsv": g.newSimpleFuncType(stringType, "obj", "arr"),
"manifestXmlJsonml": g.newSimpleFuncType(stringType, "value"),

// Arrays
Expand Down
1 change: 1 addition & 0 deletions testdata/builtinManifestCsv.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"head1,head2\nval1,val2\n,1\nval3,\n"
1 change: 1 addition & 0 deletions testdata/builtinManifestCsv.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
std.manifestCsv({ "head1": ["val1", "", "val3"], "head2": ["val2", 1], "head3": ["foo", "bar"] }, ["head1", "head2"])
Empty file.
1 change: 1 addition & 0 deletions testdata/builtinManifestCsv2.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"head1\nval1\nval2\n"
1 change: 1 addition & 0 deletions testdata/builtinManifestCsv2.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
std.manifestCsv({ "head1": ["val1", "val2"] })
Empty file.
8 changes: 8 additions & 0 deletions testdata/builtinParseCsv.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"head1": [
"val1"
],
"head2": [
"val2"
]
}
1 change: 1 addition & 0 deletions testdata/builtinParseCsv.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
std.parseCsv("head1,head2\nval1,val2")
Empty file.

0 comments on commit d520a66

Please sign in to comment.