From 9d6c31260f41c7f1afc96fbf9725963666dc8deb Mon Sep 17 00:00:00 2001 From: Gareth Oakley Date: Wed, 6 May 2020 08:42:42 +0100 Subject: [PATCH] feat: add support for uuid format --- README.md | 1 + src/samplers/object.js | 2 +- src/samplers/string.js | 12 ++++++++--- src/traverse.js | 4 ++-- src/utils.js | 43 ++++++++++++++++++++++++++++++++++++---- test/unit/object.spec.js | 13 ++++++++++++ test/unit/string.spec.js | 13 ++++++++++++ 7 files changed, 78 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 804c556..722637d 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Tool for generation samples based on OpenAPI payload/response schema - ipv6 - hostname - uri + - uuid - Infers schema type automatically following same rules as [json-schema-faker](https://www.npmjs.com/package/json-schema-faker#inferred-types) - Support for `$ref` resolving diff --git a/src/samplers/object.js b/src/samplers/object.js index f40d1b3..eec3bd4 100644 --- a/src/samplers/object.js +++ b/src/samplers/object.js @@ -14,7 +14,7 @@ export function sampleObject(schema, options = {}, spec) { return; } - const sample = traverse(schema.properties[propertyName], options, spec); + const sample = traverse(schema.properties[propertyName], options, spec, { propertyName }); if (options.skipReadOnly && sample.readOnly) { return; } diff --git a/src/samplers/string.js b/src/samplers/string.js index 4c69036..6d560ed 100644 --- a/src/samplers/string.js +++ b/src/samplers/string.js @@ -1,6 +1,6 @@ 'use strict'; -import { ensureMinLength, toRFCDateTime } from '../utils'; +import { ensureMinLength, toRFCDateTime, uuid } from '../utils'; const passwordSymbols = 'qwerty!@#$%^123456'; @@ -60,6 +60,10 @@ function uriSample() { return 'http://example.com'; } +function uuidSample(_min, _max, propertyName) { + return uuid(propertyName || 'id'); +} + const stringFormats = { 'email': emailSample, 'password': passwordSample, @@ -69,11 +73,13 @@ const stringFormats = { 'ipv6': ipv6Sample, 'hostname': hostnameSample, 'uri': uriSample, + 'uuid': uuidSample, 'default': defaultSample }; -export function sampleString(schema) { +export function sampleString(schema, options, spec, context) { let format = schema.format || 'default'; let sampler = stringFormats[format] || defaultSample; - return sampler(schema.minLength | 0, schema.maxLength); + let propertyName = context && context.propertyName; + return sampler(schema.minLength | 0, schema.maxLength, propertyName); } diff --git a/src/traverse.js b/src/traverse.js index e9c6296..3368541 100644 --- a/src/traverse.js +++ b/src/traverse.js @@ -9,7 +9,7 @@ export function clearCache() { $refCache = {}; } -export function traverse(schema, options, spec) { +export function traverse(schema, options, spec, context) { if (schema.$ref) { if (!spec) { throw new Error('Your schema contains $ref. You must provide specification in the third parameter.'); @@ -85,7 +85,7 @@ export function traverse(schema, options, spec) { } let sampler = _samplers[type]; if (sampler) { - example = sampler(schema, options, spec); + example = sampler(schema, options, spec, context); } } diff --git a/src/utils.js b/src/utils.js index e61ed53..3f2d9f2 100644 --- a/src/utils.js +++ b/src/utils.js @@ -13,10 +13,10 @@ export function toRFCDateTime(date, omitTime, milliseconds) { '-' + pad(date.getUTCDate()); if (!omitTime) { res += 'T' + pad(date.getUTCHours()) + - ':' + pad(date.getUTCMinutes()) + - ':' + pad(date.getUTCSeconds()) + - (milliseconds ? '.' + (date.getUTCMilliseconds() / 1000).toFixed(3).slice(2, 5) : '') + - 'Z'; + ':' + pad(date.getUTCMinutes()) + + ':' + pad(date.getUTCSeconds()) + + (milliseconds ? '.' + (date.getUTCMilliseconds() / 1000).toFixed(3).slice(2, 5) : '') + + 'Z'; } return res; }; @@ -46,3 +46,38 @@ export function mergeDeep(...objects) { return prev; }, Array.isArray(objects[objects.length - 1]) ? [] : {}); } + +// deterministic UUID sampler + +export function uuid(str) { + var hash = hashCode(str); + var random = jsf32(hash, hash, hash, hash); + var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + var r = (random() * 16) % 16 | 0; + return (c == 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); + return uuid; +} + +function hashCode(str) { + var hash = 0; + if (str.length == 0) return hash; + for (var i = 0; i < str.length; i++) { + var char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + return hash; +} + +function jsf32(a, b, c, d) { + return function () { + a |= 0; b |= 0; c |= 0; d |= 0; + var t = a - (b << 27 | b >>> 5) | 0; + a = b ^ (c << 17 | c >>> 15); + b = c + d | 0; + c = d + t | 0; + d = a + t | 0; + return (d >>> 0) / 4294967296; + } +} \ No newline at end of file diff --git a/test/unit/object.spec.js b/test/unit/object.spec.js index 809f53c..713f348 100644 --- a/test/unit/object.spec.js +++ b/test/unit/object.spec.js @@ -90,4 +90,17 @@ describe('sampleObject', () => { a: 'string' }); }); + + it('should pass propertyName context to samplers', () => { + res = sampleObject({ + properties: { + fooId: {type: 'string', format: 'uuid'}, + barId: {type: 'string', format: 'uuid'}, + } + }); + expect(res).to.deep.equal({ + fooId: 'fb4274c7-4fcd-4035-8958-a680548957ff', + barId: '3c966637-4898-4972-9a9d-baefa6cd6c89' + }); + }) }); diff --git a/test/unit/string.spec.js b/test/unit/string.spec.js index 7f5d531..8aaf3f3 100644 --- a/test/unit/string.spec.js +++ b/test/unit/string.spec.js @@ -4,6 +4,7 @@ const IPV4_REGEXP = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5 const IPV6_REGEXP = /^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$/; const HOSTNAME_REGEXP = /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/; const URI_REGEXP = new RegExp('([A-Za-z][A-Za-z0-9+\\-.]*):(?:(//)(?:((?:[A-Za-z0-9\\-._~!$&\'()*+,;=:]|%[0-9A-Fa-f]{2})*)@)?((?:\\[(?:(?:(?:(?:[0-9A-Fa-f]{1,4}:){6}|::(?:[0-9A-Fa-f]{1,4}:){5}|(?:[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,1}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){3}|(?:(?:[0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4})?::(?:[0-9A-Fa-f]{1,4}:){2}|(?:(?:[0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}:|(?:(?:[0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4})?::)(?:[0-9A-Fa-f]{1,4}:[0-9A-Fa-f]{1,4}|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))|(?:(?:[0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4})?::[0-9A-Fa-f]{1,4}|(?:(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4})?::)|[Vv][0-9A-Fa-f]+\\.[A-Za-z0-9\\-._~!$&\'()*+,;=:]+)\\]|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)|(?:[A-Za-z0-9\\-._~!$&\'()*+,;=]|%[0-9A-Fa-f]{2})*))(?::([0-9]*))?((?:/(?:[A-Za-z0-9\\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)|/((?:(?:[A-Za-z0-9\\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:/(?:[A-Za-z0-9\\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)?)|((?:[A-Za-z0-9\\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})+(?:/(?:[A-Za-z0-9\\-._~!$&\'()*+,;=:@]|%[0-9A-Fa-f]{2})*)*)|)(?:\\?((?:[A-Za-z0-9\\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*))?(?:\\#((?:[A-Za-z0-9\\-._~!$&\'()*+,;=:@/?]|%[0-9A-Fa-f]{2})*))?'); +const UUID_REGEXP = /^[0-9A-Fa-f]{8}(?:-[0-9A-Fa-f]{4}){3}-[0-9A-Fa-f]{12}$/; describe('sampleString', () => { let res; @@ -82,4 +83,16 @@ describe('sampleString', () => { res = sampleString({format: 'uri'}); expect(res).to.match(URI_REGEXP); }); + + it('should return valid uuid for uuid format without propertyName context', () => { + res = sampleString({format: 'uuid'}); + expect(res).to.match(UUID_REGEXP); + expect(res).to.equal('497f6eca-6276-4993-bfeb-53cbbbba6f08'); + }); + + it('should return valid uuid for uuid format with propertyName context', () => { + res = sampleString({format: 'uuid'}, null, null, {propertyName: 'fooId'}); + expect(res).to.match(UUID_REGEXP); + expect(res).to.equal('fb4274c7-4fcd-4035-8958-a680548957ff'); + }); });