-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(openapi-sampler): address pr comments
closes #246
- Loading branch information
Showing
2 changed files
with
111 additions
and
29 deletions.
There are no files selected for viewing
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 |
---|---|---|
|
@@ -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': () => 'джон.сноу@таргариен.укр', | ||
|
@@ -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( | ||
|
@@ -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; | ||
|
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 |
---|---|---|
|
@@ -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: { | ||
|
@@ -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; | ||
|
@@ -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); | ||
}); | ||
}); |