@mapbox/mapbox-gl-style-spec
Version:
a specification for mapbox gl styles
195 lines (179 loc) • 9.28 kB
text/typescript
import ValidationError from '../error/validation_error';
import {unbundle} from '../util/unbundle_jsonlint';
import validateArray from './validate_array';
import validateObject from './validate_object';
import validateFilter from './validate_filter';
import validateAppearance, {type AppearanceValidatorOptions} from './validate_appearance';
import validatePaintProperty from './validate_paint_property';
import validateLayoutProperty from './validate_layout_property';
import validateSpec from './validate';
import {isObject, isString} from '../util/get_type';
import type {StyleReference} from '../reference/latest';
import type {StyleSpecification, LayerSpecification, GeoJSONSourceSpecification} from '../types';
type LayerValidatorOptions = {
key: string;
value: unknown;
style: Partial<StyleSpecification>;
styleSpec: StyleReference;
arrayIndex: number;
};
export default function validateLayer(options: LayerValidatorOptions): ValidationError[] {
let errors: ValidationError[] = [];
const layer = options.value;
const key = options.key;
const style = options.style;
const styleSpec = options.styleSpec;
if (!isObject(layer)) {
return [new ValidationError(key, layer, `object expected`)];
}
if (!layer.type && !layer.ref) {
errors.push(new ValidationError(key, layer, 'either "type" or "ref" is required'));
}
let type = unbundle(layer.type) as string;
const ref = unbundle(layer.ref);
if (layer.id) {
const layerId = unbundle(layer.id) as string;
for (let i = 0; i < options.arrayIndex; i++) {
const otherLayer = style.layers[i];
if (unbundle(otherLayer.id) === layerId) {
errors.push(new ValidationError(key, layer.id, `duplicate layer id "${layerId}", previously used at line ${(otherLayer.id as {__line__?: number}).__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`));
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
} else if (parent.ref) {
errors.push(new ValidationError(key, layer.ref, 'ref cannot reference another ref layer'));
} else {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
type = unbundle(parent.type) as string;
}
} else if (!(type === 'background' || type === 'sky' || type === 'slot')) {
if (!layer.source) {
errors.push(new ValidationError(key, layer, 'missing required property "source"'));
} else if (!isString(layer.source)) {
errors.push(new ValidationError(`${key}.source`, layer.source, '"source" must be a string'));
} 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 as string}" requires a raster source`));
} else if (sourceType === 'raster' && type !== 'raster') {
errors.push(new ValidationError(key, layer.source, `layer "${layer.id as string}" requires a vector source`));
} else if (sourceType === 'vector' && !layer['source-layer']) {
errors.push(new ValidationError(key, layer, `layer "${layer.id as string}" 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 (sourceType === 'raster-array' && !['raster', 'raster-particle'].includes(type)) {
errors.push(new ValidationError(key, layer.source, `raster-array source can only be used with layer type \'raster\'.`));
} else if (type === 'line' && layer.paint && (layer.paint['line-gradient'] || layer.paint['line-trim-offset']) &&
(sourceType === 'geojson' && !(source as GeoJSONSourceSpecification).lineMetrics)) {
errors.push(new ValidationError(key, layer, `layer "${layer.id as string}" specifies a line-gradient, which requires the GeoJSON source to have \`lineMetrics\` enabled.`));
} else if (type === 'raster-particle' && sourceType !== 'raster-array') {
errors.push(new ValidationError(key, layer.source, `layer "${layer.id as string}" requires a \'raster-array\' source.`));
}
}
}
errors = errors.concat(validateObject({
key,
value: layer,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
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,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
valueSpec: styleSpec.layer.type,
style: options.style,
styleSpec: options.styleSpec,
object: layer,
objectKey: 'type'
});
},
filter(options) {
return validateFilter(Object.assign({layerType: type}, options));
},
layout(options) {
return validateObject({
layer: layer as LayerSpecification,
key: options.key,
value: options.value,
valueSpec: {},
style: options.style,
styleSpec: options.styleSpec,
objectElementValidators: {
'*'(options) {
return validateLayoutProperty(Object.assign({layerType: type}, options));
}
}
});
},
paint(options) {
return validateObject({
layer: layer as LayerSpecification,
key: options.key,
value: options.value,
valueSpec: {},
style: options.style,
styleSpec: options.styleSpec,
objectElementValidators: {
'*'(options) {
return validatePaintProperty(Object.assign({layerType: type, layer}, options));
}
}
});
},
appearances(options) {
const validationErrors = validateArray({
key: options.key,
value: options.value,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
valueSpec: options.valueSpec,
style: options.style,
styleSpec: options.styleSpec,
arrayElementValidator: (options) => validateAppearance(Object.assign({layerType: type, layer}, options) as AppearanceValidatorOptions)
});
// Check non-repeated names on a given layer
const appearances = Array.isArray(options.value) ? options.value : [];
const dedupedNames = new Set<string>();
appearances.forEach((a, index) => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const name: string | undefined = unbundle(a.name) as string | undefined;
if (name) {
if (dedupedNames.has(name)) {
const layerId = unbundle((layer as LayerSpecification).id) as string;
validationErrors.push(new ValidationError(options.key, name, `Duplicated appearance name "${name}" for layer "${layerId}"`));
} else {
dedupedNames.add(name);
}
}
});
return validationErrors;
}
}
}));
return errors;
}