diff --git a/docs/style-spec/expressions.md b/docs/style-spec/expressions.md index 609d5aadb6f..3988daf86c5 100644 --- a/docs/style-spec/expressions.md +++ b/docs/style-spec/expressions.md @@ -147,7 +147,8 @@ Convert the argument to the given type, producing a runtime error if the convers - `["has", key: String, obj: Object = ["properties"] ] -> Boolean` - `["at", index: Number, arr: Array|Array] -> T` - `["typeof", expr: Value] -> String` -- `["length", e: Vector|String] -> Number` +- `["length", e: Array|String] -> Number` +- `["contains", arr: Array|Array, value: T] -> Boolean` ### Feature data: - `["properties"] -> Object` the feature's `properties` object diff --git a/src/style-spec/function/definitions/contains.js b/src/style-spec/function/definitions/contains.js new file mode 100644 index 00000000000..8ff30b4cf43 --- /dev/null +++ b/src/style-spec/function/definitions/contains.js @@ -0,0 +1,60 @@ +// @flow + +const { parseExpression } = require('../expression'); +const { + array, + BooleanType, + ValueType +} = require('../types'); + +import type { Expression, ParsingContext } from '../expression'; +import type { Type, ArrayType } from '../types'; + +class Contains implements Expression { + key: string; + type: Type; + value: Expression; + array: Expression; + + constructor(key: string, value: Expression, array: Expression) { + this.key = key; + this.type = BooleanType; + this.value = value; + this.array = array; + } + + static parse(args: Array, context: ParsingContext) { + if (args.length !== 3) + return context.error(`Expected 2 arguments, but found ${args.length - 1} instead.`); + + const arrayExpr = parseExpression(args[1], context.concat(1, array(ValueType))); + if (!arrayExpr) return null; + + const t: ArrayType = (arrayExpr.type: any); + const value = parseExpression(args[2], context.concat(2, t.itemType)); + if (!value) return null; + + const itemType = value.type.kind; + if (itemType === 'Object' || itemType === 'Array' || itemType === 'Color') { + return context.error(`"contains" does not support values of type ${itemType}.`); + } + + return new Contains(context.key, value, arrayExpr); + } + + compile() { + return `this.contains(${this.array.compile()}, ${this.value.compile()})`; + } + + serialize() { + return [ 'contains', this.array.serialize(), this.value.serialize() ]; + } + + accept(visitor: Visitor) { + visitor.visit(this); + this.array.accept(visitor); + this.value.accept(visitor); + } +} + +module.exports = Contains; diff --git a/src/style-spec/function/definitions/index.js b/src/style-spec/function/definitions/index.js index 8c242e58123..1d40bc0e99d 100644 --- a/src/style-spec/function/definitions/index.js +++ b/src/style-spec/function/definitions/index.js @@ -20,6 +20,7 @@ const Var = require('./var'); const Literal = require('./literal'); const ArrayAssertion = require('./array'); const At = require('./at'); +const Contains = require('./contains'); const Match = require('./match'); const Case = require('./case'); const Curve = require('./curve'); @@ -35,6 +36,7 @@ const expressions: { [string]: Class } = { 'literal': Literal, 'array': ArrayAssertion, 'at': At, + 'contains': Contains, 'case': Case, 'match': Match, 'coalesce': Coalesce, diff --git a/src/style-spec/function/evaluation_context.js b/src/style-spec/function/evaluation_context.js index bb3fe3a4c96..2cccc2be995 100644 --- a/src/style-spec/function/evaluation_context.js +++ b/src/style-spec/function/evaluation_context.js @@ -54,6 +54,13 @@ module.exports = () => ({ return this.as(obj, ObjectType, name).hasOwnProperty(key); }, + contains: function (array: Array, value: Value) { + const type = typeOf(value).kind; + ensure(type !== 'Object' && type !== 'Array' && type !== 'Color', + `"contains" does not support values of type ${type}`); + return array.indexOf(value) >= 0; + }, + typeOf: function (x: Value): string { assert(isValue(x), `Invalid value ${String(x)}`); return toString(typeOf(x)); diff --git a/test/integration/expression-tests/contains/array/test.json b/test/integration/expression-tests/contains/array/test.json new file mode 100644 index 00000000000..8c63aa837f8 --- /dev/null +++ b/test/integration/expression-tests/contains/array/test.json @@ -0,0 +1,16 @@ +{ + "expectExpressionType": null, + "expression": ["contains", ["array", ["get", "arr"]], ["literal", []]], + "inputs": [], + "expected": { + "compiled": { + "result": "error", + "errors": [ + { + "key": "", + "error": "\"contains\" does not support values of type Array." + } + ] + } + } +} diff --git a/test/integration/expression-tests/contains/boolean/test.json b/test/integration/expression-tests/contains/boolean/test.json new file mode 100644 index 00000000000..1d9ceb133a5 --- /dev/null +++ b/test/integration/expression-tests/contains/boolean/test.json @@ -0,0 +1,21 @@ +{ + "expectExpressionType": null, + "expression": [ + "contains", + ["literal", [false, false]], + ["boolean", ["get", "item"]] + ], + "inputs": [ + [{}, {"properties": {"item": true}}], + [{}, {"properties": {"item": false}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "Boolean" + }, + "outputs": [false, true] + } +} diff --git a/test/integration/expression-tests/contains/color/test.json b/test/integration/expression-tests/contains/color/test.json new file mode 100644 index 00000000000..68c0bf8bce4 --- /dev/null +++ b/test/integration/expression-tests/contains/color/test.json @@ -0,0 +1,16 @@ +{ + "expectExpressionType": null, + "expression": ["contains", ["array", ["get", "arr"]], ["parse-color", "red"]], + "inputs": [], + "expected": { + "compiled": { + "result": "error", + "errors": [ + { + "key": "", + "error": "\"contains\" does not support values of type Color." + } + ] + } + } +} diff --git a/test/integration/expression-tests/contains/number/test.json b/test/integration/expression-tests/contains/number/test.json new file mode 100644 index 00000000000..5af395a435c --- /dev/null +++ b/test/integration/expression-tests/contains/number/test.json @@ -0,0 +1,28 @@ +{ + "expectExpressionType": null, + "expression": [ + "contains", + ["literal", [1, 2, 3, 4]], + ["number", ["get", "item"]] + ], + "inputs": [ + [{}, {"properties": {"item": 3}}], + [{}, {"properties": {"item": 5}}], + [{}, {"properties": {"item": "3"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "Boolean" + }, + "outputs": [ + true, + false, + { + "error": "Expected value to be of type Number, but found String instead." + } + ] + } +} diff --git a/test/integration/expression-tests/contains/object/test.json b/test/integration/expression-tests/contains/object/test.json new file mode 100644 index 00000000000..9d7261891a8 --- /dev/null +++ b/test/integration/expression-tests/contains/object/test.json @@ -0,0 +1,16 @@ +{ + "expectExpressionType": null, + "expression": ["contains", ["array", ["get", "arr"]], ["literal", {}]], + "inputs": [], + "expected": { + "compiled": { + "result": "error", + "errors": [ + { + "key": "", + "error": "\"contains\" does not support values of type Object." + } + ] + } + } +} diff --git a/test/integration/expression-tests/contains/string/test.json b/test/integration/expression-tests/contains/string/test.json new file mode 100644 index 00000000000..fba27114798 --- /dev/null +++ b/test/integration/expression-tests/contains/string/test.json @@ -0,0 +1,21 @@ +{ + "expectExpressionType": null, + "expression": [ + "contains", + ["literal", ["a", "b", "c"]], + ["string", ["get", "item"]] + ], + "inputs": [ + [{}, {"properties": {"item": "a"}}], + [{}, {"properties": {"item": "d"}}] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "Boolean" + }, + "outputs": [true, false] + } +} diff --git a/test/integration/expression-tests/contains/value/test.json b/test/integration/expression-tests/contains/value/test.json new file mode 100644 index 00000000000..ab439b3bf6b --- /dev/null +++ b/test/integration/expression-tests/contains/value/test.json @@ -0,0 +1,40 @@ +{ + "expectExpressionType": null, + "expression": ["contains", ["array", ["get", "arr"]], ["get", "item"]], + "inputs": [ + [{}, {"properties": {"item": 3, "arr": [1, 2, 3, 4]}}], + [{}, {"properties": {"item": 5, "arr": [1, 2, 3, 4]}}], + [{}, {"properties": {"item": "3", "arr": [1, 2, 3, 4]}}], + [{}, {"properties": {"item": "a", "arr": ["a", "b", "c"]}}], + [{}, {"properties": {"item": "d", "arr": ["a", "b", "c"]}}], + [{}, {"properties": {"item": true, "arr": [true, true]}}], + [{}, {"properties": {"item": false, "arr": [true, true]}}], + [ + {}, + { + "properties": { + "item": ["nested", "array"], + "arr": [["nested", "array"]] + } + } + ] + ], + "expected": { + "compiled": { + "result": "success", + "isFeatureConstant": false, + "isZoomConstant": true, + "type": "Boolean" + }, + "outputs": [ + true, + false, + false, + true, + false, + true, + false, + {"error": "\"contains\" does not support values of type Array"} + ] + } +}