diff --git a/docs/components/expression-metadata.js b/docs/components/expression-metadata.js index b9e74e3899e..d0cbacf17dc 100644 --- a/docs/components/expression-metadata.js +++ b/docs/components/expression-metadata.js @@ -73,6 +73,10 @@ const types = { type: 'color', parameters: ['value', { repeat: [ 'fallback: value' ] }] }], + 'to-json': [{ + type: 'json', + parameters: ['string'] + }], 'to-number': [{ type: 'number', parameters: ['value', { repeat: [ 'fallback: value' ] }] diff --git a/src/style-spec/expression/definitions/coercion.js b/src/style-spec/expression/definitions/coercion.js index 03bd1afb923..3c6e714827e 100644 --- a/src/style-spec/expression/definitions/coercion.js +++ b/src/style-spec/expression/definitions/coercion.js @@ -2,7 +2,7 @@ import assert from 'assert'; -import {BooleanType, ColorType, NumberType, StringType, ValueType} from '../types'; +import {BooleanType, ColorType, JsonType, NumberType, StringType, ValueType} from '../types'; import {Color, toString as valueToString, validateRGBA} from '../values'; import RuntimeError from '../runtime_error'; import Formatted from '../types/formatted'; @@ -17,6 +17,7 @@ import type { Type } from '../types'; const types = { 'to-boolean': BooleanType, 'to-color': ColorType, + 'to-json': JsonType, 'to-number': NumberType, 'to-string': StringType }; @@ -44,7 +45,7 @@ class Coercion implements Expression { const name: string = (args[0]: any); assert(types[name], name); - if ((name === 'to-boolean' || name === 'to-string') && args.length !== 2) + if ((name === 'to-boolean' || name === 'to-string' || name === 'to-json') && args.length !== 2) return context.error(`Expected one argument.`); const type = types[name]; @@ -99,6 +100,13 @@ class Coercion implements Expression { // There is no explicit 'to-formatted' but this coercion can be implicitly // created by properties that expect the 'formatted' type. return Formatted.fromString(valueToString(this.args[0].evaluate(ctx))); + } else if (this.type.kind === 'json') { + const input = this.args[0].evaluate(ctx); + try { + return JSON.parse(input); + } catch (error) { + throw new RuntimeError(error || `Could not parse JSON from value '${input}'`); + } } else { return valueToString(this.args[0].evaluate(ctx)); } diff --git a/src/style-spec/expression/definitions/index.js b/src/style-spec/expression/definitions/index.js index af32c11d37f..3499267cbba 100644 --- a/src/style-spec/expression/definitions/index.js +++ b/src/style-spec/expression/definitions/index.js @@ -73,6 +73,7 @@ const expressions: ExpressionRegistry = { 'string': Assertion, 'to-boolean': Coercion, 'to-color': Coercion, + 'to-json': Coercion, 'to-number': Coercion, 'to-string': Coercion, 'var': Var diff --git a/src/style-spec/expression/types.js b/src/style-spec/expression/types.js index 1956102590a..6374d234f9a 100644 --- a/src/style-spec/expression/types.js +++ b/src/style-spec/expression/types.js @@ -5,6 +5,7 @@ export type NumberTypeT = { kind: 'number' }; export type StringTypeT = { kind: 'string' }; export type BooleanTypeT = { kind: 'boolean' }; export type ColorTypeT = { kind: 'color' }; +export type JsonTypeT = { kind: 'json' }; export type ObjectTypeT = { kind: 'object' }; export type ValueTypeT = { kind: 'value' }; export type ErrorTypeT = { kind: 'error' }; @@ -19,6 +20,7 @@ export type Type = StringTypeT | BooleanTypeT | ColorTypeT | + JsonTypeT | ObjectTypeT | ValueTypeT | ArrayType | // eslint-disable-line no-use-before-define @@ -37,6 +39,7 @@ export const NumberType = { kind: 'number' }; export const StringType = { kind: 'string' }; export const BooleanType = { kind: 'boolean' }; export const ColorType = { kind: 'color' }; +export const JsonType = { kind: 'json' }; export const ObjectType = { kind: 'object' }; export const ValueType = { kind: 'value' }; export const ErrorType = { kind: 'error' }; @@ -68,6 +71,7 @@ const valueMemberTypes = [ StringType, BooleanType, ColorType, + JsonType, FormattedType, ObjectType, array(ValueType) diff --git a/src/style-spec/reference/v8.json b/src/style-spec/reference/v8.json index e68b9dbdea1..134b47e4bc7 100644 --- a/src/style-spec/reference/v8.json +++ b/src/style-spec/reference/v8.json @@ -2606,6 +2606,15 @@ } } }, + "to-json": { + "doc": "Converts the input value to a JSON. The input is converted to a JSON using the [`JSON.parse`](https://tc39.github.io/ecma262/#sec-json.parse) function of the ECMAScript Language Specification.", + "group": "Types", + "sdk-support": { + "basic functionality": { + "js": "0.52.0" + } + } + }, "to-string": { "doc": "Converts the input value to a string. If the input is `null`, the result is `\"\"`. If the input is a boolean, the result is `\"true\"` or `\"false\"`. If the input is a number, it is converted to a string as specified by the [\"NumberToString\" algorithm](https://tc39.github.io/ecma262/#sec-tostring-applied-to-the-number-type) of the ECMAScript Language Specification. If the input is a color, it is converted to a string of the form `\"rgba(r,g,b,a)\"`, where `r`, `g`, and `b` are numerals ranging from 0 to 255, and `a` ranges from 0 to 1. Otherwise, the input is converted to a string in the format specified by the [`JSON.stringify`](https://tc39.github.io/ecma262/#sec-json.stringify) function of the ECMAScript Language Specification.", "group": "Types", diff --git a/test/integration/expression-tests/to-json/2-ary/test.json b/test/integration/expression-tests/to-json/2-ary/test.json new file mode 100644 index 00000000000..df2f58175ea --- /dev/null +++ b/test/integration/expression-tests/to-json/2-ary/test.json @@ -0,0 +1,10 @@ +{ + "expression": ["to-json", ["get", "x"], ["get", "y"]], + "inputs": [[{}, {}]], + "expected": { + "compiled": { + "result": "error", + "errors": [{"key": "", "error": "Expected one argument."}] + } + } +} diff --git a/test/integration/expression-tests/to-json/basic/test.json b/test/integration/expression-tests/to-json/basic/test.json new file mode 100644 index 00000000000..d7acc56b0de --- /dev/null +++ b/test/integration/expression-tests/to-json/basic/test.json @@ -0,0 +1,21 @@ +{ + "expression": ["to-json", ["get", "x"]], + "inputs": [ + [{}, {"properties": {"x": "1"}}], + [{}, {"properties": {"x": "false"}}], + [{}, {"properties": {"x": "null"}}], + [{}, {"properties": {"x": "string"}}], + [{}, {"properties": {"x": "[1, 2]"}}], + [{}, {"properties": {"x": "{\"y\":1}"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "json" + }, + "outputs": [1, false, null, {"error":{}}, [1, 2], {"y": 1}], + "serialized": ["to-json", ["get", "x"]] + } +}