Skip to content

Commit dec1aa2

Browse files
authored
feat: Go implementation for manifestYamlDoc and escapeStringJson (#742)
* Builtins for escapeStringJson and manifestYamlDoc * Benchmark and tests
1 parent fa70aa4 commit dec1aa2

10 files changed

+453
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
std.escapeStringJson("Lorem ipsum dolor sit amet, consectetur \"adipiscing\" elit. Nullam \\nec sagittis \\u0065lit, sed do \\teiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco \flaboris nisi ut aliquip ex ea commodo consequat.\rDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n\tExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit \\anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. \\Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris.\nInteger in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst.")
+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{
2+
bar: {
3+
prometheusOperator+: {
4+
service+: {
5+
spec+: {
6+
ports: [
7+
{
8+
name: 'https',
9+
port: 8443,
10+
targetPort: 'https',
11+
},
12+
],
13+
},
14+
},
15+
serviceMonitor+: {
16+
spec+: {
17+
endpoints: [
18+
{
19+
port: 'https',
20+
scheme: 'https',
21+
honorLabels: true,
22+
bearerTokenFile: '/var/run/secrets/kubernetes.io/serviceaccount/token',
23+
tlsConfig: {
24+
insecureSkipVerify: true,
25+
},
26+
},
27+
],
28+
},
29+
},
30+
clusterRole+: {
31+
rules+: [
32+
{
33+
apiGroups: ['authentication.k8s.io'],
34+
resources: ['tokenreviews'],
35+
verbs: ['create'],
36+
},
37+
{
38+
apiGroups: ['authorization.k8s.io'],
39+
resources: ['subjectaccessreviews'],
40+
verbs: ['create'],
41+
},
42+
],
43+
},
44+
},
45+
additional+: {
46+
'$schema': "http://json-schema.org/draft-07/schema#",
47+
'09': ['no', 'yes'],
48+
},
49+
},
50+
nothing: std.manifestYamlDoc(self.bar, indent_array_in_object=true, quote_keys=true),
51+
}

builtins.go

+240-9
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"io"
3030
"math"
3131
"reflect"
32+
"regexp"
3233
"sort"
3334
"strconv"
3435
"strings"
@@ -233,17 +234,25 @@ func builtinLength(i *interpreter, x value) (value, error) {
233234
return makeValueNumber(float64(num)), nil
234235
}
235236

236-
func builtinToString(i *interpreter, x value) (value, error) {
237+
func valueToString(i *interpreter, x value) (string, error) {
237238
switch x := x.(type) {
238239
case valueString:
239-
return x, nil
240+
return x.getGoString(), nil
240241
}
242+
241243
var buf bytes.Buffer
242-
err := i.manifestAndSerializeJSON(&buf, x, false, "")
244+
if err := i.manifestAndSerializeJSON(&buf, x, false, ""); err != nil {
245+
return "", err
246+
}
247+
return buf.String(), nil
248+
}
249+
250+
func builtinToString(i *interpreter, x value) (value, error) {
251+
s, err := valueToString(i, x)
243252
if err != nil {
244253
return nil, err
245254
}
246-
return makeValueString(buf.String()), nil
255+
return makeValueString(s), nil
247256
}
248257

249258
func builtinTrace(i *interpreter, x value, y value) (value, error) {
@@ -1597,8 +1606,16 @@ func tomlIsSection(i *interpreter, val value) (bool, error) {
15971606
}
15981607
}
15991608

1600-
// tomlEncodeString encodes a string as quoted TOML string
1601-
func tomlEncodeString(s string) string {
1609+
func builtinEscapeStringJson(i *interpreter, v value) (value, error) {
1610+
s, err := valueToString(i, v)
1611+
if err != nil {
1612+
return nil, err
1613+
}
1614+
1615+
return makeValueString(unparseString(s)), nil
1616+
}
1617+
1618+
func escapeStringJson(s string) string {
16021619
res := "\""
16031620

16041621
for _, c := range s {
@@ -1630,6 +1647,11 @@ func tomlEncodeString(s string) string {
16301647
return res
16311648
}
16321649

1650+
// tomlEncodeString encodes a string as quoted TOML string
1651+
func tomlEncodeString(s string) string {
1652+
return unparseString(s)
1653+
}
1654+
16331655
// tomlEncodeKey encodes a key - returning same string if it does not need quoting,
16341656
// otherwise return it quoted; returns empty key as ”
16351657
func tomlEncodeKey(s string) string {
@@ -2021,6 +2043,209 @@ func builtinManifestJSONEx(i *interpreter, arguments []value) (value, error) {
20212043
return makeValueString(finalString), nil
20222044
}
20232045

2046+
const (
2047+
yamlIndent = " "
2048+
)
2049+
2050+
var (
2051+
yamlReserved = []string{
2052+
// Boolean types taken from https://yaml.org/type/bool.html
2053+
"true", "false", "yes", "no", "on", "off", "y", "n",
2054+
// Numerical words taken from https://yaml.org/type/float.html
2055+
".nan", "-.inf", "+.inf", ".inf", "null",
2056+
// Invalid keys that contain no invalid characters
2057+
"-", "---", "''",
2058+
}
2059+
yamlTimestampPattern = regexp.MustCompile(`^(?:[0-9]*-){2}[0-9]*$`)
2060+
yamlBinaryPattern = regexp.MustCompile(`^[-+]?0b[0-1_]+$`)
2061+
yamlHexPattern = regexp.MustCompile(`[-+]?0x[0-9a-fA-F_]+`)
2062+
)
2063+
2064+
func yamlReservedString(s string) bool {
2065+
for _, r := range yamlReserved {
2066+
if strings.EqualFold(s, r) {
2067+
return true
2068+
}
2069+
}
2070+
return false
2071+
}
2072+
2073+
func yamlBareSafe(s string) bool {
2074+
if len(s) == 0 {
2075+
return false
2076+
}
2077+
2078+
// String contains unsafe char
2079+
for _, c := range s {
2080+
isAlpha := (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
2081+
isDigit := c >= '0' && c <= '9'
2082+
2083+
if !isAlpha && !isDigit && c != '_' && c != '-' && c != '/' && c != '.' {
2084+
return false
2085+
}
2086+
}
2087+
2088+
if yamlReservedString(s) {
2089+
return false
2090+
}
2091+
2092+
if yamlTimestampPattern.MatchString(s) {
2093+
return false
2094+
}
2095+
2096+
// Check binary /
2097+
if yamlBinaryPattern.MatchString(s) || yamlHexPattern.MatchString(s) {
2098+
return false
2099+
}
2100+
2101+
// Is integer
2102+
if _, err := strconv.Atoi(s); err == nil {
2103+
return false
2104+
}
2105+
// Is float
2106+
if _, err := strconv.ParseFloat(s, 64); err == nil {
2107+
return false
2108+
}
2109+
2110+
return true
2111+
}
2112+
2113+
func builtinManifestYamlDoc(i *interpreter, arguments []value) (value, error) {
2114+
val := arguments[0]
2115+
vindentArrInObj, err := i.getBoolean(arguments[1])
2116+
if err != nil {
2117+
return nil, err
2118+
}
2119+
vQuoteKeys, err := i.getBoolean(arguments[2])
2120+
if err != nil {
2121+
return nil, err
2122+
}
2123+
2124+
var buf bytes.Buffer
2125+
2126+
var aux func(ov value, buf *bytes.Buffer, cindent string) error
2127+
aux = func(ov value, buf *bytes.Buffer, cindent string) error {
2128+
switch v := ov.(type) {
2129+
case *valueNull:
2130+
buf.WriteString("null")
2131+
case *valueBoolean:
2132+
if v.value {
2133+
buf.WriteString("true")
2134+
} else {
2135+
buf.WriteString("false")
2136+
}
2137+
case valueString:
2138+
s := v.getGoString()
2139+
if s == "" {
2140+
buf.WriteString(`""`)
2141+
} else if strings.HasSuffix(s, "\n") {
2142+
s := strings.TrimSuffix(s, "\n")
2143+
buf.WriteString("|")
2144+
for _, line := range strings.Split(s, "\n") {
2145+
buf.WriteByte('\n')
2146+
buf.WriteString(cindent)
2147+
buf.WriteString(yamlIndent)
2148+
buf.WriteString(line)
2149+
}
2150+
} else {
2151+
buf.WriteString(unparseString(s))
2152+
}
2153+
case *valueNumber:
2154+
buf.WriteString(strconv.FormatFloat(v.value, 'f', -1, 64))
2155+
case *valueArray:
2156+
if v.length() == 0 {
2157+
buf.WriteString("[]")
2158+
return nil
2159+
}
2160+
for ix, elem := range v.elements {
2161+
if ix != 0 {
2162+
buf.WriteByte('\n')
2163+
buf.WriteString(cindent)
2164+
}
2165+
thunkValue, err := elem.getValue(i)
2166+
if err != nil {
2167+
return err
2168+
}
2169+
buf.WriteByte('-')
2170+
2171+
if v, isArr := thunkValue.(*valueArray); isArr && v.length() > 0 {
2172+
buf.WriteByte('\n')
2173+
buf.WriteString(cindent)
2174+
buf.WriteString(yamlIndent)
2175+
} else {
2176+
buf.WriteByte(' ')
2177+
}
2178+
2179+
prevIndent := cindent
2180+
switch thunkValue.(type) {
2181+
case *valueArray, *valueObject:
2182+
cindent = cindent + yamlIndent
2183+
}
2184+
2185+
if err := aux(thunkValue, buf, cindent); err != nil {
2186+
return err
2187+
}
2188+
cindent = prevIndent
2189+
}
2190+
case *valueObject:
2191+
fields := objectFields(v, withoutHidden)
2192+
if len(fields) == 0 {
2193+
buf.WriteString("{}")
2194+
return nil
2195+
}
2196+
sort.Strings(fields)
2197+
for ix, fieldName := range fields {
2198+
fieldValue, err := v.index(i, fieldName)
2199+
if err != nil {
2200+
return err
2201+
}
2202+
2203+
if ix != 0 {
2204+
buf.WriteByte('\n')
2205+
buf.WriteString(cindent)
2206+
}
2207+
2208+
keyStr := fieldName
2209+
if vQuoteKeys.value || !yamlBareSafe(fieldName) {
2210+
keyStr = escapeStringJson(fieldName)
2211+
}
2212+
buf.WriteString(keyStr)
2213+
buf.WriteByte(':')
2214+
2215+
prevIndent := cindent
2216+
if v, isArr := fieldValue.(*valueArray); isArr && v.length() > 0 {
2217+
buf.WriteByte('\n')
2218+
buf.WriteString(cindent)
2219+
if vindentArrInObj.value {
2220+
buf.WriteString(yamlIndent)
2221+
cindent = cindent + yamlIndent
2222+
}
2223+
} else if v, isObj := fieldValue.(*valueObject); isObj {
2224+
if len(objectFields(v, withoutHidden)) > 0 {
2225+
buf.WriteByte('\n')
2226+
buf.WriteString(cindent)
2227+
buf.WriteString(yamlIndent)
2228+
cindent = cindent + yamlIndent
2229+
} else {
2230+
buf.WriteByte(' ')
2231+
}
2232+
} else {
2233+
buf.WriteByte(' ')
2234+
}
2235+
aux(fieldValue, buf, cindent)
2236+
cindent = prevIndent
2237+
}
2238+
}
2239+
return nil
2240+
}
2241+
2242+
if err := aux(val, &buf, ""); err != nil {
2243+
return nil, err
2244+
}
2245+
2246+
return makeValueString(buf.String()), nil
2247+
}
2248+
20242249
func builtinExtVar(i *interpreter, name value) (value, error) {
20252250
str, err := i.getString(name)
20262251
if err != nil {
@@ -2138,12 +2363,12 @@ func builtinAvg(i *interpreter, arrv value) (value, error) {
21382363
if err != nil {
21392364
return nil, err
21402365
}
2141-
2366+
21422367
len := float64(arr.length())
21432368
if len == 0 {
21442369
return nil, i.Error("Cannot calculate average of an empty array.")
21452370
}
2146-
2371+
21472372
sumValue, err := builtinSum(i, arrv)
21482373
if err != nil {
21492374
return nil, err
@@ -2153,7 +2378,7 @@ func builtinAvg(i *interpreter, arrv value) (value, error) {
21532378
return nil, err
21542379
}
21552380

2156-
avg := sum.value/len
2381+
avg := sum.value / len
21572382
return makeValueNumber(avg), nil
21582383
}
21592384

@@ -2500,6 +2725,7 @@ var funcBuiltins = buildBuiltinMap([]builtin{
25002725
&unaryBuiltin{name: "extVar", function: builtinExtVar, params: ast.Identifiers{"x"}},
25012726
&unaryBuiltin{name: "length", function: builtinLength, params: ast.Identifiers{"x"}},
25022727
&unaryBuiltin{name: "toString", function: builtinToString, params: ast.Identifiers{"a"}},
2728+
&unaryBuiltin{name: "escapeStringJson", function: builtinEscapeStringJson, params: ast.Identifiers{"str_"}},
25032729
&binaryBuiltin{name: "trace", function: builtinTrace, params: ast.Identifiers{"str", "rest"}},
25042730
&binaryBuiltin{name: "makeArray", function: builtinMakeArray, params: ast.Identifiers{"sz", "func"}},
25052731
&binaryBuiltin{name: "flatMap", function: builtinFlatMap, params: ast.Identifiers{"func", "arr"}},
@@ -2566,6 +2792,11 @@ var funcBuiltins = buildBuiltinMap([]builtin{
25662792
{name: "newline", defaultValue: &valueFlatString{value: []rune("\n")}},
25672793
{name: "key_val_sep", defaultValue: &valueFlatString{value: []rune(": ")}}}},
25682794
&generalBuiltin{name: "manifestTomlEx", function: builtinManifestTomlEx, params: []generalBuiltinParameter{{name: "value"}, {name: "indent"}}},
2795+
&generalBuiltin{name: "manifestYamlDoc", function: builtinManifestYamlDoc, params: []generalBuiltinParameter{
2796+
{name: "value"},
2797+
{name: "indent_array_in_object", defaultValue: &valueBoolean{value: false}},
2798+
{name: "quote_keys", defaultValue: &valueBoolean{value: true}},
2799+
}},
25692800
&unaryBuiltin{name: "base64", function: builtinBase64, params: ast.Identifiers{"input"}},
25702801
&unaryBuiltin{name: "encodeUTF8", function: builtinEncodeUTF8, params: ast.Identifiers{"str"}},
25712802
&unaryBuiltin{name: "decodeUTF8", function: builtinDecodeUTF8, params: ast.Identifiers{"arr"}},

builtins_benchmark_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ func Benchmark_Builtin_base64_byte_array(b *testing.B) {
5656
RunBenchmark(b, "base64_byte_array")
5757
}
5858

59+
func Benchmark_Builtin_escapeStringJson(b *testing.B) {
60+
RunBenchmark(b, "escapeStringJson")
61+
}
62+
5963
func Benchmark_Builtin_manifestJsonEx(b *testing.B) {
6064
RunBenchmark(b, "manifestJsonEx")
6165
}
@@ -64,6 +68,10 @@ func Benchmark_Builtin_manifestTomlEx(b *testing.B) {
6468
RunBenchmark(b, "manifestTomlEx")
6569
}
6670

71+
func Benchmark_Builtin_manifestYamlDoc(b *testing.B) {
72+
RunBenchmark(b, "manifestYamlDoc")
73+
}
74+
6775
func Benchmark_Builtin_comparison(b *testing.B) {
6876
RunBenchmark(b, "comparison")
6977
}

0 commit comments

Comments
 (0)