UNPKG

@mapbox/mapbox-gl-style-spec

Version:

a specification for mapbox gl styles

192 lines (163 loc) 7.02 kB
import {default as ValidationError, ValidationWarning} from '../error/validation_error'; import {unbundle, deepUnbundle} from '../util/unbundle_jsonlint'; import validateObject from './validate_object'; import validateEnum from './validate_enum'; import validateExpression from './validate_expression'; import validateString from './validate_string'; import {getType, isObject, isString} from '../util/get_type'; import {createExpression} from '../expression/index'; import * as isConstant from '../expression/is_constant'; import type {StyleReference} from '../reference/latest'; import type {StyleSpecification} from '../types'; const objectElementValidators = { promoteId: validatePromoteId }; type SourceValidatorOptions = { key: string; value: unknown; style: Partial<StyleSpecification>; styleSpec: StyleReference; }; export default function validateSource(options: SourceValidatorOptions): ValidationError[] { const value = options.value; const key = options.key; const styleSpec = options.styleSpec; const style = options.style; if (!isObject(value)) { return [new ValidationError(key, value, `object expected, ${getType(value)} found`)]; } if (!('type' in value)) { return [new ValidationError(key, value, '"type" is required')]; } const type = unbundle(value.type) as string; let errors: ValidationError[] = []; if (['vector', 'raster', 'raster-dem', 'raster-array'].includes(type)) { if (!('url' in value) && !('tiles' in value)) { errors.push(new ValidationWarning(key, value, 'Either "url" or "tiles" is required.')); } } switch (type) { case 'vector': case 'raster': case 'raster-dem': case 'raster-array': errors = errors.concat(validateObject({ key, value, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment valueSpec: styleSpec[`source_${type.replace('-', '_')}`], style: options.style, styleSpec, objectElementValidators })); return errors; case 'geojson': errors = validateObject({ key, value, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment valueSpec: styleSpec.source_geojson, style, styleSpec, objectElementValidators }); if ('cluster' in value && 'clusterProperties' in value) { if (!isObject(value.clusterProperties)) { return [new ValidationError(`${key}.clusterProperties`, value, `object expected, ${getType(value)} found`)]; } for (const prop in value.clusterProperties) { const propValue = value.clusterProperties[prop]; if (!Array.isArray(propValue)) { return [new ValidationError(`${key}.clusterProperties.${prop}`, propValue, 'array expected')]; } // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const [operator, mapExpr] = propValue; // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 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, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment valueSpec: styleSpec.source_video, style, styleSpec }); case 'image': return validateObject({ key, value, // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 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 as {type: unknown}).type, valueSpec: {values: getSourceTypeValues(styleSpec)} }); } } function getSourceTypeValues(styleSpec: StyleReference): string[] { const sourceArray = styleSpec.source as string[]; return sourceArray.reduce((memo: string[], source: string) => { const sourceType = (styleSpec as Record<string, unknown>)[source] as {type: {type: string; values?: Record<string, unknown>}}; if (sourceType.type.type === 'enum') { memo = memo.concat(Object.keys(sourceType.type.values || {})); } return memo; }, []); } type PromoteIdValidatorOptions = { key: string; value: unknown; }; function validatePromoteId({key, value}: PromoteIdValidatorOptions) { if (isString(value)) { return validateString({key, value}); } if (Array.isArray(value)) { const errors: ValidationError[] = []; const unbundledValue = deepUnbundle(value); const expression = createExpression(unbundledValue); if (expression.result === 'error') { expression.value.forEach((err) => { errors.push(new ValidationError(`${key}${err.key}`, null, `${err.message}`)); }); } // @ts-expect-error - TS2339: Property 'expression' does not exist on type 'ParsingError[] | StyleExpression'. // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const parsed = expression.value.expression; // eslint-disable-next-line @typescript-eslint/no-unsafe-argument const onlyFeatureDependent = isConstant.isGlobalPropertyConstant(parsed, ['zoom', 'heatmap-density', 'line-progress', 'raster-value', 'sky-radial-progress', 'accumulated', 'is-supported-script', 'pitch', 'distance-from-center', 'measure-light', 'raster-particle-speed']); if (!onlyFeatureDependent) { errors.push(new ValidationError(`${key}`, null, 'promoteId expression should be only feature dependent')); } return errors; } if (!isObject(value)) { return [new ValidationError(key, value, `string, expression or object expected, "${getType(value)}" found`)]; } const errors: ValidationError[] = []; for (const prop in (value as object)) { errors.push(...validatePromoteId({key: `${key}.${prop}`, value: value[prop]})); } return errors; }