From 8d295f2ef812cbb8a543a0979bb2ae19bd0f424f Mon Sep 17 00:00:00 2001 From: Bowen Tan Date: Tue, 11 Jan 2022 14:57:37 +0800 Subject: [PATCH] validate nested property expression --- .../__tests__/model/componentModel.spec.ts | 2 +- .../__tests__/validator/component.spec.ts | 6 ++- packages/editor/__tests__/validator/mock.ts | 29 ++++++------- .../editor/src/AppModel/ComponentModel.ts | 43 +++++++------------ packages/editor/src/AppModel/FieldModel.ts | 40 +++++++++++------ packages/editor/src/AppModel/IAppModel.ts | 7 +-- packages/editor/src/AppModel/TraitModel.ts | 19 ++------ .../modifyTraitPropertiesLeafOperation.ts | 2 +- .../src/validator/rules/ComponentRules.ts | 16 ++++--- .../editor/src/validator/rules/TraitRules.ts | 18 ++++---- 10 files changed, 91 insertions(+), 91 deletions(-) diff --git a/packages/editor/__tests__/model/componentModel.spec.ts b/packages/editor/__tests__/model/componentModel.spec.ts index 6d65ec00..f2e74447 100644 --- a/packages/editor/__tests__/model/componentModel.spec.ts +++ b/packages/editor/__tests__/model/componentModel.spec.ts @@ -23,7 +23,7 @@ describe('ComponentModel test', () => { expect(button1.parentSlot).toEqual('content'); expect(button1.prevSilbling).toBe(appModel.getComponentById('text2' as ComponentId)); expect(button1.nextSilbing).toBe(null); - expect(button1.properties['text'].rawValue).toEqual({ raw: 'text', format: 'plain' }); + expect(button1.rawProperties.text).toEqual({ raw: 'text', format: 'plain' }); const apiFetch = appModel.getComponentById('apiFetch' as ComponentId)!; expect(apiFetch.stateKeys).toEqual(['fetch']); expect(apiFetch.methods[0].name).toEqual('triggerFetch'); diff --git a/packages/editor/__tests__/validator/component.spec.ts b/packages/editor/__tests__/validator/component.spec.ts index ee6cb62d..80196713 100644 --- a/packages/editor/__tests__/validator/component.spec.ts +++ b/packages/editor/__tests__/validator/component.spec.ts @@ -23,7 +23,11 @@ describe('Validate component', () => { }); it('detect using non-exist variables in expression', () => { const result = schemaValidator.validate(ComponentWrongPropertyExpressionSchema); - expect(result[0].message).toBe(`Cannot find 'input' in store.`); + expect(result[0].message).toBe(`Cannot find 'data' in store.`); + }); + it('detect using non-exist variables in expression of array property', () => { + const result = schemaValidator.validate(ComponentWrongPropertyExpressionSchema); + expect(result[1].message).toBe(`Cannot find 'fetch' in store.`); }); }); }); diff --git a/packages/editor/__tests__/validator/mock.ts b/packages/editor/__tests__/validator/mock.ts index 00af44e8..755342c2 100644 --- a/packages/editor/__tests__/validator/mock.ts +++ b/packages/editor/__tests__/validator/mock.ts @@ -69,9 +69,16 @@ export const ComponentPropertyExpressionSchema: ComponentSchema[] = [ export const ComponentWrongPropertyExpressionSchema: ComponentSchema[] = [ { - id: 'hstack1', - type: 'chakra_ui/v1/hstack', - properties: { spacing: '24px', align: '{{input.value}}' }, + id: 'input1', + type: 'chakra_ui/v1/input', + properties: { + variant: 'outline', + placeholder: '{{data.value}}', + size: 'md', + isDisabled: false, + isRequired: false, + defaultValue: '', + }, traits: [], }, { @@ -90,21 +97,11 @@ export const ComponentWrongPropertyExpressionSchema: ComponentSchema[] = [ handlers: [ { type: 'onClick', - componentId: 'button1', + componentId: 'input1', method: { - name: 'click', + name: 'setInputValue', parameters: { - value: '666', - }, - }, - }, - { - type: 'onClick', - componentId: 'dialog1', - method: { - name: 'click', - parameters: { - value: '666', + value: '{{fetch.data.value}}', }, }, }, diff --git a/packages/editor/src/AppModel/ComponentModel.ts b/packages/editor/src/AppModel/ComponentModel.ts index d3e3fcd5..d29dd885 100644 --- a/packages/editor/src/AppModel/ComponentModel.ts +++ b/packages/editor/src/AppModel/ComponentModel.ts @@ -1,8 +1,4 @@ -import { - ComponentSchema, - MethodSchema, - RuntimeComponent, -} from '@sunmao-ui/core'; +import { ComponentSchema, MethodSchema, RuntimeComponent } from '@sunmao-ui/core'; import { registry } from '../setup'; import { genComponent, genTrait } from './utils'; import { @@ -22,14 +18,19 @@ import { } from './IAppModel'; import { TraitModel } from './TraitModel'; import { FieldModel } from './FieldModel'; -type ComponentSpecModel = RuntimeComponent +type ComponentSpecModel = RuntimeComponent< + MethodName, + StyleSlotName, + SlotName, + EventName +>; const SlotTraitType: TraitType = 'core/v1/slot' as TraitType; export class ComponentModel implements IComponentModel { private spec: ComponentSpecModel; id: ComponentId; type: ComponentType; - properties: Record = {}; + properties: IFieldModel; children: Record = {}; parent: IComponentModel | null = null; parentId: ComponentId | null = null; @@ -53,9 +54,7 @@ export class ComponentModel implements IComponentModel { } }); - for (const key in schema.properties) { - this.properties[key] = new FieldModel(schema.properties[key]); - } + this.properties = new FieldModel(schema.properties); } get slots() { @@ -85,7 +84,7 @@ export class ComponentModel implements IComponentModel { componentMethods.push({ name: methodName, parameters: this.spec.spec.methods[methodName as MethodName]!, - }) + }); } const traitMethods: MethodSchema[] = this.traits.reduce( (acc, t) => acc.concat(t.methods), @@ -99,11 +98,7 @@ export class ComponentModel implements IComponentModel { } get rawProperties() { - const obj: Record = {}; - for (const key in this.properties) { - obj[key] = this.properties[key].rawValue; - } - return obj; + return this.properties.rawValue; } get prevSilbling() { @@ -146,11 +141,7 @@ export class ComponentModel implements IComponentModel { } updateComponentProperty(propertyName: string, value: any) { - if (!Reflect.has(this.properties, propertyName)) { - this.properties[propertyName] = new FieldModel(value) - } else { - this.properties[propertyName].update(value); - } + this.properties.update({ [propertyName]: value }); this._isDirty = true; } @@ -215,7 +206,7 @@ export class ComponentModel implements IComponentModel { child.parentId = newId; const slotTrait = child.traits.find(t => t.type === SlotTraitType); if (slotTrait) { - slotTrait.properties.container.update({ id: newId, slot }); + slotTrait.properties.update({ container: { id: newId, slot } }); slotTrait._isDirty = true; } child._isDirty = true; @@ -263,17 +254,15 @@ export class ComponentModel implements IComponentModel { const trait = this.traits.find(t => t.id === traitId); if (!trait) return; for (const property in properties) { - if (trait.properties[property]) { - trait.updateProperty(property, properties[property]) - } - trait._isDirty = true; + trait.updateProperty(property, properties[property]); } + trait._isDirty = true; this._isDirty = true; } updateSlotTrait(parent: ComponentId, slot: SlotName) { if (this._slotTrait) { - this._slotTrait.properties.container.update({ id: parent, slot }); + this._slotTrait.properties.update({ container: { id: parent, slot } }); this._slotTrait._isDirty = true; } else { this.addTrait(SlotTraitType, { container: { id: parent, slot } }); diff --git a/packages/editor/src/AppModel/FieldModel.ts b/packages/editor/src/AppModel/FieldModel.ts index def52345..0a0bf857 100644 --- a/packages/editor/src/AppModel/FieldModel.ts +++ b/packages/editor/src/AppModel/FieldModel.ts @@ -22,6 +22,22 @@ export class FieldModel implements IFieldModel { this.update(value); } + get rawValue() { + if (isObject(this.value)) { + if (isArray(this.value)) { + return this.value.map((field) => field.rawValue) + } else { + const _thisValue = this.value as Record; + const res: Record = {}; + for (const key in _thisValue) { + res[key] = _thisValue[key].rawValue; + } + return res; + } + } + return this.value; + } + update(value: unknown) { if (isObject(value)) { if (!isObject(this.value)) { @@ -44,7 +60,7 @@ export class FieldModel implements IFieldModel { this.parseReferences(); } - getProperty(key?: string | number) { + getProperty(key?: string | number): FieldModel | unknown { if (key === undefined) { return this.value; } @@ -54,20 +70,20 @@ export class FieldModel implements IFieldModel { return undefined; } - get rawValue() { - if (isObject(this.value)) { - if (isArray(this.value)) { - return this.value.map((field) => field.rawValue) - } else { - const _thisValue = this.value as Record; - const res: Record = {}; - for (const key in _thisValue) { - res[key] = _thisValue[key].rawValue; + traverse(cb: (f: IFieldModel, key: string) => void) { + function _traverse(field: FieldModel, key: string) { + if (isObject(field.value)) { + for (const _key in field.value) { + const val = field.getProperty(_key) + if (val instanceof FieldModel) { + _traverse(val, `${key}.${_key}`) + } } - return res; + } else { + cb(field, key) } } - return this.value; + _traverse(this, '') } private parseReferences() { diff --git a/packages/editor/src/AppModel/IAppModel.ts b/packages/editor/src/AppModel/IAppModel.ts index 035a015c..eb23f1f1 100644 --- a/packages/editor/src/AppModel/IAppModel.ts +++ b/packages/editor/src/AppModel/IAppModel.ts @@ -59,7 +59,7 @@ export interface IComponentModel { appModel: IAppModel; id: ComponentId; type: ComponentType; - properties: Record; + properties: IFieldModel; // just like properties in schema rawProperties: Record; children: Record; @@ -102,7 +102,7 @@ export interface ITraitModel { parent: IComponentModel; type: TraitType; rawProperties: Record; - properties: Record; + properties: IFieldModel; methods: MethodSchema[]; stateKeys: StateKey[]; _isDirty: boolean; @@ -114,7 +114,8 @@ export interface IFieldModel { // value: any; isDynamic: boolean; update: (value: unknown) => void; - getProperty: (key?: string) => unknown | void | Record; + getProperty: (key?: string) => unknown | void | IFieldModel; + traverse: (cb: (f: IFieldModel, key: string) => void) => void rawValue: any; // ids of used components in the expression refs: Array; diff --git a/packages/editor/src/AppModel/TraitModel.ts b/packages/editor/src/AppModel/TraitModel.ts index 905c6a84..e523a0e9 100644 --- a/packages/editor/src/AppModel/TraitModel.ts +++ b/packages/editor/src/AppModel/TraitModel.ts @@ -18,7 +18,7 @@ export class TraitModel implements ITraitModel { private spec: RuntimeTrait; id: TraitId; type: TraitType; - properties: Record = {}; + properties: IFieldModel; _isDirty = false; constructor(trait: TraitSchema, public parent: IComponentModel) { @@ -28,18 +28,11 @@ export class TraitModel implements ITraitModel { this.id = `${this.parent.id}_trait${traitIdCount++}` as TraitId; this.spec = registry.getTraitByType(this.type); - for (const key in trait.properties) { - this.properties[key] = new FieldModel(trait.properties[key]); - } - this.properties; + this.properties = new FieldModel(trait.properties); } get rawProperties() { - const obj: Record = {}; - for (const key in this.properties) { - obj[key] = this.properties[key].rawValue; - } - return obj; + return this.properties.rawValue } get methods() { @@ -58,11 +51,7 @@ export class TraitModel implements ITraitModel { } updateProperty(key: string, value: any) { - if (this.properties[key]) { - this.properties[key].update(value); - } else { - this.properties[key] = new FieldModel(value); - } + this.properties.update({[key]: value}) this._isDirty = true; this.parent._isDirty = true; } diff --git a/packages/editor/src/operations/leaf/trait/modifyTraitPropertiesLeafOperation.ts b/packages/editor/src/operations/leaf/trait/modifyTraitPropertiesLeafOperation.ts index 4ed6f81d..1ea5c340 100644 --- a/packages/editor/src/operations/leaf/trait/modifyTraitPropertiesLeafOperation.ts +++ b/packages/editor/src/operations/leaf/trait/modifyTraitPropertiesLeafOperation.ts @@ -21,7 +21,7 @@ export class ModifyTraitPropertiesLeafOperation extends BaseLeafOperation { const { instancePath, params } = error; - let key = '' + let key = ''; if (instancePath) { key = instancePath.split('/')[1]; } else { - key = params.missingProperty + key = params.missingProperty; } - const fieldModel = component.properties[key]; + const fieldModel = component.properties.getProperty(key); + // if field is expression, ignore type error // fieldModel could be undefiend. if is undefined, still throw error. - if (fieldModel?.isDynamic !== true) { + if (get(fieldModel, 'isDynamic') !== true) { results.push({ message: error.message || '', componentId: component.id, @@ -45,8 +47,8 @@ class ComponentPropertyValidatorRule implements ComponentValidatorRule { }); } - for (const key in component.properties) { - const fieldModel = component.properties[key]; + // validate expression + component.properties.traverse((fieldModel, key) => { fieldModel.refs.forEach((id: string) => { if (!componentIdSpecMap[id]) { results.push({ @@ -56,7 +58,7 @@ class ComponentPropertyValidatorRule implements ComponentValidatorRule { }); } }); - } + }); return results; } diff --git a/packages/editor/src/validator/rules/TraitRules.ts b/packages/editor/src/validator/rules/TraitRules.ts index 8e84f850..f63108c2 100644 --- a/packages/editor/src/validator/rules/TraitRules.ts +++ b/packages/editor/src/validator/rules/TraitRules.ts @@ -7,6 +7,7 @@ import { import { EventHandlerSchema } from '@sunmao-ui/runtime'; import { isExpression } from '../utils'; import { ComponentId, EventName } from '../../AppModel/IAppModel'; +import { get } from 'lodash-es'; class TraitPropertyValidatorRule implements TraitValidatorRule { kind: 'trait' = 'trait'; @@ -36,9 +37,10 @@ class TraitPropertyValidatorRule implements TraitValidatorRule { } else { key = params.missingProperty; } - const fieldModel = component.properties[key]; + const fieldModel = trait.properties.getProperty(key); + // if field is expression, ignore type error // fieldModel could be undefiend. if is undefined, still throw error. - if (fieldModel?.isDynamic !== true) { + if (get(fieldModel, 'isDynamic') !== true) { results.push({ message: error.message || '', componentId: component.id, @@ -49,18 +51,18 @@ class TraitPropertyValidatorRule implements TraitValidatorRule { }); } - for (const key in trait.properties) { - const fieldModel = trait.properties[key]; - fieldModel.refs.forEach(id => { + // validate expression + trait.properties.traverse((fieldModel, key) => { + fieldModel.refs.forEach((id: string) => { if (!componentIdSpecMap[id]) { results.push({ message: `Cannot find '${id}' in store.`, componentId: component.id, - property: `traits/${key}`, + property: key, }); } }); - } + }); return results; } } @@ -127,7 +129,7 @@ class EventHandlerValidatorRule implements TraitValidatorRule { 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)) {