@nifrajs/mock 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +303 -0
- package/dist/index.js.map +1 -0
- package/package.json +31 -0
- package/src/index.ts +391 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/mock` — contract-based mock server.
|
|
3
|
+
*
|
|
4
|
+
* Reads a Nifra app's registered routes and their Standard Schema / JSON Schema
|
|
5
|
+
* response definitions, then generates fake responses from supported JSON Schema keywords. Use it
|
|
6
|
+
* for frontend-first development, agent testing, or CI smoke tests.
|
|
7
|
+
*/
|
|
8
|
+
export declare class UnsupportedMockSchemaError extends Error {
|
|
9
|
+
readonly keyword: string;
|
|
10
|
+
constructor(keyword: string, message: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Generate a mock value from a schema object. Inspects JSON Schema properties
|
|
14
|
+
* (`type`, `properties`, `items`, `enum`, `format`) that TypeBox / NifraSchema
|
|
15
|
+
* objects carry directly. Unsupported constraints fail closed with
|
|
16
|
+
* {@link UnsupportedMockSchemaError} rather than returning a known-invalid response.
|
|
17
|
+
*
|
|
18
|
+
* @param schema A JSON-Schema-shaped object (TypeBox, NifraSchema, or raw JSON Schema)
|
|
19
|
+
* @param fieldName Optional field name hint for generating contextual string values
|
|
20
|
+
* @param rng Optional seeded random function for deterministic output
|
|
21
|
+
*/
|
|
22
|
+
export declare function generateMockValue(schema: unknown, fieldName?: string | undefined, rng?: (() => number) | undefined): unknown;
|
|
23
|
+
/** Minimal route shape returned by `app.routes()`. */
|
|
24
|
+
export interface MockableRoute {
|
|
25
|
+
readonly method: string;
|
|
26
|
+
readonly path: string;
|
|
27
|
+
readonly schema?: {
|
|
28
|
+
readonly response?: unknown;
|
|
29
|
+
} | undefined;
|
|
30
|
+
}
|
|
31
|
+
/** App shape — anything with a `routes()` method. */
|
|
32
|
+
export interface MockableApp {
|
|
33
|
+
routes(): readonly MockableRoute[];
|
|
34
|
+
}
|
|
35
|
+
export interface MockServerOptions {
|
|
36
|
+
/** Seed for deterministic mock generation. */
|
|
37
|
+
readonly seed?: number | undefined;
|
|
38
|
+
/** Log mock requests to console. */
|
|
39
|
+
readonly verbose?: boolean | undefined;
|
|
40
|
+
}
|
|
41
|
+
export interface MockServer {
|
|
42
|
+
/** Handle a request against the mock routes. */
|
|
43
|
+
fetch(request: Request): Promise<Response>;
|
|
44
|
+
/** The generated mock routes for inspection. */
|
|
45
|
+
readonly mockRoutes: ReadonlyArray<{
|
|
46
|
+
method: string;
|
|
47
|
+
path: string;
|
|
48
|
+
}>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a mock server from a Nifra app's route definitions. For each route
|
|
52
|
+
* with a `schema.response`, generates a handler returning fake data that
|
|
53
|
+
* matches the response schema structure. Routes without response schemas
|
|
54
|
+
* return `{}`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function createMockServer(app: MockableApp, options?: MockServerOptions | undefined): MockServer;
|
|
57
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAsBH,qBAAa,0BAA2B,SAAQ,KAAK;IAEjD,QAAQ,CAAC,OAAO,EAAE,MAAM;gBAAf,OAAO,EAAE,MAAM,EACxB,OAAO,EAAE,MAAM;CAKlB;AAwDD;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,OAAO,EACf,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,EAC9B,GAAG,CAAC,EAAE,CAAC,MAAM,MAAM,CAAC,GAAG,SAAS,GAC/B,OAAO,CAmJT;AAMD,sDAAsD;AACtD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,MAAM,CAAC,EACZ;QACE,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAC5B,GACD,SAAS,CAAA;CACd;AAED,qDAAqD;AACrD,MAAM,WAAW,WAAW;IAC1B,MAAM,IAAI,SAAS,aAAa,EAAE,CAAA;CACnC;AAED,MAAM,WAAW,iBAAiB;IAChC,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IAClC,oCAAoC;IACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;CACvC;AAED,MAAM,WAAW,UAAU;IACzB,gDAAgD;IAChD,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC1C,gDAAgD;IAChD,QAAQ,CAAC,UAAU,EAAE,aAAa,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CACrE;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,WAAW,EAChB,OAAO,CAAC,EAAE,iBAAiB,GAAG,SAAS,GACtC,UAAU,CAgEZ"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/mock` — contract-based mock server.
|
|
3
|
+
*
|
|
4
|
+
* Reads a Nifra app's registered routes and their Standard Schema / JSON Schema
|
|
5
|
+
* response definitions, then generates fake responses from supported JSON Schema keywords. Use it
|
|
6
|
+
* for frontend-first development, agent testing, or CI smoke tests.
|
|
7
|
+
*/
|
|
8
|
+
import { reflectRoutes, reflectSchema } from "@nifrajs/core/reflection";
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Seeded PRNG (deterministic mocks)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
function seededRandom(seed) {
|
|
13
|
+
let s = seed;
|
|
14
|
+
return () => {
|
|
15
|
+
s = (s * 1664525 + 1013904223) & 0x7fffffff;
|
|
16
|
+
// Divide by 2^31 (not 2^31 - 1) so the result stays strictly below 1 and never
|
|
17
|
+
// produces an out-of-range index when scaled by an array length.
|
|
18
|
+
return s / 0x80000000;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Clamp a `rand() * length` index so an inclusive-range rng cannot index past the end. */
|
|
22
|
+
const randomIndex = (rand, length) => Math.min(Math.floor(rand() * length), length - 1);
|
|
23
|
+
export class UnsupportedMockSchemaError extends Error {
|
|
24
|
+
keyword;
|
|
25
|
+
constructor(keyword, message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.keyword = keyword;
|
|
28
|
+
this.name = "UnsupportedMockSchemaError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function numericBounds(schema) {
|
|
32
|
+
const minimum = typeof schema.minimum === "number" ? schema.minimum : 0;
|
|
33
|
+
const maximum = typeof schema.maximum === "number" ? schema.maximum : 100;
|
|
34
|
+
const exclusiveMinimum = typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : undefined;
|
|
35
|
+
const exclusiveMaximum = typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : undefined;
|
|
36
|
+
const min = exclusiveMinimum === undefined ? minimum : Math.max(minimum, exclusiveMinimum + Number.EPSILON);
|
|
37
|
+
const max = exclusiveMaximum === undefined ? maximum : Math.min(maximum, exclusiveMaximum - Number.EPSILON);
|
|
38
|
+
if (min > max)
|
|
39
|
+
throw new UnsupportedMockSchemaError("minimum", "Numeric schema has no satisfiable range");
|
|
40
|
+
return { min, max };
|
|
41
|
+
}
|
|
42
|
+
function constrainedString(schema, initial, fieldName) {
|
|
43
|
+
const minLength = typeof schema.minLength === "number" ? schema.minLength : 0;
|
|
44
|
+
const maxLength = typeof schema.maxLength === "number" ? schema.maxLength : Number.POSITIVE_INFINITY;
|
|
45
|
+
if (minLength > maxLength) {
|
|
46
|
+
throw new UnsupportedMockSchemaError("minLength", "String schema has no satisfiable length");
|
|
47
|
+
}
|
|
48
|
+
const candidates = [
|
|
49
|
+
initial,
|
|
50
|
+
fieldName ?? "mock",
|
|
51
|
+
"mock_value",
|
|
52
|
+
"test",
|
|
53
|
+
"abc",
|
|
54
|
+
"123",
|
|
55
|
+
"A1",
|
|
56
|
+
"https://example.com",
|
|
57
|
+
"test@example.com",
|
|
58
|
+
];
|
|
59
|
+
const pattern = typeof schema.pattern === "string" ? new RegExp(schema.pattern) : undefined;
|
|
60
|
+
for (const candidate of candidates) {
|
|
61
|
+
const padded = candidate.length < minLength ? candidate.padEnd(minLength, "a") : candidate;
|
|
62
|
+
const value = padded.slice(0, maxLength);
|
|
63
|
+
if (value.length >= minLength && (pattern === undefined || pattern.test(value)))
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
throw new UnsupportedMockSchemaError("pattern", `Cannot generate a value for pattern ${JSON.stringify(schema.pattern)}`);
|
|
67
|
+
}
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Schema introspection → mock value generation
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
/**
|
|
72
|
+
* Generate a mock value from a schema object. Inspects JSON Schema properties
|
|
73
|
+
* (`type`, `properties`, `items`, `enum`, `format`) that TypeBox / NifraSchema
|
|
74
|
+
* objects carry directly. Unsupported constraints fail closed with
|
|
75
|
+
* {@link UnsupportedMockSchemaError} rather than returning a known-invalid response.
|
|
76
|
+
*
|
|
77
|
+
* @param schema A JSON-Schema-shaped object (TypeBox, NifraSchema, or raw JSON Schema)
|
|
78
|
+
* @param fieldName Optional field name hint for generating contextual string values
|
|
79
|
+
* @param rng Optional seeded random function for deterministic output
|
|
80
|
+
*/
|
|
81
|
+
export function generateMockValue(schema, fieldName, rng) {
|
|
82
|
+
const rand = rng ?? Math.random;
|
|
83
|
+
const reflected = reflectSchema(schema).jsonSchema;
|
|
84
|
+
if (reflected === true || reflected === undefined)
|
|
85
|
+
return {};
|
|
86
|
+
if (reflected === false) {
|
|
87
|
+
throw new UnsupportedMockSchemaError("false", "Boolean false schema has no valid value");
|
|
88
|
+
}
|
|
89
|
+
const raw = reflected;
|
|
90
|
+
if ("const" in raw)
|
|
91
|
+
return raw.const;
|
|
92
|
+
if (raw.$ref !== undefined) {
|
|
93
|
+
throw new UnsupportedMockSchemaError("$ref", "Resolve JSON Schema references before generating mocks");
|
|
94
|
+
}
|
|
95
|
+
if (raw.not !== undefined) {
|
|
96
|
+
throw new UnsupportedMockSchemaError("not", "JSON Schema `not` generation is unsupported");
|
|
97
|
+
}
|
|
98
|
+
if (raw.oneOf !== undefined) {
|
|
99
|
+
throw new UnsupportedMockSchemaError("oneOf", "Exclusive union generation requires a schema validator");
|
|
100
|
+
}
|
|
101
|
+
const alternatives = raw.anyOf;
|
|
102
|
+
if (Array.isArray(alternatives) && alternatives.length > 0) {
|
|
103
|
+
return generateMockValue(alternatives[randomIndex(rand, alternatives.length)], fieldName, rand);
|
|
104
|
+
}
|
|
105
|
+
const intersections = raw.allOf;
|
|
106
|
+
if (Array.isArray(intersections) && intersections.length > 0) {
|
|
107
|
+
const values = intersections.map((part) => generateMockValue(part, fieldName, rand));
|
|
108
|
+
if (values.every((value) => value !== null && typeof value === "object" && !Array.isArray(value))) {
|
|
109
|
+
return Object.assign({}, ...values);
|
|
110
|
+
}
|
|
111
|
+
const first = values[0];
|
|
112
|
+
if (values.every((value) => Object.is(value, first)))
|
|
113
|
+
return first;
|
|
114
|
+
throw new UnsupportedMockSchemaError("allOf", "Could not prove that a scalar satisfies every intersection branch");
|
|
115
|
+
}
|
|
116
|
+
// Enum — pick the first value (deterministic) or random
|
|
117
|
+
const enumValues = raw.enum;
|
|
118
|
+
if (enumValues && Array.isArray(enumValues) && enumValues.length > 0) {
|
|
119
|
+
return enumValues[randomIndex(rand, enumValues.length)];
|
|
120
|
+
}
|
|
121
|
+
const declaredType = raw.type;
|
|
122
|
+
const type = Array.isArray(declaredType)
|
|
123
|
+
? (declaredType.find((candidate) => candidate !== "null") ?? "null")
|
|
124
|
+
: declaredType;
|
|
125
|
+
switch (type) {
|
|
126
|
+
case "string": {
|
|
127
|
+
const format = raw.format;
|
|
128
|
+
switch (format) {
|
|
129
|
+
case "email":
|
|
130
|
+
return constrainedString(raw, `mock@${fieldName ?? "example"}.com`, fieldName);
|
|
131
|
+
case "uri":
|
|
132
|
+
case "url":
|
|
133
|
+
return constrainedString(raw, `https://${fieldName ?? "example"}.mock.dev`, fieldName);
|
|
134
|
+
case "date":
|
|
135
|
+
return constrainedString(raw, "2025-01-15", fieldName);
|
|
136
|
+
case "date-time":
|
|
137
|
+
return constrainedString(raw, "2025-01-15T12:00:00.000Z", fieldName);
|
|
138
|
+
case "uuid":
|
|
139
|
+
return constrainedString(raw, "00000000-0000-4000-8000-000000000001", fieldName);
|
|
140
|
+
default:
|
|
141
|
+
return constrainedString(raw, `mock_${fieldName ?? "value"}`, fieldName);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
case "number": {
|
|
145
|
+
const { min, max } = numericBounds(raw);
|
|
146
|
+
const multipleOf = typeof raw.multipleOf === "number" ? raw.multipleOf : undefined;
|
|
147
|
+
if (multipleOf !== undefined && multipleOf <= 0) {
|
|
148
|
+
throw new UnsupportedMockSchemaError("multipleOf", "multipleOf must be positive");
|
|
149
|
+
}
|
|
150
|
+
if (multipleOf !== undefined) {
|
|
151
|
+
const first = Math.ceil(min / multipleOf) * multipleOf;
|
|
152
|
+
const last = Math.floor(max / multipleOf) * multipleOf;
|
|
153
|
+
if (first > last)
|
|
154
|
+
throw new UnsupportedMockSchemaError("multipleOf", "No multiple exists in range");
|
|
155
|
+
const steps = Math.floor((last - first) / multipleOf);
|
|
156
|
+
return first + Math.floor(rand() * (steps + 1)) * multipleOf;
|
|
157
|
+
}
|
|
158
|
+
return Math.round((min + rand() * (max - min)) * 100) / 100;
|
|
159
|
+
}
|
|
160
|
+
case "integer": {
|
|
161
|
+
const { min, max } = numericBounds(raw);
|
|
162
|
+
const multipleOf = typeof raw.multipleOf === "number" ? raw.multipleOf : 1;
|
|
163
|
+
const first = Math.ceil(min / multipleOf) * multipleOf;
|
|
164
|
+
const last = Math.floor(max / multipleOf) * multipleOf;
|
|
165
|
+
if (first > last)
|
|
166
|
+
throw new UnsupportedMockSchemaError("integer", "No integer exists in range");
|
|
167
|
+
const steps = Math.floor((last - first) / multipleOf);
|
|
168
|
+
return first + Math.floor(rand() * (steps + 1)) * multipleOf;
|
|
169
|
+
}
|
|
170
|
+
case "boolean":
|
|
171
|
+
return true;
|
|
172
|
+
case "null":
|
|
173
|
+
return null;
|
|
174
|
+
case "array": {
|
|
175
|
+
const items = raw.items;
|
|
176
|
+
if (!items)
|
|
177
|
+
return [];
|
|
178
|
+
const minItems = typeof raw.minItems === "number" ? raw.minItems : 1;
|
|
179
|
+
const maxItems = typeof raw.maxItems === "number" ? raw.maxItems : Math.max(minItems, 3);
|
|
180
|
+
if (minItems > maxItems) {
|
|
181
|
+
throw new UnsupportedMockSchemaError("minItems", "Array schema has no satisfiable length");
|
|
182
|
+
}
|
|
183
|
+
const count = minItems + Math.floor(rand() * (maxItems - minItems + 1));
|
|
184
|
+
const result = [];
|
|
185
|
+
for (let i = 0; i < count; i++) {
|
|
186
|
+
result.push(generateMockValue(items, `${fieldName ?? "item"}_${i}`, rand));
|
|
187
|
+
}
|
|
188
|
+
if (raw.uniqueItems === true &&
|
|
189
|
+
new Set(result.map((value) => JSON.stringify(value))).size !== result.length) {
|
|
190
|
+
throw new UnsupportedMockSchemaError("uniqueItems", "Could not generate enough unique items");
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
default: {
|
|
195
|
+
const properties = raw.properties;
|
|
196
|
+
if (!properties || typeof properties !== "object")
|
|
197
|
+
return {};
|
|
198
|
+
const result = {};
|
|
199
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
200
|
+
result[key] = generateMockValue(propSchema, key, rand);
|
|
201
|
+
}
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Create a mock server from a Nifra app's route definitions. For each route
|
|
208
|
+
* with a `schema.response`, generates a handler returning fake data that
|
|
209
|
+
* matches the response schema structure. Routes without response schemas
|
|
210
|
+
* return `{}`.
|
|
211
|
+
*/
|
|
212
|
+
export function createMockServer(app, options) {
|
|
213
|
+
const seed = options?.seed ?? 42;
|
|
214
|
+
const verbose = options?.verbose ?? false;
|
|
215
|
+
const routes = reflectRoutes(app);
|
|
216
|
+
// Pre-generate mock responses for each route
|
|
217
|
+
const mockMap = new Map();
|
|
218
|
+
const mockRoutes = [];
|
|
219
|
+
for (const route of routes) {
|
|
220
|
+
const key = `${route.method.toUpperCase()} ${route.path}`;
|
|
221
|
+
const rng = seededRandom(seed);
|
|
222
|
+
const responseSchema = route.schema?.response?.jsonSchema;
|
|
223
|
+
const mockValue = responseSchema ? generateMockValue(responseSchema, undefined, rng) : {};
|
|
224
|
+
mockMap.set(key, mockValue);
|
|
225
|
+
mockRoutes.push({ method: route.method.toUpperCase(), path: route.path });
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
mockRoutes,
|
|
229
|
+
async fetch(request) {
|
|
230
|
+
const url = new URL(request.url);
|
|
231
|
+
const method = request.method.toUpperCase();
|
|
232
|
+
const pathname = url.pathname;
|
|
233
|
+
// Exact match first
|
|
234
|
+
const exactKey = `${method} ${pathname}`;
|
|
235
|
+
if (mockMap.has(exactKey)) {
|
|
236
|
+
if (verbose)
|
|
237
|
+
console.log(`[mock] ${exactKey} → 200`);
|
|
238
|
+
return new Response(JSON.stringify(mockMap.get(exactKey)), {
|
|
239
|
+
status: 200,
|
|
240
|
+
headers: {
|
|
241
|
+
"content-type": "application/json",
|
|
242
|
+
"x-nifra-mock": "true",
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// Try matching parameterized routes (e.g. /users/:id → /users/123)
|
|
247
|
+
for (const [key, value] of mockMap) {
|
|
248
|
+
const [routeMethod, routePath] = key.split(" ", 2);
|
|
249
|
+
if (routeMethod !== method || !routePath)
|
|
250
|
+
continue;
|
|
251
|
+
if (matchParameterizedPath(routePath, pathname)) {
|
|
252
|
+
if (verbose)
|
|
253
|
+
console.log(`[mock] ${method} ${pathname} → ${routePath} → 200`);
|
|
254
|
+
return new Response(JSON.stringify(value), {
|
|
255
|
+
status: 200,
|
|
256
|
+
headers: {
|
|
257
|
+
"content-type": "application/json",
|
|
258
|
+
"x-nifra-mock": "true",
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (verbose)
|
|
264
|
+
console.log(`[mock] ${method} ${pathname} → 404`);
|
|
265
|
+
return new Response(JSON.stringify({ error: "Not Found", mock: true }), {
|
|
266
|
+
status: 404,
|
|
267
|
+
headers: { "content-type": "application/json", "x-nifra-mock": "true" },
|
|
268
|
+
});
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Match a parameterized route pattern (e.g. `/users/:id`) against a concrete pathname.
|
|
274
|
+
* Supports non-empty `:param` segments and Nifra's trailing `*wildcard` segment.
|
|
275
|
+
*/
|
|
276
|
+
function matchParameterizedPath(pattern, pathname) {
|
|
277
|
+
const patternParts = pattern.split("/");
|
|
278
|
+
const pathParts = pathname.split("/");
|
|
279
|
+
const wildcardIndex = patternParts.findIndex((part) => part.startsWith("*"));
|
|
280
|
+
if (wildcardIndex === -1 && patternParts.length !== pathParts.length)
|
|
281
|
+
return false;
|
|
282
|
+
if (wildcardIndex !== -1 && wildcardIndex !== patternParts.length - 1)
|
|
283
|
+
return false;
|
|
284
|
+
if (wildcardIndex !== -1 && pathParts.length < wildcardIndex + 1)
|
|
285
|
+
return false;
|
|
286
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
287
|
+
const pp = patternParts[i];
|
|
288
|
+
const actual = pathParts[i];
|
|
289
|
+
if (pp === undefined || actual === undefined)
|
|
290
|
+
return false;
|
|
291
|
+
if (pp.startsWith("*"))
|
|
292
|
+
return true;
|
|
293
|
+
if (pp.startsWith(":")) {
|
|
294
|
+
if (actual.length === 0)
|
|
295
|
+
return false;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (pp !== actual)
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nifrajs/mock",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Contract-based mock server — reads route schemas and generates fake responses.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"bun": "./src/index.ts",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"src"
|
|
19
|
+
],
|
|
20
|
+
"sideEffects": false,
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@nifrajs/core": "1.4.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@nifrajs/mock` — contract-based mock server.
|
|
3
|
+
*
|
|
4
|
+
* Reads a Nifra app's registered routes and their Standard Schema / JSON Schema
|
|
5
|
+
* response definitions, then generates fake responses from supported JSON Schema keywords. Use it
|
|
6
|
+
* for frontend-first development, agent testing, or CI smoke tests.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { reflectRoutes, reflectSchema } from "@nifrajs/core/reflection"
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Seeded PRNG (deterministic mocks)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
function seededRandom(seed: number): () => number {
|
|
16
|
+
let s = seed
|
|
17
|
+
return () => {
|
|
18
|
+
s = (s * 1664525 + 1013904223) & 0x7fffffff
|
|
19
|
+
// Divide by 2^31 (not 2^31 - 1) so the result stays strictly below 1 and never
|
|
20
|
+
// produces an out-of-range index when scaled by an array length.
|
|
21
|
+
return s / 0x80000000
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Clamp a `rand() * length` index so an inclusive-range rng cannot index past the end. */
|
|
26
|
+
const randomIndex = (rand: () => number, length: number): number =>
|
|
27
|
+
Math.min(Math.floor(rand() * length), length - 1)
|
|
28
|
+
|
|
29
|
+
export class UnsupportedMockSchemaError extends Error {
|
|
30
|
+
constructor(
|
|
31
|
+
readonly keyword: string,
|
|
32
|
+
message: string,
|
|
33
|
+
) {
|
|
34
|
+
super(message)
|
|
35
|
+
this.name = "UnsupportedMockSchemaError"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function numericBounds(schema: Record<string, unknown>): { min: number; max: number } {
|
|
40
|
+
const minimum = typeof schema.minimum === "number" ? schema.minimum : 0
|
|
41
|
+
const maximum = typeof schema.maximum === "number" ? schema.maximum : 100
|
|
42
|
+
const exclusiveMinimum =
|
|
43
|
+
typeof schema.exclusiveMinimum === "number" ? schema.exclusiveMinimum : undefined
|
|
44
|
+
const exclusiveMaximum =
|
|
45
|
+
typeof schema.exclusiveMaximum === "number" ? schema.exclusiveMaximum : undefined
|
|
46
|
+
const min =
|
|
47
|
+
exclusiveMinimum === undefined ? minimum : Math.max(minimum, exclusiveMinimum + Number.EPSILON)
|
|
48
|
+
const max =
|
|
49
|
+
exclusiveMaximum === undefined ? maximum : Math.min(maximum, exclusiveMaximum - Number.EPSILON)
|
|
50
|
+
if (min > max)
|
|
51
|
+
throw new UnsupportedMockSchemaError("minimum", "Numeric schema has no satisfiable range")
|
|
52
|
+
return { min, max }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function constrainedString(
|
|
56
|
+
schema: Record<string, unknown>,
|
|
57
|
+
initial: string,
|
|
58
|
+
fieldName: string | undefined,
|
|
59
|
+
): string {
|
|
60
|
+
const minLength = typeof schema.minLength === "number" ? schema.minLength : 0
|
|
61
|
+
const maxLength =
|
|
62
|
+
typeof schema.maxLength === "number" ? schema.maxLength : Number.POSITIVE_INFINITY
|
|
63
|
+
if (minLength > maxLength) {
|
|
64
|
+
throw new UnsupportedMockSchemaError("minLength", "String schema has no satisfiable length")
|
|
65
|
+
}
|
|
66
|
+
const candidates = [
|
|
67
|
+
initial,
|
|
68
|
+
fieldName ?? "mock",
|
|
69
|
+
"mock_value",
|
|
70
|
+
"test",
|
|
71
|
+
"abc",
|
|
72
|
+
"123",
|
|
73
|
+
"A1",
|
|
74
|
+
"https://example.com",
|
|
75
|
+
"test@example.com",
|
|
76
|
+
]
|
|
77
|
+
const pattern = typeof schema.pattern === "string" ? new RegExp(schema.pattern) : undefined
|
|
78
|
+
for (const candidate of candidates) {
|
|
79
|
+
const padded = candidate.length < minLength ? candidate.padEnd(minLength, "a") : candidate
|
|
80
|
+
const value = padded.slice(0, maxLength)
|
|
81
|
+
if (value.length >= minLength && (pattern === undefined || pattern.test(value))) return value
|
|
82
|
+
}
|
|
83
|
+
throw new UnsupportedMockSchemaError(
|
|
84
|
+
"pattern",
|
|
85
|
+
`Cannot generate a value for pattern ${JSON.stringify(schema.pattern)}`,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Schema introspection → mock value generation
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Generate a mock value from a schema object. Inspects JSON Schema properties
|
|
95
|
+
* (`type`, `properties`, `items`, `enum`, `format`) that TypeBox / NifraSchema
|
|
96
|
+
* objects carry directly. Unsupported constraints fail closed with
|
|
97
|
+
* {@link UnsupportedMockSchemaError} rather than returning a known-invalid response.
|
|
98
|
+
*
|
|
99
|
+
* @param schema A JSON-Schema-shaped object (TypeBox, NifraSchema, or raw JSON Schema)
|
|
100
|
+
* @param fieldName Optional field name hint for generating contextual string values
|
|
101
|
+
* @param rng Optional seeded random function for deterministic output
|
|
102
|
+
*/
|
|
103
|
+
export function generateMockValue(
|
|
104
|
+
schema: unknown,
|
|
105
|
+
fieldName?: string | undefined,
|
|
106
|
+
rng?: (() => number) | undefined,
|
|
107
|
+
): unknown {
|
|
108
|
+
const rand = rng ?? Math.random
|
|
109
|
+
|
|
110
|
+
const reflected = reflectSchema(schema).jsonSchema
|
|
111
|
+
if (reflected === true || reflected === undefined) return {}
|
|
112
|
+
if (reflected === false) {
|
|
113
|
+
throw new UnsupportedMockSchemaError("false", "Boolean false schema has no valid value")
|
|
114
|
+
}
|
|
115
|
+
const raw = reflected
|
|
116
|
+
|
|
117
|
+
if ("const" in raw) return raw.const
|
|
118
|
+
if (raw.$ref !== undefined) {
|
|
119
|
+
throw new UnsupportedMockSchemaError(
|
|
120
|
+
"$ref",
|
|
121
|
+
"Resolve JSON Schema references before generating mocks",
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
if (raw.not !== undefined) {
|
|
125
|
+
throw new UnsupportedMockSchemaError("not", "JSON Schema `not` generation is unsupported")
|
|
126
|
+
}
|
|
127
|
+
if (raw.oneOf !== undefined) {
|
|
128
|
+
throw new UnsupportedMockSchemaError(
|
|
129
|
+
"oneOf",
|
|
130
|
+
"Exclusive union generation requires a schema validator",
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
const alternatives = raw.anyOf as readonly unknown[] | undefined
|
|
134
|
+
if (Array.isArray(alternatives) && alternatives.length > 0) {
|
|
135
|
+
return generateMockValue(alternatives[randomIndex(rand, alternatives.length)], fieldName, rand)
|
|
136
|
+
}
|
|
137
|
+
const intersections = raw.allOf as readonly unknown[] | undefined
|
|
138
|
+
if (Array.isArray(intersections) && intersections.length > 0) {
|
|
139
|
+
const values = intersections.map((part) => generateMockValue(part, fieldName, rand))
|
|
140
|
+
if (
|
|
141
|
+
values.every((value) => value !== null && typeof value === "object" && !Array.isArray(value))
|
|
142
|
+
) {
|
|
143
|
+
return Object.assign({}, ...values)
|
|
144
|
+
}
|
|
145
|
+
const first = values[0]
|
|
146
|
+
if (values.every((value) => Object.is(value, first))) return first
|
|
147
|
+
throw new UnsupportedMockSchemaError(
|
|
148
|
+
"allOf",
|
|
149
|
+
"Could not prove that a scalar satisfies every intersection branch",
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Enum — pick the first value (deterministic) or random
|
|
154
|
+
const enumValues = raw.enum as readonly unknown[] | undefined
|
|
155
|
+
if (enumValues && Array.isArray(enumValues) && enumValues.length > 0) {
|
|
156
|
+
return enumValues[randomIndex(rand, enumValues.length)]
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const declaredType = raw.type as string | readonly string[] | undefined
|
|
160
|
+
const type = Array.isArray(declaredType)
|
|
161
|
+
? (declaredType.find((candidate) => candidate !== "null") ?? "null")
|
|
162
|
+
: declaredType
|
|
163
|
+
|
|
164
|
+
switch (type) {
|
|
165
|
+
case "string": {
|
|
166
|
+
const format = raw.format as string | undefined
|
|
167
|
+
switch (format) {
|
|
168
|
+
case "email":
|
|
169
|
+
return constrainedString(raw, `mock@${fieldName ?? "example"}.com`, fieldName)
|
|
170
|
+
case "uri":
|
|
171
|
+
case "url":
|
|
172
|
+
return constrainedString(raw, `https://${fieldName ?? "example"}.mock.dev`, fieldName)
|
|
173
|
+
case "date":
|
|
174
|
+
return constrainedString(raw, "2025-01-15", fieldName)
|
|
175
|
+
case "date-time":
|
|
176
|
+
return constrainedString(raw, "2025-01-15T12:00:00.000Z", fieldName)
|
|
177
|
+
case "uuid":
|
|
178
|
+
return constrainedString(raw, "00000000-0000-4000-8000-000000000001", fieldName)
|
|
179
|
+
default:
|
|
180
|
+
return constrainedString(raw, `mock_${fieldName ?? "value"}`, fieldName)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
case "number": {
|
|
185
|
+
const { min, max } = numericBounds(raw)
|
|
186
|
+
const multipleOf = typeof raw.multipleOf === "number" ? raw.multipleOf : undefined
|
|
187
|
+
if (multipleOf !== undefined && multipleOf <= 0) {
|
|
188
|
+
throw new UnsupportedMockSchemaError("multipleOf", "multipleOf must be positive")
|
|
189
|
+
}
|
|
190
|
+
if (multipleOf !== undefined) {
|
|
191
|
+
const first = Math.ceil(min / multipleOf) * multipleOf
|
|
192
|
+
const last = Math.floor(max / multipleOf) * multipleOf
|
|
193
|
+
if (first > last)
|
|
194
|
+
throw new UnsupportedMockSchemaError("multipleOf", "No multiple exists in range")
|
|
195
|
+
const steps = Math.floor((last - first) / multipleOf)
|
|
196
|
+
return first + Math.floor(rand() * (steps + 1)) * multipleOf
|
|
197
|
+
}
|
|
198
|
+
return Math.round((min + rand() * (max - min)) * 100) / 100
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
case "integer": {
|
|
202
|
+
const { min, max } = numericBounds(raw)
|
|
203
|
+
const multipleOf = typeof raw.multipleOf === "number" ? raw.multipleOf : 1
|
|
204
|
+
const first = Math.ceil(min / multipleOf) * multipleOf
|
|
205
|
+
const last = Math.floor(max / multipleOf) * multipleOf
|
|
206
|
+
if (first > last)
|
|
207
|
+
throw new UnsupportedMockSchemaError("integer", "No integer exists in range")
|
|
208
|
+
const steps = Math.floor((last - first) / multipleOf)
|
|
209
|
+
return first + Math.floor(rand() * (steps + 1)) * multipleOf
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
case "boolean":
|
|
213
|
+
return true
|
|
214
|
+
|
|
215
|
+
case "null":
|
|
216
|
+
return null
|
|
217
|
+
|
|
218
|
+
case "array": {
|
|
219
|
+
const items = raw.items as Record<string, unknown> | undefined
|
|
220
|
+
if (!items) return []
|
|
221
|
+
const minItems = typeof raw.minItems === "number" ? raw.minItems : 1
|
|
222
|
+
const maxItems = typeof raw.maxItems === "number" ? raw.maxItems : Math.max(minItems, 3)
|
|
223
|
+
if (minItems > maxItems) {
|
|
224
|
+
throw new UnsupportedMockSchemaError("minItems", "Array schema has no satisfiable length")
|
|
225
|
+
}
|
|
226
|
+
const count = minItems + Math.floor(rand() * (maxItems - minItems + 1))
|
|
227
|
+
const result: unknown[] = []
|
|
228
|
+
for (let i = 0; i < count; i++) {
|
|
229
|
+
result.push(generateMockValue(items, `${fieldName ?? "item"}_${i}`, rand))
|
|
230
|
+
}
|
|
231
|
+
if (
|
|
232
|
+
raw.uniqueItems === true &&
|
|
233
|
+
new Set(result.map((value) => JSON.stringify(value))).size !== result.length
|
|
234
|
+
) {
|
|
235
|
+
throw new UnsupportedMockSchemaError(
|
|
236
|
+
"uniqueItems",
|
|
237
|
+
"Could not generate enough unique items",
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
return result
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
default: {
|
|
244
|
+
const properties = raw.properties as Record<string, Record<string, unknown>> | undefined
|
|
245
|
+
if (!properties || typeof properties !== "object") return {}
|
|
246
|
+
|
|
247
|
+
const result: Record<string, unknown> = {}
|
|
248
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
249
|
+
result[key] = generateMockValue(propSchema, key, rand)
|
|
250
|
+
}
|
|
251
|
+
return result
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// Mock server
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
|
|
260
|
+
/** Minimal route shape returned by `app.routes()`. */
|
|
261
|
+
export interface MockableRoute {
|
|
262
|
+
readonly method: string
|
|
263
|
+
readonly path: string
|
|
264
|
+
readonly schema?:
|
|
265
|
+
| {
|
|
266
|
+
readonly response?: unknown
|
|
267
|
+
}
|
|
268
|
+
| undefined
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** App shape — anything with a `routes()` method. */
|
|
272
|
+
export interface MockableApp {
|
|
273
|
+
routes(): readonly MockableRoute[]
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export interface MockServerOptions {
|
|
277
|
+
/** Seed for deterministic mock generation. */
|
|
278
|
+
readonly seed?: number | undefined
|
|
279
|
+
/** Log mock requests to console. */
|
|
280
|
+
readonly verbose?: boolean | undefined
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export interface MockServer {
|
|
284
|
+
/** Handle a request against the mock routes. */
|
|
285
|
+
fetch(request: Request): Promise<Response>
|
|
286
|
+
/** The generated mock routes for inspection. */
|
|
287
|
+
readonly mockRoutes: ReadonlyArray<{ method: string; path: string }>
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Create a mock server from a Nifra app's route definitions. For each route
|
|
292
|
+
* with a `schema.response`, generates a handler returning fake data that
|
|
293
|
+
* matches the response schema structure. Routes without response schemas
|
|
294
|
+
* return `{}`.
|
|
295
|
+
*/
|
|
296
|
+
export function createMockServer(
|
|
297
|
+
app: MockableApp,
|
|
298
|
+
options?: MockServerOptions | undefined,
|
|
299
|
+
): MockServer {
|
|
300
|
+
const seed = options?.seed ?? 42
|
|
301
|
+
const verbose = options?.verbose ?? false
|
|
302
|
+
|
|
303
|
+
const routes = reflectRoutes(app)
|
|
304
|
+
|
|
305
|
+
// Pre-generate mock responses for each route
|
|
306
|
+
const mockMap = new Map<string, unknown>()
|
|
307
|
+
const mockRoutes: Array<{ method: string; path: string }> = []
|
|
308
|
+
|
|
309
|
+
for (const route of routes) {
|
|
310
|
+
const key = `${route.method.toUpperCase()} ${route.path}`
|
|
311
|
+
const rng = seededRandom(seed)
|
|
312
|
+
const responseSchema = route.schema?.response?.jsonSchema
|
|
313
|
+
const mockValue = responseSchema ? generateMockValue(responseSchema, undefined, rng) : {}
|
|
314
|
+
mockMap.set(key, mockValue)
|
|
315
|
+
mockRoutes.push({ method: route.method.toUpperCase(), path: route.path })
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
mockRoutes,
|
|
320
|
+
|
|
321
|
+
async fetch(request: Request): Promise<Response> {
|
|
322
|
+
const url = new URL(request.url)
|
|
323
|
+
const method = request.method.toUpperCase()
|
|
324
|
+
const pathname = url.pathname
|
|
325
|
+
|
|
326
|
+
// Exact match first
|
|
327
|
+
const exactKey = `${method} ${pathname}`
|
|
328
|
+
if (mockMap.has(exactKey)) {
|
|
329
|
+
if (verbose) console.log(`[mock] ${exactKey} → 200`)
|
|
330
|
+
return new Response(JSON.stringify(mockMap.get(exactKey)), {
|
|
331
|
+
status: 200,
|
|
332
|
+
headers: {
|
|
333
|
+
"content-type": "application/json",
|
|
334
|
+
"x-nifra-mock": "true",
|
|
335
|
+
},
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Try matching parameterized routes (e.g. /users/:id → /users/123)
|
|
340
|
+
for (const [key, value] of mockMap) {
|
|
341
|
+
const [routeMethod, routePath] = key.split(" ", 2)
|
|
342
|
+
if (routeMethod !== method || !routePath) continue
|
|
343
|
+
|
|
344
|
+
if (matchParameterizedPath(routePath, pathname)) {
|
|
345
|
+
if (verbose) console.log(`[mock] ${method} ${pathname} → ${routePath} → 200`)
|
|
346
|
+
return new Response(JSON.stringify(value), {
|
|
347
|
+
status: 200,
|
|
348
|
+
headers: {
|
|
349
|
+
"content-type": "application/json",
|
|
350
|
+
"x-nifra-mock": "true",
|
|
351
|
+
},
|
|
352
|
+
})
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (verbose) console.log(`[mock] ${method} ${pathname} → 404`)
|
|
357
|
+
return new Response(JSON.stringify({ error: "Not Found", mock: true }), {
|
|
358
|
+
status: 404,
|
|
359
|
+
headers: { "content-type": "application/json", "x-nifra-mock": "true" },
|
|
360
|
+
})
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Match a parameterized route pattern (e.g. `/users/:id`) against a concrete pathname.
|
|
367
|
+
* Supports non-empty `:param` segments and Nifra's trailing `*wildcard` segment.
|
|
368
|
+
*/
|
|
369
|
+
function matchParameterizedPath(pattern: string, pathname: string): boolean {
|
|
370
|
+
const patternParts = pattern.split("/")
|
|
371
|
+
const pathParts = pathname.split("/")
|
|
372
|
+
|
|
373
|
+
const wildcardIndex = patternParts.findIndex((part) => part.startsWith("*"))
|
|
374
|
+
if (wildcardIndex === -1 && patternParts.length !== pathParts.length) return false
|
|
375
|
+
if (wildcardIndex !== -1 && wildcardIndex !== patternParts.length - 1) return false
|
|
376
|
+
if (wildcardIndex !== -1 && pathParts.length < wildcardIndex + 1) return false
|
|
377
|
+
|
|
378
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
379
|
+
const pp = patternParts[i]
|
|
380
|
+
const actual = pathParts[i]
|
|
381
|
+
if (pp === undefined || actual === undefined) return false
|
|
382
|
+
if (pp.startsWith("*")) return true
|
|
383
|
+
if (pp.startsWith(":")) {
|
|
384
|
+
if (actual.length === 0) return false
|
|
385
|
+
continue
|
|
386
|
+
}
|
|
387
|
+
if (pp !== actual) return false
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return true
|
|
391
|
+
}
|