Skip to content

Commit 1cdfd0d

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

11 files changed

+158
-0
lines changed

builtins.go

+143
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,146 @@ 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(map[string]interface{})
1437+
var keys []string
1438+
reader := csv.NewReader(strings.NewReader(s))
1439+
for {
1440+
record, err := reader.Read()
1441+
if err == io.EOF {
1442+
break
1443+
}
1444+
if err != nil {
1445+
return nil, i.Error(fmt.Sprintf("failed to parse JSON: %s", err.Error()))
1446+
}
1447+
1448+
if len(keys) == 0 {
1449+
keys = record
1450+
for _, r := range record {
1451+
json[r] = []interface{}{}
1452+
}
1453+
} else {
1454+
for i, r := range record {
1455+
k := keys[i]
1456+
v := json[k].([]interface{})
1457+
json[k] = append(v, r)
1458+
}
1459+
}
1460+
}
1461+
return jsonToValue(i, json)
1462+
}
1463+
1464+
func builtinManifestCsv(i *interpreter, arguments []value) (value, error) {
1465+
objv := arguments[0]
1466+
hv := arguments[1]
1467+
1468+
obj, err := i.getObject(objv)
1469+
if err != nil {
1470+
return nil, err
1471+
}
1472+
1473+
var headers []string
1474+
if hv.getType() == nullType {
1475+
// default to all headers
1476+
simpleObj := obj.uncached.(*simpleObject)
1477+
for fieldName := range simpleObj.fields {
1478+
headers = append(headers, fieldName)
1479+
}
1480+
} else {
1481+
// headers are provided
1482+
ha, err := i.getArray(hv)
1483+
if err != nil {
1484+
return nil, err
1485+
}
1486+
1487+
for _, elem := range ha.elements {
1488+
header, err := i.evaluateString(elem)
1489+
if err != nil {
1490+
return nil, err
1491+
}
1492+
headers = append(headers, header.getGoString())
1493+
}
1494+
}
1495+
1496+
var buf bytes.Buffer
1497+
w := csv.NewWriter(&buf)
1498+
w.Write(headers)
1499+
1500+
for r := 0; ; r++ {
1501+
record := make([]string, len(headers))
1502+
elems := 0
1503+
for c, h := range headers {
1504+
arrv, err := obj.index(i, h)
1505+
if err != nil { // no corresponding column
1506+
// skip to next column
1507+
continue
1508+
}
1509+
1510+
v, err := i.getArray(arrv)
1511+
if err != nil {
1512+
return nil, i.Error("invalid JSON: not a valid json for CSV")
1513+
}
1514+
1515+
if r >= len(v.elements) { // if less elements in record
1516+
continue
1517+
}
1518+
val, err := v.elements[r].getValue(i)
1519+
if err != nil {
1520+
return nil, err
1521+
}
1522+
1523+
s, err := stringFromValue(i, val)
1524+
if err != nil {
1525+
return nil, err
1526+
}
1527+
record[c] = s
1528+
elems++
1529+
}
1530+
if elems == 0 { // No elements in record
1531+
break
1532+
}
1533+
w.Write(record)
1534+
}
1535+
1536+
w.Flush()
1537+
1538+
return makeValueString(buf.String()), nil
1539+
}
1540+
1541+
func stringFromValue(i *interpreter, v value) (string, error) {
1542+
switch v.getType() {
1543+
case stringType:
1544+
s, err := i.getString(v)
1545+
if err != nil {
1546+
return "", err
1547+
}
1548+
return s.getGoString(), nil
1549+
case numberType:
1550+
n, err := i.getNumber(v)
1551+
if err != nil {
1552+
return "", err
1553+
}
1554+
return fmt.Sprint(n.value), nil
1555+
case booleanType:
1556+
b, err := i.getBoolean(v)
1557+
if err != nil {
1558+
return "", err
1559+
}
1560+
return fmt.Sprint(b.value), nil
1561+
case nullType:
1562+
return "", nil
1563+
default:
1564+
// for functionType, objectType and arrayType
1565+
return "", i.Error("invalid string conversion")
1566+
}
1567+
}
1568+
14281569
func jsonEncode(v interface{}) (string, error) {
14291570
buf := new(bytes.Buffer)
14301571
enc := json.NewEncoder(buf)
@@ -2290,6 +2431,8 @@ var funcBuiltins = buildBuiltinMap([]builtin{
22902431
&unaryBuiltin{name: "parseInt", function: builtinParseInt, params: ast.Identifiers{"str"}},
22912432
&unaryBuiltin{name: "parseJson", function: builtinParseJSON, params: ast.Identifiers{"str"}},
22922433
&unaryBuiltin{name: "parseYaml", function: builtinParseYAML, params: ast.Identifiers{"str"}},
2434+
&unaryBuiltin{name: "parseCsv", function: builtinParseCSV, params: ast.Identifiers{"str"}},
2435+
&generalBuiltin{name: "manifestCsv", function: builtinManifestCsv, params: []generalBuiltinParameter{{name: "obj"}, {name: "arr", defaultValue: &nullValue}}},
22932436
&generalBuiltin{name: "manifestJsonEx", function: builtinManifestJSONEx, params: []generalBuiltinParameter{{name: "value"}, {name: "indent"},
22942437
{name: "newline", defaultValue: &valueFlatString{value: []rune("\n")}},
22952438
{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("obj"), optional("arr")}),
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", "", "val3"], "head2": ["val2", 1], "head3": ["foo", "bar"] }, ["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", "val2"] })

testdata/builtinManifestCsv2.linter.golden

Whitespace-only changes.

testdata/builtinParseCsv.golden

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

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)