@f5devcentral/f5-fast-core
Advanced tools
Comparing version 0.7.0 to 0.8.0
@@ -0,1 +1,19 @@ | ||
# v0.8.0 | ||
## Added | ||
* template: Support extended user types (e.g., "var:lib:type") for sections and partials | ||
* template: Add post-processing strategies and add one for 'application/json' to cleanup dangling commas | ||
* cli: Add guiSchema sub command that runs the parameters schema through guiUtils.modSchemaForJSONEditor() before displaying it | ||
## Fixed | ||
* template: Fix using full Mustache variable names (e.g., "var:lib:type") for dependencies and requires | ||
* template: Fix sections with a dot item overriding user definitions | ||
* template: Add missing doc strings for HTTP fetching and forwarding functions | ||
* template: Fix missing defaults from merged templates | ||
* guiUtils: Additional fixes for allOf schema in modSchemaForJSONEditor() | ||
* guiUtils: Do not error if a dependency is missing from the properties | ||
## Changed | ||
* cli: Run fetchHttp() as part of render command | ||
* guiUtils: modSchemaForJSONEditor() no longer modifies in place | ||
# v0.7.0 | ||
@@ -2,0 +20,0 @@ ## Added |
29
cli.js
@@ -13,3 +13,3 @@ #!/usr/bin/env node | ||
const FsTemplateProvider = require('./lib/template_provider').FsTemplateProvider; | ||
const generateHtmlPreview = require('./lib/gui_utils').generateHtmlPreview; | ||
const guiUtils = require('./lib/gui_utils'); | ||
@@ -54,3 +54,4 @@ const loadTemplate = (templatePath) => { | ||
.then((tmpl) => { | ||
console.log(JSON.stringify(tmpl.getParametersSchema(), null, 2)); | ||
const schema = tmpl.getParametersSchema(); | ||
console.log(JSON.stringify(schema, null, 2)); | ||
}) | ||
@@ -62,2 +63,13 @@ .catch((e) => { | ||
const templateToParametersSchemaGui = templatePath => loadTemplate(templatePath) | ||
.then((tmpl) => { | ||
let schema = tmpl.getParametersSchema(); | ||
schema = guiUtils.modSchemaForJSONEditor(schema); | ||
console.log(JSON.stringify(schema, null, 2)); | ||
}) | ||
.catch((e) => { | ||
console.error(`Failed to generate schema:\n${e.stack}`); | ||
process.exit(1); | ||
}); | ||
const validateParamData = (tmpl, parameters) => { | ||
@@ -82,2 +94,7 @@ try { | ||
const renderTemplate = (templatePath, parametersPath) => loadTemplateAndParameters(templatePath, parametersPath) | ||
.then(([tmpl, parameters]) => Promise.all([ | ||
Promise.resolve(tmpl), | ||
tmpl.fetchHttp() | ||
.then(httpParams => Object.assign({}, parameters, httpParams)) | ||
])) | ||
.then(([tmpl, parameters]) => { | ||
@@ -105,3 +122,3 @@ validateParamData(tmpl, parameters); | ||
const htmlPreview = (templatePath, parametersPath) => loadTemplateAndParameters(templatePath, parametersPath) | ||
.then(([tmpl, parameters]) => generateHtmlPreview( | ||
.then(([tmpl, parameters]) => guiUtils.generateHtmlPreview( | ||
tmpl.getParametersSchema(), | ||
@@ -141,2 +158,8 @@ tmpl.getCombinedParameters(parameters) | ||
}, argv => templateToParametersSchema(argv.file)) | ||
.command('guiSchema <file>', 'get template parameter schema (modified for use with JSON Editor) for given template source file', (yargs) => { | ||
yargs | ||
.positional('file', { | ||
describe: 'template source file to parse' | ||
}); | ||
}, argv => templateToParametersSchemaGui(argv.file)) | ||
.command('validateParameters <tmplFile> <parameterFile>', 'validate supplied template parameters with given template', (yargs) => { | ||
@@ -143,0 +166,0 @@ yargs |
@@ -7,3 +7,3 @@ 'use strict'; | ||
const { FsTemplateProvider, DataStoreTemplateProvider } = require('./lib/template_provider'); | ||
const { Template, mergeStrategies } = require('./lib/template'); | ||
const { Template, mergeStrategies, postProcessStrategies } = require('./lib/template'); | ||
const httpUtils = require('./lib/http_utils'); | ||
@@ -19,2 +19,3 @@ const guiUtils = require('./lib/gui_utils'); | ||
mergeStrategies, | ||
postProcessStrategies, | ||
httpUtils, | ||
@@ -21,0 +22,0 @@ guiUtils, |
@@ -38,2 +38,5 @@ 'use strict'; | ||
Object.keys(schema.dependencies).forEach((key) => { | ||
if (!schema.properties[key]) { | ||
return; | ||
} | ||
const depsOpt = schema.dependencies[key].reduce((acc, curr) => { | ||
@@ -74,23 +77,35 @@ acc[curr] = !( | ||
const fixAllOfOrder = (schema) => { | ||
const fixAllOfOrder = (schema, orderID) => { | ||
orderID = orderID || 0; | ||
if (schema.allOf) { | ||
schema.allOf.forEach((subSchema) => { | ||
orderID = fixAllOfOrder(subSchema, orderID); | ||
}); | ||
} | ||
if (schema.properties) { | ||
Object.keys(schema.properties).forEach((key) => { | ||
const prop = schema.properties[key]; | ||
if (!prop.propertyOrder && !keyInXOf(key, schema)) { | ||
prop.propertyOrder = 1100; | ||
prop.propertyOrder = orderID; | ||
orderID += 1; | ||
} | ||
}); | ||
} | ||
return orderID; | ||
}; | ||
const collapseAllOf = (schema) => { | ||
Object.assign(schema, mergeAllOf(schema)); | ||
fixAllOfOrder(schema); | ||
return mergeAllOf(schema); | ||
}; | ||
const modSchemaForJSONEditor = (schema) => { | ||
schema = JSON.parse(JSON.stringify(schema)); // Do not modify original schema | ||
schema.title = schema.title || 'Template'; | ||
schema = collapseAllOf(schema); | ||
injectFormatsIntoSchema(schema); | ||
addDepsToSchema(schema); | ||
fixAllOfOrder(schema); | ||
collapseAllOf(schema); | ||
@@ -112,4 +127,5 @@ return schema; | ||
const generateHtmlPreview = (schema, view) => { | ||
schema = modSchemaForJSONEditor(schema); | ||
const htmlView = { | ||
schema_data: JSON.stringify(modSchemaForJSONEditor(schema)), | ||
schema_data: JSON.stringify(schema), | ||
default_view: JSON.stringify(filterExtraProperties(view, schema)), | ||
@@ -116,0 +132,0 @@ jsoneditor: fs.readFileSync(`${__dirname}/jsoneditor.js`, 'utf8') |
@@ -16,6 +16,5 @@ 'use strict'; | ||
const _templateSchemaData = require('./template_schema').schema; | ||
const tmplSchema = require('../schema/template.json'); | ||
// Setup validator | ||
const tmplSchema = yaml.safeLoad(_templateSchemaData); | ||
const validator = new Ajv(); | ||
@@ -52,2 +51,9 @@ | ||
if (schema.type === 'object') { | ||
if (!value) { | ||
return JSON.stringify({}); | ||
} | ||
return JSON.stringify(value); | ||
} | ||
if (schema.format === 'text' && value) { | ||
@@ -86,2 +92,18 @@ return JSON.stringify(value); | ||
/** | ||
* PostProcessStrategy for targeting `application/json` Content-Type | ||
*/ | ||
function JsonPostProcessStrategy(rendered) { | ||
return JSON.stringify(yaml.safeLoad(rendered), null, 2); | ||
} | ||
/** | ||
* Object containing available post-processing strategy functions. | ||
* The property is a Content-Type MIME (e.g., `test/plain`). | ||
* The value is a PostProcessStrategy function that accepts rendered output. | ||
*/ | ||
const postProcessStrategies = { | ||
'application/json': JsonPostProcessStrategy | ||
}; | ||
// Disable HTML escaping | ||
@@ -196,9 +218,22 @@ Mustache.escape = function escape(text) { | ||
const [mstType, mstName] = [curr[0], curr[1]]; | ||
const [defName, schemaName, type] = mstName.split(':'); | ||
if (['name', '#', '>', '^'].includes(mstType)) { | ||
if (schemaName && typeof typeSchemas[schemaName] === 'undefined') { | ||
throw new Error(`Failed to find the specified schema: ${schemaName} (${mstType}, ${mstName})`); | ||
} | ||
if (schemaName) { | ||
const schemaDef = typeSchemas[schemaName].definitions[type]; | ||
if (!schemaDef) { | ||
throw new Error(`No definition for ${type} in ${schemaName} schema`); | ||
} | ||
this.definitions[type] = Object.assign({}, schemaDef, this.definitions[type]); | ||
Object.assign(this.typeDefinitions, typeSchemas[schemaName].definitions); | ||
} | ||
} | ||
switch (mstType) { | ||
case 'name': { | ||
const [defName, schemaName, type] = mstName.split(':'); | ||
const defType = type || 'string'; | ||
if (schemaName && typeof typeSchemas[schemaName] === 'undefined') { | ||
throw new Error(`Failed to find the specified schema: ${schemaName}`); | ||
} | ||
if (!schemaName && typeof primitives[defType] === 'undefined') { | ||
@@ -211,7 +246,2 @@ throw new Error(`No schema definition for ${schemaName}/${defType}`); | ||
acc.properties[defName] = Object.assign({}, schemaDef); | ||
if (!acc.properties[defName]) { | ||
throw new Error(`No definition for ${defType} in ${schemaName} schema`); | ||
} | ||
this.definitions[defType] = Object.assign({}, schemaDef, this.definitions[defType]); | ||
Object.assign(this.typeDefinitions, typeSchemas[schemaName].definitions); | ||
} else { | ||
@@ -256,6 +286,6 @@ if (defType === 'text') { | ||
case '>': { | ||
if (!knownPartials[mstName]) { | ||
throw new Error(`${mstName} does not reference a known partial`); | ||
if (!knownPartials[defName]) { | ||
throw new Error(`${defName} does not reference a known partial`); | ||
} | ||
const partial = this.typeDefinitions[mstName]; | ||
const partial = this.typeDefinitions[defName]; | ||
this._mergeSchemaInto(acc, partial, dependencies); | ||
@@ -274,12 +304,16 @@ if (partial.required) { | ||
const items = this._handleParsed(curr[4], typeSchemas); | ||
const defType = (this.definitions[mstName] && this.definitions[mstName].type) || 'array'; | ||
const newDef = Object.assign({ type: defType }, this.definitions[mstName]); | ||
const schemaDef = Object.assign( | ||
this.typeDefinitions[type] || {}, | ||
this.definitions[defName] || {} | ||
); | ||
const defType = schemaDef.type || 'array'; | ||
const newDef = Object.assign({ type: defType }, schemaDef); | ||
const asBool = defType === 'boolean' || defType === 'string'; | ||
if (defType === 'array') { | ||
newDef.skip_xform = true; | ||
newDef.items = Object.assign({}, items); | ||
newDef.items = Object.assign({}, items, newDef.items); | ||
} else if (defType === 'object') { | ||
Object.assign(newDef, items); | ||
} else if (!asBool) { | ||
throw new Error(`unsupported type for section "${mstName}": ${defType}`); | ||
throw new Error(`unsupported type for section "${defName}": ${defType}`); | ||
} | ||
@@ -292,3 +326,3 @@ | ||
} | ||
dependencies[item].push(mstName); | ||
dependencies[item].push(defName); | ||
}); | ||
@@ -305,4 +339,4 @@ } | ||
acc.properties[mstName] = Object.assign({}, newDef); | ||
required.add(mstName); | ||
acc.properties[defName] = Object.assign({}, newDef); | ||
required.add(defName); | ||
@@ -313,8 +347,13 @@ break; | ||
const items = this._handleParsed(curr[4], typeSchemas); | ||
const schemaDef = Object.assign( | ||
this.typeDefinitions[type] || {}, | ||
this.definitions[defName] || {} | ||
); | ||
if (!acc.properties[mstName]) { | ||
acc.properties[mstName] = { | ||
if (!acc.properties[defName]) { | ||
acc.properties[defName] = Object.assign({ | ||
type: 'boolean', | ||
default: primitives.boolean | ||
}; | ||
}, | ||
schemaDef); | ||
} | ||
@@ -326,7 +365,7 @@ if (items.properties) { | ||
} | ||
dependencies[item].push(mstName); | ||
dependencies[item].push(defName); | ||
if (!items.properties[item].invertDependency) { | ||
items.properties[item].invertDependency = []; | ||
} | ||
items.properties[item].invertDependency.push(mstName); | ||
items.properties[item].invertDependency.push(defName); | ||
}); | ||
@@ -336,6 +375,6 @@ } | ||
// If an inverted section is present, the section variable is not required | ||
required.delete(mstName); | ||
required.delete(defName); | ||
if (this.definitions[mstName]) { | ||
Object.assign(acc.properties[mstName], this.definitions[mstName]); | ||
if (this.definitions[defName]) { | ||
Object.assign(acc.properties[defName], this.definitions[defName]); | ||
} | ||
@@ -350,3 +389,3 @@ this._mergeSchemaInto(acc, items, dependencies); | ||
default: | ||
// console.log(`skipping ${mstName} with type of ${mstType}`); | ||
// console.log(`skipping ${defName} with type of ${mstType}`); | ||
} | ||
@@ -360,4 +399,3 @@ return acc; | ||
return { | ||
type: 'string', | ||
default: primitives.string | ||
type: 'string' | ||
}; | ||
@@ -485,2 +523,4 @@ } | ||
* @param {SchemaProvider} [schemaProvider] - SchemaProvider to use to fetch schema referenced by the template | ||
* | ||
* @returns {Promise} Promise resolves to `Template` | ||
*/ | ||
@@ -508,2 +548,4 @@ static loadMst(msttext, schemaProvider) { | ||
* @param {string} [rootDir] | ||
* | ||
* @returns {Promise} Promise resolves to `Template` | ||
*/ | ||
@@ -588,2 +630,4 @@ static loadYaml(yamltext, schemaProvider, filePath, rootDir) { | ||
* @param {object|string} obj - The JSON data to create a `Template` from | ||
* | ||
* @returns {Promise} Promise resolves to `Template` | ||
*/ | ||
@@ -681,3 +725,3 @@ static fromJson(obj) { | ||
mergedDefaults, | ||
tmpl.defaultParameters | ||
tmpl.getCombinedParameters(parameters) | ||
); | ||
@@ -739,3 +783,3 @@ }); | ||
_cleanTemplateText(text) { | ||
return text.replace(/{{([_a-zA-Z0-9]+):.*}}/g, '{{$1}}'); | ||
return text.replace(/{{([_a-zA-Z0-9#^>/]+):.*?}}/g, '{{$1}}'); | ||
} | ||
@@ -747,2 +791,4 @@ | ||
* @param {object} parameters | ||
* | ||
* @returns {string} rendered result | ||
*/ | ||
@@ -781,3 +827,3 @@ render(parameters) { | ||
return templateTexts.reduce((acc, curr) => { | ||
const rendered = templateTexts.reduce((acc, curr) => { | ||
if (curr.length === 0) { | ||
@@ -794,4 +840,16 @@ return acc; | ||
}); | ||
const postProcessStrategy = postProcessStrategies[this.contentType]; | ||
if (postProcessStrategy) { | ||
return postProcessStrategy(rendered); | ||
} | ||
return rendered; | ||
} | ||
/** | ||
* Fetch data using an HTTP request for properties that specify a URL | ||
* | ||
* @returns {object} parameters | ||
*/ | ||
fetchHttp() { | ||
@@ -836,2 +894,9 @@ const promises = []; | ||
/** | ||
* Run fetchHttp() and combine the results with the supplied parameters object to pass to render() | ||
* | ||
* @param {object} parameters | ||
* | ||
* @returns {string} rendered result | ||
*/ | ||
fetchAndRender(parameters) { | ||
@@ -844,2 +909,11 @@ return Promise.resolve() | ||
/** | ||
* Render the template using the supplied parameters object and forward the results based on `httpForward` property | ||
* | ||
* Also run fetchHttp(). | ||
* | ||
* @param {object} parameters | ||
* | ||
* @returns {Promise} Promise resolves to HTTP response results | ||
*/ | ||
forwardHttp(parameters) { | ||
@@ -846,0 +920,0 @@ if (!this.httpForward) { |
{ | ||
"name": "@f5devcentral/f5-fast-core", | ||
"version": "0.7.0", | ||
"version": "0.8.0", | ||
"author": "F5 Networks", | ||
@@ -27,5 +27,5 @@ "license": "Apache-2.0", | ||
"chai-as-promised": "^7.1.1", | ||
"eslint": "^6.8.0", | ||
"mocha": "^7.2.0", | ||
"nock": "^12.0.3", | ||
"eslint": "^7.10.0", | ||
"mocha": "^8.1.3", | ||
"nock": "^13.0.4", | ||
"nyc": "^15.1.0", | ||
@@ -55,3 +55,3 @@ "pkg": "^4.4.9" | ||
"@f5devcentral/atg-storage": "^0.1.0", | ||
"ajv": "^6.12.4", | ||
"ajv": "^6.12.5", | ||
"archiver": "^4.0.2", | ||
@@ -58,0 +58,0 @@ "deepmerge": "^4.2.2", |
140
README.md
@@ -26,6 +26,4 @@ ![Pipeline](https://github.com/f5devcentral/f5-fast-core/workflows/Pipeline/badge.svg) | ||
## Usage | ||
## CLI | ||
### CLI | ||
A command line interface is provided via a `fast` binary. | ||
@@ -41,2 +39,3 @@ The help text is provided below and also accessed via `fast --help`: | ||
fast schema <file> get template parameter schema for given template source file | ||
fast guiSchema <file> get template parameter schema (modified for use with JSON Editor) for given template source file | ||
fast validateParameters <tmplFile> <parameterFile> validate supplied template parameters with given template | ||
@@ -66,4 +65,6 @@ fast render <tmplFile> [parameterFile] render given template file with supplied parameters | ||
### Module | ||
## Module API | ||
### Simple Loading | ||
Below is a basic example for loading a template without any additional type schema: | ||
@@ -74,4 +75,4 @@ | ||
const ymldata = ` | ||
view: | ||
const yamldata = ` | ||
parameters: | ||
message: Hello! | ||
@@ -90,3 +91,3 @@ definitions: | ||
fast.Template.loadYaml(ymldata) | ||
fast.Template.loadYaml(yamldata) | ||
.then((template) => { | ||
@@ -98,3 +99,3 @@ console.log(template.getParametersSchema()); | ||
In addition to `Template.loadYaml()`, a `Template` can be created from Mustache data using `Template.loadMst()`: | ||
If a `Template` has been serialized to JSON (e.g., to send in an HTTP request), it can be deserialized with `Template.fromJson()`: | ||
@@ -104,5 +105,10 @@ ```javascript | ||
const mstdata = '{{message}}'; | ||
const yamldata = ` | ||
template: | | ||
{{message}} | ||
`; | ||
fast.Template.loadMst(ymldata) | ||
fast.Template.loadYaml(yamldata) | ||
.then(template => JSON.stringify(template)) | ||
.then(jsonData => template.fromJson(jsonData)) | ||
.then((template) => { | ||
@@ -114,2 +120,19 @@ console.log(template.getParametersSchema()); | ||
`Template` does not provide a mechanism for loading a template from a file and, instead, needs to be paired with something like Node's `fs` module: | ||
```javascript | ||
const fs = require('fs'); | ||
const fast = require('@f5devcentral/f5-fast-core'); | ||
const yamldata = fs.readFileSync('path/to/file', 'utf8'); | ||
fast.Template.loadYaml(yamldata) | ||
.then((template) => { | ||
console.log(template.getParametersSchema()); | ||
console.log(template.render({message: "Hello world!"})); | ||
}); | ||
``` | ||
### Loading with Type Schema | ||
To support user-defined types, a `SchemaProvider` must be used. | ||
@@ -123,5 +146,8 @@ The `FsSchemaProvider` can be used to load schema from disk: | ||
const schemaProvider = new fast.FsSchemaProvider(templatesPath); | ||
const mstdata = '{{virtual_port:types:port}}'; | ||
const yamldata = ` | ||
template: | | ||
{{virtual_port:types:port}} | ||
`; | ||
fast.Template.loadMst(mstdata, schemaProvider) | ||
fast.Template.loadYaml(yamldata, schemaProvider) | ||
.then((template) => { | ||
@@ -133,5 +159,7 @@ console.log(template.getParametersSchema()); | ||
### Using a TemplateProvider | ||
A higher-level API is available for loading templates via `TemplateProvider` classes. | ||
These classes will handle calling the correct load function (`Template.loadYaml()` vs `Template.loadMst()`) and can also handle schemas. | ||
For example, to load "templates sets" (a collection of template source files) from a given directory, the `FsTemplateProvider` class can be used: | ||
These classes will handle calling the correct load function (`Template.loadYaml()` vs `Template.loadMst()`) and can also automatically handle additional schema files. | ||
For example, to load "templates sets" (a directory containing template files) from a given directory, the `FsTemplateProvider` class can be used: | ||
@@ -141,4 +169,4 @@ ```javascript | ||
const templatesPath = '/path/to/templatesdir'; | ||
const templateProvider = new fast.FsTemplateProvider(templatesPath); | ||
const templateSetsPath = '/path/to/templateSetsDir'; | ||
const templateProvider = new fast.FsTemplateProvider(templateSetsPath); | ||
@@ -155,5 +183,79 @@ templateProvider.fetch('templateSetName/templateName') | ||
### HTTP Fetch | ||
To resolve external URLs in templates, a `Template.fetchHttp()` is available. | ||
This will take any definition with a `url` property, resolve it, and return an object of the results. | ||
```javascript | ||
const fast = require('@f5devcentral/f5-fast-core'); | ||
const yamldata = ` | ||
definitions: | ||
var: | ||
url: http://example.com/resource | ||
pathQuery: $.foo | ||
template: | | ||
{{var}} | ||
`; | ||
fast.Template.loadYaml(yamldata) | ||
.then(template => Promise.all[( | ||
Promise.resolve(template), | ||
() => template.fetchHttp() | ||
)]) | ||
.then(([template, httpParams]) => { | ||
console.log(template.render(httpParams)); | ||
}); | ||
``` | ||
A `Template.fetchAndRender()` convenience function is also available to do fetchHttp() and render() in a single function call. | ||
```javascript | ||
const fast = require('@f5devcentral/f5-fast-core'); | ||
const yamldata = ` | ||
definitions: | ||
var: | ||
url: http://example.com/resource | ||
pathQuery: $.foo | ||
template: | | ||
{{var}} | ||
`; | ||
fast.Template.loadYaml(yamldata) | ||
.then(template => template.fetchAndRender()) | ||
.then((rendered) => { | ||
console.log(rendered); | ||
}); | ||
``` | ||
### HTTP Forward | ||
It is common to want to submit the rendered template result to an HTTP endpoint. | ||
`f5-fast-core` makes this simpler with `Template.forwardHttp()`. | ||
This function will: | ||
* Resolve external URLs with `Template.fetchHttp()` | ||
* Render the template result | ||
* Forward the rendered result as a `POST` (by default) to the endpoint defined by the template's `httpForward` property | ||
```javascript | ||
const fast = require('@f5devcentral/f5-fast-core'); | ||
const yamldata = ` | ||
httpForward: | ||
url: http://example.com/resource | ||
definitions: | ||
var: | ||
default: foo | ||
template: | | ||
{{var}} | ||
`; | ||
fast.Template..loadYaml(yamldata) | ||
.then(template => template.forwardHttp()); // POST "foo" to http://example.com/resource | ||
``` | ||
## Development | ||
`npm` commands should be run in the core subdirectory, not at the top-level. | ||
* To check for lint errors run `npm run lint` | ||
@@ -164,6 +266,2 @@ * To run unit tests use `npm test` | ||
## Documentation | ||
For more information about FAST, see [FAST Documentation](https://clouddocs.f5.com/products/extensions/f5-appsvcs-templates/latest/) | ||
## License | ||
@@ -170,0 +268,0 @@ |
338666
2054
274
Updatedajv@^6.12.5