UNPKG

@comyata/run

Version:

Simplify data workflows and management with data templates, for browser and server.

145 lines (141 loc) 6.14 kB
import { DataNode, DataNodeObject } from '@comyata/run/DataNode'; import { NodeParserError } from '@comyata/run/Errors'; import jsonpointer from 'json-pointer'; function escapeRegex(string) { return string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); } export class Parser { constructor(nodesTypes = [], options = {}) { this.nodes = nodesTypes; this.options = { ...options, paren: options.paren || ['{', '}'] }; // todo refactor matcher and extract-expr for a better usage outside of runtime, // maybe add a `comyata-utils` for these universal field utils? const tagPattern = new RegExp(`^(?<engine>${this.nodes.filter(tag => tag.engine).map(tag => escapeRegex(tag.engine)).join('|')})${escapeRegex(this.options.paren[0])}`); const offsetParenStart = this.options.paren[0].length; const offsetParenEnd = this.options.paren[1].length; const getTagExpExtract = offsetParenEnd ? tag => { const offsetStart = tag.length + offsetParenStart; return value => { return value.slice(offsetStart, -offsetParenEnd); }; } : tag => { const offsetStart = tag.length + offsetParenStart; return value => { return value.slice(offsetStart); }; }; const nodesMap = this.nodes.reduce((nodesMap, nodeType) => { if (nodeType.engine) { nodesMap.set(nodeType.engine, [nodeType, getTagExpExtract(nodeType.engine)]); } return nodesMap; }, new Map()); const matchText = text => { const match = text.match(tagPattern); if (match?.groups?.engine) { const tagName = match.groups.engine; return nodesMap.get(tagName); } return undefined; }; if (this.options.paren[1] === '') { this.matchNode = matchText; } else { this.matchNode = text => { if (!text.endsWith(this.options.paren[1])) return undefined; return matchText(text); }; } } static dataNodeParsers = { null: (currentValue, currentPath, parent) => { return [new DataNode(parent, currentPath, 'null', currentValue).withHydrate(() => currentValue)]; }, array: (currentValue, currentPath, parent) => { const valueLength = currentValue.length; const dataNode = new DataNodeObject(parent, currentPath, 'array', currentValue).withHydrate(() => new Array(valueLength)); return [dataNode, currentValue]; }, object: (currentValue, currentPath, parent) => { const dataNode = new DataNodeObject(parent, currentPath, 'object', currentValue).withHydrate(() => ({})); return [dataNode, currentValue]; }, number: (currentValue, currentPath, parent) => { return [new DataNode(parent, currentPath, 'number', currentValue).withHydrate(() => currentValue)]; }, string: (currentValue, currentPath, parent) => { return [new DataNode(parent, currentPath, 'string', currentValue).withHydrate(() => currentValue)]; }, boolean: (currentValue, currentPath, parent) => { return [new DataNode(parent, currentPath, 'boolean', currentValue).withHydrate(() => currentValue)]; }, undefined: (_currentValue, currentPath, parent) => { return [new DataNode(parent, currentPath, 'undefined', undefined).withHydrate(() => undefined)]; } }; parseData = (currentValue, currentPath, parent) => { if (typeof currentValue === 'string') { const nodeTag = this.matchNode(currentValue); if (nodeTag) { try { return [new nodeTag[0](parent, currentPath, '', currentValue, nodeTag[1])]; } catch (e) { if (e instanceof NodeParserError) throw e; throw new NodeParserError(currentPath, parent, `Parse error` + ` at ${JSON.stringify(jsonpointer.compile(currentPath))}` + ` with ${JSON.stringify(nodeTag[0].engine)}.` + `${e instanceof Error ? '\n' + e.message : typeof e === 'object' && e && 'message' in e ? '\n' + e.message : ''}`, e); } } return Parser.dataNodeParsers.string(currentValue, currentPath, parent); } else if (typeof currentValue === 'object') { if (currentValue === null) { return Parser.dataNodeParsers.null(currentValue, currentPath, parent); } else if (Array.isArray(currentValue)) { return Parser.dataNodeParsers.array(currentValue, currentPath, parent); } return Parser.dataNodeParsers.object(currentValue, currentPath, parent); } const type = typeof currentValue; if (type in Parser.dataNodeParsers) { return Parser.dataNodeParsers[type]( // @ts-expect-error not possible to type guard value currentValue, currentPath, parent); } throw new NodeParserError(currentPath, parent, `Parse error` + ` at ${JSON.stringify(jsonpointer.compile(currentPath))}` + ` unsupported value in data, no supported parser for type ${JSON.stringify(type)}.`); }; /** * @deprecated create an instance and use it instead */ /* istanbul ignore next */ static parse(objOrEval) { return new Parser([]).parse(objOrEval); } parse(objOrEval) { const parseComments = this.options.comments; const [rootNode, nextObject] = this.parseData(objOrEval, [], undefined); const openParser = nextObject ? [[rootNode, nextObject]] : []; while (openParser.length) { const [dataNode, currentObject] = openParser.pop(); if (Array.isArray(currentObject)) { for (const [key, val] of currentObject.entries()) { const [nextDataNode, nextObject] = this.parseData(val, [...dataNode.path, key], dataNode); dataNode.append(key, nextDataNode); if (nextObject) { openParser.push([nextDataNode, nextObject]); } } } else { for (const [key, val] of Object.entries(currentObject)) { if (parseComments && key.endsWith('!')) continue; const [nextDataNode, nextObject] = this.parseData(val, [...dataNode.path, key], dataNode); dataNode.append(key, nextDataNode); if (nextObject) { openParser.push([nextDataNode, nextObject]); } } } } return rootNode; } }