Skip to content

Commit 5cf731c

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

17 files changed

+201
-7
lines changed

builtins.go

+167
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,170 @@ func builtinParseYAML(i *interpreter, str value) (value, error) {
14251426
return jsonToValue(i, elems[0])
14261427
}
14271428

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

linter/internal/types/stdlib.go

+9-7
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,14 @@ func prepareStdlib(g *typeGraph) {
9898

9999
// Parsing
100100

101-
"parseInt": g.newSimpleFuncType(numberType, "str"),
102-
"parseOctal": g.newSimpleFuncType(numberType, "str"),
103-
"parseHex": g.newSimpleFuncType(numberType, "str"),
104-
"parseJson": g.newSimpleFuncType(jsonType, "str"),
105-
"parseYaml": g.newSimpleFuncType(jsonType, "str"),
106-
"encodeUTF8": g.newSimpleFuncType(numberArrayType, "str"),
107-
"decodeUTF8": g.newSimpleFuncType(stringType, "arr"),
101+
"parseInt": g.newSimpleFuncType(numberType, "str"),
102+
"parseOctal": g.newSimpleFuncType(numberType, "str"),
103+
"parseHex": g.newSimpleFuncType(numberType, "str"),
104+
"parseJson": g.newSimpleFuncType(jsonType, "str"),
105+
"parseYaml": g.newSimpleFuncType(jsonType, "str"),
106+
"parseCsvWithHeader": g.newFuncType(jsonType, []ast.Parameter{required("str"), optional("delimiter")}),
107+
"encodeUTF8": g.newSimpleFuncType(numberArrayType, "str"),
108+
"decodeUTF8": g.newSimpleFuncType(stringType, "arr"),
108109

109110
// Manifestation
110111

@@ -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.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
{
3+
"head1": "val1",
4+
"head2": "val2"
5+
}
6+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
std.parseCsvWithHeader("head1,head2\nval1,val2")

testdata/builtinParseCsvWithHeader.linter.golden

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
{
3+
"head1": "val1",
4+
"head1__1": "val2"
5+
}
6+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
std.parseCsvWithHeader("head1,head1\nval1,val2")

testdata/builtinParseCsvWithHeader2.linter.golden

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
{
3+
"head1": "val1",
4+
"head2": "val2"
5+
}
6+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
std.parseCsvWithHeader("head1;head2\nval1;val2", ";")

testdata/builtinParseCsvWithHeader3.linter.golden

Whitespace-only changes.

0 commit comments

Comments
 (0)