refactor validator

This commit is contained in:
Bowen Tan 2021-12-27 10:47:16 +08:00
parent 3cbf92b720
commit 26300bff0c
9 changed files with 135 additions and 147 deletions

View File

@ -15,7 +15,7 @@ export class ApplicationModel implements IApplicationModel {
// modules: IModuleModel[] = [];
private schema: ApplicationComponent[] = [];
private componentMap: Record<ComponentId, IComponentModel> = {};
private componentsCount = 0
private componentsCount = 0;
constructor(components: ApplicationComponent[]) {
this.schema = components;
@ -23,12 +23,18 @@ export class ApplicationModel implements IApplicationModel {
this.resolveTree(components);
}
// all ValidComponents
get allComponents(): IComponentModel[] {
const result: IComponentModel[] = []
const result: IComponentModel[] = [];
this.traverseTree(c => {
result.push(c)
})
return result
result.push(c);
});
return result;
}
// getFrom componentMap
get allComponentsFromSchema(): IComponentModel[] {
return Object.values(this.componentMap);
}
appendChild(component: IComponentModel) {
@ -37,10 +43,10 @@ export class ApplicationModel implements IApplicationModel {
component.parentSlot = null;
component.parent = null;
if (component._slotTrait) {
component.removeTrait(component._slotTrait.id)
component.removeTrait(component._slotTrait.id);
}
this.topComponents.push(component)
this._registerComponent(component)
this.topComponents.push(component);
this._registerComponent(component);
}
toSchema(): ApplicationComponent[] {
@ -68,7 +74,6 @@ export class ApplicationModel implements IApplicationModel {
comp.parent.children[comp.parentSlot] = children.filter(c => c !== comp);
} else {
this.topComponents.splice(this.topComponents.indexOf(comp), 1);
}
}
@ -82,47 +87,54 @@ export class ApplicationModel implements IApplicationModel {
if (this.allComponents.some(c => c.id === newId)) {
return this.genId(type);
}
return newId
return newId;
}
private resolveTree(components: ApplicationComponent[]) {
const allComponents = components.map(c => {
if (this.componentMap[c.id as ComponentId]) {
throw new Error(`Duplicate component id: ${c.id}`);
} else {
const comp = new ComponentModel(this, c);
this.componentMap[c.id as ComponentId] = comp;
return comp;
}
});
allComponents.forEach(child => {
if (child.parentId && child.parentSlot) {
if (!child.parentId || !child.parentSlot) {
this.topComponents.push(child);
return;
}
const parent = this.componentMap[child.parentId];
if (parent) {
if (parent && parent.slots.includes(child.parentSlot)) {
child.parent = parent;
if (parent.children[child.parentSlot]) {
parent.children[child.parentSlot].push(child);
} else {
parent.children[child.parentSlot] = [child];
}
}
child.parent = parent;
} else {
this.topComponents.push(child);
}
});
}
private traverseTree(cb: (c: IComponentModel) => void) {
function traverse(root: IComponentModel) {
cb(root)
cb(root);
if (root.id === 'hstack2') {
console.log('traver', root.children['content' as SlotName].map(c => c.id))
console.log(
'traver',
root.children['content' as SlotName].map(c => c.id)
);
}
for (const slot in root.children) {
root.children[slot as SlotName].forEach(child => {
traverse(child)
})
traverse(child);
});
}
}
this.topComponents.forEach((parent) => {
this.topComponents.forEach(parent => {
traverse(parent);
})
});
}
}

View File

