UNPKG

test-openapi

Version:
241 lines (194 loc) 7.42 kB
/* eslint-disable-line max-lines */ 'use strict' const { get } = require('lodash') const { addErrorHandler } = require('../errors') const { crawl, promiseThen, promiseAllThen } = require('../utils') const { parseTemplate, parseEscape } = require('./parse') const { checkRecursion } = require('./recursion') const { templateHandler } = require('./error') // This is a data templating system. // As opposed to most other templating system, it works over (parsed) data // instead of (serialized) strings. // Advantages include: // - guarantees serialized data is still valid // - e.g. Handlebars producing JSON output might not be valid JSON itself // - complex arguments are readable // - format-agnostic // - e.g. `jsonnet` cannot be used with YAML // - handles circular references // - handles non serializable types (e.g. sockets) // Beyond being data-centered, this templating system: // - has simple syntax, to keep it truly declarative // - no operators, variables scoping, references, inline functions // - only includes templating features // - no comments, string escaping, text blocks // `undefined` values are not treated differently than other values. // Evaluate template // eslint-disable-next-line max-params const evalTemplate = function(data, vars = {}, opts = {}, stack) { const recursive = recursiveEval.bind(null, vars, opts) const optsA = { ...opts, vars, stack, recursive } return crawl(data, evalNode.bind(null, optsA)) } // Recursive calls, done automatically when evaluating `$$name` // eslint-disable-next-line max-params const recursiveEval = function(vars, opts, stack, data) { return evalTemplate(data, vars, opts, stack) } // Evaluate templates in an object or part of an object const evalNode = function(opts, data, path) { const template = parseTemplate(data) // There are no template markers if (template === undefined) { return data } const { type } = template if (type === 'concat') { return evalConcat({ template, opts, path }) } return eEvalSingle({ template, opts, data, path }) } // Evaluate `$$name` when it's inside a string. // Its result will be transtyped to string and concatenated. const evalConcat = function({ template: { tokens }, opts, path }) { const maybePromises = tokens.map(token => evalConcatToken({ token, opts, path }), ) // There can be several `$$name` inside a string, in which case they are // evaluated in parallel return promiseAllThen(maybePromises, concatTokens) } const evalConcatToken = function({ token, token: { type, name }, opts, path }) { // Parts between `$$name` have `type: 'raw'` if (type === 'raw') { return name } return eEvalSingle({ template: token, opts, data: name, path }) } // `tokens` are joined. // They will be implicitely transtyped to `String`. We do not use // `JSON.stringify()` because we want to be format-agnostic. // `undefined` values will be omitted. const concatTokens = function(tokens) { return tokens.join('') } const evalSingle = function({ template, opts }) { const unescapedData = parseEscape({ template }) // There was something that looked like a template but was an escaped data if (unescapedData !== undefined) { return unescapedData } // Check for infinite recursions const optsA = checkRecursion({ template, opts }) const { data, propPath } = getTopLevelProp({ template, opts: optsA }) // Unkwnown templates or templates with `undefined` values return `undefined`, // instead of throwing an error. This allows using dynamic templates, where // some properties might be defined or not. if (data === undefined) { return } // `$$name` can be an async function, fired right away return promiseThen(data, dataA => getNestedProp({ data: dataA, template, opts: optsA, propPath }), ) } const eEvalSingle = addErrorHandler(evalSingle, templateHandler) // Retrieve template's top-level value const getTopLevelProp = function({ template, template: { name }, opts: { vars }, }) { const { topName, propPath } = parseName({ name }) const data = vars[topName] const dataA = evalFunction({ data, template, propPath }) return { data: dataA, propPath } } // `$$name` and `{ $$name: arg }` can both use dot notations. // The top-level value is first evaluated (including recursively parsing its // templates) then the rest of the property path is applied. const parseName = function({ name }) { const index = name.search(BRACKETS_REGEXP) if (index === -1) { return { topName: name } } const topName = name.slice(0, index) const delimIndex = getDelimIndex({ name, index }) const propPath = name.slice(delimIndex) return { topName, propPath } } // Dot notation can also use brackets const BRACKETS_REGEXP = /[.[]/u // Brackets are kept but not dots (because of how `_.get()` works) const getDelimIndex = function({ name, index }) { if (name[index] === '[') { return index } return index + 1 } // If `$$name` (but not `{ $$name: arg }`) is a function, it is evaluated right // away with no arguments // It can be an async function. const evalFunction = function({ data, template, propPath }) { if (!shouldFireFunction({ data, template, propPath })) { return data } return data() } const shouldFireFunction = function({ data, template: { type }, propPath }) { return ( typeof data === 'function' && // Do not fire when the function is also used as an object. // This is for example how Lodash main object works. Object.keys(data).length === 0 && // `{ $$func: arg }` should be fired with the argument, not right away. // But when using `{ $$func.then.another.func: arg }`, the first `func` // should be fired right away, but not the last one. !(type === 'function' && propPath === undefined) ) } // Retrieve template's nested value (i.e. property path) const getNestedProp = function({ data, template, opts: { recursive }, propPath, }) { // A template `$$name` can contain other templates, which are then processed // recursively. // This can be used e.g. to create aliases. // This is done only on `$$name` but not `{ $$name: arg }` return value // because: // - in functions, it is most likely not the desired intention of the user // - it would require complex escaping (if user does not desire recursion) // E.g. `{ $$identity: { $$identity: $$$$name } }` -> // `{ $$identity: $$$name }` -> `$$name` const dataA = recursive(data) return promiseThen(dataA, dataB => evalNestedProp({ data: dataB, template, propPath }), ) } const evalNestedProp = function({ data, template: { type, arg }, propPath }) { const dataA = getProp({ data, propPath }) // Including `undefined` if (type !== 'function') { return dataA } // Can use `{ $$name: [...] }` to pass several arguments to the template // function. // E.g. `{ $$myFunc: [1, 2] }` will fire `$$myFunc(1, 2)` const args = Array.isArray(arg) ? arg : [arg] // Fire template when it's a function `{ $$name: arg }` // To pass more arguments, e.g. options, template functions must be bound. // E.g. a library providing templates could provide a factory function. return dataA(...args) } const getProp = function({ data, propPath }) { if (propPath === undefined) { return data } return get(data, propPath) } module.exports = { evalTemplate, }