Skip to content
This repository has been archived by the owner on Feb 12, 2022. It is now read-only.

Commit

Permalink
Merge 0c05d66 into 83da913
Browse files Browse the repository at this point in the history
  • Loading branch information
trevorr committed Feb 5, 2018
2 parents 83da913 + 0c05d66 commit 63d5a44
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 63 deletions.
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ The form and the algorithm to compute is [documented here](doc/algorithms.md).
### Usage

The Node.js interface for the library offers the `expandedForm` function to compute the expanded form.
It accepts an in-memory JSON representation of the type, the types mapping and a callback function.
If the invocation succeeds, it will return the expanded form as an argument to the provided callback function.
It accepts an in-memory JSON representation of the type, the types mapping and an optional callback function or options object.
It returns the canonical form or an exception. The following options are supported:

* `callback`: Provides a callback function via the options object.
* `topLevel` (default: `any`): The default RAML type to use when base type is not explicit and cannot be inferred.
It can be `any` or `string`, depending on whether the type comes from the `body` of a RAML service.
* `trackOriginalType` (default: `false`): Controls whether expansion should track the original type in a property called `originalType` when expanding a type reference.

#### Sync API
```js
Expand Down
17 changes: 13 additions & 4 deletions doc/algorithms.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,16 +112,25 @@ The pseudo-code for the transformation is the following:

The input for the algorithm is:
- `form` The form being expanded
- `bindings` A `Record` from `String` into `RAMLForm` holding a mapping from user defined RAML type names to RAML type forms.
- `top-level-type` a `String` with the default RAML type whose base type is not explicit and cannot be inferred, it can be `any` or `string` depending if the the type comes from the `body` of RAML service definition or any other node.
- `bindings` An object holding a mapping from user-defined RAML type names to RAML type forms
- `options` An object holding a mapping of algorithm option names to their values:
- `topLevel` A string with the default RAML type whose base type is not explicit and cannot be inferred.
It can be `any` or `string`, depending if the the type comes from the `body` of RAML service definition or any other node.
The default value is `any`.
- `trackOriginalType` Indicates whether to track original user-defined RAML type names in the `originalType` key whenever they are expanded.
The default value is `false`.
- `callback` The callback function to be called when performing expansion asynchronously. The default is synchronous expansion.

*Algorithm*

1. if `form` is a `String`
1. if `form` is a RAML built-in data type, we return `(Record "type" form)`
2. if `form` is a Type Expression, we return the output of calling the algorithm recursively with the parsed type expression and the provided `bindings`
3. if `form` is a key in `bindings`:
1. If the type hasn't been traversed yet, we return the output of invoking the algorithm recursively with the value for `form` found in `bindings` and the `bindings` mapping and we add the type to the current traverse path
1. If the type hasn't been traversed yet:
1. We return the output of invoking the algorithm recursively with the value for `form` found in `bindings` and the `bindings` mapping and we add the type to the current traverse path
2. If `trackOriginalType` is `true` in `options`, the key `originalType` in the returned type should be set to the
value for `form`
2. If the type has been traversed:
1. We mark the value for the current form as a fixpoint recursion: `$recur`
2. We find the container form matching the recursion type and we wrap it into a `(fixpoint RAMLForm)` form.
Expand All @@ -131,7 +140,7 @@ The input for the algorithm is:
1. if `type` has a defined value in `form` we initialize `type` with that value
2. if `form` has a `properties` key defined, we initialize `type` with the value `object`
3. if `form` has a `items` key defined, we initialize `type` with the value `object`
4. otherwise we initialise `type` with the value passed in `top-level-type`
4. otherwise we initialise `type` with the value of `topLevel` in `options`
2. if `type` is a `String` with value `array`
1. we initialize the value `expanded-items` with the result of invoking the algorithm on the value in `form` for the key `items` (or `any` if the key `items` is not defined)
2. we replace the value of the key `items` in `form` by `expanded-items`
Expand Down
110 changes: 64 additions & 46 deletions src/expanded.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,33 @@ const isOpaqueType = require('./util').isOpaqueType
* expanded form as an argument to the provided callback function.
*
* @param type {(Object|String)} The form being expanded
* @param types {Array} A `Record` from `String` into `RAMLForm` holding a mapping from
* user defined RAML type names to RAML type forms.
* @param cb {Function} Callback
* @param types {object} An object with entries mapping from user
* defined RAML type names to RAML type forms
* @param cb {Function|object} Callback or options
*/
module.exports.expandedForm = function expandedForm (type, types, cb) {
const keys = Object.keys(types)
const typename = keys[keys.map(t => types[t]).indexOf(type)]
let options = {}
if (typeof cb === 'object') {
options = cb
cb = options.callback
}

const visited = {}
for (const key in types) {
if (types[key] === type) {
visited[key] = true
break
}
}

if (cb == null) {
return expandForm(type, types, typename ? [typename] : [])
return expandForm(type, types, visited, options)
}

setTimeout(() => {
let result
try {
result = expandForm(type, types, typename ? [typename] : [])
result = expandForm(type, types, visited, options)
} catch (e) {
cb(e, null)
return
Expand All @@ -36,18 +47,13 @@ module.exports.expandedForm = function expandedForm (type, types, cb) {

/**
* @param form {*} The form being expanded
* @param bindings {Array} A `Record` from `String` into `RAMLForm` holding a mapping from user
* defined RAML type names to RAML type forms.
* @param context {Array} Context of already 'visited' types
* @param topLevel {String=} a `String` with the default RAML type whose base type is not
* explicit and cannot be inferred, it can be `any` or `string`
* depending if the the type comes from the `body` of RAML service
* @param bindings {object} An object with entries mapping from user
* defined RAML type names to RAML type forms
* @param visited {object} An object with properties indicating already 'visited' type names
* @param options {object} Expansion options
* @returns {object} - expanded form
*/
function expandForm (form, bindings, context, topLevel) {
topLevel = topLevel || 'any'
form = _.cloneDeep(form)

function expandForm (form, bindings, visited, options) {
// apparently they want this
if (typeof form === 'string') {
try {
Expand All @@ -61,6 +67,7 @@ function expandForm (form, bindings, context, topLevel) {

// 1. if `form` is a `String
if (typeof form === 'string') {
// strip parentheses around entire form
if (/^\(.+\)$/.test(form)) {
form = form.match(/^\((.+)\)$/)[1]
}
Expand All @@ -78,29 +85,29 @@ function expandForm (form, bindings, context, topLevel) {
{type: form.replace('?', '')},
{type: 'nil'}
]
}, bindings, context)
}, bindings, visited, options)
}
}

