Skip to content

Commit

Permalink
feat: regex sampler
Browse files Browse the repository at this point in the history
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
llllvvuu committed Aug 23, 2023
1 parent d143da1 commit 30d8f87
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 11 deletions.
52 changes: 51 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
},
"dependencies": {
"@types/json-schema": "^7.0.7",
"json-pointer": "0.6.2"
"json-pointer": "0.6.2",
"randexp": "^0.5.3"
}
}
64 changes: 55 additions & 9 deletions src/samplers/string.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,51 @@
'use strict';

import RandExp from 'randexp';

import { ensureMinLength, toRFCDateTime, uuid } from '../utils';

const passwordSymbols = 'qwerty!@#$%^123456';
const MAX_REGEX_SAMPLES = 100;

function sampleRegex(pattern, placeholder, min, max) {
if (!pattern) return placeholder;

let res;
let i = 0;

do {
RandExp.prototype.randInt = (from, to) => Math.min(from + i, to);
res = new RandExp(pattern).gen();
i++;
} while (res.length < min && i < MAX_REGEX_SAMPLES);

// Handle case where we skipped over the range.
// 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 the number of * and + is more than 100 times
// the size of the range, it may fail.
if (max && max >= min && res.length > max) {
for (let j = 0; j < MAX_REGEX_SAMPLES; j++) {
RandExp.prototype.randInt = (from, to) => Math.max(
from,
Math.min(from + i - 2 + Math.floor(Math.random() * 2), to),
);
const candidate = new RandExp(pattern).gen();
console.log(candidate);
if (candidate.length <= max && candidate.length >= min) return 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]';
Expand Down Expand Up @@ -42,12 +85,9 @@ 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) {
const res = sampleRegex(pattern, 'string', min, max);
return truncateString(res, min, max)
}

function ipv4Sample() {
Expand Down Expand Up @@ -96,8 +136,9 @@ function relativeJsonPointerSample() {
return '1/relative/json/pointer';
}

function regexSample() {
return '/regex/';
function regexSample(min, max, _propertyName, pattern) {
const res = sampleRegex(pattern, '/regex/', min, max);
return truncateString(res, min, max);
}

const stringFormats = {
Expand Down Expand Up @@ -127,5 +168,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,
);
}
11 changes: 11 additions & 0 deletions test/unit/string.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,17 @@ describe('sampleString', () => {
expect(res).to.equal('fb4274c7-4fcd-4035-8958-a680548957ff');
});

it('should return valid string for regex with min and max', () => {
const schema = {
format: 'regex',
pattern: 'foo-\\d+\\d+\\d+\\d+\\d+-bar',
minLength: 19,
maxLength: 19,
};
res = sampleString(schema, null, null, {propertyName: 'fooId'});
expect(res).to.match(/foo-\d{11}-bar/);
});

it.each([
'email',
// 'idn-email', // unsupported by ajv-formats
Expand Down

0 comments on commit 30d8f87

Please sign in to comment.