planning
All checks were successful
Publish To Prod / deploy_and_publish (push) Successful in 35s

This commit is contained in:
2024-10-14 09:15:30 +02:00
parent bcba00a730
commit 6e64e138e2
21059 changed files with 2317811 additions and 1 deletions

View File

@@ -0,0 +1,91 @@
// @flow
import extend from '../util/extend.js';
import {unbundle, deepUnbundle} from '../util/unbundle_jsonlint.js';
import {isExpression} from '../expression/index.js';
import {isFunction} from '../function/index.js';
import validateFunction from './validate_function.js';
import validateExpression from './validate_expression.js';
import validateObject from './validate_object.js';
import validateArray from './validate_array.js';
import validateBoolean from './validate_boolean.js';
import validateNumber from './validate_number.js';
import validateColor from './validate_color.js';
import validateEnum from './validate_enum.js';
import validateFilter from './validate_filter.js';
import validateLayer from './validate_layer.js';
import validateSource from './validate_source.js';
import validateLight from './validate_light.js';
import validateTerrain from './validate_terrain.js';
import validateFog from './validate_fog.js';
import validateString from './validate_string.js';
import validateFormatted from './validate_formatted.js';
import validateImage from './validate_image.js';
import validateProjection from './validate_projection.js';
import type {StyleReference} from '../reference/latest.js';
import type {StyleSpecification} from '../types.js';
import type ValidationError from '../error/validation_error.js';
const VALIDATORS = {
'*'() {
return [];
},
'array': validateArray,
'boolean': validateBoolean,
'number': validateNumber,
'color': validateColor,
'enum': validateEnum,
'filter': validateFilter,
'function': validateFunction,
'layer': validateLayer,
'object': validateObject,
'source': validateSource,
'light': validateLight,
'terrain': validateTerrain,
'fog': validateFog,
'string': validateString,
'formatted': validateFormatted,
'resolvedImage': validateImage,
'projection': validateProjection
};
// Main recursive validation function. Tracks:
//
// - key: string representing location of validation in style tree. Used only
// for more informative error reporting.
// - value: current value from style being evaluated. May be anything from a
// high level object that needs to be descended into deeper or a simple
// scalar value.
// - valueSpec: current spec being evaluated. Tracks value.
// - styleSpec: current full spec being evaluated.
export type ValidationOptions = {
key: string;
value: Object;
valueSpec: Object;
style: $Shape<StyleSpecification>;
styleSpec: StyleReference;
}
export default function validate(options: ValidationOptions): Array<ValidationError> {
const value = options.value;
const valueSpec = options.valueSpec;
const styleSpec = options.styleSpec;
if (valueSpec.expression && isFunction(unbundle(value))) {
return validateFunction(options);
} else if (valueSpec.expression && isExpression(deepUnbundle(value))) {
return validateExpression(options);
} else if (valueSpec.type && VALIDATORS[valueSpec.type]) {
return VALIDATORS[valueSpec.type](options);
} else {
const valid = validateObject(extend({}, options, {
valueSpec: valueSpec.type ? styleSpec[valueSpec.type] : valueSpec
}));
return valid;
}
}

View File

@@ -0,0 +1,62 @@
// @flow
import getType from '../util/get_type.js';
import validate from './validate.js';
import ValidationError from '../error/validation_error.js';
import type {ValidationOptions} from './validate.js';
type Options = ValidationOptions & {
arrayElementValidator: Function;
};
export default function validateArray(options: Options): Array<ValidationError> {
const array = options.value;
const arraySpec = options.valueSpec;
const style = options.style;
const styleSpec = options.styleSpec;
const key = options.key;
const validateArrayElement = options.arrayElementValidator || validate;
if (getType(array) !== 'array') {
return [new ValidationError(key, array, `array expected, ${getType(array)} found`)];
}
if (arraySpec.length && array.length !== arraySpec.length) {
return [new ValidationError(key, array, `array length ${arraySpec.length} expected, length ${array.length} found`)];
}
if (arraySpec['min-length'] && array.length < arraySpec['min-length']) {
return [new ValidationError(key, array, `array length at least ${arraySpec['min-length']} expected, length ${array.length} found`)];
}
let arrayElementSpec = {
"type": arraySpec.value,
"values": arraySpec.values,
"minimum": arraySpec.minimum,
"maximum": arraySpec.maximum,
function: undefined
};
if (styleSpec.$version < 7) {
arrayElementSpec.function = arraySpec.function;
}
if (getType(arraySpec.value) === 'object') {
arrayElementSpec = arraySpec.value;
}
let errors = [];
for (let i = 0; i < array.length; i++) {
errors = errors.concat(validateArrayElement({
array,
arrayIndex: i,
value: array[i],
valueSpec: arrayElementSpec,
style,
styleSpec,
key: `${key}[${i}]`
}));
}
return errors;
}

View File

@@ -0,0 +1,18 @@
// @flow
import getType from '../util/get_type.js';
import ValidationError from '../error/validation_error.js';
import type {ValidationOptions} from './validate.js';
export default function validateBoolean(options: ValidationOptions): Array<ValidationError> {
const value = options.value;
const key = options.key;
const type = getType(value);
if (type !== 'boolean') {
return [new ValidationError(key, value, `boolean expected, ${type} found`)];
}
return [];
}

