diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a9ff811 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +# Change Log + +This project adheres to [Semantic Versioning](http://semver.org/). +Every release, along with the migration instructions, is documented on the Github [Releases](https://github.com/docker/pulpo/releases) page. diff --git a/README.md b/README.md index d8d354e..1dd95a2 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,44 @@ Hydrate also allows for a second argument to be passed in that contains options: * cast (**default true**) - whether or not to cast the value before validating * validate (**default true**) - whether or not to validate a given value +#### Referencing Other Configuration Values +It is possible to reference other values in the configuration, which will be populated with resolved values found in the referenced key. This works for both **default** values and passed in values: + +```js +const schema = new Pulpo({...schema...}); + +const config = schema.hydrate({ + server: { + port: 8888, + hostname: 'localhost', + host: 'http://${server.hostname}:${server.port}' + } +}); + +console.log(config.server.host); +// http://localhost:8888 +``` + +#### Using Functions for Configuration Values +When needed, a function can be passed in as a **config** or **default** value. These functions are passed two arguments: the config object and the string path of the property being resolved: + +```js +const schema = new Pulpo({...schema...}); + +const config = schema.hydrate({ + server: { + port: 8888, + hostname: 'localhost', + host: (config) => `http://${config['server.hostname']}:${config['server.port']}` + } +}); + +console.log(config.server.host); +// http://localhost:8888 +``` + +**Note**: Configuration values are accessed through their full dot-notation strings + Properties --- Properties are definitions for a given configuration key. diff --git a/package.json b/package.json index b02be3d..e09b88f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bonito/pulpo", - "version": "1.0.0", + "version": "1.1.0", "description": "Configuration mechanism", "author": "Patrick Camacho ", "license": "MIT", @@ -17,7 +17,8 @@ "build": "tsc", "deps": "next-update --tldr --keep", "start": "npm run -s test -- --watch --coverage", - "test": "jest" + "test": "jest", + "prepublish": "npm run -s test && npm run -s build" }, "devDependencies": { "jest-cli": "15.1.1", diff --git a/src/schema.ts b/src/schema.ts index 07aca10..2646684 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -24,6 +24,34 @@ export interface ParsedSchemaDefinition { [optName: string]: Property; } +interface HydratedConfig { + [optName: string]: any; +} + +function stringLookup(str: string, config: HydratedConfig): string { + return str.replace(/(\$\{(.*)\})/gi, (match, group1, key) => { + return dotty.get(config, key); + }); +} + +function getter(config: HydratedConfig, value: any, path: string, validate: boolean): any { + let resolvedValue: any; + + switch (typeof value) { + case 'function': + resolvedValue = value(config, path); + break; + case 'string': + resolvedValue = stringLookup(value, config); + break; + default: + resolvedValue = value; + } + + if (validate) this.definition[path].validate(resolvedValue); + return resolvedValue; +}; + export default class Schema { definition: ParsedSchemaDefinition; @@ -60,28 +88,32 @@ export default class Schema { } hydrate(rawConfig: Object, options: HydrateOptionsDefinition = {}): Object { - const hydratedConfig = Object.keys(this.definition).reduce((obj, key) => { + const flags = { + transform: !Reflect.has(options, 'transform') || options.transform, + cast: !Reflect.has(options, 'cast') || options.cast, + validate: !Reflect.has(options, 'validate') || options.validate, + } + + // Loop over and hydrate the object with getters + + const hydratedConfig: HydratedConfig = Object.keys(this.definition).reduce((obj, key) => { const property = this.definition[key]; let value = property.resolve(rawConfig); - if (!Reflect.has(options, 'transform') || options.transform) { - value = property.transform(value, rawConfig); - } + if (flags.transform) value = property.transform(value, rawConfig); + if (flags.cast) value = property.cast(value); - if (!Reflect.has(options, 'cast') || options.cast) { - value = property.cast(value); - } + Object.defineProperty(obj, key, {get: getter.bind(this, obj, value, key, flags.validate) }); + return obj + }, {}); - if (!Reflect.has(options, 'validate') || options.validate) { - property.validate(value); - } + return Object.keys(this.definition).reduce((obj: HydratedConfig, key: string) => { + const value: any = hydratedConfig[key]; - dotty.put(obj, key, value); + if (value) dotty.put(obj, key, value); return obj; }, {}); - - return hydratedConfig; } } diff --git a/test/fixtures/self-referencing-schema.json b/test/fixtures/self-referencing-schema.json new file mode 100644 index 0000000..d83bfad --- /dev/null +++ b/test/fixtures/self-referencing-schema.json @@ -0,0 +1,13 @@ +{ + "foo": { + "bar": { + "description": "nested schema key", + "type": "string", + "default": "${baz}/foo" + } + }, + "baz": { + "description": "top level value", + "type": "string" + } +} diff --git a/test/integration/schema.spec.ts b/test/integration/schema.spec.ts index 7b0465c..92333c1 100644 --- a/test/integration/schema.spec.ts +++ b/test/integration/schema.spec.ts @@ -2,6 +2,7 @@ import Schema from '../../src/schema'; import nestedSchema = require('../fixtures/nested-schema.json'); import brokenNestedSchema = require('../fixtures/broken-nested-schema.json'); +import selfReferencingSchema = require('../fixtures/self-referencing-schema.json'); describe('Resolve', () => { it('Accepts a provided value', () => { @@ -92,7 +93,7 @@ describe('Cast and Validate', () => { number: { description: 'description', type: 'number', - default: 8888, + default: () => 8888, }, string: { description: 'description', @@ -102,7 +103,7 @@ describe('Cast and Validate', () => { boolean: { description: 'description', type: 'boolean', - default: false, + default: () => false, }, array: { description: 'description', @@ -112,7 +113,7 @@ describe('Cast and Validate', () => { object: { description: 'description', type: 'object', - default: {}, + default: () => {}, } }); @@ -172,3 +173,15 @@ describe('parsing nested schemas', () => { }); }); }); + +describe('parsing self referencing strings', () => { + it('handles self references', () => { + const schema = new Schema(selfReferencingSchema); + expect(schema.hydrate({ baz: 'testing' })).toEqual({ + foo: { + bar: 'testing/foo' + }, + baz: 'testing' + }); + }); +});