if (form.endsWith('[]')) { // Array
const match = form.match(/^(.+)\[]$/)[1]
return {
type: 'array',
items: expandForm(match, bindings, context)
items: expandForm(match, bindings, visited, options)
}
}

// 1.2. if `form` is a Type Expression, we return the output of calling the algorithm
// recursively with the parsed type expression and the provided `bindings`
if (/^[^\s|]*(?:\s*\|\s*[^\s|]*)+$/.test(form)) { // union
const options = form.split('|').map(s => s.trim())
return expandUnion({anyOf: options, type: 'union'}, bindings, context)
const alternatives = form.split('|').map(s => s.trim())
return expandUnion({anyOf: alternatives, type: 'union'}, bindings, visited, options)
}

// 1.3. if `form` is a key in `bindings`
if (form in bindings) {
// 1.3.2. If the type has been traversed
if (context.indexOf(form) !== -1) {
if (form in visited) {
// 1.3.2.1. We mark the value for the current form as a fixpoint recursion: `$recur`
// 1.3.2.2. We find the container form matching the recursion type and we wrap it into a `(fixpoint RAMLForm)` form.
// not sure what that means
Expand All @@ -109,53 +116,64 @@ function expandForm (form, bindings, context, topLevel) {
// 1.3.1. If the type hasn't been traversed yet, we return the output of invoking
// the algorithm recursively with the value for `form` found in `bindings` and the
// `bindings` mapping and we add the type to the current traverse path
return expandForm(bindings[form], bindings, context.concat([form]))
visited = Object.assign({ [form]: true }, visited)
let type = bindings[form]
if (options.trackOriginalType && !(typeof type === 'object')) {
type = { type } // ensure type is in object form
}
type = expandForm(type, bindings, visited, options)
// set originalType after recursive expansion to retain first type expanded
if (options.trackOriginalType) {
type.originalType = form
}
return type
}
}

// 1.4. else we return an error
throw new Error('could not resolve: ' + form)
} else if (typeof form === 'object') {
form = _.cloneDeep(form)
// 2. if `form` is a `Record`
// 2.1. we initialize a variable `type`
// 2.1.1. if `type` has a defined value in `form` we initialize `type` with that value
// 2.1.2. if `form` has a `properties` key defined, we initialize `type` with the value `object`
// 2.1.3. if `form` has a `items` key defined, we initialize `type` with the value `object`
// 2.1.4. otherwise we initialise `type` with the value passed in `top-level-type`
form.type = form.type || (form.properties && 'object') || (form.items && 'array') || topLevel
form.type = form.type || (form.properties && 'object') || (form.items && 'array') || options.topLevel || 'any'

if (typeof form.type === 'string') {
if (form.type === 'array') {
// 2.2. if `type` is a `String` with value `array`
return expandArray(form, bindings, context)
return expandArray(form, bindings, visited, options)
} else if (form.type === 'object') {
// 2.3 if `type` is a `String` with value `object`
return expandObject(form, bindings, context)
return expandObject(form, bindings, visited, options)
} else if (form.type === 'union') {
// 2.4. if `type` is a `String` with value `union`
return expandUnion(form, bindings, context)
return expandUnion(form, bindings, visited, options)
} else if (form.type in bindings) {
// 2.5. if `type` is a `String` with value in `bindings`
form = expandNested(form, bindings, context)
form.type = expandForm(form.type, bindings, context)
form = expandNested(form, bindings, visited, options)
form.type = expandForm(form.type, bindings, visited, options)
} else {
form = Object.assign(form, expandForm(form.type, bindings, context))
form = Object.assign(form, expandForm(form.type, bindings, visited, options))
}
} else if (Array.isArray(form.type)) {
// 2.7. if `type` is a `Seq[RAMLForm]`
form = expandNested(form, bindings, context)
form.type = form.type.map(t => expandForm(t, bindings, context))
form = expandNested(form, bindings, visited, options)
form.type = form.type.map(t => expandForm(t, bindings, visited, options))
} else if (typeof form.type === 'object') {
// 2.6. if `type` is a `Record`
form = expandNested(form, bindings, context)
form.type = expandForm(form.type, bindings, context)
form = expandNested(form, bindings, visited, options)
form.type = expandForm(form.type, bindings, visited, options)
} else {
form = Object.assign(form, expandForm(form.type, bindings, context))
form = Object.assign(form, expandForm(form.type, bindings, visited, options))
}

if (form.facets != null) {
_.each(form.facets, (propValue, propName) => {
form.facets[propName] = expandForm(propValue, bindings, context)
form.facets[propName] = expandForm(propValue, bindings, visited, options)
})
}

Expand All @@ -165,24 +183,24 @@ function expandForm (form, bindings, context, topLevel) {
throw new Error('form can only be a string or an object')
}

function expandNested (form, bindings, context) {
if (form.properties !== undefined) form = expandObject(form, bindings, context)
if (form.anyOf !== undefined) form = expandUnion(form, bindings, context)
if (form.items !== undefined) form = expandArray(form, bindings, context)
function expandNested (form, bindings, visited, options) {
if (form.properties !== undefined) form = expandObject(form, bindings, visited, options)
if (form.anyOf !== undefined) form = expandUnion(form, bindings, visited, options)
if (form.items !== undefined) form = expandArray(form, bindings, visited, options)
return form
}

function expandArray (form, bindings, context) {
form.items = expandForm(form.items || 'any', bindings, context)
function expandArray (form, bindings, visited, options) {
form.items = expandForm(form.items || 'any', bindings, visited, options)
return form
}

function expandObject (form, bindings, context) {
function expandObject (form, bindings, visited, options) {
const props = form.properties
for (let propName in props) {
if (!props.hasOwnProperty(propName)) continue

let expandedPropVal = expandForm(props[propName] || 'any', bindings, context)
let expandedPropVal = expandForm(props[propName] || 'any', bindings, visited, options)
if (propName.endsWith('?')) {
delete props[propName]
propName = propName.slice(0, -1)
Expand All @@ -199,7 +217,7 @@ function expandObject (form, bindings, context) {
return form
}

function expandUnion (form, bindings, context) {
form.anyOf = form.anyOf.map(elem => expandForm(elem, bindings, context))
function expandUnion (form, bindings, visited, options) {
form.anyOf = form.anyOf.map(elem => expandForm(elem, bindings, visited, options))
return form
}
40 changes: 33 additions & 7 deletions test/expanded.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,43 @@
const describe = require('mocha/lib/mocha.js').describe
const it = require('mocha/lib/mocha.js').it

const _ = require('lodash')
const expect = require('chai').expect
const types = require('./fixtures/types')
const forms = require('./fixtures/expanded_forms')
const expandedForm = require('..').expandedForm

describe('expandedForm()', function () {
_.each(types, function (type, name) {
it('should generate expanded form of type ' + name, function () {
const expForm = expandedForm(types[name], types)
expect(expForm).to.deep.equal(forms[name])
})
})
for (const name in types) {
if (name in forms) {
it('should generate expanded form of type ' + name, function () {
const expForm = expandedForm(types[name], types)
expect(expForm).to.deep.equal(forms[name])
})

it('should asynchronously generate expanded form of type ' + name, function (done) {
expandedForm(types[name], types, function (e, result) {
expect(result).to.deep.equal(forms[name])
done()
})
})
}

const trackedName = name + '_tracked'
if (trackedName in forms) {
it('should generate expanded form of type ' + name + ' with original types tracked', function () {
const expForm = expandedForm(types[name], types, { trackOriginalType: true })
expect(expForm).to.deep.equal(forms[trackedName])
})

it('should asynchronously generate expanded form of type ' + name + ' with original types tracked', function (done) {
expandedForm(types[name], types, {
trackOriginalType: true,
callback: function (e, result) {
expect(result).to.deep.equal(forms[trackedName])
done()
}
})
})
}
}
})
30 changes: 30 additions & 0 deletions test/fixtures/canonical_forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -1264,6 +1264,10 @@ module.exports = {
}
}]
},
BigNumber: {
type: 'union',
anyOf: [{ type: 'number' }, { type: 'string' }]
},
Invoice: {
type: 'union',
anyOf: [{
Expand Down Expand Up @@ -1386,6 +1390,32 @@ module.exports = {
},
additionalProperties: true
},
ComplexTracked_unhoisted: {
type: 'object',
properties: {
amounts: {
type: 'union',
anyOf: [
{
type: 'union',
anyOf: [{ type: 'number' }, { type: 'string' }]
},
{
type: 'union',
anyOf: [{
type: 'array',
items: { type: 'number' }
}, {
type: 'array',
items: { type: 'string' }
}]
}
],
required: true
}
},
additionalProperties: true
},
T1: {
type: 'object',
properties: {},
Expand Down
Loading

0 comments on commit 63d5a44

Please sign in to comment.