mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2025-04-18 22:00:22 +08:00
add schema validator
This commit is contained in:
parent
c910f635b3
commit
30d1932d2e
@ -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
@ -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());
|
||||
|
106
packages/editor/src/validator/SchemaValidator.ts
Normal file
106
packages/editor/src/validator/SchemaValidator.ts
Normal 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;
|
||||
}
|
||||
}
|
16
packages/editor/src/validator/ValidateResult.ts
Normal file
16
packages/editor/src/validator/ValidateResult.ts
Normal 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;
|
||||
}
|
||||
}
|
4
packages/editor/src/validator/index.ts
Normal file
4
packages/editor/src/validator/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export * from './SchemaValidator';
|
||||
export * from './interfaces';
|
||||
export * from './rules';
|
||||
export * from './ValidateResult';
|
66
packages/editor/src/validator/interfaces.ts
Normal file
66
packages/editor/src/validator/interfaces.ts
Normal 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);
|
153
packages/editor/src/validator/rules.ts
Normal file
153
packages/editor/src/validator/rules.ts
Normal 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(),
|
||||
];
|
@ -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({
|
||||
|
17
yarn.lock
17
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user