Skip to content

Commit 043504e

Browse files
frozenheliumtnagorra
authored andcommitted
Add schema validator for geojson validation
1 parent d318012 commit 043504e

File tree

2 files changed

+137
-33
lines changed

2 files changed

+137
-33
lines changed

manager-dashboard/app/views/NewTutorial/index.tsx

Lines changed: 137 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
isDefined,
55
unique,
66
isNotDefined,
7+
isTruthyString,
78
} from '@togglecorp/fujs';
89
import {
910
useForm,
@@ -63,6 +64,7 @@ import {
6364
PROJECT_TYPE_COMPLETENESS,
6465
PROJECT_TYPE_CHANGE_DETECTION,
6566
PROJECT_TYPE_FOOTPRINT,
67+
ProjectType,
6668
} from '#utils/common';
6769

6870
import {
@@ -81,15 +83,14 @@ import {
8183
MAX_INFO_PAGES,
8284
MAX_OPTIONS,
8385
deleteKey,
86+
TutorialTasksGeoJSON,
8487
} from './utils';
88+
8589
import CustomOptionPreview from './CustomOptionInput/CustomOptionPreview';
8690
import CustomOptionInput from './CustomOptionInput';
8791
import ScenarioPageInput from './ScenarioPageInput';
8892
import InformationPageInput from './InformationPageInput';
8993
import styles from './styles.css';
90-
import { TutorialTasksGeoJSON } from './utils';
91-
import { FootprintProperties } from './utils';
92-
import { BuildAreaProperties, ChangeDetectionProperties } from './utils';
9394

9495
type CustomScreen = Omit<TutorialFormType['scenarioPages'][number], 'scenarioId'>;
9596
function sanitizeScreens(scenarioPages: TutorialFormType['scenarioPages']) {
@@ -355,41 +356,138 @@ function NewTutorial(props: Props) {
355356
geoProps: GeoJSON.GeoJSON | undefined,
356357
) => {
357358
const tutorialTasks = geoProps as TutorialTasksGeoJSON;
358-
if (!tutorialTasks) {
359-
return;
360-
}
359+
function getGeoJSONError() {
360+
if (isNotDefined(tutorialTasks.features) || !Array.isArray(tutorialTasks.features)) {
361+
return 'GeoJson does not contain iterable properties';
362+
}
361363

362-
const everyTaskHasScreenAndReferenceProperty = tutorialTasks.features.every(
363-
(feature) => isDefined(feature.properties.screen) && isDefined(feature.properties.reference),
364-
);
364+
type ValidType ='number' | 'string' | 'boolean';
365+
function checkSchema<T extends object>(
366+
obj: T,
367+
schema: Record<string, ValidType | ValidType[]>,
368+
) {
369+
const schemaKeys = Object.keys(schema);
370+
const errors = schemaKeys.map(
371+
(key) => {
372+
const expectedType = schema[key];
373+
374+
const keySafe = key as keyof T;
375+
const currentValue: unknown = obj[keySafe];
376+
const valueType = typeof currentValue;
377+
378+
if (Array.isArray(expectedType)) {
379+
const indexOfType = expectedType.findIndex(
380+
(type) => type === valueType,
381+
);
382+
if (indexOfType === -1) {
383+
return `type of ${key} expected to be one of type ${expectedType.join(', ')}`;
384+
}
385+
} else if (typeof currentValue !== expectedType) {
386+
return `type of ${key} expected to be of ${expectedType}`;
387+
}
365388

366-
if (!everyTaskHasScreenAndReferenceProperty) {
367-
setError((prevValue) => ({
368-
...getErrorObject(prevValue),
369-
tutorialTasks: 'GeoJson does not contain property "screen" or "reference"',
370-
}));
389+
return undefined;
390+
},
391+
).filter(isDefined);
392+
393+
return errors;
394+
}
395+
396+
if (value?.projectType === PROJECT_TYPE_FOOTPRINT) {
397+
const errors = tutorialTasks.features.map(
398+
(feature) => checkSchema(feature.properties, {
399+
id: ['string', 'number'],
400+
reference: 'number',
401+
screen: 'number',
402+
}).join(', '),
403+
).filter(isTruthyString);
404+
405+
if (errors.length > 0) {
406+
return `Invalid GeoJson for Footprint: ${errors[0]} (${errors.length} total errors)`;
407+
}
408+
}
409+
410+
if (value?.projectType === PROJECT_TYPE_CHANGE_DETECTION) {
411+
const errors = tutorialTasks.features.map(
412+
(feature) => checkSchema(feature.properties, {
413+
reference: 'number',
414+
screen: 'number',
415+
task_id: 'string',
416+
tile_x: 'number',
417+
tile_y: 'number',
418+
tile_z: 'number',
419+
}).join(', '),
420+
).filter(isTruthyString);
421+
422+
if (errors.length > 0) {
423+
// NOTE: only showing errors first error
424+
return `Invalid GeoJson for Change Detection: ${errors[0]} (${errors.length} total errors)`;
425+
}
426+
}
427+
428+
if (value?.projectType === PROJECT_TYPE_BUILD_AREA) {
429+
const errors = tutorialTasks.features.map(
430+
(feature) => checkSchema(feature.properties, {
431+
reference: 'number',
432+
screen: 'number',
433+
task_id: 'string',
434+
tile_x: 'number',
435+
tile_y: 'number',
436+
tile_z: 'number',
437+
}).join(', '),
438+
).filter(isTruthyString);
439+
440+
if (errors.length > 0) {
441+
// NOTE: only showing errors first error
442+
return `Invalid GeoJson for Build Area: ${errors[0]} (${errors.length} total errors)`;
443+
}
444+
}
371445

446+
if (value?.projectType === PROJECT_TYPE_COMPLETENESS) {
447+
const errors = tutorialTasks.features.map(
448+
(feature) => checkSchema(feature.properties, {
449+
reference: 'number',
450+
screen: 'number',
451+
task_id: 'string',
452+
tile_x: 'number',
453+
tile_y: 'number',
454+
tile_z: 'number',
455+
}).join(', '),
456+
).filter(isTruthyString);
457+
458+
if (errors.length > 0) {
459+
return `Invalid GeoJson for Completeness: ${errors[0]} (${errors.length} total errors)`;
460+
}
461+
}
462+
// TODO: validate references
463+
464+
return undefined;
465+
}
466+
467+
if (!tutorialTasks) {
468+
setFieldValue(undefined, 'tutorialTasks');
469+
setFieldValue(undefined, 'scenarioPages');
372470
return;
373471
}
374472

375-
if (value?.projectType === PROJECT_TYPE_COMPLETENESS
376-
|| value?.projectType === PROJECT_TYPE_BUILD_AREA
377-
|| value?.projectType === PROJECT_TYPE_CHANGE_DETECTION) {
378-
379-
tutorialTasks.features.every(
380-
(feature: BuildAreaProperties | ChangeDetectionProperties) => isDefined(feature.properties.task_id),
381-
);
473+
const errors = getGeoJSONError();
474+
if (errors) {
475+
setFieldValue(undefined, 'tutorialTasks');
476+
setFieldValue(undefined, 'scenarioPages');
477+
setError((prevError) => ({
478+
...getErrorObject(prevError),
479+
tutorialTasks: errors,
480+
}));
481+
return;
382482
}
383483

384-
385484
setFieldValue(tutorialTasks, 'tutorialTasks');
386485

387-
// FIXME: we need to validate the geojson here
388-
389-
const uniqueArray = tutorialTasks && unique(
390-
tutorialTasks.features, ((geo) => geo?.properties.screen),
486+
const uniqueArray = unique(
487+
tutorialTasks.features,
488+
((geo) => geo?.properties.screen),
391489
);
392-
const sorted = uniqueArray?.sort((a, b) => a.properties.screen - b.properties.screen);
490+
const sorted = uniqueArray.sort((a, b) => a.properties.screen - b.properties.screen);
393491
const tutorialTaskArray = sorted?.map((geo) => (
394492
{
395493
scenarioId: geo.properties.screen,
@@ -400,10 +498,7 @@ function NewTutorial(props: Props) {
400498
));
401499

402500
setFieldValue(tutorialTaskArray, 'scenarioPages');
403-
404-
}, [setFieldValue]);
405-
406-
console.log(formError);
501+
}, [setFieldValue, setError, value?.projectType]);
407502

408503
const handleAddInformationPage = React.useCallback(
409504
(template: InformationPageTemplateKey) => {
@@ -480,6 +575,16 @@ function NewTutorial(props: Props) {
480575
customOptions,
481576
informationPages,
482577
} = value;
578+
579+
const handleProjectTypeChange = React.useCallback(
580+
(newValue: ProjectType | undefined) => {
581+
setFieldValue(undefined, 'tutorialTasks');
582+
setFieldValue(undefined, 'scenarioPages');
583+
setFieldValue(newValue, 'projectType');
584+
},
585+
[setFieldValue],
586+
);
587+
483588
return (
484589
<div className={_cs(styles.newTutorial, className)}>
485590
<Heading level={1}>
@@ -491,7 +596,7 @@ function NewTutorial(props: Props) {
491596
>
492597
<SegmentInput
493598
name={'projectType' as const}
494-
onChange={setFieldValue}
599+
onChange={handleProjectTypeChange}
495600
value={value.projectType}
496601
label="Project Type"
497602
hint="Select the type of your project."

manager-dashboard/app/views/NewTutorial/utils.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,6 @@ export type TutorialTasksGeoJSON = GeoJSON.FeatureCollection<
341341
BuildAreaProperties | FootprintProperties | ChangeDetectionProperties
342342
>;
343343

344-
345344
export type CustomOptions = {
346345
optionId: number; // we clear this before sending to server
347346
title: string;

0 commit comments

Comments
 (0)