validate nested property expression

This commit is contained in:
Bowen Tan 2022-01-11 14:57:37 +08:00
parent ffc4f5377d
commit 8d295f2ef8
10 changed files with 91 additions and 91 deletions

View File

@ -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');

View File

@ -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.`);
});
});
});

View File

@ -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}}',
},
},
},

View File

@ -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<MethodName, StyleSlotName, SlotName, EventName>
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<string, IFieldModel> = {};
properties: IFieldModel;
children: Record<SlotName, IComponentModel[]> = {};
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<string, any> = {};
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 } });

View File

@ -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<string, IFieldModel>;
const res: Record<string, any> = {};
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<string, IFieldModel>;
const res: Record<string, any> = {};
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() {

View File

@ -59,7 +59,7 @@ export interface IComponentModel {
appModel: IAppModel;
id: ComponentId;
type: ComponentType;
properties: Record<string, IFieldModel>;
properties: IFieldModel;
// just like properties in schema
rawProperties: Record<string, any>;
children: Record<SlotName, IComponentModel[]>;
@ -102,7 +102,7 @@ export interface ITraitModel {
parent: IComponentModel;
type: TraitType;
rawProperties: Record<string, any>;
properties: Record<string, IFieldModel>;
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<string, IFieldModel>;
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<ComponentId | ModuleId>;

View File

@ -18,7 +18,7 @@ export class TraitModel implements ITraitModel {
private spec: RuntimeTrait;
id: TraitId;
type: TraitType;
properties: Record<string, IFieldModel> = {};
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<string, any> = {};
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;
}

View File

@ -21,7 +21,7 @@ export class ModifyTraitPropertiesLeafOperation extends BaseLeafOperation<Modify
}
const trait = component.traits[this.context.traitIndex];
for (const property in this.context.properties) {
const oldValue = trait.properties[property]?.rawValue;
const oldValue = trait.rawProperties[property];
this.previousState[property] = oldValue;
let newValue = this.context.properties[property];
if (_.isFunction(newValue)) {

View File

@ -1,3 +1,4 @@
import { get } from 'lodash-es';
import {
ComponentValidatorRule,
ComponentValidateContext,
@ -27,15 +28,16 @@ class ComponentPropertyValidatorRule implements ComponentValidatorRule {
if (!valid) {
validate.errors!.forEach(error => {
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;
}

View File

@ -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)) {