@octalmesh/seagull-core 0.0.2 → 0.1.1
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/CHANGELOG.md +38 -0
- package/README.md +173 -39
- package/dist/index.mjs +218 -152
- package/package.json +6 -6
- package/src/config/loader.test.ts +505 -0
- package/src/config/loader.ts +31 -12
- package/src/config/publishing.test.ts +92 -0
- package/src/config/publishing.ts +9 -5
- package/src/config/resolve-config-file.test.ts +60 -0
- package/src/config/schema.test.ts +466 -0
- package/src/config/schema.ts +16 -10
- package/src/config/spec-format.test.ts +54 -0
- package/src/config/spec-format.ts +48 -0
- package/src/config/template.test.ts +168 -0
- package/src/config/types.ts +3 -3
- package/src/generator/generator.test.ts +59 -0
- package/src/generator/registry.test.ts +84 -0
- package/src/generator/types.ts +9 -14
- package/src/generators/openapi-generator-cli/openapi-generator-cli.generator.test.ts +259 -0
- package/src/generators/openapi-generator-cli/patchers/go-module.patcher.test.ts +119 -0
- package/src/generators/openapi-generator-cli/patchers/maven.patcher.test.ts +141 -0
- package/src/generators/openapi-generator-cli/patchers/npm.patcher.test.ts +132 -0
- package/src/generators/openapi-typescript/openapi-typescript.generator.test.ts +190 -0
- package/src/git/git.test.ts +234 -0
- package/src/git/git.ts +50 -4
- package/src/index.ts +8 -0
- package/src/process/exec.test.ts +103 -0
- package/src/process/exec.ts +1 -2
- package/src/process/resolve-bin.test.ts +43 -0
- package/src/readme/default-templates.test.ts +179 -0
- package/src/readme/default-templates.ts +5 -10
- package/src/readme/readme-renderer.test.ts +121 -0
- package/src/readme/readme-renderer.ts +3 -7
- package/src/redocly/redocly-sync.test.ts +190 -0
- package/src/redocly/redocly-sync.ts +3 -1
- package/src/test-support/fixtures.ts +65 -0
- package/src/version/version.test.ts +93 -0
- package/src/version/version.ts +4 -2
package/src/config/schema.ts
CHANGED
|
@@ -25,16 +25,23 @@ export const varsTreeSchema: z.ZodType<VarsTree> = z.lazy(() =>
|
|
|
25
25
|
),
|
|
26
26
|
);
|
|
27
27
|
|
|
28
|
-
export const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
export const specFormatSchema = z.enum(["json", "yaml"]);
|
|
29
|
+
export type SpecFormat = z.infer<typeof specFormatSchema>;
|
|
30
|
+
|
|
31
|
+
export const specFormatListSchema = z
|
|
32
|
+
.union([specFormatSchema, z.array(specFormatSchema).min(1)])
|
|
33
|
+
.default("json")
|
|
34
|
+
.transform((value) => (Array.isArray(value) ? value : [value]))
|
|
35
|
+
.refine((formats) => new Set(formats).size === formats.length, {
|
|
36
|
+
message: "paths.specFormat entries must be unique",
|
|
37
|
+
});
|
|
32
38
|
|
|
33
39
|
export const pathsSchema = z.object({
|
|
34
40
|
dist: z.string().min(1).default("dist"),
|
|
35
41
|
specs: z.string().min(1).optional(),
|
|
36
42
|
docs: z.string().min(1).optional(),
|
|
37
43
|
sdk: z.string().min(1).optional(),
|
|
44
|
+
specFormat: specFormatListSchema,
|
|
38
45
|
});
|
|
39
46
|
|
|
40
47
|
export const docsSchema = z.object({
|
|
@@ -73,8 +80,8 @@ export const mavenCoordsSchema = z.object({
|
|
|
73
80
|
/**
|
|
74
81
|
* Publishing conventions - git branch/tag naming, and where registry-backed
|
|
75
82
|
* artifacts (npm, Maven) get pushed. Every field is a template supporting the
|
|
76
|
-
* usual `{...}` placeholders (`{service}`, `{id}`, `{
|
|
77
|
-
*
|
|
83
|
+
* usual `{...}` placeholders (`{service}`, `{id}`, `{vars.*}`, and for `tag`
|
|
84
|
+
* only, also `{version}`).
|
|
78
85
|
*
|
|
79
86
|
* Required at the root level - seagull has no built-in opinion on branch/tag
|
|
80
87
|
* naming or which registry to use, so this has to come from the config, not
|
|
@@ -98,7 +105,7 @@ export const publishingSchema = z.object({
|
|
|
98
105
|
/**
|
|
99
106
|
* Template for the `repository.url` field written into generated
|
|
100
107
|
* `package.json` (and shown in default README templates), e.g.
|
|
101
|
-
* `"https://github.com/{
|
|
108
|
+
* `"https://github.com/{vars.repository.owner}/{vars.repository.repo}"`.
|
|
102
109
|
*/
|
|
103
110
|
repositoryUrl: z.string().min(1),
|
|
104
111
|
npm: z.object({
|
|
@@ -246,13 +253,12 @@ export const contractSchema = z.object({
|
|
|
246
253
|
|
|
247
254
|
export const rootConfigSchema = z.object({
|
|
248
255
|
/**
|
|
249
|
-
* The config schema version this file targets. Currently must be `1`
|
|
256
|
+
* The config schema version this file targets. Currently, must be `1`
|
|
250
257
|
* (the only version that exists) - see {@link CONFIG_SCHEMA_VERSION}.
|
|
251
258
|
*/
|
|
252
259
|
configVersion: z.literal(CONFIG_SCHEMA_VERSION),
|
|
253
|
-
github: githubSchema,
|
|
254
260
|
vars: varsTreeSchema.default({}),
|
|
255
|
-
paths: pathsSchema.default({ dist: "dist" }),
|
|
261
|
+
paths: pathsSchema.default({ dist: "dist", specFormat: ["json"] }),
|
|
256
262
|
docs: docsSchema,
|
|
257
263
|
/**
|
|
258
264
|
* Publishing conventions (branch/tag naming, registry URLs), applied to
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { parseBundledSpec, specFilename } from "./spec-format";
|
|
4
|
+
|
|
5
|
+
describe("specFilename", () => {
|
|
6
|
+
it("joins the contract name and format with a dot", () => {
|
|
7
|
+
expect(specFilename("auth", "json")).toBe("auth.json");
|
|
8
|
+
expect(specFilename("auth", "yaml")).toBe("auth.yaml");
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
describe("parseBundledSpec", () => {
|
|
13
|
+
it("parses 'json' as strict JSON", () => {
|
|
14
|
+
const result = parseBundledSpec(
|
|
15
|
+
'{"openapi":"3.1.0","info":{"version":"1.0.0"}}',
|
|
16
|
+
"json",
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
expect(result).toEqual({
|
|
20
|
+
openapi: "3.1.0",
|
|
21
|
+
info: { version: "1.0.0" },
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("rejects invalid JSON when format is 'json', even if it happens to be valid YAML", () => {
|
|
26
|
+
// A bare, unquoted top-level scalar - valid YAML, not valid JSON. Proves
|
|
27
|
+
// parseBundledSpec doesn't quietly fall back to a lenient parser.
|
|
28
|
+
expect(() => parseBundledSpec("not json", "json")).toThrow();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("parses 'yaml' as YAML", () => {
|
|
32
|
+
const result = parseBundledSpec(
|
|
33
|
+
'openapi: "3.1.0"\ninfo:\n version: "1.0.0"\n',
|
|
34
|
+
"yaml",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(result).toEqual({
|
|
38
|
+
openapi: "3.1.0",
|
|
39
|
+
info: { version: "1.0.0" },
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("accepts plain JSON text when format is 'yaml' (JSON is valid YAML)", () => {
|
|
44
|
+
const result = parseBundledSpec(
|
|
45
|
+
'{"openapi":"3.1.0","info":{"version":"1.0.0"}}',
|
|
46
|
+
"yaml",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(result).toEqual({
|
|
50
|
+
openapi: "3.1.0",
|
|
51
|
+
info: { version: "1.0.0" },
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { parse as parseYaml } from "yaml";
|
|
2
|
+
|
|
3
|
+
import type { SpecFormat } from "./schema";
|
|
4
|
+
|
|
5
|
+
export type { SpecFormat } from "./schema";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The "primary" format among a `paths.specFormat` list - the one SDK generation,
|
|
9
|
+
* version/hash resolution, and the docs site actually read from when more than
|
|
10
|
+
* one format is configured. By convention, that's whichever format was listed
|
|
11
|
+
* first.
|
|
12
|
+
*
|
|
13
|
+
* @param formats - `config.paths.specFormat` (always non-empty).
|
|
14
|
+
* @returns The primary format.
|
|
15
|
+
*/
|
|
16
|
+
export function primarySpecFormat(formats: SpecFormat[]): SpecFormat {
|
|
17
|
+
return formats[0]!;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The filename a contract's bundled spec is written to/read from, e.g.
|
|
22
|
+
* `auth.json` or `auth.yaml` - one place computing this so `bundle`, `generate`,
|
|
23
|
+
* and `docs generate` can't disagree about the extension.
|
|
24
|
+
*
|
|
25
|
+
* @param contractName - The contract's `name`.
|
|
26
|
+
* @param format - `config.paths.specFormat`.
|
|
27
|
+
* @returns The filename (no directory), e.g. `"auth.yaml"`.
|
|
28
|
+
*/
|
|
29
|
+
export function specFilename(contractName: string, format: SpecFormat): string {
|
|
30
|
+
return `${contractName}.${format}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parses a bundled spec's raw file contents according to its configured format.
|
|
35
|
+
* JSON and YAML are both valid inputs to Redocly/openapi-generator/
|
|
36
|
+
* openapi-typescript, so seagull's own parsing has to match: `JSON.parse`
|
|
37
|
+
* rejects trailing commas and comments that valid YAML permits, and would
|
|
38
|
+
* silently misparse a YAML document that happens to look JSON-ish.
|
|
39
|
+
*
|
|
40
|
+
* @param raw - The raw bundled spec file contents.
|
|
41
|
+
* @param format - `config.paths.specFormat`.
|
|
42
|
+
* @returns The parsed document.
|
|
43
|
+
*/
|
|
44
|
+
export function parseBundledSpec(raw: string, format: SpecFormat): unknown {
|
|
45
|
+
return format === "json"
|
|
46
|
+
? (JSON.parse(raw) as unknown)
|
|
47
|
+
: parseYaml(raw, { merge: true });
|
|
48
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { buildTemplateContext, interpolate, interpolateDeep } from "./template";
|
|
4
|
+
|
|
5
|
+
describe("interpolate", () => {
|
|
6
|
+
it("replaces a single placeholder", () => {
|
|
7
|
+
expect(interpolate("hello {name}", { name: "world" })).toBe("hello world");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("replaces multiple placeholders, including repeats", () => {
|
|
11
|
+
expect(interpolate("{a}-{b}-{a}", { a: "x", b: "y" })).toBe("x-y-x");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("supports dotted placeholder keys", () => {
|
|
15
|
+
expect(
|
|
16
|
+
interpolate("@{vars.org}/{service}-client", {
|
|
17
|
+
"vars.org": "octalmesh",
|
|
18
|
+
service: "auth",
|
|
19
|
+
}),
|
|
20
|
+
).toBe("@octalmesh/auth-client");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns the template unchanged when it has no placeholders", () => {
|
|
24
|
+
expect(interpolate("no placeholders here", {})).toBe(
|
|
25
|
+
"no placeholders here",
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("leaves non-placeholder braces-free text alone alongside real placeholders", () => {
|
|
30
|
+
expect(interpolate("v{version}!!", { version: "1.0.0" })).toBe("v1.0.0!!");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("throws a descriptive error for an unknown placeholder", () => {
|
|
34
|
+
expect(() => interpolate("{missing}", { known: "x" })).toThrow(
|
|
35
|
+
/Unknown template placeholder "\{missing\}" in "\{missing\}" \(available: known\)/,
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("lists available keys sorted in the error message", () => {
|
|
40
|
+
expect(() => interpolate("{missing}", { zeta: "1", alpha: "2" })).toThrow(
|
|
41
|
+
/available: alpha, zeta/,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("throws on an empty context when a placeholder is present", () => {
|
|
46
|
+
expect(() => interpolate("{x}", {})).toThrow(/available: /);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("does not treat a placeholder-shaped key with invalid characters as one", () => {
|
|
50
|
+
expect(interpolate("{not a placeholder}", {})).toBe("{not a placeholder}");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("resolves a value that itself contains no braces even if empty string", () => {
|
|
54
|
+
expect(interpolate("[{x}]", { x: "" })).toBe("[]");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("interpolateDeep", () => {
|
|
59
|
+
it("interpolates a bare string", () => {
|
|
60
|
+
expect(interpolateDeep("{a}", { a: "1" })).toBe("1");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("leaves non-string primitives untouched", () => {
|
|
64
|
+
expect(interpolateDeep(42, {})).toBe(42);
|
|
65
|
+
expect(interpolateDeep(true, {})).toBe(true);
|
|
66
|
+
expect(interpolateDeep(null, {})).toBe(null);
|
|
67
|
+
expect(interpolateDeep(undefined, {})).toBe(undefined);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("recurses into arrays", () => {
|
|
71
|
+
expect(interpolateDeep(["{a}", "{b}", 3], { a: "1", b: "2" })).toEqual([
|
|
72
|
+
"1",
|
|
73
|
+
"2",
|
|
74
|
+
3,
|
|
75
|
+
]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("recurses into plain objects, preserving shape", () => {
|
|
79
|
+
const result = interpolateDeep(
|
|
80
|
+
{ npmName: "{pkg}", withGoMod: true, count: 3 },
|
|
81
|
+
{ pkg: "@org/pkg" },
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(result).toEqual({
|
|
85
|
+
npmName: "@org/pkg",
|
|
86
|
+
withGoMod: true,
|
|
87
|
+
count: 3,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("recurses into nested structures (object of arrays of objects)", () => {
|
|
92
|
+
const result = interpolateDeep(
|
|
93
|
+
{ list: [{ id: "{id}" }, { id: "static" }] },
|
|
94
|
+
{ id: "42" },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(result).toEqual({ list: [{ id: "42" }, { id: "static" }] });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("produces a deep copy rather than mutating the input", () => {
|
|
101
|
+
const input = { a: { b: "{x}" } };
|
|
102
|
+
const result = interpolateDeep(input, { x: "y" });
|
|
103
|
+
|
|
104
|
+
expect(result).not.toBe(input);
|
|
105
|
+
expect(result.a).not.toBe(input.a);
|
|
106
|
+
expect(input.a.b).toBe("{x}");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe("buildTemplateContext", () => {
|
|
111
|
+
it("flattens a nested scope into dot-path keys", () => {
|
|
112
|
+
expect(
|
|
113
|
+
buildTemplateContext({
|
|
114
|
+
service: "auth",
|
|
115
|
+
repository: { owner: "OctalMesh", repo: "ows-contracts" },
|
|
116
|
+
vars: { org: "octalmesh", nested: { deep: "value" } },
|
|
117
|
+
}),
|
|
118
|
+
).toEqual({
|
|
119
|
+
service: "auth",
|
|
120
|
+
"repository.owner": "OctalMesh",
|
|
121
|
+
"repository.repo": "ows-contracts",
|
|
122
|
+
"vars.org": "octalmesh",
|
|
123
|
+
"vars.nested.deep": "value",
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("stringifies numbers and booleans at leaf positions", () => {
|
|
128
|
+
expect(
|
|
129
|
+
buildTemplateContext({ vars: { port: 8080, enabled: true } }),
|
|
130
|
+
).toEqual({ "vars.port": "8080", "vars.enabled": "true" });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("skips undefined leaves entirely", () => {
|
|
134
|
+
expect(buildTemplateContext({ a: "x", b: undefined })).toEqual({ a: "x" });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("does not descend into arrays as if they were nested scopes", () => {
|
|
138
|
+
const result = buildTemplateContext({ list: ["a", "b"] as never });
|
|
139
|
+
|
|
140
|
+
expect(result).toEqual({ list: "a,b" });
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("returns an empty object for an empty scope", () => {
|
|
144
|
+
expect(buildTemplateContext({})).toEqual({});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("round-trips with interpolate for a realistic artifact context", () => {
|
|
148
|
+
const context = buildTemplateContext({
|
|
149
|
+
service: "auth",
|
|
150
|
+
id: "ts-client",
|
|
151
|
+
repository: { owner: "OctalMesh", repo: "ows-contracts" },
|
|
152
|
+
vars: { org: "octalmesh" },
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(interpolate("sdk/svc-{service}/{id}", context)).toBe(
|
|
156
|
+
"sdk/svc-auth/ts-client",
|
|
157
|
+
);
|
|
158
|
+
expect(interpolate("@{vars.org}/{service}-client", context)).toBe(
|
|
159
|
+
"@octalmesh/auth-client",
|
|
160
|
+
);
|
|
161
|
+
expect(
|
|
162
|
+
interpolate(
|
|
163
|
+
"https://github.com/{repository.owner}/{repository.repo}",
|
|
164
|
+
context,
|
|
165
|
+
),
|
|
166
|
+
).toBe("https://github.com/OctalMesh/ows-contracts");
|
|
167
|
+
});
|
|
168
|
+
});
|
package/src/config/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { VarsTree } from "./schema";
|
|
1
|
+
import type { SpecFormat, VarsTree } from "./schema";
|
|
2
2
|
|
|
3
|
-
export type { VarsTree } from "./schema";
|
|
3
|
+
export type { SpecFormat, VarsTree } from "./schema";
|
|
4
4
|
export type SdkTool = "openapi-generator" | "openapi-typescript";
|
|
5
5
|
export type SdkLang = "typescript" | "go" | "java";
|
|
6
6
|
export type SdkKind = "client" | "server";
|
|
@@ -114,9 +114,9 @@ export interface ResolvedConfig {
|
|
|
114
114
|
specs: string;
|
|
115
115
|
docs: string;
|
|
116
116
|
sdk: string;
|
|
117
|
+
specFormat: SpecFormat[];
|
|
117
118
|
};
|
|
118
119
|
|
|
119
|
-
github: { owner: string; repo: string };
|
|
120
120
|
vars: VarsTree;
|
|
121
121
|
|
|
122
122
|
docs: {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { SdkTool } from "../config/types";
|
|
4
|
+
import { Generator } from "./generator";
|
|
5
|
+
import type { GenerateContext, PrepareContext } from "./types";
|
|
6
|
+
|
|
7
|
+
class StubGenerator extends Generator {
|
|
8
|
+
readonly tool: SdkTool = "openapi-typescript";
|
|
9
|
+
readonly generateSpy = vi.fn((_ctx: GenerateContext) => Promise.resolve());
|
|
10
|
+
|
|
11
|
+
async generate(ctx: GenerateContext): Promise<void> {
|
|
12
|
+
await this.generateSpy(ctx);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("Generator", () => {
|
|
17
|
+
it("does not require 'prepare' to be implemented", () => {
|
|
18
|
+
const generator = new StubGenerator();
|
|
19
|
+
|
|
20
|
+
expect(typeof generator.prepare).toBe("undefined");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("subclasses can implement 'prepare' as an optional hook", async () => {
|
|
24
|
+
const prepareSpy = vi.fn((_ctx: PrepareContext) => Promise.resolve());
|
|
25
|
+
|
|
26
|
+
class WithPrepare extends Generator {
|
|
27
|
+
readonly tool: SdkTool = "openapi-generator";
|
|
28
|
+
|
|
29
|
+
override async prepare(ctx: PrepareContext): Promise<void> {
|
|
30
|
+
await prepareSpy(ctx);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
generate(): Promise<void> {
|
|
34
|
+
return Promise.resolve();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const generator = new WithPrepare();
|
|
39
|
+
|
|
40
|
+
await generator.prepare?.({ rootDir: "/tmp", entries: [] });
|
|
41
|
+
|
|
42
|
+
expect(prepareSpy).toHaveBeenCalledOnce();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("exposes 'tool' as a readonly discriminator", () => {
|
|
46
|
+
const generator = new StubGenerator();
|
|
47
|
+
|
|
48
|
+
expect(generator.tool).toBe("openapi-typescript");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("requires 'generate' to be implemented and calls through to it", async () => {
|
|
52
|
+
const generator = new StubGenerator();
|
|
53
|
+
const ctx = {} as GenerateContext;
|
|
54
|
+
|
|
55
|
+
await generator.generate(ctx);
|
|
56
|
+
|
|
57
|
+
expect(generator.generateSpy).toHaveBeenCalledWith(ctx);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { SdkTool } from "../config/types";
|
|
4
|
+
import { Generator } from "./generator";
|
|
5
|
+
import { GeneratorRegistry } from "./registry";
|
|
6
|
+
import type { GenerateContext } from "./types";
|
|
7
|
+
|
|
8
|
+
class FakeGenerator extends Generator {
|
|
9
|
+
readonly tool: SdkTool;
|
|
10
|
+
|
|
11
|
+
constructor(tool: SdkTool) {
|
|
12
|
+
super();
|
|
13
|
+
this.tool = tool;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
generate(_ctx: GenerateContext): Promise<void> {
|
|
17
|
+
return Promise.resolve();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("GeneratorRegistry", () => {
|
|
22
|
+
it("resolves a generator by its registered tool", () => {
|
|
23
|
+
const registry = new GeneratorRegistry();
|
|
24
|
+
const generator = new FakeGenerator("openapi-generator");
|
|
25
|
+
|
|
26
|
+
registry.register(generator);
|
|
27
|
+
|
|
28
|
+
expect(registry.resolve("openapi-generator")).toBe(generator);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns 'this' from register(), enabling chaining", () => {
|
|
32
|
+
const registry = new GeneratorRegistry();
|
|
33
|
+
const result = registry
|
|
34
|
+
.register(new FakeGenerator("openapi-generator"))
|
|
35
|
+
.register(new FakeGenerator("openapi-typescript"));
|
|
36
|
+
|
|
37
|
+
expect(result).toBe(registry);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("throws a descriptive error for an unregistered tool", () => {
|
|
41
|
+
const registry = new GeneratorRegistry();
|
|
42
|
+
|
|
43
|
+
registry.register(new FakeGenerator("openapi-typescript"));
|
|
44
|
+
|
|
45
|
+
expect(() => registry.resolve("openapi-generator")).toThrow(
|
|
46
|
+
/No generator implementation registered for tool "openapi-generator" \(available: openapi-typescript\)/,
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("throws with an empty available-list when nothing is registered", () => {
|
|
51
|
+
const registry = new GeneratorRegistry();
|
|
52
|
+
|
|
53
|
+
expect(() => registry.resolve("openapi-generator")).toThrow(
|
|
54
|
+
/\(available: \)/,
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("re-registering the same tool replaces the previous generator", () => {
|
|
59
|
+
const registry = new GeneratorRegistry();
|
|
60
|
+
const first = new FakeGenerator("openapi-generator");
|
|
61
|
+
const second = new FakeGenerator("openapi-generator");
|
|
62
|
+
|
|
63
|
+
registry.register(first).register(second);
|
|
64
|
+
|
|
65
|
+
expect(registry.resolve("openapi-generator")).toBe(second);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("tools() lists every distinct registered tool", () => {
|
|
69
|
+
const registry = new GeneratorRegistry();
|
|
70
|
+
|
|
71
|
+
registry
|
|
72
|
+
.register(new FakeGenerator("openapi-generator"))
|
|
73
|
+
.register(new FakeGenerator("openapi-typescript"));
|
|
74
|
+
|
|
75
|
+
expect(registry.tools().sort()).toEqual([
|
|
76
|
+
"openapi-generator",
|
|
77
|
+
"openapi-typescript",
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("tools() returns an empty array for a fresh registry", () => {
|
|
82
|
+
expect(new GeneratorRegistry().tools()).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
});
|
package/src/generator/types.ts
CHANGED
|
@@ -2,18 +2,12 @@ import type {
|
|
|
2
2
|
ResolvedArtifact,
|
|
3
3
|
ResolvedArtifactEntry,
|
|
4
4
|
ResolvedContract,
|
|
5
|
-
SdkTool,
|
|
6
5
|
} from "../config/types";
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
SdkTool,
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
/** Passed once per tool to {@link Generator.prepare}, before any of that
|
|
16
|
-
* tool's {@link Generator.generate} calls run. */
|
|
7
|
+
/**
|
|
8
|
+
* Passed once per tool to {@link Generator.prepare}, before any of that
|
|
9
|
+
* tool's {@link Generator.generate} calls run.
|
|
10
|
+
*/
|
|
17
11
|
export interface PrepareContext {
|
|
18
12
|
rootDir: string;
|
|
19
13
|
/**
|
|
@@ -23,16 +17,17 @@ export interface PrepareContext {
|
|
|
23
17
|
entries: ResolvedArtifactEntry[];
|
|
24
18
|
}
|
|
25
19
|
|
|
26
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Passed once per artifact to {@link Generator.generate}.
|
|
22
|
+
*/
|
|
27
23
|
export interface GenerateContext {
|
|
28
24
|
rootDir: string;
|
|
29
25
|
contract: ResolvedContract;
|
|
30
26
|
artifact: ResolvedArtifact;
|
|
31
27
|
version: string;
|
|
32
|
-
github: { owner: string; repo: string };
|
|
33
28
|
/**
|
|
34
|
-
* Absolute path to the contract's bundled
|
|
35
|
-
* (`<specsDir>/<contract
|
|
29
|
+
* Absolute path to the contract's bundled spec
|
|
30
|
+
* (`<specsDir>/<contract>.<paths.specFormat>` - `json` or `yaml`).
|
|
36
31
|
*/
|
|
37
32
|
specInputPath: string;
|
|
38
33
|
}
|