diff --git a/ast/builtins.go b/ast/builtins.go
index 37d3201790..413c7dc368 100644
--- a/ast/builtins.go
+++ b/ast/builtins.go
@@ -142,6 +142,7 @@ var DefaultBuiltins = [...]*Builtin{
// Encoding
JSONMarshal,
+ JSONMarshalWithOptions,
JSONUnmarshal,
JSONIsValid,
Base64Encode,
@@ -1710,6 +1711,27 @@ var JSONMarshal = &Builtin{
Categories: encoding,
}
+var JSONMarshalWithOptions = &Builtin{
+ Name: "json.marshal_with_options",
+ Description: "Serializes the input term JSON, with additional formatting options via the `opts` parameter. " +
+ "`opts` accepts keys `pretty` (enable multi-line/formatted JSON), `prefix` (string to prefix lines with, default empty string) and `indent` (string to indent with, default `\\t`).",
+ Decl: types.NewFunction(
+ types.Args(
+ types.Named("x", types.A).Description("the term to serialize"),
+ types.Named("opts", types.NewObject(
+ []*types.StaticProperty{
+ types.NewStaticProperty("pretty", types.B),
+ types.NewStaticProperty("indent", types.S),
+ types.NewStaticProperty("prefix", types.S),
+ },
+ types.NewDynamicProperty(types.S, types.A),
+ )).Description("encoding options"),
+ ),
+ types.Named("y", types.S).Description("the JSON string representation of `x`, with configured prefix/indent string(s) as appropriate"),
+ ),
+ Categories: encoding,
+}
+
var JSONUnmarshal = &Builtin{
Name: "json.unmarshal",
Description: "Deserializes the input string.",
diff --git a/builtin_metadata.json b/builtin_metadata.json
index 87e8c51ec6..0d5ddd63ef 100644
--- a/builtin_metadata.json
+++ b/builtin_metadata.json
@@ -60,6 +60,7 @@
"hex.encode",
"json.is_valid",
"json.marshal",
+ "json.marshal_with_options",
"json.unmarshal",
"urlquery.decode",
"urlquery.decode_object",
@@ -10476,6 +10477,31 @@
},
"wasm": true
},
+ "json.marshal_with_options": {
+ "args": [
+ {
+ "description": "the term to serialize",
+ "name": "x",
+ "type": "any"
+ },
+ {
+ "description": "encoding options",
+ "name": "opts",
+ "type": "object\u003cindent: string, prefix: string, pretty: boolean\u003e[string: any]"
+ }
+ ],
+ "available": [
+ "edge"
+ ],
+ "description": "Serializes the input term JSON, with additional formatting options via the `opts` parameter. `opts` accepts keys `pretty` (enable multi-line/formatted JSON), `prefix` (string to prefix lines with, default empty string) and `indent` (string to indent with, default `\\t`).",
+ "introduced": "edge",
+ "result": {
+ "description": "the JSON string representation of `x`, with configured prefix/indent string(s) as appropriate",
+ "name": "y",
+ "type": "string"
+ },
+ "wasm": false
+ },
"json.match_schema": {
"args": [
{
diff --git a/capabilities.json b/capabilities.json
index f3d03f5f4c..06c04773c1 100644
--- a/capabilities.json
+++ b/capabilities.json
@@ -2191,6 +2191,51 @@
"type": "function"
}
},
+ {
+ "name": "json.marshal_with_options",
+ "decl": {
+ "args": [
+ {
+ "type": "any"
+ },
+ {
+ "dynamic": {
+ "key": {
+ "type": "string"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "static": [
+ {
+ "key": "indent",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "prefix",
+ "value": {
+ "type": "string"
+ }
+ },
+ {
+ "key": "pretty",
+ "value": {
+ "type": "boolean"
+ }
+ }
+ ],
+ "type": "object"
+ }
+ ],
+ "result": {
+ "type": "string"
+ },
+ "type": "function"
+ }
+ },
{
"name": "json.match_schema",
"decl": {
diff --git a/docs/content/policy-reference.md b/docs/content/policy-reference.md
index 5fe72f5d50..4fccb934af 100644
--- a/docs/content/policy-reference.md
+++ b/docs/content/policy-reference.md
@@ -414,6 +414,18 @@ The following table shows examples of how ``glob.match`` works:
{{< builtin-table types >}}
{{< builtin-table encoding >}}
+The `json.marshal_with_options` builtin's `opts` parameter accepts the following properties:
+
+| Field | Required | Type | Default | Description |
+| :---- | :------- | :--- | :------ | :---------- |
+| ``pretty`` | No | ``bool`` | `true` if `indent` or `prefix` are declared,
`false` otherwise | Enables multi-line, human-readable JSON output ("pretty-printing").
If this property is `true`, then objects will be marshaled into multi-line JSON with either user-specified or default indent/prefix options. If this property is `false`, `indent`/`prefix` will be ignored and this builtin functions identically to `json.marshal()`. |
+| ``indent`` | No | ``string`` | ``"\t"``
(Horizontal tab, character 0x09) | The string to use when indenting nested keys in the emitted JSON. One or more copies of this string will be included before child elements in every object or array. |
+| ``prefix`` | No | ``string`` | ``""``
(empty) | The string to prefix lines with in the emitted JSON. One copy of this string will be prepended to each line. |
+
+Default values will be used if:
+* `opts` is an empty object.
+* `opts` does not contain the named property.
+
{{< builtin-table cat=tokensign title="Token Signing" >}}
OPA provides two builtins that implement JSON Web Signature [RFC7515](https://tools.ietf.org/html/rfc7515) functionality.
diff --git a/test/cases/testdata/jsonbuiltins/test-json-marshal-with-options.yaml b/test/cases/testdata/jsonbuiltins/test-json-marshal-with-options.yaml
new file mode 100644
index 0000000000..f4ad2b6dbe
--- /dev/null
+++ b/test/cases/testdata/jsonbuiltins/test-json-marshal-with-options.yaml
@@ -0,0 +1,144 @@
+---
+cases:
+ - data: {}
+ modules:
+ - |
+ package test
+
+ p = x {
+ x := json.marshal_with_options([1234500000 + 67890, 1e6 * 2, 1e109 / 1e100], {"indent": " "})
+ }
+ note: jsonbuiltins/marshal_with_options-explicit-indent
+ query: data.test.p = x
+ want_result:
+ - x: |-
+ [
+ 1234567890,
+ 2000000,
+ 1000000000
+ ]
+ - data: {}
+ modules:
+ - |
+ package test
+
+ p = x {
+ x := json.marshal_with_options([1234500000 + 67890, 1e6 * 2, 1e109 / 1e100], {})
+ }
+ note: jsonbuiltins/marshal_with_options-empty-object
+ query: data.test.p = x
+ want_result:
+ - x: "[1234567890,2000000,1000000000]"
+ - data: {}
+ modules:
+ - |
+ package test
+
+ p = x {
+ x := json.marshal_with_options([1234500000 + 67890, 1e6 * 2, 1e109 / 1e100], {"pretty": true})
+ }
+ note: jsonbuiltins/marshal_with_options-defaults
+ query: data.test.p = x
+ want_result:
+ - x: |-
+ [
+ 1234567890,
+ 2000000,
+ 1000000000
+ ]
+ - data: {}
+ modules:
+ - |
+ package test
+
+ p = x {
+ x := json.marshal_with_options([1234500000 + 67890, 1e6 * 2, 1e109 / 1e100], {"pretty": false, "prefix": "NO!", "indent": "BAD!"})
+ }
+ note: jsonbuiltins/marshal_with_options-explicit-disable
+ query: data.test.p = x
+ want_result:
+ - x: "[1234567890,2000000,1000000000]"
+ - data: {}
+ modules:
+ - |
+ package test
+
+ p = x {
+ x := json.marshal_with_options([1234500000 + 67890, 1e6 * 2, 1e109 / 1e100], {"prefix": "JSON => "})
+ }
+ note: jsonbuiltins/marshal_with_options-prefix
+ query: data.test.p = x
+ want_result:
+ - x: |-
+ JSON => [
+ JSON => 1234567890,
+ JSON => 2000000,
+ JSON => 1000000000
+ JSON => ]
+ - data: {}
+ modules:
+ - |
+ package test
+
+ p = x {
+ x := json.marshal_with_options({"foo": "bar", "bar": "baz"}, {"prefix": "JSON => "})
+ }
+ note: jsonbuiltins/marshal_with_options-object
+ query: data.test.p = x
+ want_result:
+ - x: |-
+ JSON => {
+ JSON => "bar": "baz",
+ JSON => "foo": "bar"
+ JSON => }
+ - data: {}
+ modules:
+ - |
+ package test
+
+ p = x {
+ x := json.marshal_with_options([], {"indent": " ", "prefix": "---"})
+ }
+ note: jsonbuiltins/marshal_with_options-empty-array
+ query: data.test.p = x
+ want_result:
+ - x: |-
+ ---[]
+ - data: {}
+ modules:
+ - |
+ package test
+
+ p = x {
+ x := json.marshal_with_options([[[[[[[]]]]]]], {"indent": " "})
+ }
+ note: jsonbuiltins/marshal_with_options-deep-array
+ query: data.test.p = x
+ want_result:
+ - x: |-
+ [
+ [
+ [
+ [
+ [
+ [
+ []
+ ]
+ ]
+ ]
+ ]
+ ]
+ ]
+ - data: {}
+ modules:
+ - |
+ package test
+
+ p = x {
+ x := json.marshal_with_options([], {"indent": " ", "include_winning_lottery_numbers": true})
+ }
+ note: jsonbuiltins/marshal_with_options-invalid-key
+ query: data.test.p = x
+ strict_error: true
+ want_error: object contained unknown key "include_winning_lottery_numbers"
+ want_error_code: eval_type_error
diff --git a/topdown/encoding.go b/topdown/encoding.go
index 19ba323d19..f3475a60d0 100644
--- a/topdown/encoding.go
+++ b/topdown/encoding.go
@@ -35,6 +35,92 @@ func builtinJSONMarshal(_ BuiltinContext, operands []*ast.Term, iter func(*ast.T
return iter(ast.StringTerm(string(bs)))
}
+func builtinJSONMarshalWithOpts(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
+
+ asJSON, err := ast.JSON(operands[0].Value)
+ if err != nil {
+ return err
+ }
+
+ indentWith := "\t"
+ prefixWith := ""
+ implicitPrettyPrint := false
+ userDeclaredExplicitPrettyPrint := false
+ shouldPrettyPrint := false
+
+ marshalOpts, err := builtins.ObjectOperand(operands[1].Value, 2)
+ if err != nil {
+ return err
+ }
+
+ for idx, k := range marshalOpts.Keys() {
+
+ val := marshalOpts.Get(k)
+
+ key, err := builtins.StringOperand(k.Value, idx)
+ if err != nil {
+ return builtins.NewOperandErr(2, "failed to stringify key %v at index %d: %v", k, idx, err)
+ }
+
+ switch key {
+
+ case "prefix":
+ prefixOpt, err := builtins.StringOperand(val.Value, idx)
+ if err != nil {
+ return builtins.NewOperandErr(2, "key %s failed cast to string: %v", key, err)
+ }
+ prefixWith = string(prefixOpt)
+ implicitPrettyPrint = true
+
+ case "indent":
+ indentOpt, err := builtins.StringOperand(val.Value, idx)
+ if err != nil {
+ return builtins.NewOperandErr(2, "key %s failed cast to string: %v", key, err)
+
+ }
+ indentWith = string(indentOpt)
+ implicitPrettyPrint = true
+
+ case "pretty":
+ userDeclaredExplicitPrettyPrint = true
+ explicitPrettyPrint, ok := val.Value.(ast.Boolean)
+ if !ok {
+ return builtins.NewOperandErr(2, "key %s failed cast to bool", key)
+ }
+
+ shouldPrettyPrint = bool(explicitPrettyPrint)
+
+ default:
+ return builtins.NewOperandErr(2, "object contained unknown key %s", key)
+ }
+
+ }
+
+ if !userDeclaredExplicitPrettyPrint {
+ shouldPrettyPrint = implicitPrettyPrint
+ }
+
+ var bs []byte
+
+ if shouldPrettyPrint {
+ bs, err = json.MarshalIndent(asJSON, prefixWith, indentWith)
+ } else {
+ bs, err = json.Marshal(asJSON)
+ }
+
+ if err != nil {
+ return err
+ }
+
+ if shouldPrettyPrint {
+ // json.MarshalIndent() function will not prefix the first line of emitted JSON
+ return iter(ast.StringTerm(prefixWith + string(bs)))
+ }
+
+ return iter(ast.StringTerm(string(bs)))
+
+}
+
func builtinJSONUnmarshal(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
str, err := builtins.StringOperand(operands[0].Value, 1)
@@ -299,6 +385,7 @@ func builtinHexDecode(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Ter
func init() {
RegisterBuiltinFunc(ast.JSONMarshal.Name, builtinJSONMarshal)
+ RegisterBuiltinFunc(ast.JSONMarshalWithOptions.Name, builtinJSONMarshalWithOpts)
RegisterBuiltinFunc(ast.JSONUnmarshal.Name, builtinJSONUnmarshal)
RegisterBuiltinFunc(ast.JSONIsValid.Name, builtinJSONIsValid)
RegisterBuiltinFunc(ast.Base64Encode.Name, builtinBase64Encode)