View File

@@ -0,0 +1,23 @@
// @flow
import ValidationError from '../error/validation_error.js';
import getType from '../util/get_type.js';
import {parseCSSColor} from 'csscolorparser';
import type {ValidationOptions} from './validate.js';
export default function validateColor(options: ValidationOptions): Array<ValidationError> {
const key = options.key;
const value = options.value;
const type = getType(value);
if (type !== 'string') {
return [new ValidationError(key, value, `color expected, ${type} found`)];
}
if (parseCSSColor(value) === null) {
return [new ValidationError(key, value, `color expected, "${value}" found`)];
}
return [];
}

View File

@@ -0,0 +1,24 @@
// @flow
import ValidationError from '../error/validation_error.js';
import {unbundle} from '../util/unbundle_jsonlint.js';
import type {ValidationOptions} from './validate.js';
export default function validateEnum(options: ValidationOptions): Array<ValidationError> {
const key = options.key;
const value = options.value;
const valueSpec = options.valueSpec;
const errors = [];
if (Array.isArray(valueSpec.values)) { // <=v7
if (valueSpec.values.indexOf(unbundle(value)) === -1) {
errors.push(new ValidationError(key, value, `expected one of [${valueSpec.values.join(', ')}], ${JSON.stringify(value)} found`));
}
} else { // >=v8
if (Object.keys(valueSpec.values).indexOf(unbundle(value)) === -1) {
errors.push(new ValidationError(key, value, `expected one of [${Object.keys(valueSpec.values).join(', ')}], ${JSON.stringify(value)} found`));
}
}
return errors;
}

View File

@@ -0,0 +1,77 @@
// @flow
import ValidationError from '../error/validation_error.js';
import {createExpression, createPropertyExpression} from '../expression/index.js';
import {deepUnbundle} from '../util/unbundle_jsonlint.js';
import {isStateConstant, isGlobalPropertyConstant, isFeatureConstant} from '../expression/is_constant.js';
import CompoundExpression from '../expression/compound_expression.js';
import type {Expression} from '../expression/expression.js';
export default function validateExpression(options: any): Array<ValidationError> {
const expression = (options.expressionContext === 'property' ? createPropertyExpression : createExpression)(deepUnbundle(options.value), options.valueSpec);
if (expression.result === 'error') {
return expression.value.map((error) => {
return new ValidationError(`${options.key}${error.key}`, options.value, error.message);
});
}
const expressionObj = (expression.value: any).expression || (expression.value: any)._styleExpression.expression;
if (options.expressionContext === 'property' && (options.propertyKey === 'text-font') &&
!expressionObj.outputDefined()) {
return [new ValidationError(options.key, options.value, `Invalid data expression for "${options.propertyKey}". Output values must be contained as literals within the expression.`)];
}
if (options.expressionContext === 'property' && options.propertyType === 'layout' &&
(!isStateConstant(expressionObj))) {
return [new ValidationError(options.key, options.value, '"feature-state" data expressions are not supported with layout properties.')];
}
if (options.expressionContext === 'filter') {
return disallowedFilterParameters(expressionObj, options);
}
if (options.expressionContext && options.expressionContext.indexOf('cluster') === 0) {
if (!isGlobalPropertyConstant(expressionObj, ['zoom', 'feature-state'])) {
return [new ValidationError(options.key, options.value, '"zoom" and "feature-state" expressions are not supported with cluster properties.')];
}
if (options.expressionContext === 'cluster-initial' && !isFeatureConstant(expressionObj)) {
return [new ValidationError(options.key, options.value, 'Feature data expressions are not supported with initial expression part of cluster properties.')];
}
}
return [];
}
export function disallowedFilterParameters(e: Expression, options: any): Array<ValidationError> {
const disallowedParameters = new Set([
'zoom',
'feature-state',
'pitch',
'distance-from-center'
]);
if (options.valueSpec && options.valueSpec.expression) {
for (const param of options.valueSpec.expression.parameters) {
disallowedParameters.delete(param);
}
}
if (disallowedParameters.size === 0) {
return [];
}
const errors = [];
if (e instanceof CompoundExpression) {
if (disallowedParameters.has(e.name)) {
return [new ValidationError(options.key, options.value, `["${e.name}"] expression is not supported in a filter for a ${options.object.type} layer with id: ${options.object.id}`)];
}
}
e.eachChild((arg) => {
errors.push(...disallowedFilterParameters(arg, options));
});
return errors;
}

View File

