Skip to content

Commit

Permalink
fix(openapi-sampler): address pr comments
Browse files Browse the repository at this point in the history
closes #246
  • Loading branch information
ostridm committed Aug 13, 2024
1 parent f7b7513 commit 3f97800
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 29 deletions.
84 changes: 56 additions & 28 deletions packages/openapi-sampler/src/samplers/StringSampler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Sampler, OpenAPISchema } from './Sampler';
import RandExp from 'randexp';

export class StringSampler implements Sampler {
private readonly MAX_PATTERN_SAMPLE_LENGTH = 500;
private readonly stringFormats = {
'email': () => '[email protected]',
'idn-email': () => 'джон.сноу@таргариен.укр',
Expand Down Expand Up @@ -44,71 +45,71 @@ export class StringSampler implements Sampler {

const { minLength: min, maxLength: max } = schema;

return this.checkLength(sampler(min || 0, max, schema), format, min, max);
return this.checkLength(sampler(min, max, schema), format, min, max);
}

private patternSample(
pattern: string | RegExp,
min?: number,
max?: number
): string {
this.assertLength(min, max);

const randExp = new RandExp(pattern);
randExp.randInt = (a, b) => Math.floor((a + b) / 2);

if (min) {
return this.sampleMinLength(randExp, min, max);
if (min !== undefined) {
return this.sampleWithMinLength(randExp, min, max);
}

randExp.max = max ?? randExp.max;
randExp.randInt = (a, b) => Math.floor((a + b) / 2);

const result = randExp.gen();

return !!max && result.length > max && this.hasInfiniteQuantifier(pattern)
? this.sampleInfiniteQuantifier(randExp, max)
return max !== undefined && result.length > max && this.hasInfiniteQuantifier(pattern)
? this.sampleWithMaxLength(randExp, max)
: result;
}

private hasInfiniteQuantifier(pattern: string | RegExp) {
const pat = pattern.toString();
const pat = typeof pattern === 'string' ? pattern : pattern.source;

return ['+', '*', ',}'].some((q) => pat.includes(q));
return /(\+|\*|\{\d*,\})/.test(pat);
}

private sampleInfiniteQuantifier(randExp: RandExp, max: number): string {
randExp.randInt = (a, b) => Math.floor((a + b) / 2);

for (let i = 1, lmax = max; lmax > 0; lmax = Math.floor(max / ++i)) {
randExp.max = lmax;
private sampleWithMaxLength(randExp: RandExp, max: number): string {
let result = '';

const result = randExp.gen();
for (let i = 1; i <= Math.min(max, 20); i++) {
randExp.max = Math.floor(max / i);
result = randExp.gen();

if (result.length <= max) {
return result;
break;
}
}

return '';
return result;
}

private sampleMinLength(randExp: RandExp, min: number, max: number) {
private sampleWithMinLength(randExp: RandExp, min: number, max?: number) {
// ADHOC: make a probe for regex using min quantifier value
// e.g. ^[a]+[b]+$ expect 'ab', ^[a-z]*$ expect ''

randExp.max = 0;
const randInt = randExp.randInt;
randExp.max = min;
randExp.randInt = (a, _) => a;

const result = randExp.gen();

if (result.length >= min) {
return result;
}
let result = randExp.gen();

// ADHOC: fallback for failed min quantifier probe with doubled min length
randExp.randInt = randInt;

randExp.max = 2 * min;
randExp.randInt = (a, b) => Math.floor((a + b) / 2);
if (result.length < min) {
// ADHOC: fallback for failed min quantifier probe with doubled min length
randExp.max = 2 * min;
result = this.adjustMaxLength(randExp.gen(), max);
}

return this.adjustMaxLength(randExp.gen(), max);
return result;
}

private checkLength(
Expand Down Expand Up @@ -137,6 +138,33 @@ export class StringSampler implements Sampler {
return value;
}

private assertLength(min?: number, max?: number) {
const pairs = [
{ key: 'minLength', value: min },
{ key: 'maxLength', value: max }
];

const boundariesStr = pairs
.filter((p) => !this.checkBoundary(p.value))
.map((p) => `${p.key}=${p.value}`)
.join(', ');

if (boundariesStr) {
throw new Error(
`Sample string cannot be generated by boundaries: ${boundariesStr}`
);
}
}

private checkBoundary(boundary?: number) {
return (
boundary === undefined ||
(typeof boundary === 'number' &&
boundary >= 0 &&
boundary <= this.MAX_PATTERN_SAMPLE_LENGTH)
);
}

private adjustLength(sample: string, min: number, max: number): string {
const minLength = min ? min : 0;
const maxLength = max ? max : sample.length;
Expand Down
56 changes: 55 additions & 1 deletion packages/openapi-sampler/tests/string.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('StringSampler', () => {
pattern: '^[A-Za-z0-9._%-]+@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{1,4}$',
type: 'string'
},
expected: '[email protected]'
expected: '[email protected]'
},
{
input: {
Expand Down Expand Up @@ -420,6 +420,44 @@ describe('StringSampler', () => {
},
expected:
'Sample string cannot be generated by boundaries: maxLength=35, format=uuid'
},
{
input: {
maxLength: Number.MAX_SAFE_INTEGER,
format: 'pattern',
pattern: '^[A-Za-z0-9._%-]+@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{1,4}$',
type: 'string'
},
expected:
'Sample string cannot be generated by boundaries: maxLength=9007199254740991'
},
{
input: {
minLength: Number.MAX_SAFE_INTEGER,
format: 'pattern',
pattern: '^[A-Za-z0-9._%-]+@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{1,4}$',
type: 'string'
},
expected:
'Sample string cannot be generated by boundaries: minLength=9007199254740991'
},
{
input: {
maxLength: 501,
format: 'pattern',
pattern: '^[A-Za-z0-9._%-]+@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{1,4}$',
type: 'string'
},
expected: 'Sample string cannot be generated by boundaries: maxLength=501'
},
{
input: {
minLength: -1,
format: 'pattern',
pattern: '^[A-Za-z0-9._%-]+@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{1,4}$',
type: 'string'
},
expected: 'Sample string cannot be generated by boundaries: minLength=-1'
}
].forEach(({ input, expected }) => {
const { type, ...restrictions } = input;
Expand All @@ -431,4 +469,20 @@ describe('StringSampler', () => {
expect(result).toThrowError(expected);
});
});

it.each([10, 100, 500])(`should handle maxLength=%d gracefully`, (input) => {
// arrange
const pattern = /^[A-Za-z0-9._%-]+@(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{1,4}$/;

// act
const result = sample({
maxLength: input,
format: 'pattern',
pattern: pattern.source,
type: 'string'
});

// assert
expect(result).toMatch(pattern);
});
});

0 comments on commit 3f97800

Please sign in to comment.