add schema validator

This commit is contained in:
Bowen Tan 2021-12-10 17:42:04 +08:00
parent c910f635b3
commit 30d1932d2e
10 changed files with 1996 additions and 2 deletions

View File

@ -37,6 +37,8 @@
"@emotion/styled": "^11",
"@sunmao-ui/core": "^0.3.2",
"@sunmao-ui/runtime": "^0.3.6",
"ajv": "^8.8.2",
"ajv-formats": "^2.1.1",
"codemirror": "^5.63.3",
"formik": "^2.2.9",
"framer-motion": "^4",

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,9 @@ import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
import { Editor } from './components/Editor';
import { UserCenterApp } from './constants';
import { App as _App, registry, stateManager, ui } from './setup';
import { SchemaValidator, rules } from './validator';
type Options = Partial<{
components: Parameters<Registry['registerComponent']>[0][];
@ -66,3 +68,7 @@ export default function renderApp(options: Options = {}) {
container
);
}
(window as any).validator = new SchemaValidator(UserCenterApp);
(window as any).validator.addRules(rules);
console.log('validResult', (window as any).validator.validate());

View File

@ -0,0 +1,106 @@
import { ApplicationComponent, Application } from '@sunmao-ui/core';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import { registry } from '../setup';
import {
ISchemaValidator,
ComponentValidatorRule,
AllComponentsValidatorRule,
TraitValidatorRule,
ValidatorRule,
} from './interfaces';
import { ValidateResult } from './ValidateResult';
export class SchemaValidator implements ISchemaValidator {
private components: ApplicationComponent[];
private traitRules: TraitValidatorRule[] = [];
private componentRules: ComponentValidatorRule[] = [];
private allComponentsRules: AllComponentsValidatorRule[] = [];
private result: ValidateResult[] = [];
private ajv: Ajv;
constructor(private app: Application) {
this.components = this.app.spec.components;
this.ajv = addFormats(new Ajv({}), [
'date-time',
'time',
'date',
'email',
'hostname',
'ipv4',
'ipv6',
'uri',
'uri-reference',
'uuid',
'uri-template',
'json-pointer',
'relative-json-pointer',
'regex',
])
.addKeyword('kind')
.addKeyword('modifier');
}
addRules(rules: ValidatorRule[]) {
rules.forEach(rule => {
switch (rule.kind) {
case 'component':
this.componentRules.push(rule);
break;
case 'allComponents':
this.allComponentsRules.push(rule);
break;
case 'trait':
this.traitRules.push(rule);
break;
}
});
}
validate() {
this.result = [];
this.allComponentsRules.forEach(rule => {
const r = rule.validate({ components: this.components, ajv: this.ajv });
if (r.length > 0) {
this.result = this.result.concat(r);
}
});
this.componentRules.forEach(rule => {
this.components.forEach(component => {
const r = rule.validate({
component,
components: this.components,
registry,
ajv: this.ajv,
});
if (r.length > 0) {
this.result = this.result.concat(r);
}
});
});
this.traitRules.forEach(rule => {
this.components.forEach(component => {
component.traits.forEach(trait => {
const r = rule.validate({
trait,
component,
components: this.components,
registry,
ajv: this.ajv,
});
if (r.length > 0) {
this.result = this.result.concat(r);
}
});
});
});
return this.result;
}
fix() {
this.result.forEach(r => {
r.fix();
});
return this.components;
}
}

View File

@ -0,0 +1,16 @@
export class ValidateResult {
position = {
componentId: '',
traitIndex: -1,
};
isValid = false
constructor(
public message: string,
componentId = '',
traitIndex = -1,
public fix: () => void = () => undefined
) {
this.position.componentId = componentId;
this.position.traitIndex = traitIndex;
}
}

View File

@ -0,0 +1,4 @@
export * from './SchemaValidator';
export * from './interfaces';
export * from './rules';
export * from './ValidateResult';

View File