@@ -0,0 +1,127 @@
// @flow
import ValidationError from '../error/validation_error.js';
import validateExpression from './validate_expression.js';
import validateEnum from './validate_enum.js';
import getType from '../util/get_type.js';
import {unbundle, deepUnbundle} from '../util/unbundle_jsonlint.js';
import extend from '../util/extend.js';
import {isExpressionFilter} from '../feature_filter/index.js';
import type {ValidationOptions} from './validate.js';
type Options = ValidationOptions & {
layerType: string;
}
export default function validateFilter(options: Options): Array<ValidationError> {
if (isExpressionFilter(deepUnbundle(options.value))) {
// We default to a layerType of `fill` because that points to a non-dynamic filter definition within the style-spec.
const layerType = options.layerType || 'fill';
return validateExpression(extend({}, options, {
expressionContext: 'filter',
valueSpec: options.styleSpec[`filter_${layerType}`]
}));
} else {
return validateNonExpressionFilter(options);
}
}
function validateNonExpressionFilter(options) {
const value = options.value;
const key = options.key;
if (getType(value) !== 'array') {
return [new ValidationError(key, value, `array expected, ${getType(value)} found`)];
}
const styleSpec = options.styleSpec;
let type;
let errors = [];
if (value.length < 1) {
return [new ValidationError(key, value, 'filter array must have at least 1 element')];
}
errors = errors.concat(validateEnum({
key: `${key}[0]`,
value: value[0],
valueSpec: styleSpec.filter_operator,
style: options.style,
styleSpec: options.styleSpec
}));
switch (unbundle(value[0])) {
case '<':
case '<=':
case '>':
case '>=':
if (value.length >= 2 && unbundle(value[1]) === '$type') {
errors.push(new ValidationError(key, value, `"$type" cannot be use with operator "${value[0]}"`));
}
/* falls through */
case '==':
case '!=':
if (value.length !== 3) {
errors.push(new ValidationError(key, value, `filter array for operator "${value[0]}" must have 3 elements`));
}
/* falls through */
case 'in':
case '!in':
if (value.length >= 2) {
type = getType(value[1]);
if (type !== 'string') {
errors.push(new ValidationError(`${key}[1]`, value[1], `string expected, ${type} found`));
}
}
for (let i = 2; i < value.length; i++) {
type = getType(value[i]);
if (unbundle(value[1]) === '$type') {
errors = errors.concat(validateEnum({
key: `${key}[${i}]`,
value: value[i],
valueSpec: styleSpec.geometry_type,
style: options.style,
styleSpec: options.styleSpec
}));
} else if (type !== 'string' && type !== 'number' && type !== 'boolean') {
errors.push(new ValidationError(`${key}[${i}]`, value[i], `string, number, or boolean expected, ${type} found`));
}
}
break;
case 'any':
case 'all':
case 'none':
for (let i = 1; i < value.length; i++) {
errors = errors.concat(validateNonExpressionFilter({
key: `${key}[${i}]`,
value: value[i],
style: options.style,
styleSpec: options.styleSpec
}));
}
break;
case 'has':
case '!has':
type = getType(value[1]);
if (value.length !== 2) {
errors.push(new ValidationError(key, value, `filter array for "${value[0]}" operator must have 2 elements`));
} else if (type !== 'string') {
errors.push(new ValidationError(`${key}[1]`, value[1], `string expected, ${type} found`));
}
break;
case 'within':
type = getType(value[1]);
if (value.length !== 2) {
errors.push(new ValidationError(key, value, `filter array for "${value[0]}" operator must have 2 elements`));
} else if (type !== 'object') {
errors.push(new ValidationError(`${key}[1]`, value[1], `object expected, ${type} found`));
}
break;
}
return errors;
}

View File

@@ -0,0 +1,49 @@
// @flow
import ValidationError from '../error/validation_error.js';
import validate from './validate.js';
import getType from '../util/get_type.js';
import type {ValidationOptions} from './validate.js';
export default function validateFog(options: ValidationOptions): Array<ValidationError> {
const fog = options.value;
const style = options.style;
const styleSpec = options.styleSpec;
const fogSpec = styleSpec.fog;
let errors = [];
const rootType = getType(fog);
if (fog === undefined) {
return errors;
} else if (rootType !== 'object') {
errors = errors.concat([new ValidationError('fog', fog, `object expected, ${rootType} found`)]);
return errors;
}
for (const key in fog) {
const transitionMatch = key.match(/^(.*)-transition$/);
if (transitionMatch && fogSpec[transitionMatch[1]] && fogSpec[transitionMatch[1]].transition) {
errors = errors.concat(validate({
key,
value: fog[key],
valueSpec: styleSpec.transition,
style,
styleSpec
}));
} else if (fogSpec[key]) {
errors = errors.concat(validate({
key,
value: fog[key],
valueSpec: fogSpec[key],
style,
styleSpec
}));
} else {
errors = errors.concat([new ValidationError(key, fog[key], `unknown property "${key}"`)]);
}
}
return errors;
}

View File

@@ -0,0 +1,15 @@
// @flow
import validateExpression from './validate_expression.js';
import validateString from './validate_string.js';
import type {ValidationOptions} from './validate.js';
import type ValidationError from '../error/validation_error.js';
export default function validateFormatted(options: ValidationOptions): Array<ValidationError> {
if (validateString(options).length === 0) {
return [];
}
return validateExpression(options);
}

View File