@ -60,7 +60,9 @@ export class ComponentModel implements IComponentModel {
get stateKeys() {
if (!this.spec) return [];
const componentStateKeys = Object.keys(this.spec.spec.state.properties || {}) as StateKey[];
const componentStateKeys = Object.keys(
this.spec.spec.state.properties || {}
) as StateKey[];
const traitStateKeys: StateKey[] = this.traits.reduce(
(acc, t) => acc.concat(t.stateKeys),
[] as StateKey[]
@ -74,10 +76,10 @@ export class ComponentModel implements IComponentModel {
get methods() {
if (!this.spec) return [];
const componentMethods = this.spec.spec.methods.map(m => m.name) as MethodName[];
const componentMethods = this.spec.spec.methods as any;
const traitMethods: MethodName[] = this.traits.reduce(
(acc, t) => acc.concat(t.methods),
[] as MethodName[]
[] as any
);
return [...componentMethods, ...traitMethods];
}
@ -161,7 +163,7 @@ export class ComponentModel implements IComponentModel {
}
parent.children[slot].push(this);
parent.appModel._registerComponent(this)
parent.appModel._registerComponent(this);
this.parent = parent;
this.parentSlot = slot;
this.parentId = parent.id;
@ -176,7 +178,11 @@ export class ComponentModel implements IComponentModel {
slotChildren.splice(slotChildren.indexOf(child), 1);
child._isDirty = true;
this._isDirty = true;
console.log('after',this.id, this.allComponents.map(c => c.id))
console.log(
'after',
this.id,
this.allComponents.map(c => c.id)
);
}
}
@ -188,6 +194,11 @@ export class ComponentModel implements IComponentModel {
}
changeId(newId: ComponentId) {
const isIdExist = !!this.appModel.getComponentById(newId);
if (isIdExist) {
throw Error(`Id ${newId} already exist`);
return this;
}
this.id = newId;
for (const slot in this.children) {
const slotChildren = this.children[slot as SlotName];

View File

@ -1,3 +1,4 @@
import { JSONSchema7 } from 'json-schema';
import { ApplicationComponent, ComponentTrait } from '@sunmao-ui/core';
export type ComponentId = string & {
@ -38,6 +39,7 @@ export interface IApplicationModel {
topComponents: IComponentModel[];
// modules: IModuleModel[];
allComponents: IComponentModel[];
allComponentsFromSchema: IComponentModel[];
toSchema(): ApplicationComponent[];
createComponent(type: ComponentType, id?: ComponentId): IComponentModel;
getComponentById(id: ComponentId): IComponentModel | undefined;
@ -57,6 +59,7 @@ export interface IComponentModel {
id: ComponentId;
type: ComponentType;
properties: Record<string, IFieldModel>;
rawProperties: Record<string, any>;
children: Record<SlotName, IComponentModel[]>;
parent: IComponentModel | null;
parentId: ComponentId | null;
@ -65,7 +68,7 @@ export interface IComponentModel {
stateKeys: StateKey[];
slots: SlotName[];
styleSlots: StyleSlotName[];
methods: MethodName[];
methods: Array<{name: MethodName, parameters: JSONSchema7}>;
events: EventName[];
allComponents: IComponentModel[];
nextSilbing: IComponentModel | null;
@ -95,7 +98,7 @@ export interface ITraitModel {
type: TraitType;
rawProperties: Record<string, any>;
properties: Record<string, IFieldModel>;
methods: MethodName[];
methods: Array<{name: MethodName, parameters: JSONSchema7}>;
stateKeys: StateKey[];
_isDirty: boolean;
toSchema(): ComponentTrait;

View File

@ -44,7 +44,7 @@ export class TraitModel implements ITraitModel {
}
get methods() {
return (this.spec ? this.spec.spec.methods.map(m => m.name) : []) as MethodName[];
return this.spec ? this.spec.spec.methods as any : []
}
get stateKeys() {

View File

@ -1,6 +1,7 @@
import { ApplicationComponent, RuntimeComponentSpec } from '@sunmao-ui/core';
import { Registry } from '@sunmao-ui/runtime';
import Ajv from 'ajv';
import { ApplicationModel } from '../operations/AppModel/AppModel';
import {
ISchemaValidator,
ComponentValidatorRule,
@ -43,12 +44,14 @@ export class SchemaValidator implements ISchemaValidator {
}
validate(components: ApplicationComponent[]) {
const appModel = new ApplicationModel(components);
this.genComponentIdSpecMap(components);
this.result = [];
const baseContext = {
components,
validators: this.validatorMap,
registry: this.registry,
appModel,
componentIdSpecMap: this.componentIdSpecMap,
ajv: this.ajv,
};
@ -58,8 +61,8 @@ export class SchemaValidator implements ISchemaValidator {
this.result = this.result.concat(r);
}
});
appModel.allComponents.forEach(component => {
this.componentRules.forEach(rule => {
components.forEach(component => {
const r = rule.validate({
component,
...baseContext,
@ -67,10 +70,8 @@ export class SchemaValidator implements ISchemaValidator {
if (r.length > 0) {
this.result = this.result.concat(r);
}
});
});
this.traitRules.forEach(rule => {
components.forEach(component => {
component.traits.forEach(trait => {
const r = rule.validate({
trait,
@ -83,6 +84,7 @@ export class SchemaValidator implements ISchemaValidator {
});
});
});
});
return this.result;
}

View File

@ -1,6 +1,7 @@
import { RuntimeComponentSpec, ApplicationComponent, ComponentTrait } from '@sunmao-ui/core';
import { ApplicationComponent } from '@sunmao-ui/core';
import { Registry } from '@sunmao-ui/runtime';
import Ajv, { ValidateFunction } from 'ajv';
import { IApplicationModel, IComponentModel, ITraitModel } from '../operations/AppModel/IAppModel';
export interface ValidatorMap {
components: Record<string, ValidateFunction>;
@ -10,18 +11,17 @@ export interface ValidatorMap {
interface BaseValidateContext {
validators: ValidatorMap;
registry: Registry;
components: ApplicationComponent[];
componentIdSpecMap: Record<string, RuntimeComponentSpec>;
appModel: IApplicationModel;
ajv: Ajv
}
export interface ComponentValidateContext extends BaseValidateContext {
component: ApplicationComponent;
component: IComponentModel;
}
export interface TraitValidateContext extends BaseValidateContext {
trait: ComponentTrait;
component: ApplicationComponent;
trait: ITraitModel;
component: IComponentModel;
}
export type AllComponentsValidateContext = BaseValidateContext;
@ -33,19 +33,19 @@ export type ValidateContext =
export interface ComponentValidatorRule {
kind: 'component';
validate: (validateContext: ComponentValidateContext) => ValidateErrorResult[];
fix?: (validateContext: ComponentValidateContext) => ApplicationComponent;
fix?: (validateContext: ComponentValidateContext) => void;
}
export interface AllComponentsValidatorRule {
kind: 'allComponents';
validate: (validateContext: AllComponentsValidateContext) => ValidateErrorResult[];
fix?: (validateContext: AllComponentsValidateContext) => ApplicationComponent[];
fix?: (validateContext: AllComponentsValidateContext) => void[];
}
export interface TraitValidatorRule {
kind: 'trait';
validate: (validateContext: TraitValidateContext) => ValidateErrorResult[];
fix?: (validateContext: TraitValidateContext) => ApplicationComponent;
fix?: (validateContext: TraitValidateContext) => void;
}
export type ValidatorRule =

View File

@ -4,69 +4,43 @@ import {
ValidateErrorResult,
} from '../interfaces';
class RepeatIdValidatorRule implements AllComponentsValidatorRule {
kind: 'allComponents' = 'allComponents';
validate({ components }: AllComponentsValidateContext): ValidateErrorResult[] {
const componentIds = new Set<string>();
const results: ValidateErrorResult[] = [];
components.forEach(component => {
if (componentIds.has(component.id)) {
results.push({
message: 'Duplicate component id.',
componentId: component.id,
fix: () => {
`${component.id}_${Math.floor(Math.random() * 10000)}`;
},
});
} else {
componentIds.add(component.id);
}
});
return results;
}
}
class ParentValidatorRule implements AllComponentsValidatorRule {
kind: 'allComponents' = 'allComponents';
validate({
components,
componentIdSpecMap,
appModel,
}: AllComponentsValidateContext): ValidateErrorResult[] {
const results: ValidateErrorResult[] = [];
components.forEach(c => {
const slotTrait = c.traits.find(t => t.type === 'core/v1/slot');
if (slotTrait) {
const { id: parentId, slot } = slotTrait.properties.container as any;
const parent = components.find(c => c.id === parentId)!;
const allComponents = appModel.allComponents
const allComponentsFromSchema = appModel.allComponentsFromSchema
if (allComponents.length === allComponentsFromSchema.length) {
return results
}
const orphenComponents = allComponentsFromSchema.filter(c => !allComponents.find(c2 => c2.id === c.id))
orphenComponents.forEach(c => {
const parent = appModel.getComponentById(c.parentId!)
if (!parent) {
results.push({
message: `Cannot find parent component: ${parentId}.`,
message: `Cannot find parent component: ${c.parentId}.`,
componentId: c.id,
traitType: slotTrait.type,
traitType: 'core/v1/slot',
property: '/container/id',
});
} else {
const parentSpec = componentIdSpecMap[parent.id];
if (!parentSpec) {
}
if (parent && !parent.slots.includes(c.parentSlot!)) {
results.push({
message: `Component is not registered: ${parent.type}.`,
componentId: parent.id,
});
} else if (!parentSpec.spec.slots.includes(slot)) {
results.push({
message: `Parent component '${parent.id}' does not have slot: ${slot}.`,
message: `Parent component '${parent.id}' does not have slot: ${c.parentSlot}.`,
componentId: c.id,
traitType: slotTrait.type,
traitType: 'core/v1/slot',
property: '/container/slot',
});
}
}
}
});
return results;
}
}
export const AllComponentsRules = [new RepeatIdValidatorRule(), new ParentValidatorRule()];
export const AllComponentsRules = [new ParentValidatorRule()];

View File

@ -18,14 +18,14 @@ class ComponentPropertyValidatorRule implements ComponentValidatorRule {
});
return results;
}
const valid = validate(component.properties);
const properties = component.rawProperties
const valid = validate(properties);
if (!valid) {
validate.errors!.forEach(error => {
if (error.keyword === 'type') {
const { instancePath } = error;
const path = instancePath.split('/')[1];
const value = component.properties[path];
const value = properties[path];
// if value is an expression, skip it
if (isExpression(value)) {
return;
@ -54,13 +54,13 @@ class ModuleValidatorRule implements ComponentValidatorRule {
const results: ValidateErrorResult[] = [];
let moduleSpec
try {
moduleSpec = registry.getModuleByType(component.properties.type as string);
moduleSpec = registry.getModuleByType(component.rawProperties.type.value as string);
} catch (err) {
moduleSpec = undefined
}
if (!moduleSpec) {
results.push({
message: `Module is not registered: ${component.properties.type}.`,
message: `Module is not registered: ${component.rawProperties.type}.`,
componentId: component.id,
property: '/type',
});

View File

@ -6,6 +6,7 @@ import {
} from '../interfaces';
import { EventHandlerSchema } from '@sunmao-ui/runtime';
import { isExpression } from '../utils';
import { ComponentId, EventName } from '../../operations/AppModel/IAppModel';
class TraitPropertyValidatorRule implements TraitValidatorRule {
kind: 'trait' = 'trait';
@ -25,13 +26,13 @@ class TraitPropertyValidatorRule implements TraitValidatorRule {
return results;
}
const valid = validate(trait.properties);
const valid = validate(trait.rawProperties);
if (!valid) {
validate.errors!.forEach(error => {
if (error.keyword === 'type') {
const { instancePath } = error;
const path = instancePath.split('/')[1];
const value = trait.properties[path];
const value = trait.rawProperties[path];
// if value is an expression, skip it
if (isExpression(value)) {
@ -55,25 +56,24 @@ class EventHandlerValidatorRule implements TraitValidatorRule {
traitMethods = ['setValue', 'resetValue', 'triggerFetch'];
validate({
appModel,
trait,
component,
components,
componentIdSpecMap,
ajv,
}: TraitValidateContext): ValidateErrorResult[] {
const results: ValidateErrorResult[] = [];
if (trait.type !== 'core/v1/event') {
return results;
}
const handlers = trait.properties.handlers as Static<typeof EventHandlerSchema>[];
const handlers = trait.rawProperties.handlers as Static<typeof EventHandlerSchema>[];
handlers.forEach((handler, i) => {
const {
type: eventName,
componentId: targetId,
method: { name: methodName, parameters },
} = handler;
const componentSpec = componentIdSpecMap[component.id];
if (!componentSpec.spec.events.includes(eventName)) {
if (!component.events.includes(eventName as EventName)) {
results.push({
message: `Component does not have event: ${eventName}.`,
componentId: component.id,
@ -86,7 +86,7 @@ class EventHandlerValidatorRule implements TraitValidatorRule {
return;
}
const targetComponent = components.find(c => c.id === targetId);
const targetComponent = appModel.getComponentById(targetId as ComponentId);
if (!targetComponent) {
results.push({
message: `Event target component is not exist: ${targetId}.`,
@ -97,22 +97,8 @@ class EventHandlerValidatorRule implements TraitValidatorRule {
return;
}
const targetComponentSpec = componentIdSpecMap[targetComponent.id];
if (!targetComponentSpec) {
results.push({
message: `Event target component is not registered: ${targetId}.`,
componentId: component.id,
traitType: trait.type,
property: `/handlers/${i}/componentId`,
});
return;
}
const methodSchema = targetComponentSpec.spec.methods.find(
m => m.name === methodName
);
if (!methodSchema && !this.traitMethods.includes(methodName)) {
const method = targetComponent.methods.find(m => m.name === methodName);
if (!method) {
results.push({
message: `Event target component does not have method: ${methodName}.`,
componentId: component.id,
@ -123,10 +109,10 @@ class EventHandlerValidatorRule implements TraitValidatorRule {
}
if (
methodSchema?.parameters &&
!ajv.validate(methodSchema.parameters, parameters)
method.parameters &&
!ajv.validate(method.parameters, parameters)
) {
ajv.errors!.forEach(error => {
ajv.errors!.forEach(error => {JSON
if (error.keyword === 'type') {
const { instancePath } = error;
const path = instancePath.split('/')[1];