@specbook/core 0.3.0 → 0.5.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/package.json +12 -1
- package/src/config.ts +1 -1
- package/src/faker-runtime.ts +88 -0
- package/src/faker.ts +6 -78
- package/src/index.ts +11 -2
- package/src/reporters/index.ts +1 -1
- package/src/reporters/list.ts +1 -1
- package/src/reporters/trace.ts +1 -1
- package/src/runner/run.ts +2 -1
- package/src/runner/transport.ts +37 -2
- package/src/template.ts +50 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specbook/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The runner, mock engine, template engine and shared proxy behind Specbook: Storybook for APIs.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"specbook",
|
|
@@ -49,6 +49,17 @@
|
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"uvu": "0.5.6"
|
|
51
51
|
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@faker-js/faker": "^9.3.0"
|
|
54
|
+
},
|
|
55
|
+
"peerDependenciesMeta": {
|
|
56
|
+
"@faker-js/faker": {
|
|
57
|
+
"optional": true
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"@faker-js/faker": "9.3.0"
|
|
62
|
+
},
|
|
52
63
|
"files": [
|
|
53
64
|
"src",
|
|
54
65
|
"!**/*.test.*"
|
package/src/config.ts
CHANGED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// Locale-aware faker proxy. `@faker-js/faker` is a peer dependency of the user's project;
|
|
2
|
+
// the runtime (CLI / UI) installs the concrete instance for the selected locale before a run.
|
|
3
|
+
|
|
4
|
+
type FakerLike = Record<string, any>
|
|
5
|
+
|
|
6
|
+
// Held on globalThis so the CLI's bundled copy of core and the project's own import share one instance.
|
|
7
|
+
const KEY = '__specbookFaker'
|
|
8
|
+
const store = globalThis as unknown as Record<string, FakerLike | undefined>
|
|
9
|
+
|
|
10
|
+
export const faker: FakerLike = new Proxy({} as FakerLike, {
|
|
11
|
+
get(_t, key) {
|
|
12
|
+
const current = store[KEY]
|
|
13
|
+
if (!current) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
'faker is not initialised. Install "@faker-js/faker" in your project; specbook sets it up before each run.',
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
return current[key as string]
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
export function setFaker(instance: FakerLike | undefined): void {
|
|
23
|
+
store[KEY] = instance
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FakerConfig {
|
|
27
|
+
/** locale name -> fallback chain of faker locale export names, e.g. { de: ['de', 'en', 'base'] } */
|
|
28
|
+
locales?: Record<string, string[]>
|
|
29
|
+
default?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface FakerModule {
|
|
33
|
+
Faker: new (opts: { locale: unknown[] }) => FakerLike
|
|
34
|
+
[locale: string]: unknown
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The locale exports a run needs, in fallback order: the chain for `locale`, then `base`. */
|
|
38
|
+
export function fakerLocaleNames(config: FakerConfig = {}, locale?: string): string[] {
|
|
39
|
+
const name = locale ?? config.default ?? 'en'
|
|
40
|
+
return [...new Set([...(config.locales?.[name] ?? [name, 'en']), 'base'])]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Every locale export a project can reach: the default's chain and each configured locale's, plus `en` and `base`. */
|
|
44
|
+
export function fakerLocaleExports(config: FakerConfig = {}): string[] {
|
|
45
|
+
const names = new Set<string>(['en', ...fakerLocaleNames(config)])
|
|
46
|
+
for (const name of Object.keys(config.locales ?? {}))
|
|
47
|
+
for (const n of fakerLocaleNames(config, name)) names.add(n)
|
|
48
|
+
return [...names].sort()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The module `installFaker` needs. A bundled build offers each locale as `locales[name]()` so only
|
|
53
|
+
* the ones a run falls back through are fetched; a plain faker module already has them all.
|
|
54
|
+
*/
|
|
55
|
+
export async function prepareFaker(
|
|
56
|
+
mod: Record<string, any>,
|
|
57
|
+
config: FakerConfig = {},
|
|
58
|
+
locale?: string,
|
|
59
|
+
): Promise<FakerModule> {
|
|
60
|
+
const locales = mod.locales as Record<string, () => Promise<unknown>> | undefined
|
|
61
|
+
if (!locales) return mod as FakerModule
|
|
62
|
+
const ready: Record<string, unknown> = { Faker: mod.Faker }
|
|
63
|
+
await Promise.all(
|
|
64
|
+
fakerLocaleNames(config, locale).map(async (n) => {
|
|
65
|
+
const load = locales[n]
|
|
66
|
+
if (load) ready[n] = await load()
|
|
67
|
+
}),
|
|
68
|
+
)
|
|
69
|
+
return ready as FakerModule
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Build a Faker for `locale` from the (dynamically imported) module, install it, and optionally seed it. */
|
|
73
|
+
export function installFaker(
|
|
74
|
+
mod: FakerModule,
|
|
75
|
+
config: FakerConfig = {},
|
|
76
|
+
opts: { locale?: string; seed?: number } = {},
|
|
77
|
+
): FakerLike {
|
|
78
|
+
const name = opts.locale ?? config.default ?? 'en'
|
|
79
|
+
const locales = fakerLocaleNames(config, name).map((n) => {
|
|
80
|
+
const l = mod[n]
|
|
81
|
+
if (!l) throw new Error(`Unknown faker locale "${n}" (from "${name}")`)
|
|
82
|
+
return l
|
|
83
|
+
})
|
|
84
|
+
const instance = new mod.Faker({ locale: locales })
|
|
85
|
+
if (opts.seed !== undefined) instance.seed(opts.seed)
|
|
86
|
+
setFaker(instance)
|
|
87
|
+
return instance
|
|
88
|
+
}
|
package/src/faker.ts
CHANGED
|
@@ -1,80 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// The `faker` request and flow files import (`@specbook/core/faker`), typed as faker's own `Faker` so a typo or a
|
|
2
|
+
// wrong argument fails `tsc` and the editor completes. `@faker-js/faker` is the project's dependency (`specbook init` adds it);
|
|
3
|
+
// the runtime in ./faker-runtime installs the real instance before each run.
|
|
3
4
|
|
|
4
|
-
type
|
|
5
|
+
import type { Faker } from '@faker-js/faker'
|
|
6
|
+
import { faker as runtime } from './faker-runtime'
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
const KEY = '__specbookFaker'
|
|
8
|
-
const store = globalThis as unknown as Record<string, FakerLike | undefined>
|
|
9
|
-
|
|
10
|
-
export const faker: FakerLike = new Proxy({} as FakerLike, {
|
|
11
|
-
get(_t, key) {
|
|
12
|
-
const current = store[KEY]
|
|
13
|
-
if (!current) {
|
|
14
|
-
throw new Error(
|
|
15
|
-
'faker is not initialised. Install "@faker-js/faker" in your project; specbook sets it up before each run.',
|
|
16
|
-
)
|
|
17
|
-
}
|
|
18
|
-
return current[key as string]
|
|
19
|
-
},
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
export function setFaker(instance: FakerLike | undefined): void {
|
|
23
|
-
store[KEY] = instance
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface FakerConfig {
|
|
27
|
-
/** locale name -> fallback chain of faker locale export names, e.g. { de: ['de', 'en', 'base'] } */
|
|
28
|
-
locales?: Record<string, string[]>
|
|
29
|
-
default?: string
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface FakerModule {
|
|
33
|
-
Faker: new (opts: { locale: unknown[] }) => FakerLike
|
|
34
|
-
[locale: string]: unknown
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** The locale exports a run needs, in fallback order: the chain for `locale`, then `base`. */
|
|
38
|
-
export function fakerLocaleNames(config: FakerConfig = {}, locale?: string): string[] {
|
|
39
|
-
const name = locale ?? config.default ?? 'en'
|
|
40
|
-
return [...new Set([...(config.locales?.[name] ?? [name, 'en']), 'base'])]
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The module `installFaker` needs. A bundled build offers each locale as `locales[name]()` so only
|
|
45
|
-
* the ones a run falls back through are fetched; a plain faker module already has them all.
|
|
46
|
-
*/
|
|
47
|
-
export async function prepareFaker(
|
|
48
|
-
mod: Record<string, any>,
|
|
49
|
-
config: FakerConfig = {},
|
|
50
|
-
locale?: string,
|
|
51
|
-
): Promise<FakerModule> {
|
|
52
|
-
const locales = mod.locales as Record<string, () => Promise<unknown>> | undefined
|
|
53
|
-
if (!locales) return mod as FakerModule
|
|
54
|
-
const ready: Record<string, unknown> = { Faker: mod.Faker }
|
|
55
|
-
await Promise.all(
|
|
56
|
-
fakerLocaleNames(config, locale).map(async (n) => {
|
|
57
|
-
const load = locales[n]
|
|
58
|
-
if (load) ready[n] = await load()
|
|
59
|
-
}),
|
|
60
|
-
)
|
|
61
|
-
return ready as FakerModule
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Build a Faker for `locale` from the (dynamically imported) module, install it, and optionally seed it. */
|
|
65
|
-
export function installFaker(
|
|
66
|
-
mod: FakerModule,
|
|
67
|
-
config: FakerConfig = {},
|
|
68
|
-
opts: { locale?: string; seed?: number } = {},
|
|
69
|
-
): FakerLike {
|
|
70
|
-
const name = opts.locale ?? config.default ?? 'en'
|
|
71
|
-
const locales = fakerLocaleNames(config, name).map((n) => {
|
|
72
|
-
const l = mod[n]
|
|
73
|
-
if (!l) throw new Error(`Unknown faker locale "${n}" (from "${name}")`)
|
|
74
|
-
return l
|
|
75
|
-
})
|
|
76
|
-
const instance = new mod.Faker({ locale: locales })
|
|
77
|
-
if (opts.seed !== undefined) instance.seed(opts.seed)
|
|
78
|
-
setFaker(instance)
|
|
79
|
-
return instance
|
|
80
|
-
}
|
|
8
|
+
export const faker = runtime as unknown as Faker
|
package/src/index.ts
CHANGED
|
@@ -5,8 +5,15 @@ export * from './diff'
|
|
|
5
5
|
export * from './environment'
|
|
6
6
|
export type { Example, ExampleLoader, FileReader, Row } from './example'
|
|
7
7
|
export { fromFormRows, fromRows, toFormRows, toRows } from './example'
|
|
8
|
-
export type { FakerConfig } from './faker'
|
|
9
|
-
export {
|
|
8
|
+
export type { FakerConfig } from './faker-runtime'
|
|
9
|
+
export {
|
|
10
|
+
faker,
|
|
11
|
+
fakerLocaleExports,
|
|
12
|
+
fakerLocaleNames,
|
|
13
|
+
installFaker,
|
|
14
|
+
prepareFaker,
|
|
15
|
+
setFaker,
|
|
16
|
+
} from './faker-runtime'
|
|
10
17
|
export * from './mock'
|
|
11
18
|
export * from './oauth'
|
|
12
19
|
export * from './proxy/index'
|
|
@@ -31,8 +38,10 @@ export {
|
|
|
31
38
|
export {
|
|
32
39
|
collectPlaceholders,
|
|
33
40
|
deepMerge,
|
|
41
|
+
FakerPlaceholderError,
|
|
34
42
|
render,
|
|
35
43
|
renderString,
|
|
44
|
+
rootName,
|
|
36
45
|
TemplateError,
|
|
37
46
|
usesFaker,
|
|
38
47
|
} from './template'
|
package/src/reporters/index.ts
CHANGED
|
@@ -54,7 +54,7 @@ export interface JUnitOptions {
|
|
|
54
54
|
hostname?: string
|
|
55
55
|
/** shown as `<properties>` on each suite (env, git sha, seed…) */
|
|
56
56
|
properties?: Record<string, string | undefined>
|
|
57
|
-
/** service → origin, for the trace in `<system-out>` */
|
|
57
|
+
/** service → the address its calls go to (origin and base path), for the trace in `<system-out>` */
|
|
58
58
|
origins?: Record<string, string>
|
|
59
59
|
}
|
|
60
60
|
|
package/src/reporters/list.ts
CHANGED
|
@@ -13,7 +13,7 @@ export interface ListOptions {
|
|
|
13
13
|
quiet?: boolean
|
|
14
14
|
/** show the request and response of a failed step (default), of every step, or not at all */
|
|
15
15
|
trace?: 'on-failure' | 'always' | 'off'
|
|
16
|
-
/** service →
|
|
16
|
+
/** service → the address its calls go to (origin and base path), so a trace shows the URL that was called */
|
|
17
17
|
origins?: Record<string, string>
|
|
18
18
|
/** paths are shown relative to this folder */
|
|
19
19
|
cwd?: string
|
package/src/reporters/trace.ts
CHANGED
|
@@ -16,7 +16,7 @@ const SHOWN_RESPONSE_HEADERS = [
|
|
|
16
16
|
]
|
|
17
17
|
|
|
18
18
|
export interface TraceOptions {
|
|
19
|
-
/** service →
|
|
19
|
+
/** service → the address its calls go to (`https://api.example.com/v1`: origin and base path), to show the full URL */
|
|
20
20
|
origins?: Record<string, string>
|
|
21
21
|
/** characters of a body shown before it is cut (default 2048) */
|
|
22
22
|
maxBody?: number
|
package/src/runner/run.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
isFakerName,
|
|
12
12
|
isPlainObject,
|
|
13
13
|
render,
|
|
14
|
+
rootName,
|
|
14
15
|
} from '../template'
|
|
15
16
|
import type {
|
|
16
17
|
ApiResponse,
|
|
@@ -201,7 +202,7 @@ function usesOf(
|
|
|
201
202
|
const out: VarUse[] = []
|
|
202
203
|
for (const name of names) {
|
|
203
204
|
if (isDeferred(name) || isFakerName(name)) continue
|
|
204
|
-
const key = name
|
|
205
|
+
const key = rootName(name)
|
|
205
206
|
if (seen.has(key)) continue
|
|
206
207
|
seen.add(key)
|
|
207
208
|
for (let i = layers.length - 1; i >= 0; i--) {
|
package/src/runner/transport.ts
CHANGED
|
@@ -58,6 +58,40 @@ export function absoluteBase(baseUrl: string, pageHref?: string): string {
|
|
|
58
58
|
/** How long a request waits for an answer when nothing says otherwise. */
|
|
59
59
|
export const DEFAULT_TIMEOUT_MS = 30_000
|
|
60
60
|
|
|
61
|
+
/** What node's fetch says in `error.cause.code`, in words a person can act on. */
|
|
62
|
+
const WHY: Record<string, string> = {
|
|
63
|
+
ECONNREFUSED: 'connection refused',
|
|
64
|
+
ENOTFOUND: 'host not found',
|
|
65
|
+
EAI_AGAIN: 'host not found',
|
|
66
|
+
ECONNRESET: 'connection reset',
|
|
67
|
+
ETIMEDOUT: 'connection timed out',
|
|
68
|
+
UND_ERR_CONNECT_TIMEOUT: 'connection timed out',
|
|
69
|
+
EHOSTUNREACH: 'host unreachable',
|
|
70
|
+
ENETUNREACH: 'network unreachable',
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** node's bare "fetch failed" says nothing; its cause says why. A browser gives no cause, so its error is left as it is. */
|
|
74
|
+
function unreachable(e: unknown, url: string): unknown {
|
|
75
|
+
const cause = (
|
|
76
|
+
e as { cause?: { code?: string; message?: string; errors?: { code?: string }[] } } | undefined
|
|
77
|
+
)?.cause
|
|
78
|
+
if (!(e instanceof TypeError) || !cause) return e
|
|
79
|
+
const code = cause.code ?? cause.errors?.find((x) => x.code)?.code
|
|
80
|
+
const tls =
|
|
81
|
+
code && /^CERT_|^ERR_TLS|SELF_SIGNED|UNABLE_TO_VERIFY/.test(code) ? 'certificate problem' : ''
|
|
82
|
+
// a reason there are words for, else the cause's own message ("bad port"), else nothing to add
|
|
83
|
+
const own = typeof cause.message === 'string' && cause.message.length <= 120 ? cause.message : ''
|
|
84
|
+
const why = (code && WHY[code]) || tls || own
|
|
85
|
+
if (!why) return e
|
|
86
|
+
let host = url
|
|
87
|
+
try {
|
|
88
|
+
host = new URL(url).host
|
|
89
|
+
} catch {
|
|
90
|
+
/* an address that is not a URL is shown as it is */
|
|
91
|
+
}
|
|
92
|
+
return new Error(`Could not reach ${host}: ${why}.`, { cause: e })
|
|
93
|
+
}
|
|
94
|
+
|
|
61
95
|
export interface FetchTransportOptions {
|
|
62
96
|
baseUrl: (service: string) => string
|
|
63
97
|
fetch?: typeof fetch
|
|
@@ -85,8 +119,9 @@ export function createFetchTransport(opts: FetchTransportOptions): Transport {
|
|
|
85
119
|
}, req.timeoutMs)
|
|
86
120
|
: undefined
|
|
87
121
|
signal?.addEventListener('abort', () => ctl.abort())
|
|
122
|
+
const url = buildUrl(opts.baseUrl(req.service), req)
|
|
88
123
|
try {
|
|
89
|
-
const res = await f(
|
|
124
|
+
const res = await f(url, {
|
|
90
125
|
method: req.method,
|
|
91
126
|
headers,
|
|
92
127
|
body,
|
|
@@ -112,7 +147,7 @@ export function createFetchTransport(opts: FetchTransportOptions): Transport {
|
|
|
112
147
|
if (timedOut) throw new Error(`Timed out after ${req.timeoutMs}ms: no answer from the API.`)
|
|
113
148
|
// a person pressing Stop, or the run's own deadline: say so, not "This operation was aborted"
|
|
114
149
|
if (signal?.aborted) throw new Error('Stopped before the API answered.')
|
|
115
|
-
throw e
|
|
150
|
+
throw unreachable(e, url)
|
|
116
151
|
} finally {
|
|
117
152
|
if (timer) clearTimeout(timer)
|
|
118
153
|
}
|
package/src/template.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
// Template engine: `{{ name }}` placeholders, variable lookup only (no expressions).
|
|
2
2
|
|
|
3
|
-
import { faker } from './faker'
|
|
3
|
+
import { faker } from './faker-runtime'
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
const
|
|
5
|
+
// a name is words joined by dots, and a position in an array can be a dot (`tags.0.name`) or brackets (`tags[0].name`)
|
|
6
|
+
const NAME = String.raw`[\w.$\[\]-]+`
|
|
7
|
+
const WHOLE = new RegExp(String.raw`^\s*\{\{\s*(${NAME})\s*\}\}\s*$`)
|
|
8
|
+
const EMBED = new RegExp(String.raw`\{\{\s*(${NAME})\s*\}\}`, 'g')
|
|
9
|
+
|
|
10
|
+
/** The variable a placeholder starts from: `tags` for `tags[0].name`, `pet` for `pet.category.name`. */
|
|
11
|
+
export const rootName = (name: string): string => name.split(/[.[]/, 1)[0] as string
|
|
7
12
|
|
|
8
13
|
/** Namespaces resolved at the transport boundary, never in the request model. */
|
|
9
14
|
export const DEFERRED_NAMESPACES = ['secrets', 'session'] as const
|
|
@@ -22,16 +27,30 @@ export function isDeferred(name: string): boolean {
|
|
|
22
27
|
|
|
23
28
|
export const isFakerName = (name: string) => name.startsWith('faker.')
|
|
24
29
|
|
|
30
|
+
/** A `{{faker.…}}` that names no generator, or is not written as a bare `faker.<module>.<method>`. */
|
|
31
|
+
export class FakerPlaceholderError extends TemplateError {
|
|
32
|
+
constructor(
|
|
33
|
+
variable: string,
|
|
34
|
+
override readonly message: string,
|
|
35
|
+
) {
|
|
36
|
+
super(variable)
|
|
37
|
+
this.name = 'FakerPlaceholderError'
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
/** `faker.person.firstName` calls the generator on the active faker instance, fresh on every render. */
|
|
26
42
|
function fromFaker(path: string): { found: boolean; value?: unknown } {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
const [, group, method, ...rest] = path.split('.')
|
|
44
|
+
const unknown = () =>
|
|
45
|
+
new FakerPlaceholderError(
|
|
46
|
+
path,
|
|
47
|
+
`Unknown faker generator "{{${path}}}": use faker.<module>.<method>`,
|
|
48
|
+
)
|
|
49
|
+
if (!group || !method || rest.length > 0) throw unknown()
|
|
50
|
+
const mod = (faker as Record<string, any>)[group]
|
|
51
|
+
const fn = typeof mod === 'object' && mod !== null ? mod[method] : undefined
|
|
52
|
+
if (typeof fn !== 'function' || method === 'constructor') throw unknown()
|
|
53
|
+
return { found: true, value: fn.call(mod) }
|
|
35
54
|
}
|
|
36
55
|
|
|
37
56
|
/** True when any placeholder in `value` needs the faker instance. */
|
|
@@ -39,11 +58,15 @@ export function usesFaker(value: unknown): boolean {
|
|
|
39
58
|
return [...collectPlaceholders(value)].some(isFakerName)
|
|
40
59
|
}
|
|
41
60
|
|
|
61
|
+
/** `tags[0].name` is `tags.0.name`: a position in brackets is the same step as one after a dot. */
|
|
62
|
+
const steps = (path: string) => path.replace(/\[(\d+)\]/g, '.$1').split('.')
|
|
63
|
+
|
|
42
64
|
function lookup(scope: Record<string, unknown>, path: string): { found: boolean; value?: unknown } {
|
|
43
65
|
if (isFakerName(path)) return fromFaker(path)
|
|
44
66
|
let cur: unknown = scope
|
|
45
|
-
for (const part of path
|
|
46
|
-
|
|
67
|
+
for (const part of steps(path)) {
|
|
68
|
+
// a value's own properties only: `toString` and `constructor` are not data, so a placeholder cannot reach them
|
|
69
|
+
if (cur !== null && typeof cur === 'object' && Object.hasOwn(cur as object, part)) {
|
|
47
70
|
cur = (cur as Record<string, unknown>)[part]
|
|
48
71
|
} else {
|
|
49
72
|
return { found: false }
|
|
@@ -52,7 +75,21 @@ function lookup(scope: Record<string, unknown>, path: string): { found: boolean;
|
|
|
52
75
|
return { found: true, value: cur }
|
|
53
76
|
}
|
|
54
77
|
|
|
78
|
+
/** `{{faker.number.int(5)}}` does not read as a placeholder: refuse it rather than send it as it is. */
|
|
79
|
+
function rejectFakerCall(s: string): void {
|
|
80
|
+
if (!s.includes('faker')) return
|
|
81
|
+
const left = /\{\{\s*(faker\.[\s\S]*?)(?:\}\}|$)/.exec(s.replace(EMBED, ''))
|
|
82
|
+
if (left) {
|
|
83
|
+
const name = (left[1] as string).trim()
|
|
84
|
+
throw new FakerPlaceholderError(
|
|
85
|
+
name,
|
|
86
|
+
`"{{${name}}}" is not a faker placeholder: placeholders take no arguments. Use faker in a request file for those.`,
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
55
91
|
export function renderString(s: string, scope: Record<string, unknown>): unknown {
|
|
92
|
+
rejectFakerCall(s)
|
|
56
93
|
const whole = WHOLE.exec(s)
|
|
57
94
|
if (whole) {
|
|
58
95
|
const name = whole[1] as string
|