forked from Redocly/openapi-sampler
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This fixes Redocly#152 except in an edge case where both `pattern` and `maxLength` are used, and the sampler skips over the valid length range. It introduces a tiny dependency (<10 KB uncompressed) which doesn't really have any prominent competitors.
- Loading branch information
Showing
4 changed files
with
141 additions
and
11 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,66 @@ | ||
'use strict'; | ||
|
||
import RandExp from 'randexp'; | ||
|
||
import { ensureMinLength, toRFCDateTime, uuid } from '../utils'; | ||
|
||
const passwordSymbols = 'qwerty!@#$%^123456'; | ||
const MAX_REGEX_SAMPLES = 100; | ||
|
||
function sampleRegex(pattern, min, max) { | ||
let res; | ||
let i = 0; | ||
let length; | ||
let prevLength; | ||
|
||
// Increase length of the sample until it satisfies the minimum. | ||
do { | ||
RandExp.prototype.randInt = (from, to) => Math.min(from + i, to); | ||
res = new RandExp(pattern).gen(); | ||
prevLength = length; | ||
length = res.length; | ||
i++; | ||
} while (length < min && i < MAX_REGEX_SAMPLES); | ||
|
||
// Handle case where we went past the maximum. | ||
// Example: /\d*\d*foo/, will sample foo, 11foo, 2222foo, etc. | ||
// | ||
// HACK: RandExp doesn't expose an API to set the value of a specific sample, | ||
// so we'll just fuzz it. If no satisfying string is found, | ||
// we prefer to return a string that is too long than to return a string | ||
// that doesn't fit the regex. | ||
if (max && max >= min && res.length > max) { | ||
// probability that 2N fair coinflips returns N heads ~ 1 / sqrt(pi * N) | ||
// probability that 2N biased coinflips (probabilty 1/2N) returns 1 heads ~ 1 / e | ||
// so the fuzz will succeed for any reasonable regex (+, * don't appear more than 30 times) | ||
const targetProbability = ((min + max) / 2 - prevLength) / (length - prevLength) | ||
|
||
for (let j = 0; j < MAX_REGEX_SAMPLES; j++) { | ||
RandExp.prototype.randInt = (from, to) => Math.max( | ||
from, | ||
Math.min(from + i - 2 + (Math.random() < targetProbability ? 1 : 0), to), | ||
); | ||
const candidate = new RandExp(pattern).gen(); | ||
if (candidate.length >= min) { | ||
if (candidate.length <= max) { | ||
return candidate; | ||
} else if (candidate.length < res.length) { | ||
res = candidate; | ||
} | ||
} | ||
} | ||
} | ||
|
||
return res; | ||
} | ||
|
||
function truncateString(str, min, max) { | ||
let res = ensureMinLength(str, min); | ||
if (max && res.length > max) { | ||
res = res.substring(0, max); | ||
} | ||
return res; | ||
} | ||
|
||
function emailSample() { | ||
return '[email protected]'; | ||
|
@@ -42,12 +100,10 @@ function timeSample(min, max) { | |
return commonDateTimeSample({ min, max, omitTime: false, omitDate: true }).slice(1); | ||
} | ||
|
||
function defaultSample(min, max) { | ||
let res = ensureMinLength('string', min); | ||
if (max && res.length > max) { | ||
res = res.substring(0, max); | ||
} | ||
return res; | ||
function defaultSample(min, max, _propertyName, pattern) { | ||
return pattern | ||
? sampleRegex(pattern, min, max) | ||
: truncateString('string', min, max) | ||
} | ||
|
||
function ipv4Sample() { | ||
|
@@ -96,8 +152,10 @@ function relativeJsonPointerSample() { | |
return '1/relative/json/pointer'; | ||
} | ||
|
||
function regexSample() { | ||
return '/regex/'; | ||
function regexSample(min, max, _propertyName, pattern) { | ||
return pattern | ||
? sampleRegex(pattern, min, max) | ||
: truncateString('/regex/', min, max) | ||
} | ||
|
||
const stringFormats = { | ||
|
@@ -127,5 +185,10 @@ export function sampleString(schema, options, spec, context) { | |
let format = schema.format || 'default'; | ||
let sampler = stringFormats[format] || defaultSample; | ||
let propertyName = context && context.propertyName; | ||
return sampler(schema.minLength | 0, schema.maxLength, propertyName); | ||
return sampler( | ||
schema.minLength || 0, | ||
schema.maxLength, | ||
propertyName, | ||
schema.pattern, | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters