validate expression refs

This commit is contained in:
Bowen Tan 2022-01-10 16:39:35 +08:00
parent 68242a595e
commit b5cbcd1666
14 changed files with 187 additions and 59 deletions

View File

@ -21,7 +21,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'].value).toEqual({ raw: 'text', format: 'plain' });
expect(button1.properties['text'].rawValue).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

@ -5,13 +5,26 @@ describe('Field test', () => {
const field = new FieldModel('Hello, world!');
expect(field.isDynamic).toEqual(false);
expect(field.refs).toEqual([]);
expect(field.value).toEqual('Hello, world!');
expect(field.rawValue).toEqual('Hello, world!');
});
it('parse expression', () => {
const field = new FieldModel('{{input.value}} + {{select.value}}');
expect(field.isDynamic).toEqual(true);
expect(field.refs).toEqual(['input', 'select']);
expect(field.value).toEqual('{{input.value}} + {{select.value}}');
expect(field.rawValue).toEqual('{{input.value}} + {{select.value}}');
});
it('parse object property', () => {
const field = new FieldModel({raw: '{{input.value}}', format: 'md'});
expect(field.isDynamic).toEqual(false);
expect(field.refs).toEqual([]);
expect(field.rawValue).toEqual({raw: '{{input.value}}', format: 'md'});
expect(field.getProperty('raw').value).toEqual('{{input.value}}');
expect(field.getProperty('raw').isDynamic).toEqual(true);
expect(field.getProperty('raw').refs).toEqual(['input']);
expect(field.getProperty('format').value).toEqual('md');
expect(field.getProperty('format').isDynamic).toEqual(false);
expect(field.getProperty('format').refs).toEqual([]);
});
});

View File

@ -1,5 +1,9 @@
import { registry } from '../../src/setup';
import { ComponentInvalidSchema,ComponentPropertyExpressionSchema } from './mock';
import {
ComponentInvalidSchema,
ComponentPropertyExpressionSchema,
ComponentWrongPropertyExpressionSchema,
} from './mock';
import { SchemaValidator } from '../../src/validator';
const schemaValidator = new SchemaValidator(registry);
@ -13,9 +17,13 @@ describe('Validate component', () => {
it('detect wrong type', () => {
expect(result[1].message).toBe(`must be string`);
});
it('ignore expreesion', () => {
it('ignore expression', () => {
const result = schemaValidator.validate(ComponentPropertyExpressionSchema);
expect(result.length).toBe(0);
})
});
it('detect using non-exist variables in expression', () => {
const result = schemaValidator.validate(ComponentWrongPropertyExpressionSchema);
expect(result[0].message).toBe(`Cannot find 'input' in store.`);
});
});
});

View File

