From 4202f5a95d69c69bd1d6adc675ea9d9aed639986 Mon Sep 17 00:00:00 2001 From: Nils Knappmeier Date: Wed, 12 Jul 2017 22:59:11 +0200 Subject: [PATCH] Array restrictions --- handlebars/helpers.js | 231 ++++++++++++------ handlebars/partials/base/body.hbs | 2 +- .../json-schema/additionalProperties.hbs | 7 - handlebars/partials/json-schema/allOf.hbs | 20 -- handlebars/partials/json-schema/anyOf.hbs | 17 -- .../partials/json-schema/array-items.hbs | 9 - handlebars/partials/json-schema/array.hbs | 52 ++++ handlebars/partials/json-schema/body.hbs | 35 --- handlebars/partials/json-schema/datatype.hbs | 54 ---- .../partials/json-schema/definitions.hbs | 4 +- .../partials/json-schema/main-panel.hbs | 25 -- handlebars/partials/json-schema/panel.hbs | 19 ++ .../partials/json-schema/properties.hbs | 30 --- handlebars/partials/json-schema/reference.hbs | 4 - handlebars/partials/json-schema/schema.hbs | 44 ++++ .../partials/json-schema/type-object.hbs | 7 - handlebars/partials/json-schema/type.hbs | 12 + .../partials/json-schema/util/section.hbs | 10 + less/theme.less | 151 +----------- package.json | 6 +- .../array-spec.js} | 2 +- test/array-spec/schema.json | 71 ++++++ test/enum-spec/enum-spec.js | 67 +++++ test/enum-spec/schema.json | 48 ++++ test/helper-spec.js | 98 ++++++-- .../numeric-restrictions-spec.js | 67 +++++ .../schema.json | 29 +-- test/string-restrictions-spec/schema.json | 29 +++ .../string-restrictions-spec.js | 67 +++++ test/types/schema.json | 35 +++ test/types/types-spec.js | 43 ++++ 31 files changed, 827 insertions(+), 468 deletions(-) delete mode 100644 handlebars/partials/json-schema/additionalProperties.hbs delete mode 100644 handlebars/partials/json-schema/allOf.hbs delete mode 100644 handlebars/partials/json-schema/anyOf.hbs delete mode 100644 handlebars/partials/json-schema/array-items.hbs create mode 100644 handlebars/partials/json-schema/array.hbs delete mode 100644 handlebars/partials/json-schema/body.hbs delete mode 100644 handlebars/partials/json-schema/datatype.hbs delete mode 100644 handlebars/partials/json-schema/main-panel.hbs create mode 100644 handlebars/partials/json-schema/panel.hbs delete mode 100644 handlebars/partials/json-schema/properties.hbs delete mode 100644 handlebars/partials/json-schema/reference.hbs create mode 100644 handlebars/partials/json-schema/schema.hbs delete mode 100644 handlebars/partials/json-schema/type-object.hbs create mode 100644 handlebars/partials/json-schema/type.hbs create mode 100644 handlebars/partials/json-schema/util/section.hbs rename test/{ranges/ranges-spec.js => array-spec/array-spec.js} (98%) create mode 100644 test/array-spec/schema.json create mode 100644 test/enum-spec/enum-spec.js create mode 100644 test/enum-spec/schema.json create mode 100644 test/numeric-restrictions/numeric-restrictions-spec.js rename test/{ranges => numeric-restrictions}/schema.json (60%) create mode 100644 test/string-restrictions-spec/schema.json create mode 100644 test/string-restrictions-spec/string-restrictions-spec.js create mode 100644 test/types/schema.json create mode 100644 test/types/types-spec.js diff --git a/handlebars/helpers.js b/handlebars/helpers.js index b97491d..bfe0a67 100644 --- a/handlebars/helpers.js +++ b/handlebars/helpers.js @@ -1,4 +1,4 @@ -var util = require('util') +var Handlebars = require('handlebars') /* eslint-disable camelcase */ @@ -8,107 +8,184 @@ var util = require('util') */ module.exports = { json_schema__datatype, - json_schema__range + json_schema__subschema_name, + json_schema__numeric_restrictions, + json_schema__string_restrictions, + json_schema__could_be_numeric, + json_schema__could_be_of_type, + json_schema__array_item_restrictions, + json_schema__doclink, + json_schema__is_array } /** - * Returns a descriptive string for a datatype - * @param {object} value a json-schema datatype-object + * Return true, if the type is numeric (integer or number) or *could be numeric* + * + * The type *could be* numeric, if it is an array that includes number or integer, + * or if it is not specified. + * + * @this {undefined|string|string[]} the json-schema oject + */ +function json_schema__could_be_numeric () { + var could_be_a = json_schema__could_be_of_type.bind(this) + return could_be_a('number') || could_be_a('integer') +} + +/** + * Return true, if the type is a string or *could be* a string + * + * The type *could be* string, if it is an array that includes number or integer, + * or if it is not specified. + * + * @param {string} type the type property of the schema + */ +function json_schema__could_be_of_type (type) { + const actualType = this.type + return (actualType === null) || // type not defined + (actualType === undefined) || + actualType === type || // explicit check matches + (Array.isArray(actualType) && actualType.indexOf(type) >= 0) // array contains type +} + +/** + * Returns a formal string that can be used generic parameter for an array. + * @param {object} schema a json-schema + * @param {string|string[]} types one or more types of potentially multiple types in the schema that should be used right now. * @returns {String} a string like string[] or object[][] * @access public * @memberOf helpers */ -function json_schema__datatype (value) { - if (!value) return null - if (value['anyOf'] || value['allOf'] || value['oneOf']) { - return '' +function json_schema__datatype (schema, types) { + if (schema == null) { + return null + } + /** + * The type(s) that are processed + * @type {string|string[]} + */ + if (types == null) { + return '*' } - if (!value.type) { - return 'object' + if (Array.isArray(types)) { + // "type" is an array (i.e. multiple types) -> process each type + return types.map(type => json_schema__datatype(schema, type)).join('|') } - if (value.type === 'array') { - return json_schema__datatype(value.items || {}) + '[]' + // "type" is a string + if (types === 'array') { + let items = schema.items || {} + return `array<${json_schema__datatype(items, items.type)}>` } - return value.type + return types } /** - * - * @param range a json-schema object with minimum, maximum, exclusiveMinimum, exclusiveMaximum - * @param {number=} [range.minimum] - * @param {number=} [range.maximum] - * @param {string} [range.type] the json-type (integer, or number) - * @param {boolean} [range.minimumExclusive] - * @param {boolean} [range.maximumExclusive] + * Extract then name of a subschema from a $ref property + * @param {string} url + * @returns {*} * @access public * @memberOf helpers */ -function json_schema__range (range) { - switch (range.type) { - case 'integer': - case 'number': - return numericRange(range) - case 'string': - return stringLengthRange(range) - default: - return '' - } +function json_schema__subschema_name (url) { + return url.replace('#/definitions/', '') } /** - * Render a range for numeric types (integer, number) - * @param range - * @returns {*} + * Computes numeric restrictions based on properties of the given json-schema. + * + * If exclusiveMinimum or exclusiveMaximum is a boolean, it will be treated as defined in + * https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.2.1 + * + * If it is a number, it will be treated as in + * https://tools.ietf.org/html/draft-wright-json-schema-validation-01 + * + * @param schema a json-schema object with minimum, maximum, exclusiveMinimum, exclusiveMaximum and multipleOf + * @param {number=} [schema.minimum] + * @param {number=} [schema.maximum] + * @param {string} [schema.type] the json-type (integer, or number) + * @param {boolean|number=} [schema.minimumExclusive] + * @param {boolean|number=} [schema.maximumExclusive] + * @param {number=} [schema.multipleOf] + * @access public + * @memberOf helpers */ -function numericRange (range) { - const hasMinimum = range.minimum || range.minimum === 0 - const hasMaximum = range.maximum || range.maximum === 0 - - if (!hasMinimum && !hasMaximum) { - // There is no range - return '' +function json_schema__numeric_restrictions (schema) { + let min = schema.minimum + let max = schema.maximum + let minExclusive = schema.minimumExclusive + let maxExclusive = schema.maximumExclusive + // Convert draft 4 to draft 6 + if (minExclusive === true) { + minExclusive = min + min = null } - - let numberSet = range.type === 'integer' - ? '\u2208 \u2124' // ELEMENT OF - DOUBLE-STRUCK CAPITAL Z - : '\u2208 \u211D' // ELEMENT OF - DOUBLE-STRUCK CAPITAL R - - if (hasMinimum && !hasMaximum) { - return util.format(', { x %s | x %s %d }', - numberSet, - range.minimumExclusive ? '>' : '\u2265', - range.minimum) - } else if (hasMaximum && !hasMinimum) { - return util.format(', { x %s | x %s %d }', - numberSet, - range.maximumExclusive ? '<' : '\u2264', - range.maximum) - } else { - // if (hasMaxmium && hasMinimum) - return util.format(', { x %s | %d %s x %s %d }', - numberSet, - range.minimum, - range.minimumExclusive ? '<' : '\u2264', - range.maximumExclusive ? '<' : '\u2264', - range.maximum) + if (maxExclusive === true) { + maxExclusive = max + max = null } + + return [ + min != null && `x ≥ ${min}`, + max != null && `x ≤ ${max}`, + minExclusive != null && `x > ${minExclusive}`, + maxExclusive != null && `x < ${maxExclusive}`, + schema.multipleOf != null && `x \u2208 ${schema.multipleOf}*\u2124` // ELEMENT OF - DOUBLE-STRUCK CAPITAL Z + ].filter(x => x) +} + +function json_schema__string_restrictions (schema, options) { + return [ + schema.minLength != null && `x.length ≥ ${schema.minLength}`, + schema.maxLength != null && `x.length ≤ ${schema.maxLength}`, + schema.pattern != null && safe`x matches ${schema.pattern}` + ].filter(x => x) +} + +function json_schema__array_item_restrictions (schema, options) { + return [ + schema.minItems != null && schema.maxItems != null && `The array must have ${schema.minItems} to ${schema.maxItems} items.`, + schema.minItems == null && schema.maxItems != null && `The array must have at most ${schema.maxItems} items.`, + schema.minItems != null && schema.maxItems == null && `The array must have at least ${schema.minItems} items.`, + schema.uniqueItems != null && 'The items of the array must be unique' + ].filter(x => x) +} + +function safe (strings, ...values) { + let escapedValues = values.map(Handlebars.Utils.escapeExpression) + let rawString = String.raw.apply(this, [strings].concat(escapedValues)) + return new Handlebars.SafeString(rawString) } /** - * Render string length restrictions - * @param range - * @param {number} range.maxLength - * @param {number} range.minLength + * Render a link to a json-schema docs section from the json-schema documentation + * @param {string} sectionName the section name (e.g. items) + * @param options */ -function stringLengthRange (range) { - if (range.maxLength && range.minLength) { - return `, ${range.minLength} to ${range.maxLength} chars` - } - if (range.maxLength) { - return `, up to ${range.maxLength} chars` - } - if (range.minLength) { - return `, at least ${range.minLength} chars` +function json_schema__doclink (sectionName, options) { + let section = sections[sectionName] + let description = descriptions[sectionName] + let text = '' + if (options && options.hash && options.hash.text) { + text = description } - return '' + return safe`${text} (🛈 ${section})` } + +const schemaBase = 'https://tools.ietf.org/html/draft-wright-json-schema-validation-01#section' + +const descriptions = { + 'items': 'All items must match the following schema', + 'items_array': 'The first items must match the following schemas', + 'contains': 'At least one item must match the following schema', + 'additionalItems': 'Additional items must match the following schema' +} + +const sections = { + 'items': '6.9', + 'items_array': '6.9', + 'contains': '6.14', + 'additionalItems': '6.10' +} + +function json_schema__is_array (value) { + return Array.isArray(value) +} \ No newline at end of file diff --git a/handlebars/partials/base/body.hbs b/handlebars/partials/base/body.hbs index 5126b11..d66086a 100644 --- a/handlebars/partials/base/body.hbs +++ b/handlebars/partials/base/body.hbs @@ -4,7 +4,7 @@ what should be displayed. @public --}} -{{>json-schema/main-panel}} +{{>json-schema/schema}} {{#if definitions}}

Definitions

{{>json-schema/definitions}} diff --git a/handlebars/partials/json-schema/additionalProperties.hbs b/handlebars/partials/json-schema/additionalProperties.hbs deleted file mode 100644 index b636b4b..0000000 --- a/handlebars/partials/json-schema/additionalProperties.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{! -Show a subschema for additionaProperties of a `object` definition. -}} -
- {{>json-schema/datatype}} - {{>json-schema/body}} -
diff --git a/handlebars/partials/json-schema/allOf.hbs b/handlebars/partials/json-schema/allOf.hbs deleted file mode 100644 index 949c3d4..0000000 --- a/handlebars/partials/json-schema/allOf.hbs +++ /dev/null @@ -1,20 +0,0 @@ -{{! -renders a json-schema "allOf"-definition. -}} -
- {{! Display ref (i.e. superclasses)}} -
-
    - {{#each .}} - {{>json-schema/reference $ref}} - {{/each}} -
-
-
- {{#each .}} - {{#unless $ref}} - {{>json-schema/body}} - {{/unless}} - {{/each}} -
-
diff --git a/handlebars/partials/json-schema/anyOf.hbs b/handlebars/partials/json-schema/anyOf.hbs deleted file mode 100644 index 3024e8e..0000000 --- a/handlebars/partials/json-schema/anyOf.hbs +++ /dev/null @@ -1,17 +0,0 @@ -{{! -renders a json-schema "anyOf"-definition. -}} -
- {{! Display ref (i.e. superclasses)}} -
- {{#each .}} -
- {{>json-schema/datatype}} -
-
- {{>json-schema/body $ref=""}} -
- {{/each}} -
- -
diff --git a/handlebars/partials/json-schema/array-items.hbs b/handlebars/partials/json-schema/array-items.hbs deleted file mode 100644 index 6dfaca2..0000000 --- a/handlebars/partials/json-schema/array-items.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{! -renders a json-schema "items"-definition of array-types. -}} -
- {{>json-schema/datatype .}} -
- {{>json-schema/body}} -
-
\ No newline at end of file diff --git a/handlebars/partials/json-schema/array.hbs b/handlebars/partials/json-schema/array.hbs new file mode 100644 index 0000000..e2a3365 --- /dev/null +++ b/handlebars/partials/json-schema/array.hbs @@ -0,0 +1,52 @@ +{{#with (json_schema__array_item_restrictions .)}} + {{#if length}} +
+
+
    + {{#each .}} +
  • {{.}}
  • + {{/each}} +
+
+
+ {{/if}} +{{/with}} + + +{{#if items}} + {{#if (json_schema__is_array items)}} +

{{json_schema__doclink 'items_array' text=true}}

+ {{#each items}} +
+
+ Index {{@index}}: +
+
+
+ {{>json-schema/schema .}} +
+
+
+ {{/each}} + +

{{json_schema__doclink 'additionalItems' text=true}}

+ {{#if additionalItems}} +
+ {{>json-schema/schema additionalItems}} +
+ {{/if}} + {{else}} +

{{json_schema__doclink 'items' text=true}}

+
+ {{>json-schema/schema items}} +
+ {{/if}} +{{/if}} + +{{#if contains}} +

{{json_schema__doclink 'contains' text=true}}

+
+ {{>json-schema/schema contains}} +
+{{/if}} + diff --git a/handlebars/partials/json-schema/body.hbs b/handlebars/partials/json-schema/body.hbs deleted file mode 100644 index c71356c..0000000 --- a/handlebars/partials/json-schema/body.hbs +++ /dev/null @@ -1,35 +0,0 @@ -{{! - Renders a json-schema without the surrounding panel. - @param {boolean} showType whether or not to show the plain datatype for primitives -}} -{{#if description}} -
- {{md description}} -
-{{/if}} -{{#if example}} -
- {{json example}} -
-{{/if}} - -{{#if type}} - {{! Render differently depending on type}} - {{#ifeq type 'object'}} - {{>json-schema/type-object}} - {{else}} - {{#ifeq type 'array'}} - {{#if items}} - {{>json-schema/array-items items}} - {{/if}} - {{/ifeq}} - {{/ifeq}} -{{else}} - {{>json-schema/type-object}} -{{/if}} -{{#if allOf}} - {{>json-schema/allOf allOf}} -{{/if}} -{{#if anyOf}} - {{>json-schema/anyOf anyOf}} -{{/if}} diff --git a/handlebars/partials/json-schema/datatype.hbs b/handlebars/partials/json-schema/datatype.hbs deleted file mode 100644 index 6e180fe..0000000 --- a/handlebars/partials/json-schema/datatype.hbs +++ /dev/null @@ -1,54 +0,0 @@ -{{! - When properties are renderered this partial renders the datatype of a property, - with a link to the type-definition (in case of a $ref). - Depending on the input, it renders an augmented data-type (e.g. "string[]"), - the 'format'-value (e.g. "date-time") and "enum"-values. - - @param {boolean} discriminator true, this property is a swagger-discriminator (in which case enums are rendered as links) -}} - - - {{~#if $ref}} - {{~>json-schema/reference $ref}} - {{~else}} - {{~json_schema__datatype .}} - {{~/if}} - -{{~#if format}} - ({{format}}) -{{~/if~}} - - -{{!-- Enum values --}} -{{~#if enum}} - , x ∈ { - {{#each enum}} - {{! Render enums in the descriminator as links to the appropriate definitions}} - {{#if ../discriminator}} - {{.}} - {{else}} - {{.}} - {{/if}} - {{#ifeq ../default .}} - (default) - {{/ifeq}} - {{#unless @last}},{{/unless}} - {{/each}} - } - -{{~/if~}} - -{{!-- min- and max-values --}} - - {{~json_schema__range . ~}} - - -{{!-- Default values (for non-enum types) --}} -{{~#unless enum}} - {{~#if default}} - {{~default}} - {{/if}} -{{~/unless}} -{{#if pattern}} - , must match {{pattern}} -{{/if}} diff --git a/handlebars/partials/json-schema/definitions.hbs b/handlebars/partials/json-schema/definitions.hbs index 20ca8ea..704aeba 100644 --- a/handlebars/partials/json-schema/definitions.hbs +++ b/handlebars/partials/json-schema/definitions.hbs @@ -1,6 +1,8 @@ {{!Renders all subschemas}} {{#if definitions}} {{#eachSorted definitions}} - {{>json-schema/main-panel . title=@key anchor=true}} + {{#>json-schema/panel title=@key anchor=true}} + {{>json-schema/schema .}} + {{/json-schema/panel}} {{/eachSorted}} {{/if}} diff --git a/handlebars/partials/json-schema/main-panel.hbs b/handlebars/partials/json-schema/main-panel.hbs deleted file mode 100644 index e42be06..0000000 --- a/handlebars/partials/json-schema/main-panel.hbs +++ /dev/null @@ -1,25 +0,0 @@ -{{! - Renders a json.schema inside a bootstrap-panel. - @public - @readonly -}} -
- {{#if title}} -
- {{#if anchor}} -

{{title}}: - {{>json-schema/datatype}} -

- {{else}} -

{{title}}

- {{/if}} -
- {{/if}} -
- {{#if $ref}} - {{>json-schema/reference $ref}} - {{else}} - {{>json-schema/body}} - {{/if}} -
-
\ No newline at end of file diff --git a/handlebars/partials/json-schema/panel.hbs b/handlebars/partials/json-schema/panel.hbs new file mode 100644 index 0000000..60432bb --- /dev/null +++ b/handlebars/partials/json-schema/panel.hbs @@ -0,0 +1,19 @@ +{{! + Renders a bootstrap-panel + @public + @readonly +}} +
+ {{#if title}} +
+ +

+ {{#if anchor}}{{/if}} + {{title}} +

+
+ {{/if}} +
+ {{> @partial-block}} +
+
\ No newline at end of file diff --git a/handlebars/partials/json-schema/properties.hbs b/handlebars/partials/json-schema/properties.hbs deleted file mode 100644 index 8209e95..0000000 --- a/handlebars/partials/json-schema/properties.hbs +++ /dev/null @@ -1,30 +0,0 @@ -{{!Renders json-schema object properties.}} -{{#if properties}} -
-
- {{#each properties}} -
- {{!Single property, default type is "object"}} - {{@key}}: - {{>json-schema/datatype discriminator=(equal @key ../discriminator)}} - {{#ifcontains ../required @key}} - - {{/ifcontains}} - {{#ifeq @key ../discriminator}} - - {{/ifeq}} - {{#if readOnly}} - - {{/if}} -
-
- {{md description}} - {{!Show details of nested property schema}} -
- {{>json-schema/body $ref="" description=""}} -
-
- {{/each}} -
-
-{{/if}} diff --git a/handlebars/partials/json-schema/reference.hbs b/handlebars/partials/json-schema/reference.hbs deleted file mode 100644 index 8166abe..0000000 --- a/handlebars/partials/json-schema/reference.hbs +++ /dev/null @@ -1,4 +0,0 @@ -{{!Renders a reference to a subschema}} -{{#if .}} - {{json_schema__subschema_name .}} -{{/if}} \ No newline at end of file diff --git a/handlebars/partials/json-schema/schema.hbs b/handlebars/partials/json-schema/schema.hbs new file mode 100644 index 0000000..7eb41fc --- /dev/null +++ b/handlebars/partials/json-schema/schema.hbs @@ -0,0 +1,44 @@ +{{!-- + Renders a json-schema. This is the entry-point of the schema. + @this {object} the json-schema +--}} +
+ {{>json-schema/type .}} + {{#if enum}} +
Allowed values (enum)
+
+
    + {{#each enum}} +
  • {{json . space=0}}
  • + {{/each}} +
+
+ {{/if}} +{{!-- {{#if (json_schema__could_be_numeric)}} +
If the value is a number:
+
+
    + {{#each (json_schema__numeric_restrictions .)}} +
  • {{.}}
  • + {{else}} +
  • none
  • + {{/each}} +
+
+ {{/if}} + {{#if (json_schema__could_be_of_type 'string')}} +
String restrictions:
+
+
    + {{#each (json_schema__string_restrictions .)}} +
  • {{.}}
  • + {{else}} +
  • none
  • + {{/each}} +
+
+ {{/if}}--}} + {{#if (json_schema__could_be_of_type 'array')}} + {{> json-schema/array .}} + {{/if}} +
\ No newline at end of file diff --git a/handlebars/partials/json-schema/type-object.hbs b/handlebars/partials/json-schema/type-object.hbs deleted file mode 100644 index 2ca0fe8..0000000 --- a/handlebars/partials/json-schema/type-object.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{!-- - Renders the properties of an `object` ---}} -{{>json-schema/properties}} -{{#if additionalProperties}} - {{>json-schema/additionalProperties additionalProperties}} -{{/if}} diff --git a/handlebars/partials/json-schema/type.hbs b/handlebars/partials/json-schema/type.hbs new file mode 100644 index 0000000..42c9202 --- /dev/null +++ b/handlebars/partials/json-schema/type.hbs @@ -0,0 +1,12 @@ +{{#>json-schema/util/section title='Type'}} + {{#if type}} + {{#each type}} + {{json_schema__datatype .. .}} + {{#unless @last}} or {{/unless}} + {{else}} + {{json_schema__datatype . type}} + {{/each}} + {{else}} + any type + {{/if}} +{{/json-schema/util/section}} diff --git a/handlebars/partials/json-schema/util/section.hbs b/handlebars/partials/json-schema/util/section.hbs new file mode 100644 index 0000000..69828e4 --- /dev/null +++ b/handlebars/partials/json-schema/util/section.hbs @@ -0,0 +1,10 @@ +{{!-- + Helper partial to render a restriction (array, numeric etc). + Use as block-partial + + @param {string} title a title for the restrictions +--}} +
+
{{title}}
+
{{>@partial-block}}
+
diff --git a/less/theme.less b/less/theme.less index fb91fea..51491b4 100644 --- a/less/theme.less +++ b/less/theme.less @@ -1,155 +1,28 @@ @import "labels.less"; @import "panels.less"; +@import "wells.less"; .panel-definition { .panel-variant(@definition-panel-border-color, @definition-panel-header-text-color, @definition-panel-header-bg-color, @definition-panel-border-color); } -// Named sections -.named-section(@title) { - &:before { - font-weight: bold; - color: @named-section-text-color; - text-transform: uppercase; - content: @title; - padding-bottom: 0.5em; - display: block; - } - &:not(:last-child) { - padding-bottom: 1.5em; - } -} - -.json-schema-description { - .named-section(@msg-json-section-description); -} - -.json-schema-properties { - .named-section(@msg-json-section-properties); - - dd:not(:last-child) { - padding-bottom: 1em; - } - dl { - margin: 0; - } +.json-schema--subschema { + .well() } -.json-schema-example { - .named-section(@msg-json-section-example); -} +.json-schema--schema { + section { + .make-row(); -.json-schema-array-items { - .named-section(@msg-json-section-items); -} - -.json-schema-allOf-inherited { - .named-section(@msg-json-section-inherited); - ul { - .list-unstyled(); - } -} - -.json-schema-anyOf { - > dl { - border-left: 2px solid @definition-panel-inner-border-color; - padding-left: 1em; - - dt:not(:first-child):before { - content: "or "; + > .header { + font-size: 1em; + .make-md-column(1); } - dt:first-child:before { - content: "either "; + > .contents { + .make-md-column(11); } - } -} - -.json-schema-additionalProperties { - .named-section(@msg-json-section-additionalProperties); -} - -.json-inner-schema { - .json-schema-properties, - .json-schema-array-items, - .json-schema-description, - .json-schema-example { - padding-left: 1em; - margin-top: 0.5em; - padding-bottom: 0.5em; - border-left: 2px solid @definition-panel-inner-border-color; - } -} - -.named-label(@name) { - &:before { - .label(); - .label-default(); - content: @name; - } -} - -.json-property-discriminator { - .named-label(@msg-json-discriminator) -} - -.json-property-required { - .named-label(@msg-json-required) -} - -.json-property-read-only { - .named-label(@msg-json-read-only) -} - -.json-property-pattern { - font-weight: lighter; - font-size: small; -} - -.json-schema--regex { - &:before, &:after { - color: lighten(@text-color, 30%); - content: '/'; - } -} - -.json-property-type { - font-style: italic; - font-weight: 100; -} - -.json-property-format { - font-size: smaller; -} - -.json-property-enum { - font-weight: lighter; - font-size: small; -} - -.json-property-default-value { - font-weight: lighter; - font-size: small; - &:before { - content: '(default: "'; - } - &:after { - content: '")'; - } -} - -.json-property-enum-item { - &:before, &:after { - content: "\"" - } - font-weight: lighter; - font-size: small; - -} - -.json-schema--reference { - font-size: 90%; -} +} \ No newline at end of file diff --git a/package.json b/package.json index c2163d2..3ef2246 100644 --- a/package.json +++ b/package.json @@ -26,10 +26,12 @@ }, "homepage": "https://github.com/bootprint/bootprint-json-schema", "dependencies": { - "bootprint-base": "^2.0.0-rc.2" + "bootprint-base": "^2.0.0-rc.2", + "handlebars": "^4.0.10", + "highlight.js": "^9.12.0" }, "devDependencies": { - "bootprint-unit-testing": "^2.1.0", + "bootprint-unit-testing": "^2.2.0", "chai": "^4.0.2", "chai-as-promised": "^7.0.0", "dirty-chai": "^2.0.0", diff --git a/test/ranges/ranges-spec.js b/test/array-spec/array-spec.js similarity index 98% rename from test/ranges/ranges-spec.js rename to test/array-spec/array-spec.js index feaad8b..4af8ff4 100644 --- a/test/ranges/ranges-spec.js +++ b/test/array-spec/array-spec.js @@ -12,7 +12,7 @@ var expect = require('chai').expect var tester = require('bootprint-unit-testing') -describe('The ranges-fixture', function () { +describe.only('array-spec', function () { this.timeout(10000) var bptest = tester(require('../..'), __dirname, require('./schema.json')) diff --git a/test/array-spec/schema.json b/test/array-spec/schema.json new file mode 100644 index 0000000..b3a0f43 --- /dev/null +++ b/test/array-spec/schema.json @@ -0,0 +1,71 @@ +{ + "id": "http://some.site.somewhere/entry-schema#", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "schem to test arrays", + "definitions": { + "string array": { + "type": "array", + "items": { + "type": "string" + } + }, + "array of string arrays": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "restricted integer array": { + "type": "array", + "items": { + "maximum": 5, + "minimum": 2, + "type": "integer" + } + }, + "array contains items": { + "type": "array", + "contains": { + "maximum": 5, + "minimum": 2, + "type": "integer" + } + }, + "unique array": { + "type": "array", + "uniqueItems": true, + "items": { + "type": "string" + } + }, + "fixed array items": { + "type": "array", + "uniqueItems": true, + "items": [ + { + "type": "string", + "minLength": 5 + }, + { + "type": "number", + "maximum": 5 + } + ], + "additionalItems": { + "type": "string", + "maxLength": 4 + } + }, + "restricted array length": { + "type": "array", + "minItems": 2, + "maxItems": 5, + "items": { + "type": "string" + } + } + } +} \ No newline at end of file diff --git a/test/enum-spec/enum-spec.js b/test/enum-spec/enum-spec.js new file mode 100644 index 0000000..1830a26 --- /dev/null +++ b/test/enum-spec/enum-spec.js @@ -0,0 +1,67 @@ +/*! + * bootprint-swagger + * + * Copyright (c) 2015 Nils Knappmeier. + * Released under the MIT license. + */ + +/* global describe */ +/* global it */ +/* global before */ +var expect = require('chai').expect + +var tester = require('bootprint-unit-testing') + +describe('enum', function () { + this.timeout(10000) + var bptest = tester(require('../..'), __dirname, require('./schema.json')) + + // Run bootprint. The parsed "index.html"-file (cheerio) is then available + // under "bptest.$" + before(bptest.run) + + it('should have a lower bounded range for age', function () { + expect(bptest.$('dt[data-property-name="age"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('age: integer, { x ∈ ℤ | x ≥ 0 }') + }) + + it('should have a upper bounded range for bestFriends', function () { + expect(bptest.$('dt[data-property-name="bestFriends"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('bestFriends: integer, { x ∈ ℤ | x ≤ 5 }') + }) + + it('should have a bounded range with exclusive boundaries for two_to_three', function () { + expect(bptest.$('dt[data-property-name="two_to_three"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('two_to_three: number, { x ∈ ℝ | 2 < x < 3 }') + }) + + it('should have a lower bounded range with exclusive boundaries for more_than_two', function () { + expect(bptest.$('dt[data-property-name="more_than_two"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('more_than_two: number, { x ∈ ℝ | x > 2 }') + }) + + it('should have a upper bounded range with exclusive boundaries for less_than_three', function () { + expect(bptest.$('dt[data-property-name="less_than_three"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('less_than_three: number, { x ∈ ℝ | x < 3 }') + }) + + it('should not write a range for strings', function () { + expect(bptest.$('dt[data-property-name="string_with_range"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('string_with_range: string') + }) + + it('should have a lower length bound for string_min_length', function () { + expect(bptest.$('dt[data-property-name="string_min_length"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('string_min_length: string, at least 2 chars') + }) + + it('should have an upper length bound for string_max_length', function () { + expect(bptest.$('dt[data-property-name="string_max_length"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('string_max_length: string, up to 5 chars') + }) + + it('should have an bounded length for string_min_max_length', function () { + expect(bptest.$('dt[data-property-name="string_min_max_length"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('string_min_max_length: string, 2 to 5 chars') + }) +}) diff --git a/test/enum-spec/schema.json b/test/enum-spec/schema.json new file mode 100644 index 0000000..af829f8 --- /dev/null +++ b/test/enum-spec/schema.json @@ -0,0 +1,48 @@ +{ + "id": "http://some.site.somewhere/entry-schema#", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "schema to test enums", + "definitions": { + "string enum": { + "type": "string", + "enum": [ + "abc", + "def", + "ghi" + ] + }, + "number enum": { + "type": "number", + "enum": [ + 1, + 2, + 3 + ] + }, + "object enum": { + "type": "object", + "enum": [ + { + "a": 1 + }, + { + "b": "Nils" + } + ] + }, + "mixed": { + "enum": [ + { + "a": "object" + }, + [ + "an", + "array" + ], + 1, + "a string", + true + ] + } + } +} \ No newline at end of file diff --git a/test/helper-spec.js b/test/helper-spec.js index 6312d1a..230c995 100644 --- a/test/helper-spec.js +++ b/test/helper-spec.js @@ -1,7 +1,9 @@ /* global describe */ /* global it */ -var expect = require('chai').expect +const chai = require('chai') +const expect = chai.expect +chai.use(require('dirty-chai')) var helpers = require('../handlebars/helpers.js') var Handlebars = require('handlebars').create() @@ -12,44 +14,98 @@ function run (template, json) { } describe('The Handlebars-helpers:', function () { + describe('the helper "json_schema__could_be_numeric"', function () { + function testWith (schemaObject) { + return helpers.json_schema__could_be_numeric.call(schemaObject) + } + + it('should return true, if the type is integer', function () { + expect(testWith({type: 'integer'})).to.be.true() + }) + it('should return true, if the type is number', function () { + expect(testWith({type: 'number'})).to.be.true() + }) + it('should return true, if the type is undefined', function () { + expect(testWith({})).to.be.true() + }) + it('should return true, if the integer is part of the type-array', function () { + expect(testWith({type: ['string', 'integer']})).to.be.true() + }) + it('should return true, if the numeber is part of the type-array', function () { + expect(testWith({type: ['string', 'number']})).to.be.true() + }) + it('should return false, if type is string', function () { + expect(testWith({type: 'string'})).to.be.false() + }) + it('should return false, if type is an array without number or integer', function () { + expect(testWith({type: ['string', 'object']})).to.be.false() + }) + }) + + describe('the helper "json_schema__could_be_of_type"', function () { + function testWith (type, schemaObject) { + return helpers.json_schema__could_be_of_type.call(schemaObject, type) + } + + it('should return true, if the type matches directly', function () { + expect(testWith('string', {type: 'string'})).to.be.true() + }) + it('should return true, if the type is undefined', function () { + expect(testWith('string', {})).to.be.true() + }) + it('should return true, if the type is part of the type-array', function () { + expect(testWith('string', {type: ['string', 'integer']})).to.be.true() + }) + it('should return false, if type does not match', function () { + expect(testWith('string', {type: 'object'})).to.be.false() + }) + it('should return false, if type is an array without the expected type', function () { + expect(testWith('array', {type: ['string', 'object']})).to.be.false() + }) + }) + describe('the json_schema__datatype helper', function () { // Call the datatype-helper - function dt (obj) { - return run('{{json_schema__datatype type}}', {type: obj}) + function dt (obj, types) { + // The callee of the helper should always pass 2 arguments + types = types || (obj && obj.type) + return helpers.json_schema__datatype.call({}, obj, types) } it('should handle undefined gracefully', function () { - expect(dt(undefined)).to.equal('') + expect(dt(undefined)).to.be.null() }) it('should handle null gracefully', function () { - expect(dt(null)).to.equal('') + expect(dt(null)).to.be.null() }) - it('should return an empty string for null and undefined gracefully, as well as composite schema, since they are rendered in partials ', function () { - expect(dt({anyOf: []})).to.equal('') - expect(dt({oneOf: []})).to.equal('') - expect(dt({allOf: []})).to.equal('') + it('should return any, if no type is specified ', function () { + expect(dt({})).to.equal('*') }) - it('should treat unspecific types as object', function () { - expect(dt({})).to.equal('object') + it('should concatate multiple types with a pipe', function () { + expect(dt({type: ['string', 'integer']})).to.equal('string|integer') }) - it('should treat unspecific array types as object[]', function () { - expect(dt({type: 'array'})).to.equal('object[]') - }) - - it('should treat arrays with unspecific types object[]', function () { - expect(dt({type: 'array', items: {}})).to.equal('object[]') + it('should treat arrays with unspecific types as array<*>', function () { + expect(dt({type: 'array', items: {}})).to.equal('array<*>') }) it('should write type[] for specific arrays', function () { - expect(dt({type: 'array', items: {type: 'string'}})).to.equal('string[]') + expect(dt({type: 'array', items: {type: 'string'}})).to.equal('array') }) it('should write recurse multiple steps of array types', function () { - expect(dt({type: 'array', items: {type: 'array', items: {type: 'string'}}})).to.equal('string[][]') + expect(dt({type: 'array', items: {type: 'array', items: {type: 'string'}}})).to.equal('array>') + }) + + it('should write concatenated types as generic array paramters', function () { + expect(dt({type: 'array', items: {type: ['string', 'integer']}})).to.equal('array') + }) + + it('should write array-items only to the array type of a multi-type', function () { + expect(dt({type: ['integer', 'array'], items: {type: ['string']}})).to.equal('integer|array') }) }) @@ -64,11 +120,11 @@ describe('The Handlebars-helpers:', function () { }) it('should handle empty integer ranges gracefully', function () { - expect(range({ type: 'integer' })).to.equal('') + expect(range({type: 'integer'})).to.equal('') }) it('should handle empty number ranges gracefully', function () { - expect(range({ type: 'number' })).to.equal('') + expect(range({type: 'number'})).to.equal('') }) it('should ignore minimum and maximum for non-numeric types', function () { diff --git a/test/numeric-restrictions/numeric-restrictions-spec.js b/test/numeric-restrictions/numeric-restrictions-spec.js new file mode 100644 index 0000000..fdb8ae9 --- /dev/null +++ b/test/numeric-restrictions/numeric-restrictions-spec.js @@ -0,0 +1,67 @@ +/*! + * bootprint-swagger + * + * Copyright (c) 2015 Nils Knappmeier. + * Released under the MIT license. + */ + +/* global describe */ +/* global it */ +/* global before */ +var expect = require('chai').expect + +var tester = require('bootprint-unit-testing') + +describe('Numeric-restrictions', function () { + this.timeout(10000) + var bptest = tester(require('../..'), __dirname, require('./schema.json')) + + // Run bootprint. The parsed "index.html"-file (cheerio) is then available + // under "bptest.$" + before(bptest.run) + + it('should display a lower bounded range for age', function () { + expect(bptest.textIn('dt[data-property-schema="age"]').trim()) + .to.equal('{ x ∈ ℤ | x ≥ 0 }') + }) + + it('should have a upper bounded range for bestFriends', function () { + expect(bptest.$('dt[data-property-name="bestFriends"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('bestFriends: integer, { x ∈ ℤ | x ≤ 5 }') + }) + + it('should have a bounded range with exclusive boundaries for two_to_three', function () { + expect(bptest.$('dt[data-property-name="two_to_three"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('two_to_three: number, { x ∈ ℝ | 2 < x < 3 }') + }) + + it('should have a lower bounded range with exclusive boundaries for more_than_two', function () { + expect(bptest.$('dt[data-property-name="more_than_two"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('more_than_two: number, { x ∈ ℝ | x > 2 }') + }) + + it('should have a upper bounded range with exclusive boundaries for less_than_three', function () { + expect(bptest.$('dt[data-property-name="less_than_three"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('less_than_three: number, { x ∈ ℝ | x < 3 }') + }) + + it('should not write a range for strings', function () { + expect(bptest.$('dt[data-property-name="string_with_range"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('string_with_range: string') + }) + + it('should have a lower length bound for string_min_length', function () { + expect(bptest.$('dt[data-property-name="string_min_length"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('string_min_length: string, at least 2 chars') + }) + + it('should have an upper length bound for string_max_length', function () { + expect(bptest.$('dt[data-property-name="string_max_length"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('string_max_length: string, up to 5 chars') + }) + + it('should have an bounded length for string_min_max_length', function () { + expect(bptest.$('dt[data-property-name="string_min_max_length"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('string_min_max_length: string, 2 to 5 chars') + }) +}) diff --git a/test/ranges/schema.json b/test/numeric-restrictions/schema.json similarity index 60% rename from test/ranges/schema.json rename to test/numeric-restrictions/schema.json index d40ffa7..0c2b4ef 100644 --- a/test/ranges/schema.json +++ b/test/numeric-restrictions/schema.json @@ -1,9 +1,8 @@ { "id": "http://some.site.somewhere/entry-schema#", "$schema": "http://json-schema.org/draft-04/schema#", - "description": "schema for an fstab entry", - "type": "object", - "properties": { + "description": "schema to test numeric restrictions", + "definitions": { "age": { "type": "integer", "minimum": 0 @@ -29,22 +28,16 @@ "minimum": 2.0, "minimumExclusive": true }, - "string_with_range": { - "type": "string", - "minimum": 2 + "multiples_of_three": { + "multipleOf": 3 }, - "string_min_length": { - "type": "string", - "minLength": 2 - }, - "string_max_length": { - "type": "string", - "maxLength": 5 - }, - "string_min_max_length": { - "type": "string", - "minLength": 2, - "maxLength": 5 + "allRestrictions": { + "type": "number", + "multipleOf": 3, + "maximum": 16.0, + "maximumExclusive": true, + "minimum": 2.0, + "minimumExclusive": true } } } \ No newline at end of file diff --git a/test/string-restrictions-spec/schema.json b/test/string-restrictions-spec/schema.json new file mode 100644 index 0000000..26fe4be --- /dev/null +++ b/test/string-restrictions-spec/schema.json @@ -0,0 +1,29 @@ +{ + "id": "http://some.site.somewhere/entry-schema#", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "schema to test numeric restrictions", + "definitions": { + "name": { + "type": "string", + "minLength": 3, + "maxLength": 10 + }, + "lots_of_a": { + "minLength": "1000", + "pattern": "a*" + }, + "string_min_length": { + "type": "string", + "minLength": 2 + }, + "string_max_length": { + "type": "string", + "maxLength": 5 + }, + "string_min_max_length": { + "type": "string", + "minLength": 2, + "maxLength": 5 + } + } +} \ No newline at end of file diff --git a/test/string-restrictions-spec/string-restrictions-spec.js b/test/string-restrictions-spec/string-restrictions-spec.js new file mode 100644 index 0000000..88207b3 --- /dev/null +++ b/test/string-restrictions-spec/string-restrictions-spec.js @@ -0,0 +1,67 @@ +/*! + * bootprint-swagger + * + * Copyright (c) 2015 Nils Knappmeier. + * Released under the MIT license. + */ + +/* global describe */ +/* global it */ +/* global before */ +var expect = require('chai').expect + +var tester = require('bootprint-unit-testing') + +describe('string-restrictions', function () { + this.timeout(10000) + var bptest = tester(require('../..'), __dirname, require('./schema.json')) + + // Run bootprint. The parsed "index.html"-file (cheerio) is then available + // under "bptest.$" + before(bptest.run) + + it('should have a lower bounded range for age', function () { + expect(bptest.$('dt[data-property-name="age"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('age: integer, { x ∈ ℤ | x ≥ 0 }') + }) + + it('should have a upper bounded range for bestFriends', function () { + expect(bptest.$('dt[data-property-name="bestFriends"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('bestFriends: integer, { x ∈ ℤ | x ≤ 5 }') + }) + + it('should have a bounded range with exclusive boundaries for two_to_three', function () { + expect(bptest.$('dt[data-property-name="two_to_three"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('two_to_three: number, { x ∈ ℝ | 2 < x < 3 }') + }) + + it('should have a lower bounded range with exclusive boundaries for more_than_two', function () { + expect(bptest.$('dt[data-property-name="more_than_two"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('more_than_two: number, { x ∈ ℝ | x > 2 }') + }) + + it('should have a upper bounded range with exclusive boundaries for less_than_three', function () { + expect(bptest.$('dt[data-property-name="less_than_three"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('less_than_three: number, { x ∈ ℝ | x < 3 }') + }) + + it('should not write a range for strings', function () { + expect(bptest.$('dt[data-property-name="string_with_range"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('string_with_range: string') + }) + + it('should have a lower length bound for string_min_length', function () { + expect(bptest.$('dt[data-property-name="string_min_length"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('string_min_length: string, at least 2 chars') + }) + + it('should have an upper length bound for string_max_length', function () { + expect(bptest.$('dt[data-property-name="string_max_length"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('string_max_length: string, up to 5 chars') + }) + + it('should have an bounded length for string_min_max_length', function () { + expect(bptest.$('dt[data-property-name="string_min_max_length"]').text().replace(/\s+/g, ' ').trim()) + .to.equal('string_min_max_length: string, 2 to 5 chars') + }) +}) diff --git a/test/types/schema.json b/test/types/schema.json new file mode 100644 index 0000000..ed195fd --- /dev/null +++ b/test/types/schema.json @@ -0,0 +1,35 @@ +{ + "id": "http://some.site.somewhere/entry-schema#", + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Schema for testing different types", + "type": "object", + "definitions": { + "simpleString": { + "type": "string" + }, + "simpleArray": { + "type": "array" + }, + "simpleInteger": { + "type": "integer" + }, + "unspecified": { + }, + "stringOrInteger": { + "type": [ + "string", + "integer" + ] + }, + "stringOrStringArray": { + "type": [ + "string", + "array" + ], + "items": { + "type": "string" + } + } + + } +} \ No newline at end of file diff --git a/test/types/types-spec.js b/test/types/types-spec.js new file mode 100644 index 0000000..043d357 --- /dev/null +++ b/test/types/types-spec.js @@ -0,0 +1,43 @@ +/*! + * bootprint-json-schema + * + * Copyright (c) 2017 Nils Knappmeier. + * Released under the MIT license. + */ + +/* global describe */ +/* global it */ +/* global before */ +var expect = require('chai').expect + +var tester = require('bootprint-unit-testing') + +describe('The types-property', function () { + this.timeout(10000) + var bptest = tester(require('../..'), __dirname, require('./schema.json')) + + // Run bootprint. The parsed "index.html"-file (cheerio) is then available + // under "bptest.$" + before(bptest.run) + + it('should show simple types', function () { + expect(bptest.textIn('#definition-simpleInteger .json-schema--schema')).to.equal('Type: integer') + expect(bptest.textIn('#definition-simpleString .json-schema--schema')).to.equal('Type: string') + }) + + it('should show alternative types separated by "or"', function () { + expect(bptest.textIn('#definition-stringOrInteger .json-schema--schema')).to.equal('Type: string or integer') + }) + + it('should show unspecified types as "any"', function () { + expect(bptest.textIn('#definition-unspecified .json-schema--schema')).to.equal('Type: any type') + }) + + it('should show an array as "array<*>" if no items-type is specified', function () { + expect(bptest.textIn('#definition-simpleArray .json-schema--schema')).to.equal('Type: array<*>') + }) + + it('should apply items-types to the array type ', function () { + expect(bptest.textIn('#definition-stringOrStringArray .json-schema--schema')).to.equal('Type: string or array') + }) +})