Skip to content

Commit ea2ea7c

Browse files
committed
Initial commit
1 parent 791ea62 commit ea2ea7c

File tree

5 files changed

+651
-0
lines changed

5 files changed

+651
-0
lines changed

src/compositeNodes.tsx

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import * as React from "react";
2+
3+
import {
4+
FormContext,
5+
Node,
6+
NodeAny,
7+
NodeOutputValue,
8+
NodeSchema,
9+
NodeState,
10+
} from "./schemaTypes";
11+
12+
export type NodeS = {
13+
[key: string]: NodeState<any>;
14+
};
15+
16+
export type NodeO = {
17+
[key: string]: NodeOutputValue<any>;
18+
};
19+
20+
export type ChildrenMap<T> = {
21+
[key: string]: T;
22+
};
23+
24+
export abstract class CompositeNode<O, M extends NodeSchema> extends Node<NodeS, O, M> {
25+
26+
abstract getChildrenMapFromSchema(): ChildrenMap<NodeSchema>;
27+
28+
abstract getCompositeOutput(output: NodeO): NodeOutputValue<O>;
29+
30+
renderComposite(context: FormContext, children: ChildrenMap<React.ReactNode>): React.ReactNode {
31+
return Object.values(children);
32+
}
33+
34+
resolveInitialState() {
35+
const initialState = {};
36+
Object.keys(this.getChildrenMapFromSchema()).forEach(key => {
37+
initialState[key] = null;
38+
});
39+
return initialState;
40+
}
41+
42+
getRawOutput() {
43+
const output = {};
44+
Object.keys(this.getChildrenMapFromSchema()).forEach(key => {
45+
output[key] = this.findChild(key).getOutput();
46+
});
47+
return this.getCompositeOutput(output);
48+
}
49+
50+
onChildStateChanged(state: NodeState<any>, source: NodeAny, originalSource?: NodeAny) {
51+
this.setState({
52+
[source.getTag()]: state,
53+
});
54+
}
55+
56+
resolveChildren() {
57+
const childrenMap: ChildrenMap<NodeSchema> = this.getChildrenMapFromSchema();
58+
59+
return Object.keys(childrenMap).map(key => {
60+
const child = this.resolveNode(childrenMap[key], this, this.getConfig());
61+
child.setTag(key);
62+
return child;
63+
});
64+
}
65+
66+
render(context: FormContext): React.ReactNode {
67+
const childrenMap: ChildrenMap<React.ReactNode> = {};
68+
69+
Object.keys(this.getChildrenMapFromSchema()).forEach((key: string, index: number) => {
70+
childrenMap[key] = this.findChild(key).render(context);
71+
});
72+
73+
return this.renderComposite(context, childrenMap);
74+
}
75+
}

src/defaultParserConfig.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { SchemaParserConfig } from "./schemaTypes";
2+
3+
export const defaultParserConfig: SchemaParserConfig = {
4+
handlers: {
5+
ROOT: {
6+
default: null,
7+
},
8+
STRING: {
9+
default: null,
10+
},
11+
OBJECT: {
12+
default: null,
13+
},
14+
},
15+
rootState: null,
16+
rootSetState: () => {},
17+
rootSetContext: () => {},
18+
rootModifyContext: () => {},
19+
uidGenerator: null,
20+
uidGeneratorFactory: () => {
21+
let uid = 0;
22+
return () => {
23+
++uid;
24+
return uid;
25+
};
26+
},
27+
};