@ -57,16 +57,25 @@ export const ComponentInvalidSchema: ComponentSchema[] = [
export const ComponentPropertyExpressionSchema: ComponentSchema[] = [
{
id: 'text1',
id: 'list',
type: 'chakra_ui/v1/list',
properties: {
listData: '{{data}}',
template: '{{template}}',
listData: '{{ [] }}',
template: '{{ {} }}',
},
traits: [],
},
];
export const ComponentWrongPropertyExpressionSchema: ComponentSchema[] = [
{
id: 'hstack1',
type: 'chakra_ui/v1/hstack',
properties: { spacing: '24px', align: '{{input.value}}' },
traits: [],
},
];
export const TraitInvalidSchema: ComponentSchema[] = [
{
id: 'text1',

View File

@ -101,7 +101,7 @@ export class ComponentModel implements IComponentModel {
get rawProperties() {
const obj: Record<string, any> = {};
for (const key in this.properties) {
obj[key] = this.properties[key].value;
obj[key] = this.properties[key].rawValue;
}
return obj;
}

View File

@ -2,7 +2,7 @@ import { parseExpression } from '@sunmao-ui/runtime';
import * as acorn from 'acorn';
import * as acornLoose from 'acorn-loose';
import { simple as simpleWalk } from 'acorn-walk';
import { flattenDeep } from 'lodash-es';
import { flattenDeep, isArray, isObject, isPlainObject } from 'lodash-es';
import { ComponentId, IFieldModel, ModuleId } from './IAppModel';
(window as any).acorn = acorn;
@ -11,8 +11,10 @@ import { ComponentId, IFieldModel, ModuleId } from './IAppModel';
const regExp = new RegExp('.*{{.*}}.*');
isPlainObject
export class FieldModel implements IFieldModel {
value: any;
private value: unknown | Record<string, IFieldModel>;
isDynamic = false;
refs: Array<ComponentId | ModuleId> = [];
@ -21,26 +23,63 @@ export class FieldModel implements IFieldModel {
}
update(value: unknown) {
this.value = value;
if (isObject(value) && !isArray(value)) {
if (!isObject(this.value)) {
this.value = {};
}
for (const key in value) {
const val = (value as Record<string, unknown>)[key];
const _thisValue = this.value as Record<string, IFieldModel>;
if (!_thisValue[key]) {
_thisValue[key] = new FieldModel(val);
} else {
_thisValue[key].update(val);
}
}
} else {
this.value = value;
}
this.isDynamic = typeof value === 'string' && regExp.test(value);
this.parseReferences();
}
parseReferences() {
getProperty(key?: string) {
if (!key) {
return this.value;
}
if (key && typeof this.value === 'object') {
return (this.value as any)[key];
}
return undefined;
}
get rawValue() {
if (isObject(this.value) && !isArray(this.value) ) {
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;
}
private parseReferences() {
if (!this.isDynamic || typeof this.value !== 'string') return;
const refs: string[] = [];
const exps = flattenDeep(parseExpression(this.value as string).filter(exp => typeof exp !== 'string'))
const exps = flattenDeep(
parseExpression(this.value as string).filter(exp => typeof exp !== 'string')
);
exps.forEach(exp => {
simpleWalk(acornLoose.parse(exp, acorn.defaultOptions), {
simpleWalk(acornLoose.parse(exp, { ecmaVersion: 2020 }), {
Identifier: node => {
console.log('node', node);
refs.push(exp.slice(node.start, node.end));
},
});
})
});
this.refs = refs as Array<ComponentId | ModuleId>;
console.log('this.refs', this.refs)
}
}

View File

@ -111,9 +111,11 @@ export interface ITraitModel {
}
export interface IFieldModel {
value: any;
// value: any;
isDynamic: boolean;
update: (value: any) => void;
update: (value: unknown) => void;
getProperty: (key?: string) => unknown | void | Record<string, IFieldModel>;
rawValue: any;
// ids of used components in the expression
refs: Array<ComponentId | ModuleId>;
}

View File

@ -37,7 +37,7 @@ export class TraitModel implements ITraitModel {
get rawProperties() {
const obj: Record<string, any> = {};
for (const key in this.properties) {
obj[key] = this.properties[key].value;
obj[key] = this.properties[key].rawValue;
}
return obj;
}

7
packages/editor/src/global.d.ts vendored Normal file
View File

@ -0,0 +1,7 @@
declare module 'acorn-loose' {
function parse(
input: string,
options?: import('acorn').Options,
): import('acorn').Node;
export { parse };
}

View File

@ -15,7 +15,7 @@ export class ModifyComponentPropertiesLeafOperation extends BaseLeafOperation<Mo
const component = appModel.getComponentById(this.context.componentId as ComponentId);
if (component) {
for (const property in this.context.properties) {
const oldValue = component.properties[property]?.value;
const oldValue = component.rawProperties[property];
// assign previous data
this.previousState[property] = oldValue;
let newValue = this.context.properties[property];

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]?.value;
const oldValue = trait.properties[property]?.rawValue;
this.previousState[property] = oldValue;
let newValue = this.context.properties[property];
if (_.isFunction(newValue)) {

View File

@ -1,4 +1,4 @@
import { ComponentSchema } from '@sunmao-ui/core';
import { ComponentSchema, RuntimeComponent } from '@sunmao-ui/core';
import { Registry } from '@sunmao-ui/runtime';
import Ajv, { ValidateFunction } from 'ajv';
import { IAppModel, IComponentModel, ITraitModel } from '../AppModel/IAppModel';
@ -9,10 +9,15 @@ export interface ValidatorMap {
}
interface BaseValidateContext {
components: ComponentSchema[];
validators: ValidatorMap;
registry: Registry;
appModel: IAppModel;
ajv: Ajv;
componentIdSpecMap: Record<
string,
RuntimeComponent<string, string, string, string>
>;
}
export interface ComponentValidateContext extends BaseValidateContext {
@ -23,11 +28,14 @@ export interface TraitValidateContext extends BaseValidateContext {
trait: ITraitModel;
component: IComponentModel;
}
export type PropertyValidateContext = BaseValidateContext;
export type AllComponentsValidateContext = BaseValidateContext;
export type ValidateContext =
| ComponentValidateContext
| TraitValidateContext
| PropertyValidateContext
| AllComponentsValidateContext;
export interface ComponentValidatorRule {
@ -48,9 +56,16 @@ export interface TraitValidatorRule {
fix?: (validateContext: TraitValidateContext) => void;
}
export interface PropertyValidatorRule {
kind: 'property';
validate: (validateContext: PropertyValidateContext) => ValidateErrorResult[];
fix?: (validateContext: PropertyValidateContext) => void;
}
export type ValidatorRule =
| ComponentValidatorRule
| AllComponentsValidatorRule
| PropertyValidatorRule
| TraitValidatorRule;
export interface ISchemaValidator {

View File

@ -3,12 +3,15 @@ import {
ComponentValidateContext,
ValidateErrorResult,
} from '../interfaces';
import { isExpression } from '../utils';
class ComponentPropertyValidatorRule implements ComponentValidatorRule {
kind: 'component' = 'component';
validate({ component, validators }: ComponentValidateContext): ValidateErrorResult[] {
validate({
component,
validators,
componentIdSpecMap,
}: ComponentValidateContext): ValidateErrorResult[] {
const results: ValidateErrorResult[] = [];
const validate = validators.components[component.type];
if (!validate) {
@ -18,27 +21,43 @@ class ComponentPropertyValidatorRule implements ComponentValidatorRule {
});
return results;
}
const properties = component.rawProperties
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 = properties[path];
// if value is an expression, skip it
if (isExpression(value)) {
return;
}
const { instancePath, params } = error;
let key = ''
if (instancePath) {
key = instancePath.split('/')[1];
} else {
key = params.missingProperty
}
const fieldModel = component.properties[key];
// fieldModel could be undefiend. if is undefined, still throw error.
if (fieldModel?.isDynamic !== true) {
results.push({
message: error.message || '',
componentId: component.id,
property: error.instancePath,
});
}
results.push({
message: error.message || '',
componentId: component.id,
property: error.instancePath,
});
});
}
for (const key in component.properties) {
const fieldModel = component.properties[key];
fieldModel.refs.forEach((id: string) => {
if (!componentIdSpecMap[id]) {
results.push({
message: `Cannot find '${id}' in store.`,
componentId: component.id,
property: key,
});
}
});
}
return results;
}
}
@ -52,11 +71,11 @@ class ModuleValidatorRule implements ComponentValidatorRule {
}
const results: ValidateErrorResult[] = [];
let moduleSpec
let moduleSpec;
try {
moduleSpec = registry.getModuleByType(component.rawProperties.type.value as string);
} catch (err) {
moduleSpec = undefined
moduleSpec = undefined;
}
if (!moduleSpec) {
results.push({

View File

@ -15,6 +15,7 @@ class TraitPropertyValidatorRule implements TraitValidatorRule {
trait,
component,
validators,
componentIdSpecMap,
}: TraitValidateContext): ValidateErrorResult[] {
const results: ValidateErrorResult[] = [];
const validate = validators.traits[trait.type];
@ -28,22 +29,36 @@ class TraitPropertyValidatorRule implements TraitValidatorRule {
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.rawProperties[path];
// if value is an expression, skip it
if (isExpression(value)) {
return;
}
const { instancePath, params } = error;
let key = '';
if (instancePath) {
key = instancePath.split('/')[1];
} else {
key = params.missingProperty;
}
const fieldModel = component.properties[key];
// fieldModel could be undefiend. if is undefined, still throw error.
if (fieldModel?.isDynamic !== true) {
results.push({
message: error.message || '',
componentId: component.id,
traitType: trait.type,
property: error.instancePath,
});
}
});
}
for (const key in trait.properties) {
const fieldModel = trait.properties[key];
fieldModel.refs.forEach(id => {
if (!componentIdSpecMap[id]) {
results.push({
message: `Cannot find '${id}' in store.`,
componentId: component.id,
property: `traits/${key}`,
});
}
results.push({
message: error.message || '',
componentId: component.id,
traitType: trait.type,
property: error.instancePath,
});
});
}
return results;
@ -132,6 +147,7 @@ class EventHandlerValidatorRule implements TraitValidatorRule {
return results;
}
}
export const TraitRules = [
new TraitPropertyValidatorRule(),
new EventHandlerValidatorRule(),