@ -0,0 +1,66 @@
import { ApplicationComponent, ComponentTrait } from '@sunmao-ui/core';
import { Registry } from '@sunmao-ui/runtime';
import Ajv from 'ajv';
import { ValidateResult } from './ValidateResult';
export interface ComponentValidateContext {
component: ApplicationComponent;
components: ApplicationComponent[];
registry: Registry;
ajv: Ajv;
}
export interface TraitValidateContext {
trait: ComponentTrait;
component: ApplicationComponent;
components: ApplicationComponent[];
registry: Registry;
ajv: Ajv;
}
export interface AllComponentsValidateContext {
components: ApplicationComponent[];
ajv: Ajv;
}
export type ValidateContext =
| ComponentValidateContext
| TraitValidateContext
| AllComponentsValidateContext;
export interface ComponentValidatorRule {
kind: 'component';
validate: (validateContext: ComponentValidateContext) => ValidateResult[];
// fix: (
// component: ApplicationComponent,
// components: ApplicationComponent[]
// ) => ApplicationComponent;
}
export interface AllComponentsValidatorRule {
kind: 'allComponents';
validate: (validateContext: AllComponentsValidateContext) => ValidateResult[];
// fix: (components: ApplicationComponent[]) => ApplicationComponent[];
}
export interface TraitValidatorRule {
kind: 'trait';
validate: (validateContext: TraitValidateContext) => ValidateResult[];
// fix: (
// trait: ComponentTrait,
// component: ApplicationComponent,
// components: ApplicationComponent[]
// ) => ApplicationComponent;
}
export type ValidatorRule =
| ComponentValidatorRule
| AllComponentsValidatorRule
| TraitValidatorRule;
export interface ISchemaValidator {
addRules: (rule: ValidatorRule[]) => void;
validate: () => ValidateResult[];
fix: () => void;
}
export const DefaultValidateResult = new ValidateResult('ok', '', -1, () => undefined);

View File

