Skip to content

Commit

Permalink
2288-Input and internal config validation (#2289)
Browse files Browse the repository at this point in the history
  • Loading branch information
ychoquet authored Jun 26, 2024
1 parent e5d7d14 commit 2b421fb
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 22 deletions.
7 changes: 6 additions & 1 deletion packages/geoview-core/src/api/config/config-api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import cloneDeep from 'lodash/cloneDeep';

import { CV_DEFAULT_MAP_FEATURE_CONFIG, CV_CONFIG_GEOCORE_TYPE } from '@config/types/config-constants';
import { Cast, TypeJsonValue, TypeJsonObject, toJsonObject, TypeJsonArray } from '@config/types/config-types';
import { MapFeatureConfig } from '@config/types/classes/map-feature-config';
Expand Down Expand Up @@ -341,12 +343,15 @@ export class ConfigApi {
// doesn't accept string config. Note that convertStringToJson returns undefined if the string config cannot
// be translated to a json object.
const providedMapFeatureConfig: TypeJsonObject | undefined =
typeof mapConfig === 'string' ? ConfigApi.#convertStringToJson(mapConfig as string) : (mapConfig as TypeJsonObject);
// We clone to prevent modifications from leaking back to the user object.
typeof mapConfig === 'string' ? ConfigApi.#convertStringToJson(mapConfig as string) : (cloneDeep(mapConfig) as TypeJsonObject);

try {
// If the user provided a valid string config with the mandatory map property, process geocore layers to translate them to their GeoView layers
if (!providedMapFeatureConfig) throw new MapConfigError('The string configuration provided cannot be translated to a json object');
if (!providedMapFeatureConfig.map) throw new MapConfigError('The map property is mandatory');
providedMapFeatureConfig.map.listOfGeoviewLayerConfig = (providedMapFeatureConfig.map.listOfGeoviewLayerConfig ||
[]) as TypeJsonObject;

const inputLength = providedMapFeatureConfig.map.listOfGeoviewLayerConfig.length;
providedMapFeatureConfig.map.listOfGeoviewLayerConfig = (await ConfigApi.convertGeocoreToGeoview(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EsriDynamicLayerEntryConfig } from '@config/types/classes/sub-layer-con
import { GroupLayerEntryConfig } from '@config/types/classes/sub-layer-config/group-layer-entry-config';
import { TypeJsonObject } from '@config/types/config-types';
import { TypeDisplayLanguage, TypeLayerInitialSettings } from '@config/types/map-schema-types';
import { isvalidComparedToSchema } from '@config/utils';
import { isvalidComparedToInputSchema, isvalidComparedToInternalSchema } from '@config/utils';
import { MapFeatureConfig } from '@config/types/classes/map-feature-config';
import { AbstractGeoviewEsriLayerConfig } from '@config/types/classes/geoview-config/abstract-geoview-esri-layer-config';
import { EntryConfigBaseClass } from '@/api/config/types/classes/sub-layer-config/entry-config-base-class';
Expand All @@ -28,8 +28,11 @@ export class EsriDynamicLayerConfig extends AbstractGeoviewEsriLayerConfig {
*/
constructor(layerConfig: TypeJsonObject, language: TypeDisplayLanguage, mapFeatureConfig?: MapFeatureConfig) {
super(layerConfig, language, mapFeatureConfig);
// Input schema validation.
if (!isvalidComparedToSchema(this.geoviewLayerSchema, layerConfig)) this.setErrorDetectedFlag();

// validate the structure
if (!isvalidComparedToInputSchema(this.geoviewLayerSchema, layerConfig)) this.setErrorDetectedFlag();
if (!isvalidComparedToInternalSchema(this.geoviewLayerSchema, this, true)) this.setErrorDetectedFlag();
// validate the values.
this.validate();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { EsriFeatureLayerEntryConfig } from '@config/types/classes/sub-layer-con
import { GroupLayerEntryConfig } from '@config/types/classes/sub-layer-config/group-layer-entry-config';
import { TypeJsonObject } from '@config/types/config-types';
import { TypeDisplayLanguage, TypeLayerInitialSettings } from '@config/types/map-schema-types';
import { isvalidComparedToSchema } from '@config/utils';
import { isvalidComparedToInputSchema, isvalidComparedToInternalSchema } from '@config/utils';
import { MapFeatureConfig } from '@config/types/classes/map-feature-config';
import { AbstractGeoviewEsriLayerConfig } from '@config/types/classes/geoview-config/abstract-geoview-esri-layer-config';
import { EntryConfigBaseClass } from '@/api/config/types/classes/sub-layer-config/entry-config-base-class';
Expand All @@ -28,8 +28,11 @@ export class EsriFeatureLayerConfig extends AbstractGeoviewEsriLayerConfig {
*/
constructor(layerConfig: TypeJsonObject, language: TypeDisplayLanguage, mapFeatureConfig?: MapFeatureConfig) {
super(layerConfig, language, mapFeatureConfig);
// Input schema validation.
if (!isvalidComparedToSchema(this.geoviewLayerSchema, layerConfig)) this.setErrorDetectedFlag();

// validate the structure
if (!isvalidComparedToInputSchema(this.geoviewLayerSchema, layerConfig)) this.setErrorDetectedFlag();
if (!isvalidComparedToInternalSchema(this.geoviewLayerSchema, this, true)) this.setErrorDetectedFlag();
// validate the values.
this.validate();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
VALID_PROJECTION_CODES,
CV_MAP_CENTER,
} from '@config/types/config-constants';
import { isvalidComparedToSchema } from '@config/utils';
import { isvalidComparedToInputSchema, isvalidComparedToInternalSchema } from '@config/utils';
import {
Extent,
TypeAppBarProps,
Expand Down Expand Up @@ -96,7 +96,7 @@ export class MapFeatureConfig {
*/
constructor(providedMapFeatureConfig: TypeJsonObject, language: TypeDisplayLanguage) {
// Input schema validation.
this.#errorDetected = !isvalidComparedToSchema(CV_MAP_CONFIG_SCHEMA_PATH, providedMapFeatureConfig);
this.#errorDetected = !isvalidComparedToInputSchema(CV_MAP_CONFIG_SCHEMA_PATH, providedMapFeatureConfig);

this.#language = language;
// set map configuration
Expand Down Expand Up @@ -130,6 +130,7 @@ export class MapFeatureConfig {
this.schemaVersionUsed =
(providedMapFeatureConfig.schemaVersionUsed as TypeValidVersions) || CV_DEFAULT_MAP_FEATURE_CONFIG.schemaVersionUsed;
if (this.#errorDetected) this.#makeMapConfigValid(providedMapFeatureConfig); // Tries to apply a correction to invalid properties
if (!isvalidComparedToInternalSchema(CV_MAP_CONFIG_SCHEMA_PATH, this)) this.setErrorDetectedFlag();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CV_CONST_SUB_LAYER_TYPES, CV_CONST_LEAF_LAYER_SCHEMA_PATH } from '@conf
import { Cast, TypeJsonObject } from '@config/types/config-types';
import { AbstractGeoviewLayerConfig } from '@config/types/classes/geoview-config/abstract-geoview-layer-config';
import { AbstractBaseEsriLayerEntryConfig } from '@config/types/classes/sub-layer-config/abstract-base-esri-layer-entry-config';
import { isvalidComparedToSchema } from '@config/utils';
import { isvalidComparedToInputSchema, isvalidComparedToInternalSchema } from '@config/utils';
import {
TypeStyleConfig,
TypeLayerEntryType,
Expand Down Expand Up @@ -48,8 +48,9 @@ export class EsriDynamicLayerEntryConfig extends AbstractBaseEsriLayerEntryConfi
this.setErrorDetectedFlag();
throw new GeoviewLayerInvalidParameterError('LayerIdInvalidType', [this.layerPath]);
}
// Input schema validation. When the entryType property is undefined, isvalidComparedToSchema uses the input schema.
if (!isvalidComparedToSchema(this.schemaPath, layerConfig)) this.setErrorDetectedFlag();
// Validate the structure
if (!isvalidComparedToInputSchema(this.schemaPath, layerConfig)) this.setErrorDetectedFlag();
if (!isvalidComparedToInternalSchema(this.schemaPath, this, true)) this.setErrorDetectedFlag();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { CV_CONST_SUB_LAYER_TYPES, CV_CONST_LEAF_LAYER_SCHEMA_PATH } from '@conf
import { Cast, TypeJsonObject } from '@config/types/config-types';
import { AbstractGeoviewLayerConfig } from '@config/types/classes/geoview-config/abstract-geoview-layer-config';
import { AbstractBaseEsriLayerEntryConfig } from '@config/types/classes/sub-layer-config/abstract-base-esri-layer-entry-config';
import { isvalidComparedToSchema } from '@config/utils';
import { isvalidComparedToInputSchema, isvalidComparedToInternalSchema } from '@config/utils';
import {
TypeStyleConfig,
TypeLayerEntryType,
Expand Down Expand Up @@ -48,8 +48,9 @@ export class EsriFeatureLayerEntryConfig extends AbstractBaseEsriLayerEntryConfi
this.setErrorDetectedFlag();
throw new GeoviewLayerInvalidParameterError('LayerIdInvalidType', [this.layerPath]);
}
// Input schema validation. When the entryType property is undefined, isvalidComparedToSchema uses the input schema.
if (!isvalidComparedToSchema(this.schemaPath, layerConfig)) this.setErrorDetectedFlag();
// Validate the structure
if (!isvalidComparedToInputSchema(this.schemaPath, layerConfig)) this.setErrorDetectedFlag();
if (!isvalidComparedToInternalSchema(this.schemaPath, this, true)) this.setErrorDetectedFlag();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,10 @@
"description": "The id of the layer for referencing within the viewer (does not relate directly to any external service). The id will have the language extension (id-'lang').",
"type": "string"
},
"useInternalSchema": {
"description": "Flag used to select the type of schema validation to apply (input/internal).",
"type": "boolean"
},
"geoviewLayerName": {
"description": "The display name of the layer (English/French). If it is not present the viewer will make an attempt to scrape this information.",
"oneOf": [
Expand Down Expand Up @@ -752,6 +756,10 @@
"description": "The id of the layer to display on the map.",
"type": "string"
},
"useInternalSchema": {
"description": "Flag used to select the type of schema validation to apply (input/internal).",
"type": "boolean"
},
"layerName": {
"description": "The display name of the layer (English/French). If it is not present the viewer will make an attempt to scrape this information.",
"oneOf": [
Expand Down
36 changes: 29 additions & 7 deletions packages/geoview-core/src/api/config/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import Ajv from 'ajv';
import cloneDeep from 'lodash/cloneDeep';

import { CV_CONST_SUB_LAYER_TYPES, CV_CONST_LAYER_TYPES } from '@config/types/config-constants';
import { TypeJsonObject } from '@config/types/config-types';
import schema from '@config/types/config-validation-schema.json';
import { MapFeatureConfig } from '@config/types/classes/map-feature-config';
import { TypeGeoviewLayerType, TypeLayerEntryType, TypeLocalizedString } from '@config/types/map-schema-types';
import { EntryConfigBaseClass } from '@/api/config/types/classes/sub-layer-config/entry-config-base-class';

import { logger } from '@/core/utils/logger';

type NewType = TypeGeoviewLayerType;
Expand Down Expand Up @@ -39,17 +41,13 @@ export const convertLayerTypeToEntry = (layerType: NewType): TypeLayerEntryType

/**
* Validate a section of the configuration against the schema identified by the schema path.
*
* Since the useInternalSchema is never provided by the users and set internally before the
* call to this method, we use it as a flag to indicate the schema type (input/internal) to
* use for the schema validation.
*
* @param {string} schemaPath The path to the schema section to use for the validation.
* @param {object} targetObject The map feature configuration to validate.
*
* @returns {boolean} A boolean indicating that the schema section is valid (true) or invalide (false).
*/
export function isvalidComparedToSchema(schemaPath: string, targetObject: object): boolean {
export function isvalidComparedToInputSchema(schemaPath: string, targetObject: object): boolean {
// create a validator object
const validator = new Ajv({
strict: false,
Expand Down Expand Up @@ -77,7 +75,6 @@ export function isvalidComparedToSchema(schemaPath: string, targetObject: object
}
logger.logWarning('='.repeat(200), '\nSchema error: ', error, '\nObject affected: ', node);
}
(targetObject as MapFeatureConfig | EntryConfigBaseClass)?.setErrorDetectedFlag?.();
return false;
}
return true;
Expand All @@ -87,6 +84,31 @@ export function isvalidComparedToSchema(schemaPath: string, targetObject: object
return false;
}

/**
* Validate a section of the configuration against the schema identified by the schema path.
* The internal schema is used internally by the viewer when we instanciate or modify a configuration object
* to make sure nothing has been broken and prove that the GeoView metadata are conform.
*
* Since the useInternalSchema is never provided by the users and set internally before the
* validation call, we use it as a flag to indicate we want to use the internal schema type
* for the schema validation.
*
* The addInternalFlag must be set to true when we want to validate a GeoView layer or a sublayer.
* All other types have the same definition for the input and internal schemas.
*
* @param {string} schemaPath The path to the schema section to use for the validation.
* @param {object} targetObject The map feature configuration to validate.
* @param {boolean} useInternalSchema Adds useInternalSchema flag to the object to be validated.
*
* @returns {boolean} A boolean indicating that the schema section is valid (true) or invalide (false).
*/
export function isvalidComparedToInternalSchema(schemaPath: string, targetObject: object, useInternalSchema = false): boolean {
// The clone operation copies only the public properties, no private using #.
const targetObjectToValidate: object = cloneDeep(targetObject);
if (useInternalSchema) Object.assign(targetObjectToValidate, { useInternalSchema });
return isvalidComparedToInputSchema(schemaPath, targetObjectToValidate);
}

/**
* Normalize the localized string parameter. If a language is set and the other is not, the undefined language is set to
* the value of the other.
Expand Down

0 comments on commit 2b421fb

Please sign in to comment.