@@ -0,0 +1,216 @@
// @flow
import ValidationError from '../error/validation_error.js';
import getType from '../util/get_type.js';
import validate from './validate.js';
import validateObject from './validate_object.js';
import validateArray from './validate_array.js';
import validateNumber from './validate_number.js';
import {isExpression} from '../expression/index.js';
import {unbundle, deepUnbundle} from '../util/unbundle_jsonlint.js';
import {
supportsPropertyExpression,
supportsZoomExpression,
supportsInterpolation
} from '../util/properties.js';
import type {ValidationOptions} from './validate.js';
export default function validateFunction(options: ValidationOptions): any {
const functionValueSpec = options.valueSpec;
const functionType = unbundle(options.value.type);
let stopKeyType;
let stopDomainValues: {[string | number]: boolean} = {};
let previousStopDomainValue;
let previousStopDomainZoom;
const isZoomFunction = functionType !== 'categorical' && options.value.property === undefined;
const isPropertyFunction = !isZoomFunction;
const isZoomAndPropertyFunction =
getType(options.value.stops) === 'array' &&
getType(options.value.stops[0]) === 'array' &&
getType(options.value.stops[0][0]) === 'object';
const errors = validateObject({
key: options.key,
value: options.value,
valueSpec: options.styleSpec.function,
style: options.style,
styleSpec: options.styleSpec,
objectElementValidators: {
stops: validateFunctionStops,
default: validateFunctionDefault
}
});
if (functionType === 'identity' && isZoomFunction) {
errors.push(new ValidationError(options.key, options.value, 'missing required property "property"'));
}
if (functionType !== 'identity' && !options.value.stops) {
errors.push(new ValidationError(options.key, options.value, 'missing required property "stops"'));
}
if (functionType === 'exponential' && options.valueSpec.expression && !supportsInterpolation(options.valueSpec)) {
errors.push(new ValidationError(options.key, options.value, 'exponential functions not supported'));
}
if (options.styleSpec.$version >= 8) {
if (isPropertyFunction && !supportsPropertyExpression(options.valueSpec)) {
errors.push(new ValidationError(options.key, options.value, 'property functions not supported'));
} else if (isZoomFunction && !supportsZoomExpression(options.valueSpec)) {
errors.push(new ValidationError(options.key, options.value, 'zoom functions not supported'));
}
}
if ((functionType === 'categorical' || isZoomAndPropertyFunction) && options.value.property === undefined) {
errors.push(new ValidationError(options.key, options.value, '"property" property is required'));
}
return errors;
function validateFunctionStops(options: ValidationOptions) {
if (functionType === 'identity') {
return [new ValidationError(options.key, options.value, 'identity function may not have a "stops" property')];
}
let errors = [];
const value = options.value;
errors = errors.concat(validateArray({
key: options.key,
value,
valueSpec: options.valueSpec,
style: options.style,
styleSpec: options.styleSpec,
arrayElementValidator: validateFunctionStop
}));
if (getType(value) === 'array' && value.length === 0) {
errors.push(new ValidationError(options.key, value, 'array must have at least one stop'));
}
return errors;
}
function validateFunctionStop(options: ValidationOptions) {
let errors = [];
const value = options.value;
const key = options.key;
if (getType(value) !== 'array') {
return [new ValidationError(key, value, `array expected, ${getType(value)} found`)];
}
if (value.length !== 2) {
return [new ValidationError(key, value, `array length 2 expected, length ${value.length} found`)];
}
if (isZoomAndPropertyFunction) {
if (getType(value[0]) !== 'object') {
return [new ValidationError(key, value, `object expected, ${getType(value[0])} found`)];
}
if (value[0].zoom === undefined) {
return [new ValidationError(key, value, 'object stop key must have zoom')];
}
if (value[0].value === undefined) {
return [new ValidationError(key, value, 'object stop key must have value')];
}
const nextStopDomainZoom = unbundle(value[0].zoom);
if (typeof nextStopDomainZoom !== 'number') {
return [new ValidationError(key, value[0].zoom, 'stop zoom values must be numbers')];
}
if (previousStopDomainZoom && previousStopDomainZoom > nextStopDomainZoom) {
return [new ValidationError(key, value[0].zoom, 'stop zoom values must appear in ascending order')];
}
if (nextStopDomainZoom !== previousStopDomainZoom) {
previousStopDomainZoom = nextStopDomainZoom;
previousStopDomainValue = undefined;
stopDomainValues = {};
}
errors = errors.concat(validateObject({
key: `${key}[0]`,
value: value[0],
valueSpec: {zoom: {}},
style: options.style,
styleSpec: options.styleSpec,
objectElementValidators: {zoom: validateNumber, value: validateStopDomainValue}
}));
} else {
errors = errors.concat(validateStopDomainValue({
key: `${key}[0]`,
value: value[0],
valueSpec: {},
style: options.style,
styleSpec: options.styleSpec
}, value));
}
if (isExpression(deepUnbundle(value[1]))) {
return errors.concat([new ValidationError(`${key}[1]`, value[1], 'expressions are not allowed in function stops.')]);
}
return errors.concat(validate({
key: `${key}[1]`,
value: value[1],
valueSpec: functionValueSpec,
style: options.style,
styleSpec: options.styleSpec
}));
}
function validateStopDomainValue(options: ValidationOptions, stop) {
const type = getType(options.value);
const value = unbundle(options.value);
const reportValue = options.value !== null ? options.value : stop;
if (!stopKeyType) {
stopKeyType = type;
} else if (type !== stopKeyType) {
return [new ValidationError(options.key, reportValue, `${type} stop domain type must match previous stop domain type ${stopKeyType}`)];
}
if (type !== 'number' && type !== 'string' && type !== 'boolean' && typeof value !== 'number' && typeof value !== 'string' && typeof value !== 'boolean') {
return [new ValidationError(options.key, reportValue, 'stop domain value must be a number, string, or boolean')];
}
if (type !== 'number' && functionType !== 'categorical') {
let message = `number expected, ${type} found`;
if (supportsPropertyExpression(functionValueSpec) && functionType === undefined) {
message += '\nIf you intended to use a categorical function, specify `"type": "categorical"`.';
}
return [new ValidationError(options.key, reportValue, message)];
}
if (functionType === 'categorical' && type === 'number' && (typeof value !== 'number' || !isFinite(value) || Math.floor(value) !== value)) {
return [new ValidationError(options.key, reportValue, `integer expected, found ${String(value)}`)];
}
if (functionType !== 'categorical' && type === 'number' && typeof value === 'number' && typeof previousStopDomainValue === 'number' && previousStopDomainValue !== undefined && value < previousStopDomainValue) {
return [new ValidationError(options.key, reportValue, 'stop domain values must appear in ascending order')];
} else {
previousStopDomainValue = value;
}
if (functionType === 'categorical' && (value: any) in stopDomainValues) {
return [new ValidationError(options.key, reportValue, 'stop domain values must be unique')];
} else {
stopDomainValues[(value: any)] = true;
}
return [];
}
function validateFunctionDefault(options: ValidationOptions) {
return validate({
key: options.key,
value: options.value,
valueSpec: functionValueSpec,
style: options.style,
styleSpec: options.styleSpec
});
}
}