@ -0,0 +1,153 @@
import {
AllComponentsValidatorRule,
ComponentValidatorRule,
TraitValidatorRule,
AllComponentsValidateContext,
ComponentValidateContext,
TraitValidateContext,
} from './interfaces';
import { ValidateResult } from './ValidateResult';
export class RepeatIdValidatorRule implements AllComponentsValidatorRule {
kind: 'allComponents' = 'allComponents';
validate({ components }: AllComponentsValidateContext): ValidateResult[] {
const componentIds = new Set<string>();
const results: ValidateResult[] = [];
components.forEach(component => {
if (componentIds.has(component.id)) {
results.push(
new ValidateResult('Duplicate component id', component.id, 0, () => {
component.id = `${component.id}_${Math.floor(Math.random() * 10000)}`;
})
);
} else {
componentIds.add(component.id);
}
});
return results;
}
}
export class ParentValidatorRule implements AllComponentsValidatorRule {
kind: 'allComponents' = 'allComponents';
validate({ components }: AllComponentsValidateContext): ValidateResult[] {
const results: ValidateResult[] = [];
const componentIds = components.map(component => component.id);
components.forEach(c => {
const slotTraitIndex = c.traits.findIndex(t => t.type === 'core/v1/slot');
const slotTrait = c.traits[slotTraitIndex];
if (slotTrait) {
const { id: parentId } = slotTrait.properties.container as any;
if (!componentIds.includes(parentId)) {
results.push(
new ValidateResult(
`Cannot find parent component: ${parentId}.`,
c.id,
slotTraitIndex,
() => {
slotTrait.properties.container = {
id: componentIds[0],
slot: 'content',
};
}
)
);
}
}
});
return results;
}
}
export class ComponentPropertyValidatorRule implements ComponentValidatorRule {
kind: 'component' = 'component';
validate({ component, registry, ajv }: ComponentValidateContext): ValidateResult[] {
const results: ValidateResult[] = [];
const spec = registry.getComponentByType(component.type);
if (!spec) {
results.push(
new ValidateResult(`Cannot find component spec: ${component.type}.`, component.id)
);
return results;
}
const propertySchema = spec.spec.properties;
const regExp = new RegExp('.*{{.*}}.*');
const validate = ajv.compile(propertySchema);
const valid = validate(component.properties);
if (!valid) {
validate.errors!.forEach(error => {
let errorMsg = error.message;
if (error.keyword === 'type') {
const { instancePath } = error;
const path = instancePath.split('/')[1];
const value = component.properties[path];
if (typeof value === 'string' && regExp.test(value)) {
return;
} else {
errorMsg = `${error.instancePath} ${error.message}`;
}
}
results.push(new ValidateResult(errorMsg || '', component.id));
});
}
return results;
}
}
export class TraitPropertyValidatorRule implements TraitValidatorRule {
kind: 'trait' = 'trait';
validate({ trait, component, registry, ajv }: TraitValidateContext): ValidateResult[] {
const results: ValidateResult[] = [];
const spec = registry.getTraitByType(trait.type);
const traitIndex = component.traits.indexOf(trait);
if (!spec) {
results.push(
new ValidateResult(
`Cannot find trait spec: ${trait.type}.`,
component.id,
traitIndex
)
);
return results;
}
const propertySchema = spec.spec.properties;
const regExp = new RegExp('.*{{.*}}.*');
const validate = ajv.compile(propertySchema);
const valid = validate(trait.properties);
if (!valid) {
validate.errors!.forEach(error => {
let errorMsg = error.message;
if (error.keyword === 'type') {
const { instancePath } = error;
const path = instancePath.split('/')[1];
const value = trait.properties[path];
if (typeof value === 'string' && regExp.test(value)) {
return;
} else {
errorMsg = `${error.instancePath} ${error.message}`;
}
}
results.push(new ValidateResult(errorMsg || '', component.id, traitIndex));
});
}
return results;
}
}
export const rules = [
new RepeatIdValidatorRule(),
new ParentValidatorRule(),
new ComponentPropertyValidatorRule(),
new TraitPropertyValidatorRule(),
];

View File

@ -6,7 +6,7 @@ export const EventHandlerSchema = Type.Object(
componentId: Type.String(),
method: Type.Object({
name: Type.String(),
parameters: Type.Record(Type.String(), Type.String()),
parameters: Type.Record(Type.String(), Type.Any()),
}),
wait: Type.Optional(
Type.Object({
@ -22,7 +22,7 @@ export const EventHandlerSchema = Type.Object(
),
disabled: Type.Optional(Type.Boolean()),
},
{ $id: 'eventHanlder' }
{ description: 'eventHanlder' }
);
export const FetchTraitPropertiesSchema = Type.Object({

View File

@ -3368,6 +3368,13 @@ aggregate-error@^3.0.0:
clean-stack "^2.0.0"
indent-string "^4.0.0"
ajv-formats@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520"
integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==
dependencies:
ajv "^8.0.0"
ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4:
version "6.12.6"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
@ -3378,6 +3385,16 @@ ajv@^6.10.0, ajv@^6.12.3, ajv@^6.12.4:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
ajv@^8.0.0, ajv@^8.8.2:
version "8.8.2"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.8.2.tgz#01b4fef2007a28bf75f0b7fc009f62679de4abbb"
integrity sha512-x9VuX+R/jcFj1DHo/fCp99esgGDWiHENrKxaCENuCxpoMCmAt/COCGVDwA7kleEpEzJjDnvh3yGoOuLu0Dtllw==
dependencies:
fast-deep-equal "^3.1.1"
json-schema-traverse "^1.0.0"
require-from-string "^2.0.2"
uri-js "^4.2.2"
ajv@^8.0.1:
version "8.6.3"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.6.3.tgz#11a66527761dc3e9a3845ea775d2d3c0414e8764"