mirror of
https://github.com/smartxworks/sunmao-ui.git
synced 2024-11-21 03:15:49 +08:00
validate expression refs
This commit is contained in:
parent
68242a595e
commit
b5cbcd1666
@ -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');
|
||||
|
@ -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([]);
|
||||
});
|
||||
});
|
||||
|
@ -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.`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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
7
packages/editor/src/global.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
declare module 'acorn-loose' {
|
||||
function parse(
|
||||
input: string,
|
||||
options?: import('acorn').Options,
|
||||
): import('acorn').Node;
|
||||
export { parse };
|
||||
}
|
@ -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];
|
||||
|
@ -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)) {
|
||||
|
@ -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 {
|
||||
|
@ -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({
|
||||
|
@ -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(),
|
||||
|
Loading…
Reference in New Issue
Block a user