Skip to content

Commit 1dc9fac

Browse files
committed
feat: Add std.parseCsv and std.manifestCsv
1 parent 868d9c6 commit 1dc9fac

11 files changed

+153
-0
lines changed

builtins.go

+140
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"bytes"
2121
"crypto/md5"
2222
"encoding/base64"
23+
"encoding/csv"
2324
"encoding/hex"
2425
"encoding/json"
2526
"fmt"
@@ -1425,6 +1426,143 @@ func builtinParseYAML(i *interpreter, str value) (value, error) {
14251426
return jsonToValue(i, elems[0])
14261427
}
14271428

1429+
func builtinParseCSV(i *interpreter, str value) (value, error) {
1430+
sval, err := i.getString(str)
1431+
if err != nil {
1432+
return nil, err
1433+
}
1434+
s := sval.getGoString()
1435+
1436+
json := make([]interface{}, 0)
1437+
var keys []string
1438+
1439+
reader := csv.NewReader(strings.NewReader(s))
1440+
for row := 0; ; row++ {
1441+
record, err := reader.Read()
1442+
if err == io.EOF {
1443+
break
1444+
}
1445+
if err != nil {
1446+
return nil, i.Error(fmt.Sprintf("failed to parse JSON: %s", err.Error()))
1447+
}
1448+
1449+
if row == 0 { // consider first row as header
1450+
keys = record
1451+
} else {
1452+
j := make(map[string]interface{})
1453+
for i, k := range keys {
1454+
j[k] = record[i]
1455+
}
1456+
json = append(json, j)
1457+
}
1458+
}
1459+
return jsonToValue(i, json)
1460+
}
1461+
1462+
func builtinManifestCsv(i *interpreter, arguments []value) (value, error) {
1463+
arrv := arguments[0]
1464+
hv := arguments[1]
1465+
1466+
arr, err := i.getArray(arrv)
1467+
if err != nil {
1468+
return nil, err
1469+
}
1470+
1471+
var headers []string
1472+
if hv.getType() == nullType {
1473+
if len(arr.elements) == 0 { // no elements to select headers
1474+
return makeValueString(""), nil
1475+
}
1476+
1477+
// default to all headers
1478+
obj, err := i.evaluateObject(arr.elements[0])
1479+
if err != nil {
1480+
return nil, err
1481+
}
1482+
1483+
simpleObj := obj.uncached.(*simpleObject)
1484+
for fieldName := range simpleObj.fields {
1485+
headers = append(headers, fieldName)
1486+
}
1487+
} else {
1488+
// headers are provided
1489+
ha, err := i.getArray(hv)
1490+
if err != nil {
1491+
return nil, err
1492+
}
1493+
1494+
for _, elem := range ha.elements {
1495+
header, err := i.evaluateString(elem)
1496+
if err != nil {
1497+
return nil, err
1498+
}
1499+
headers = append(headers, header.getGoString())
1500+
}
1501+
}
1502+
1503+
var buf bytes.Buffer
1504+
w := csv.NewWriter(&buf)
1505+
1506+
// Write headers
1507+
w.Write(headers)
1508+
1509+
// Write rest of the rows
1510+
for _, elem := range arr.elements {
1511+
obj, err := i.evaluateObject(elem)
1512+
if err != nil {
1513+
return nil, err
1514+
}
1515+
1516+
record := make([]string, len(headers))
1517+
for c, h := range headers {
1518+
val, err := obj.index(i, h)
1519+
if err != nil { // no corresponding column
1520+
// skip to next column
1521+
continue
1522+
}
1523+
1524+
s, err := stringFromValue(i, val)
1525+
if err != nil {
1526+
return nil, err
1527+
}
1528+
record[c] = s
1529+
}
1530+
w.Write(record)
1531+
}
1532+
1533+
w.Flush()
1534+
1535+
return makeValueString(buf.String()), nil
1536+
}
1537+
1538+
func stringFromValue(i *interpreter, v value) (string, error) {
1539+
switch v.getType() {
1540+
case stringType:
1541+
s, err := i.getString(v)
1542+
if err != nil {
1543+
return "", err
1544+
}
1545+
return s.getGoString(), nil
1546+
case numberType:
1547+
n, err := i.getNumber(v)
1548+
if err != nil {
1549+
return "", err
1550+
}
1551+
return fmt.Sprint(n.value), nil
1552+
case booleanType:
1553+
b, err := i.getBoolean(v)
1554+
if err != nil {
1555+
return "", err
1556+
}
1557+
return fmt.Sprint(b.value), nil
1558+
case nullType:
1559+
return "", nil
1560+
default:
1561+
// for functionType, objectType and arrayType
1562+
return "", i.Error("invalid string conversion")
1563+
}
1564+
}
1565+
14281566
func jsonEncode(v interface{}) (string, error) {
14291567
buf := new(bytes.Buffer)
14301568
enc := json.NewEncoder(buf)
@@ -2290,6 +2428,8 @@ var funcBuiltins = buildBuiltinMap([]builtin{
22902428
&unaryBuiltin{name: "parseInt", function: builtinParseInt, params: ast.Identifiers{"str"}},
22912429
&unaryBuiltin{name: "parseJson", function: builtinParseJSON, params: ast.Identifiers{"str"}},
22922430
&unaryBuiltin{name: "parseYaml", function: builtinParseYAML, params: ast.Identifiers{"str"}},
2431+
&unaryBuiltin{name: "parseCsv", function: builtinParseCSV, params: ast.Identifiers{"str"}},
2432+
&generalBuiltin{name: "manifestCsv", function: builtinManifestCsv, params: []generalBuiltinParameter{{name: "json"}, {name: "headers", defaultValue: &nullValue}}},
22932433
&generalBuiltin{name: "manifestJsonEx", function: builtinManifestJSONEx, params: []generalBuiltinParameter{{name: "value"}, {name: "indent"},
22942434
{name: "newline", defaultValue: &valueFlatString{value: []rune("\n")}},
22952435
{name: "key_val_sep", defaultValue: &valueFlatString{value: []rune(": ")}}}},

linter/internal/types/stdlib.go

+2
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ func prepareStdlib(g *typeGraph) {
103103
"parseHex": g.newSimpleFuncType(numberType, "str"),
104104
"parseJson": g.newSimpleFuncType(jsonType, "str"),
105105
"parseYaml": g.newSimpleFuncType(jsonType, "str"),
106+
"parseCsv": g.newSimpleFuncType(jsonType, "str"),
106107
"encodeUTF8": g.newSimpleFuncType(numberArrayType, "str"),
107108
"decodeUTF8": g.newSimpleFuncType(stringType, "arr"),
108109

@@ -116,6 +117,7 @@ func prepareStdlib(g *typeGraph) {
116117
"manifestJsonMinified": g.newSimpleFuncType(stringType, "value"),
117118
"manifestYamlDoc": g.newSimpleFuncType(stringType, "value"),
118119
"manifestYamlStream": g.newSimpleFuncType(stringType, "value"),
120+
"manifestCsv": g.newFuncType(stringType, []ast.Parameter{required("json"), optional("headers")}),
119121
"manifestXmlJsonml": g.newSimpleFuncType(stringType, "value"),
120122

121123
// Arrays

testdata/builtinManifestCsv.golden

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"head1,head2\nval1,val2\n,1\nval3,\n"

testdata/builtinManifestCsv.jsonnet

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
std.manifestCsv([{ "head1": "val1", "head2": "val2", "head3": "foo" }, { "head2": 1, "head3": "bar" }, { "head1": "val3" }], ["head1", "head2"])

testdata/builtinManifestCsv.linter.golden

Whitespace-only changes.

testdata/builtinManifestCsv2.golden

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"head1\nval1\nval2\n"

testdata/builtinManifestCsv2.jsonnet

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
std.manifestCsv([{ "head1": "val1" }, { "head1": "val2" }])

testdata/builtinManifestCsv2.linter.golden

Whitespace-only changes.

testdata/builtinParseCsv.golden

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
{
3+
"head1": "val1",
4+
"head2": "val2"
5+
}
6+
]

testdata/builtinParseCsv.jsonnet

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
std.parseCsv("head1,head2\nval1,val2")

testdata/builtinParseCsv.linter.golden

Whitespace-only changes.

0 commit comments

Comments
 (0)