View File

@@ -0,0 +1,24 @@
// @flow
import ValidationError from '../error/validation_error.js';
import validateString from './validate_string.js';
import type {ValidationOptions} from './validate.js';
export default function(options: ValidationOptions): Array<ValidationError> {
const value = options.value;
const key = options.key;
const errors = validateString(options);
if (errors.length) return errors;
if (value.indexOf('{fontstack}') === -1) {
errors.push(new ValidationError(key, value, '"glyphs" url must include a "{fontstack}" token'));
}
if (value.indexOf('{range}') === -1) {
errors.push(new ValidationError(key, value, '"glyphs" url must include a "{range}" token'));
}
return errors;
}

View File

@@ -0,0 +1,15 @@
// @flow
import validateExpression from './validate_expression.js';
import validateString from './validate_string.js';
import type {ValidationOptions} from './validate.js';
import type ValidationError from '../error/validation_error.js';
export default function validateImage(options: ValidationOptions): Array<ValidationError> {
if (validateString(options).length === 0) {
return [];
}
return validateExpression(options);
}

View File

@@ -0,0 +1,149 @@
// @flow
import ValidationError from '../error/validation_error.js';
import {unbundle} from '../util/unbundle_jsonlint.js';
import validateObject from './validate_object.js';
import validateFilter from './validate_filter.js';
import validatePaintProperty from './validate_paint_property.js';
import validateLayoutProperty from './validate_layout_property.js';
import validateSpec from './validate.js';
import extend from '../util/extend.js';
import type {ValidationOptions} from './validate.js';
import type {LayerSpecification} from '../types.js';
type Options = ValidationOptions & {
value: LayerSpecification;
arrayIndex: number;
}
export default function validateLayer(options: Options): Array<ValidationError> {
let errors = [];
const layer = options.value;
const key = options.key;
const style = options.style;
const styleSpec = options.styleSpec;
if (!layer.type && !layer.ref) {
errors.push(new ValidationError(key, layer, 'either "type" or "ref" is required'));
}
let type = unbundle(layer.type);
const ref = unbundle(layer.ref);
if (layer.id) {
const layerId = unbundle(layer.id);
for (let i = 0; i < options.arrayIndex; i++) {
const otherLayer = style.layers[i];
if (unbundle(otherLayer.id) === layerId) {
// $FlowFixMe[prop-missing] - id.__line__ is added dynamically during the readStyle step
errors.push(new ValidationError(key, layer.id, `duplicate layer id "${layer.id}", previously used at line ${otherLayer.id.__line__}`));
}
}
}
if ('ref' in layer) {
['type', 'source', 'source-layer', 'filter', 'layout'].forEach((p) => {
if (p in layer) {
errors.push(new ValidationError(key, layer[p], `"${p}" is prohibited for ref layers`));
}
});
let parent;
style.layers.forEach((layer) => {
if (unbundle(layer.id) === ref) parent = layer;
});
if (!parent) {
if (typeof ref === 'string')
errors.push(new ValidationError(key, layer.ref, `ref layer "${ref}" not found`));
} else if (parent.ref) {
errors.push(new ValidationError(key, layer.ref, 'ref cannot reference another ref layer'));
} else {
type = unbundle(parent.type);
}
} else if (!(type === 'background' || type === 'sky')) {
if (!layer.source) {
errors.push(new ValidationError(key, layer, 'missing required property "source"'));
} else {
const source = style.sources && style.sources[layer.source];
const sourceType = source && unbundle(source.type);
if (!source) {
errors.push(new ValidationError(key, layer.source, `source "${layer.source}" not found`));
} else if (sourceType === 'vector' && type === 'raster') {
errors.push(new ValidationError(key, layer.source, `layer "${layer.id}" requires a raster source`));
} else if (sourceType === 'raster' && type !== 'raster') {
errors.push(new ValidationError(key, layer.source, `layer "${layer.id}" requires a vector source`));
} else if (sourceType === 'vector' && !layer['source-layer']) {
errors.push(new ValidationError(key, layer, `layer "${layer.id}" must specify a "source-layer"`));
} else if (sourceType === 'raster-dem' && type !== 'hillshade') {
errors.push(new ValidationError(key, layer.source, 'raster-dem source can only be used with layer type \'hillshade\'.'));
} else if (type === 'line' && layer.paint && (layer.paint['line-gradient'] || layer.paint['line-trim-offset']) &&
(sourceType !== 'geojson' || !source.lineMetrics)) {
errors.push(new ValidationError(key, layer, `layer "${layer.id}" specifies a line-gradient, which requires a GeoJSON source with \`lineMetrics\` enabled.`));
}
}
}
errors = errors.concat(validateObject({
key,
value: layer,
valueSpec: styleSpec.layer,
style: options.style,
styleSpec: options.styleSpec,
objectElementValidators: {
'*'() {
return [];
},
// We don't want to enforce the spec's `"requires": true` for backward compatibility with refs;
// the actual requirement is validated above. See https://github.com/mapbox/mapbox-gl-js/issues/5772.
type() {
return validateSpec({
key: `${key}.type`,
value: layer.type,
valueSpec: styleSpec.layer.type,
style: options.style,
styleSpec: options.styleSpec,
object: layer,
objectKey: 'type'
});
},
filter(options) {
return validateFilter(extend({layerType: type}, options));
},
layout(options) {
return validateObject({
layer,
key: options.key,
value: options.value,
valueSpec: {},
style: options.style,
styleSpec: options.styleSpec,
objectElementValidators: {
'*'(options) {
return validateLayoutProperty(extend({layerType: type}, options));
}
}
});
},
paint(options) {
return validateObject({
layer,
key: options.key,
value: options.value,
valueSpec: {},
style: options.style,
styleSpec: options.styleSpec,
objectElementValidators: {
'*'(options) {
return validatePaintProperty(extend({layerType: type}, options));
}
}
});
}
}
}));
return errors;
}