src/schemaParser.tsx

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
2+
import {
3+
isNodeErrorPure,
4+
Node,
5+
NodeAny,
6+
NodeError,
7+
NodeHandler,
8+
NodeMetaOutputValue,
9+
NodeSchema,
10+
NodeType,
11+
NodeTypeSchemas,
12+
RootNode,
13+
SchemaNodeHandlersMappingForType,
14+
SchemaParserConfig,
15+
SchemaParserConfigOpt,
16+
} from "./schemaTypes";
17+
18+
import { defaultParserConfig } from "./defaultParserConfig";
19+
20+
export interface AjvError {
21+
dataPath: string;
22+
keyword: string;
23+
message?: string;
24+
params: any;
25+
schemaPath: string;
26+
node?: NodeAny;
27+
}
28+
29+
function getHandlerForUI<M extends NodeSchema>(node: M, handlers: SchemaNodeHandlersMappingForType<M>): NodeHandler<any, any, M> {
30+
return node.ui ? handlers[node.ui] : handlers.default;
31+
}
32+
33+
function getHandlerForType<M extends NodeSchema>(node: M, config: SchemaParserConfig): SchemaNodeHandlersMappingForType<NodeTypeSchemas[M["type"]]> {
34+
const t: NodeType = node.type;
35+
switch (t) {
36+
case NodeType.OBJECT:
37+
return config.handlers.OBJECT;
38+
case NodeType.STRING:
39+
return config.handlers.STRING;
40+
case NodeType.ROOT:
41+
return null;
42+
}
43+
const never: never = t;
44+
return never;
45+
}
46+
47+
function createNode<M extends NodeSchema>(node: M, parentNode: NodeAny, config: SchemaParserConfig, handler: NodeHandler<any, any, M>): Node<any, any, M> {
48+
const astNode: Node<any, any, M> = new handler(
49+
handler, node.type, node, parentNode, config, recTransformSchemaIntoTree,
50+
);
51+
astNode.resolve();
52+
53+
parentNode.addChild(astNode);
54+
return astNode;
55+
}
56+
57+
export function recTransformSchemaIntoTree<M extends NodeSchema>(node: M, parentNode: NodeAny, config: SchemaParserConfig): Node<any, any, M> {
58+
return createNode(
59+
node, parentNode, config,
60+
getHandlerForUI(node, getHandlerForType(node, config)),
61+
);
62+
}
63+
64+
export function isNodeMetaOutputValue(data: any): data is NodeMetaOutputValue<any> {
65+
return (data && (typeof data.__data) !== "undefined" && data.__source);
66+
}
67+
68+
function recGetMetaOutputSourceNodeByPath(metaOutput: any, path: Array<string> | string): any {
69+
if (isNodeMetaOutputValue(metaOutput)) {
70+
if (path !== null && path.length === 0) {
71+
return metaOutput.__source;
72+
} else {
73+
return recGetMetaOutputSourceNodeByPath(metaOutput.__data, path);
74+
}
75+
} else if (path === null) {
76+
return metaOutput;
77+
} else if (typeof path === "string") {
78+
return recGetMetaOutputSourceNodeByPath(metaOutput, path.split("."));
79+
} else if (path.length === 0) {
80+
return recGetMetaOutputSourceNodeByPath(metaOutput, null);
81+
} else {
82+
return recGetMetaOutputSourceNodeByPath(metaOutput[path[0]], path.slice(1));
83+
}
84+
}
85+
86+
export function getMetaOutputSourceNodeByPath(metaOutput: any, path: string): any {
87+
const objPath = [];
88+
for (let match,matcher=/^([^\.\[]+)|\.([^\.\[]+)|\["([^"]+)"\]|\[(\d+)\]/g; match = matcher.exec(path);) {
89+
objPath.push(Array.from(match).slice(1).filter(x => x !== undefined)[0]);
90+
}
91+
return recGetMetaOutputSourceNodeByPath(metaOutput, objPath);
92+
}
93+
94+
export function transformOutputToRawData(metaOutput: any): any {
95+
if (isNodeMetaOutputValue(metaOutput)) {
96+
return transformOutputToRawData(metaOutput.__data);
97+
} else if (Array.isArray(metaOutput)) {
98+
metaOutput.map(item => transformOutputToRawData(item));
99+
} else if (metaOutput instanceof Object) {
100+
const result = {};
101+
Object.keys(metaOutput).forEach(key => {
102+
result[key] = transformOutputToRawData(metaOutput[key]);
103+
});
104+
return result;
105+
} else {
106+
return metaOutput;
107+
}
108+
}
109+
110+
export async function validateRoot(rootNode: RootNode) {
111+
const Ajv = ((await import("ajv")) as unknown as any).default;
112+
113+
const ajv = new Ajv({
114+
allErrors: true,
115+
...rootNode.getConfig().ajvOptions,
116+
});
117+
const validateSchema = ajv.compile(rootNode.getSchema() as unknown as object);
118+
const output = rootNode.getOutput();
119+
const data = transformOutputToRawData(output);
120+
121+
validateSchema(data);
122+
123+
let errors: Array<NodeError> = validateSchema.errors || [];
124+
const errorsMap: WeakMap<NodeAny, Array<NodeError>> = new WeakMap();
125+
126+
const customErrors: Array<NodeError> = rootNode.validateCustom();
127+
errors = errors.concat(customErrors);
128+
129+
errors.forEach(error => {
130+
let source: NodeAny = null;
131+
if (isNodeErrorPure(error)) {
132+
source = error.source;
133+
} else {
134+
source = getMetaOutputSourceNodeByPath(output, error.dataPath);
135+
}
136+
137+
if (source !== null) {
138+
const err: NodeError = error;
139+
140+
if (errorsMap.has(source)) {
141+
errorsMap.get(source).push(err);
142+
} else {
143+
errorsMap.set(source, [ err ]);
144+
}
145+
}
146+
});
147+
148+
rootNode.setContext({
149+
errors: errors || [],
150+
getErrorsForNode: (node: NodeAny) => {
151+
return errorsMap.get(node) || [];
152+
},
153+
});
154+
}
155+
156+
export function transformSchemaIntoTree<M extends NodeSchema>(node: M, rootNode: RootNode = null, config: SchemaParserConfigOpt = null): RootNode {
157+
const conf = {...defaultParserConfig, ...config};
158+
if (!conf.uidGenerator) {
159+
conf.uidGenerator = conf.uidGeneratorFactory();
160+
}
161+
162+
if (!rootNode) {
163+
rootNode = new RootNode(
164+
node, conf, recTransformSchemaIntoTree, transformOutputToRawData, validateRoot,
165+
);
166+
rootNode.resolve();
167+
}
168+
169+
recTransformSchemaIntoTree(node, rootNode, conf);
170+
return rootNode;
171+
}

0 commit comments

Comments
 (0)