mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-21 03:15:49 +08:00
refactor validator
This commit is contained in:
parent
3cbf92b720
commit
26300bff0c
@ -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);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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;
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 =
|
||||
|
@ -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()];
|
||||
|
@ -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',
|
||||
});
|
||||
|
@ -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];
|
||||
|
Loading…
Reference in New Issue
Block a user