View File

@@ -0,0 +1,10 @@
// @flow
import validateProperty from './validate_property.js';
import type ValidationError from '../error/validation_error.js';
import type {PropertyValidationOptions} from './validate_property.js';
export default function validateLayoutProperty(options: PropertyValidationOptions): Array<ValidationError> {
return validateProperty(options, 'layout');
}

View File

@@ -0,0 +1,50 @@
// @flow
import ValidationError from '../error/validation_error.js';
import getType from '../util/get_type.js';
import validate from './validate.js';
import type {ValidationOptions} from './validate.js';
export default function validateLight(options: ValidationOptions): Array<ValidationError> {
const light = options.value;
const styleSpec = options.styleSpec;
const lightSpec = styleSpec.light;
const style = options.style;
let errors = [];
const rootType = getType(light);
if (light === undefined) {
return errors;
} else if (rootType !== 'object') {
errors = errors.concat([new ValidationError('light', light, `object expected, ${rootType} found`)]);
return errors;
}
for (const key in light) {
const transitionMatch = key.match(/^(.*)-transition$/);
if (transitionMatch && lightSpec[transitionMatch[1]] && lightSpec[transitionMatch[1]].transition) {
errors = errors.concat(validate({
key,
value: light[key],
valueSpec: styleSpec.transition,
style,
styleSpec
}));
} else if (lightSpec[key]) {
errors = errors.concat(validate({
key,
value: light[key],
valueSpec: lightSpec[key],
style,
styleSpec
}));
} else {
errors = errors.concat([new ValidationError(key, light[key], `unknown property "${key}"`)]);
}
}
return errors;
}

View File

@@ -0,0 +1,50 @@
// @flow
import getType from '../util/get_type.js';
import ValidationError from '../error/validation_error.js';
import type {ValidationOptions} from './validate.js';
type Options = ValidationOptions & {
arrayIndex: number;
}
export default function validateNumber(options: Options): Array<ValidationError> {
const key = options.key;
const value = options.value;
const valueSpec = options.valueSpec;
let type = getType(value);
// eslint-disable-next-line no-self-compare
if (type === 'number' && value !== value) {
type = 'NaN';
}
if (type !== 'number') {
return [new ValidationError(key, value, `number expected, ${type} found`)];
}
if ('minimum' in valueSpec) {
let specMin = valueSpec.minimum;
if (getType(valueSpec.minimum) === 'array') {
const i = options.arrayIndex;
specMin = valueSpec.minimum[i];
}
if (value < specMin) {
return [new ValidationError(key, value, `${value} is less than the minimum value ${specMin}`)];
}
}
if ('maximum' in valueSpec) {
let specMax = valueSpec.maximum;
if (getType(valueSpec.maximum) === 'array') {
const i = options.arrayIndex;
specMax = valueSpec.maximum[i];
}
if (value > specMax) {
return [new ValidationError(key, value, `${value} is greater than the maximum value ${specMax}`)];
}
}
return [];
}

View File

