mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
topdown: Add json.marshal_with_options() builtin for indented/"pretty-printed" and/or line-prefixed JSON (#6636)
Fixes #6630 Signed-off-by: Sean Williams <72675818+sean-r-williams@users.noreply.github.com>
This commit is contained in:
@@ -142,6 +142,7 @@ var DefaultBuiltins = [...]*Builtin{
|
||||
|
||||
// Encoding
|
||||
JSONMarshal,
|
||||
JSONMarshalWithOptions,
|
||||
JSONUnmarshal,
|
||||
JSONIsValid,
|
||||
Base64Encode,
|
||||
@@ -1707,6 +1708,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.",
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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, <br/>`false` otherwise | Enables multi-line, human-readable JSON output ("pretty-printing"). <br/>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"`` <br/> (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`` | ``""`` <br/> (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.
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user