@@ -0,0 +1,71 @@
// @flow
import ValidationError from '../error/validation_error.js';
import getType from '../util/get_type.js';
import validateSpec from './validate.js';
import type {ValidationOptions} from './validate.js';
type Options = ValidationOptions & {
objectElementValidators?: Function;
};
export default function validateObject(options: Options): Array<ValidationError> {
const key = options.key;
const object = options.value;
const elementSpecs = options.valueSpec || {};
const elementValidators = options.objectElementValidators || {};
const style = options.style;
const styleSpec = options.styleSpec;
let errors = [];
const type = getType(object);
if (type !== 'object') {
return [new ValidationError(key, object, `object expected, ${type} found`)];
}
for (const objectKey in object) {
const elementSpecKey = objectKey.split('.')[0]; // treat 'paint.*' as 'paint'
const elementSpec = elementSpecs[elementSpecKey] || elementSpecs['*'];
let validateElement;
if (elementValidators[elementSpecKey]) {
validateElement = elementValidators[elementSpecKey];
} else if (elementSpecs[elementSpecKey]) {
validateElement = validateSpec;
} else if (elementValidators['*']) {
validateElement = elementValidators['*'];
} else if (elementSpecs['*']) {
validateElement = validateSpec;
}
if (!validateElement) {
errors.push(new ValidationError(key, object[objectKey], `unknown property "${objectKey}"`));
continue;
}
errors = errors.concat(validateElement({
key: (key ? `${key}.` : key) + objectKey,
value: object[objectKey],
valueSpec: elementSpec,
style,
styleSpec,
object,
objectKey
// $FlowFixMe[extra-arg]
}, object));
}
for (const elementSpecKey in elementSpecs) {
// Don't check `required` when there's a custom validator for that property.
if (elementValidators[elementSpecKey]) {
continue;
}
if (elementSpecs[elementSpecKey].required && elementSpecs[elementSpecKey]['default'] === undefined && object[elementSpecKey] === undefined) {
errors.push(new ValidationError(key, object, `missing required property "${elementSpecKey}"`));
}
}
return errors;
}

View File

@@ -0,0 +1,10 @@
// @flow
import validateProperty from './validate_property.js';
import type ValidationError from '../error/validation_error.js';
import type {PropertyValidationOptions} from './validate_property.js';
export default function validatePaintProperty(options: PropertyValidationOptions): Array<ValidationError> {
return validateProperty(options, 'paint');
}

View File

@@ -0,0 +1,34 @@
// @flow
import ValidationError from '../error/validation_error.js';
import getType from '../util/get_type.js';
import validate from './validate.js';
import type {ValidationOptions} from './validate.js';
export default function validateProjection(options: ValidationOptions): Array<ValidationError> {
const projection = options.value;
const styleSpec = options.styleSpec;
const projectionSpec = styleSpec.projection;
const style = options.style;
let errors = [];
const rootType = getType(projection);
if (rootType === 'object') {
for (const key in projection) {
errors = errors.concat(validate({
key,
value: projection[key],
valueSpec: projectionSpec[key],
style,
styleSpec
}));
}
} else if (rootType !== 'string') {
errors = errors.concat([new ValidationError('projection', projection, `object or string expected, ${rootType} found`)]);
}
return errors;
}

View File

@@ -0,0 +1,72 @@
// @flow
import validate from './validate.js';
import ValidationError from '../error/validation_error.js';
import getType from '../util/get_type.js';
import {isFunction} from '../function/index.js';
import {unbundle, deepUnbundle} from '../util/unbundle_jsonlint.js';
import {supportsPropertyExpression} from '../util/properties.js';
import type {ValidationOptions} from './validate.js';
export type PropertyValidationOptions = ValidationOptions & {
objectKey: string;
layerType: string;
}
export default function validateProperty(options: PropertyValidationOptions, propertyType: string): Array<ValidationError> {
const key = options.key;
const style = options.style;
const styleSpec = options.styleSpec;
const value = options.value;
const propertyKey = options.objectKey;
const layerSpec = styleSpec[`${propertyType}_${options.layerType}`];
if (!layerSpec) return [];
const transitionMatch = propertyKey.match(/^(.*)-transition$/);
if (propertyType === 'paint' && transitionMatch && layerSpec[transitionMatch[1]] && layerSpec[transitionMatch[1]].transition) {
return validate({
key,
value,
valueSpec: styleSpec.transition,
style,
styleSpec
});
}
const valueSpec = options.valueSpec || layerSpec[propertyKey];
if (!valueSpec) {
return [new ValidationError(key, value, `unknown property "${propertyKey}"`)];
}
let tokenMatch;
if (getType(value) === 'string' && supportsPropertyExpression(valueSpec) && !valueSpec.tokens && (tokenMatch = /^{([^}]+)}$/.exec(value))) {
return [new ValidationError(
key, value,
`"${propertyKey}" does not support interpolation syntax\n` +
`Use an identity property function instead: \`{ "type": "identity", "property": ${JSON.stringify(tokenMatch[1])} }\`.`)];
}
const errors = [];
if (options.layerType === 'symbol') {
if (propertyKey === 'text-field' && style && !style.glyphs) {
errors.push(new ValidationError(key, value, 'use of "text-field" requires a style "glyphs" property'));
}
if (propertyKey === 'text-font' && isFunction(deepUnbundle(value)) && unbundle(value.type) === 'identity') {
errors.push(new ValidationError(key, value, '"text-font" does not support identity functions'));
}
}
return errors.concat(validate({
key: options.key,
value,
valueSpec,
style,
styleSpec,
expressionContext: 'property',
propertyType,
propertyKey
}));
}

View File

@@ -0,0 +1,124 @@
// @flow
import ValidationError from '../error/validation_error.js';
import {unbundle} from '../util/unbundle_jsonlint.js';
import validateObject from './validate_object.js';
import validateEnum from './validate_enum.js';
import validateExpression from './validate_expression.js';
import validateString from './validate_string.js';
import getType from '../util/get_type.js';
import type {ValidationOptions} from './validate.js';
const objectElementValidators = {
promoteId: validatePromoteId
};
export default function validateSource(options: ValidationOptions): Array<ValidationError> {
const value = options.value;
const key = options.key;
const styleSpec = options.styleSpec;
const style = options.style;
if (!value.type) {
return [new ValidationError(key, value, '"type" is required')];
}
const type = unbundle(value.type);
let errors;
switch (type) {
case 'vector':
case 'raster':
case 'raster-dem':
errors = validateObject({
key,
value,
valueSpec: styleSpec[`source_${type.replace('-', '_')}`],
style: options.style,
styleSpec,
objectElementValidators
});
return errors;
case 'geojson':
errors = validateObject({
key,
value,
valueSpec: styleSpec.source_geojson,
style,
styleSpec,
objectElementValidators
});
if (value.cluster) {
for (const prop in value.clusterProperties) {
const [operator, mapExpr] = value.clusterProperties[prop];
const reduceExpr = typeof operator === 'string' ? [operator, ['accumulated'], ['get', prop]] : operator;
errors.push(...validateExpression({
key: `${key}.${prop}.map`,
value: mapExpr,
expressionContext: 'cluster-map'
}));
errors.push(...validateExpression({
key: `${key}.${prop}.reduce`,
value: reduceExpr,
expressionContext: 'cluster-reduce'
}));
}
}
return errors;
case 'video':
return validateObject({
key,
value,
valueSpec: styleSpec.source_video,
style,
styleSpec
});
case 'image':
return validateObject({
key,
value,
valueSpec: styleSpec.source_image,
style,
styleSpec
});
case 'canvas':
return [new ValidationError(key, null, `Please use runtime APIs to add canvas sources, rather than including them in stylesheets.`, 'source.canvas')];
default:
return validateEnum({
key: `${key}.type`,
value: value.type,
valueSpec: {values: getSourceTypeValues(styleSpec)},
style,
styleSpec
});
}
}
function getSourceTypeValues(styleSpec) {
return styleSpec.source.reduce((memo, source) => {
const sourceType = styleSpec[source];
if (sourceType.type.type === 'enum') {
memo = memo.concat(Object.keys(sourceType.type.values));
}
return memo;
}, []);
}
function validatePromoteId({key, value}) {
if (getType(value) === 'string') {
return validateString({key, value});
} else {
const errors = [];
for (const prop in value) {
errors.push(...validateString({key: `${key}.${prop}`, value: value[prop]}));
}
return errors;
}
}

View File

@@ -0,0 +1,18 @@
// @flow
import getType from '../util/get_type.js';
import ValidationError from '../error/validation_error.js';
import type {ValidationOptions} from './validate.js';
export default function validateString(options: $Shape<ValidationOptions>): Array<ValidationError> {
const value = options.value;
const key = options.key;
const type = getType(value);
if (type !== 'string') {
return [new ValidationError(key, value, `string expected, ${type} found`)];
}
return [];
}

View File

@@ -0,0 +1,63 @@
// @flow
import ValidationError from '../error/validation_error.js';
import validate from './validate.js';
import getType from '../util/get_type.js';
import {unbundle} from '../util/unbundle_jsonlint.js';
import type {ValidationOptions} from './validate.js';
export default function validateTerrain(options: ValidationOptions): Array<ValidationError> {
const terrain = options.value;
const key = options.key;
const style = options.style;
const styleSpec = options.styleSpec;
const terrainSpec = styleSpec.terrain;
let errors = [];
const rootType = getType(terrain);
if (terrain === undefined) {
return errors;
} else if (rootType !== 'object') {
errors = errors.concat([new ValidationError('terrain', terrain, `object expected, ${rootType} found`)]);
return errors;
}
for (const key in terrain) {
const transitionMatch = key.match(/^(.*)-transition$/);
if (transitionMatch && terrainSpec[transitionMatch[1]] && terrainSpec[transitionMatch[1]].transition) {
errors = errors.concat(validate({
key,
value: terrain[key],
valueSpec: styleSpec.transition,
style,
styleSpec
}));
} else if (terrainSpec[key]) {
errors = errors.concat(validate({
key,
value: terrain[key],
valueSpec: terrainSpec[key],
style,
styleSpec
}));
} else {
errors = errors.concat([new ValidationError(key, terrain[key], `unknown property "${key}"`)]);
}
}
if (!terrain.source) {
errors.push(new ValidationError(key, terrain, `terrain is missing required property "source"`));
} else {
const source = style.sources && style.sources[terrain.source];
const sourceType = source && unbundle(source.type);
if (!source) {
errors.push(new ValidationError(key, terrain.source, `source "${terrain.source}" not found`));
} else if (sourceType !== 'raster-dem') {
errors.push(new ValidationError(key, terrain.source, `terrain cannot be used with a source of type ${String(sourceType)}, it only be used with a "raster-dem" source type`));
}
}
return errors;
}