@kizunajs/fetch 2.0.0-beta.11 → 2.0.0-beta.13

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.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { z } from "zod";
1
+ import { $brand, z } from "zod";
2
2
  import { AUTO_GUARD_BRAND, AUTO_RESPONSES_BRAND, ExtractPathParams, HasPathParams, Method, ProblemDetails, RequestContextHeaderInputs, RouteDefinition, RoutePath, Routes, StreamMessageOf, StreamResponseDefinition, ValidationError, ValidationError as ValidationError$1, ValidationErrorFor, isProblemDetails, isValidationError } from "kizunajs";
3
3
 
4
4
  //#region src/client.d.ts
@@ -163,4 +163,4 @@ interface GeneratedRoutes {
163
163
  */
164
164
  declare const createGeneratedClient: (routes: GeneratedRoutes, config: ClientConfig) => Record<string, unknown>;
165
165
  //#endregion
166
- export { CLIENT_ROUTE, Client, ClientArgs, ClientConfig, ClientMethod, ClientResponse, ContextHeaderInputs, GeneratedResponse, GeneratedRoute, GeneratedRoutes, OutgoingRequest, type ValidationError, createGeneratedClient, isProblemDetails, isValidationError, routeOf };
166
+ export { type $brand, CLIENT_ROUTE, Client, ClientArgs, ClientConfig, ClientMethod, ClientResponse, ContextHeaderInputs, GeneratedResponse, GeneratedRoute, GeneratedRoutes, OutgoingRequest, type ValidationError, createGeneratedClient, isProblemDetails, isValidationError, routeOf };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AUTO_GUARD_BRAND, AUTO_RESPONSES_BRAND, ExtractPathParams, HasPathParams, Method, ProblemDetails, RequestContextHeaderInputs, RouteDefinition, RoutePath, Routes, StreamMessageOf, StreamResponseDefinition, ValidationError, ValidationError as ValidationError$1, ValidationErrorFor, isProblemDetails, isValidationError } from "kizunajs";
2
- import { z } from "zod";
2
+ import { $brand, z } from "zod";
3
3
 
4
4
  //#region src/client.d.ts
5
5
  type ResponseUnion<R extends RouteDefinition> = { [S in keyof R['responses']]: {
@@ -163,4 +163,4 @@ interface GeneratedRoutes {
163
163
  */
164
164
  declare const createGeneratedClient: (routes: GeneratedRoutes, config: ClientConfig) => Record<string, unknown>;
165
165
  //#endregion
166
- export { CLIENT_ROUTE, Client, ClientArgs, ClientConfig, ClientMethod, ClientResponse, ContextHeaderInputs, GeneratedResponse, GeneratedRoute, GeneratedRoutes, OutgoingRequest, type ValidationError, createGeneratedClient, isProblemDetails, isValidationError, routeOf };
166
+ export { type $brand, CLIENT_ROUTE, Client, ClientArgs, ClientConfig, ClientMethod, ClientResponse, ContextHeaderInputs, GeneratedResponse, GeneratedRoute, GeneratedRoutes, OutgoingRequest, type ValidationError, createGeneratedClient, isProblemDetails, isValidationError, routeOf };
package/dist/server.cjs CHANGED
@@ -10,6 +10,7 @@ let zod = require("zod");
10
10
  var TypeCollector = class {
11
11
  named = /* @__PURE__ */ new Map();
12
12
  claimed = /* @__PURE__ */ new Set();
13
+ brands = /* @__PURE__ */ new Map();
13
14
  /**
14
15
  * Claims a name before its body is walked, so a model that refers to itself
15
16
  * resolves to its own name instead of recursing. Answers whether the caller
@@ -23,12 +24,55 @@ var TypeCollector = class {
23
24
  add(type) {
24
25
  this.named.set(type.name, type);
25
26
  }
27
+ /**
28
+ * Declares a brand as a named type, `UserId = string & $brand<"UserId">`,
29
+ * and answers the name.
30
+ */
31
+ addBrand(brand, base, example) {
32
+ const name = (0, kizunajs_generator.sanitizeIdentifier)(brand);
33
+ const existing = this.brands.get(name);
34
+ if (existing !== void 0 && (existing.brand !== brand || existing.base !== base)) throw new Error(`@kizunajs/fetch: brand ${quote(brand)} wraps both ${existing.base} and ${base}. Give each its own name.`);
35
+ if (existing === void 0) this.brands.set(name, {
36
+ brand,
37
+ base,
38
+ example
39
+ });
40
+ return name;
41
+ }
42
+ usesBrand() {
43
+ return this.brands.size > 0;
44
+ }
45
+ /**
46
+ * Each brand's name, the type it wraps, and a value for its `@example`, for
47
+ * the constructors the file exports.
48
+ */
49
+ allBrands() {
50
+ return [...this.brands].map(([name, { base, example }]) => ({
51
+ name,
52
+ base,
53
+ example
54
+ }));
55
+ }
26
56
  all() {
27
- return [...this.named.values()].sort((left, right) => left.name.localeCompare(right.name));
57
+ for (const name of this.brands.keys()) if (this.named.has(name)) throw new Error(`@kizunajs/fetch: brand ${quote(name)} has the same name as a model. Give each its own name.`);
58
+ const brands = [...this.brands].map(([name, { brand, base }]) => ({
59
+ name,
60
+ description: `A branded \`${base}\`. Responses hand one back, and \`to${name}\` makes one from a plain value.`,
61
+ body: `${base} & $brand<${quote(brand)}>`
62
+ }));
63
+ return [...this.named.values(), ...brands].sort((left, right) => left.name.localeCompare(right.name));
28
64
  }
29
65
  };
30
66
  const quote = (value) => JSON.stringify(value);
31
67
  /**
68
+ * What a brand wraps: the scalars, as the JSON carries them.
69
+ */
70
+ const BRANDABLE_BASES = new Set([
71
+ "string",
72
+ "number",
73
+ "boolean"
74
+ ]);
75
+ /**
32
76
  * A literal as the type the JSON carries, so `z.literal(true)` is `true`.
33
77
  */
34
78
  const literalType = (value) => {
@@ -161,9 +205,15 @@ const objectBody = (schema, collector, hint) => {
161
205
  };
162
206
  /**
163
207
  * The TypeScript type a schema describes. Named models are collected and
164
- * referenced; anonymous shapes are inlined.
208
+ * referenced; anonymous shapes are inlined. A brand is named like a model.
165
209
  */
166
210
  const typeOf = (schema, collector, hint) => {
211
+ const type = unbrandedTypeOf(schema, collector, hint);
212
+ const brand = (0, kizunajs_generator.readMetaBrand)(schema);
213
+ if (brand === void 0 || !BRANDABLE_BASES.has(type)) return type;
214
+ return collector.addBrand(brand, type, sampleValue(schema, 0, brand));
215
+ };
216
+ const unbrandedTypeOf = (schema, collector, hint) => {
167
217
  if ((0, kizunajs_generator.isFileSchema)(schema)) return "File | Blob";
168
218
  if ((0, kizunajs_generator.isBinarySchema)(schema)) return "Uint8Array";
169
219
  const def = (0, kizunajs_generator.readDef)(schema);
@@ -388,14 +438,27 @@ const emitRequestContext = (contract, collector) => {
388
438
  }
389
439
  return fields.length > 0 ? `export interface RequestContext {\n${indent(fields.join("\n"))}\n}` : void 0;
390
440
  };
441
+ /**
442
+ * A constructor per brand, so a value from outside the API becomes one with
443
+ * `toUserId('usr_k7f3q9')` rather than a cast.
444
+ */
445
+ const emitBrandConstructors = (collector, namespaceName) => collector.allBrands().map(({ name, base, example }) => `
446
+ ${docComment([
447
+ `Brands a value from outside the API, like a URL segment, as \`${namespaceName}.${name}\`. A value from a response is branded already.`,
448
+ "",
449
+ "@example",
450
+ `const id = to${name}(${example});`
451
+ ])}export const to${name} = (value: ${base}): ${namespaceName}.${name} => value as ${namespaceName}.${name};
452
+ `).join("");
391
453
  const generateFetchClient = (contract, options = {}) => {
392
454
  const { namespace: namespaceName = "API", runtimeModule = "@kizunajs/fetch", regenerateCommand = "kizuna generate", source } = options;
393
455
  const collector = new TypeCollector();
394
456
  const tree = emitTree((0, kizunajs_generator.listedRoutes)(contract), "", collector, namespaceName);
457
+ const requestContext = emitRequestContext(contract, collector);
395
458
  const api = [...collector.all().map((model) => {
396
459
  return `${model.description ? `/**\n * ${model.description}\n */\n` : ""}export type ${model.name} = ${model.body};`;
397
460
  }), ...tree.declarations].join("\n\n");
398
- const requestContext = emitRequestContext(contract, collector);
461
+ const brands = emitBrandConstructors(collector, namespaceName);
399
462
  return `/* eslint-disable */
400
463
  /**
401
464
  ${[
@@ -405,7 +468,7 @@ ${[
405
468
  ` * Regenerate: ${regenerateCommand}`
406
469
  ].join("\n")}
407
470
  */
408
- import { createGeneratedClient, type ClientConfig, type ClientMethod, type GeneratedRoutes } from '${runtimeModule}';
471
+ import { createGeneratedClient, ${collector.usesBrand() ? "type $brand, " : ""}type ClientConfig, type ClientMethod, type GeneratedRoutes } from '${runtimeModule}';
409
472
 
410
473
  export namespace ${namespaceName} {
411
474
  ${indent(api)}
@@ -418,7 +481,7 @@ ${indent(tree.signatures)}
418
481
  const routes: GeneratedRoutes = {
419
482
  ${indent(tree.table)}
420
483
  };
421
-
484
+ ${brands}
422
485
  ${requestContext === void 0 ? `/**
423
486
  * A client for this API. Pass the base URL and anything else the runtime takes.
424
487
  */
package/dist/server.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ValidationErrorSchema } from "kizunajs/schemas";
2
- import { defineClient, isBinarySchema, isFileSchema, isStreamResponse, isVoidSchema, listedRoutes, readDef, readDeprecation, readDiscriminatedUnion, readMetaDescription, readMetaExamples, readMetaId, readObjectShape, resolveResponseBody, resolveResponseHeaders, streamContentType, toPascalCase, unwrapOptionalWrappers } from "kizunajs/generator";
2
+ import { defineClient, isBinarySchema, isFileSchema, isStreamResponse, isVoidSchema, listedRoutes, readDef, readDeprecation, readDiscriminatedUnion, readMetaBrand, readMetaDescription, readMetaExamples, readMetaId, readObjectShape, resolveResponseBody, resolveResponseHeaders, sanitizeIdentifier, streamContentType, toPascalCase, unwrapOptionalWrappers } from "kizunajs/generator";
3
3
  import { z } from "zod";
4
4
  //#region src/zod-to-typescript.ts
5
5
  /**
@@ -9,6 +9,7 @@ import { z } from "zod";
9
9
  var TypeCollector = class {
10
10
  named = /* @__PURE__ */ new Map();
11
11
  claimed = /* @__PURE__ */ new Set();
12
+ brands = /* @__PURE__ */ new Map();
12
13
  /**
13
14
  * Claims a name before its body is walked, so a model that refers to itself
14
15
  * resolves to its own name instead of recursing. Answers whether the caller
@@ -22,12 +23,55 @@ var TypeCollector = class {
22
23
  add(type) {
23
24
  this.named.set(type.name, type);
24
25
  }
26
+ /**
27
+ * Declares a brand as a named type, `UserId = string & $brand<"UserId">`,
28
+ * and answers the name.
29
+ */
30
+ addBrand(brand, base, example) {
31
+ const name = sanitizeIdentifier(brand);
32
+ const existing = this.brands.get(name);
33
+ if (existing !== void 0 && (existing.brand !== brand || existing.base !== base)) throw new Error(`@kizunajs/fetch: brand ${quote(brand)} wraps both ${existing.base} and ${base}. Give each its own name.`);
34
+ if (existing === void 0) this.brands.set(name, {
35
+ brand,
36
+ base,
37
+ example
38
+ });
39
+ return name;
40
+ }
41
+ usesBrand() {
42
+ return this.brands.size > 0;
43
+ }
44
+ /**
45
+ * Each brand's name, the type it wraps, and a value for its `@example`, for
46
+ * the constructors the file exports.
47
+ */
48
+ allBrands() {
49
+ return [...this.brands].map(([name, { base, example }]) => ({
50
+ name,
51
+ base,
52
+ example
53
+ }));
54
+ }
25
55
  all() {
26
- return [...this.named.values()].sort((left, right) => left.name.localeCompare(right.name));
56
+ for (const name of this.brands.keys()) if (this.named.has(name)) throw new Error(`@kizunajs/fetch: brand ${quote(name)} has the same name as a model. Give each its own name.`);
57
+ const brands = [...this.brands].map(([name, { brand, base }]) => ({
58
+ name,
59
+ description: `A branded \`${base}\`. Responses hand one back, and \`to${name}\` makes one from a plain value.`,
60
+ body: `${base} & $brand<${quote(brand)}>`
61
+ }));
62
+ return [...this.named.values(), ...brands].sort((left, right) => left.name.localeCompare(right.name));
27
63
  }
28
64
  };
29
65
  const quote = (value) => JSON.stringify(value);
30
66
  /**
67
+ * What a brand wraps: the scalars, as the JSON carries them.
68
+ */
69
+ const BRANDABLE_BASES = new Set([
70
+ "string",
71
+ "number",
72
+ "boolean"
73
+ ]);
74
+ /**
31
75
  * A literal as the type the JSON carries, so `z.literal(true)` is `true`.
32
76
  */
33
77
  const literalType = (value) => {
@@ -160,9 +204,15 @@ const objectBody = (schema, collector, hint) => {
160
204
  };
161
205
  /**
162
206
  * The TypeScript type a schema describes. Named models are collected and
163
- * referenced; anonymous shapes are inlined.
207
+ * referenced; anonymous shapes are inlined. A brand is named like a model.
164
208
  */
165
209
  const typeOf = (schema, collector, hint) => {
210
+ const type = unbrandedTypeOf(schema, collector, hint);
211
+ const brand = readMetaBrand(schema);
212
+ if (brand === void 0 || !BRANDABLE_BASES.has(type)) return type;
213
+ return collector.addBrand(brand, type, sampleValue(schema, 0, brand));
214
+ };
215
+ const unbrandedTypeOf = (schema, collector, hint) => {
166
216
  if (isFileSchema(schema)) return "File | Blob";
167
217
  if (isBinarySchema(schema)) return "Uint8Array";
168
218
  const def = readDef(schema);
@@ -387,14 +437,27 @@ const emitRequestContext = (contract, collector) => {
387
437
  }
388
438
  return fields.length > 0 ? `export interface RequestContext {\n${indent(fields.join("\n"))}\n}` : void 0;
389
439
  };
440
+ /**
441
+ * A constructor per brand, so a value from outside the API becomes one with
442
+ * `toUserId('usr_k7f3q9')` rather than a cast.
443
+ */
444
+ const emitBrandConstructors = (collector, namespaceName) => collector.allBrands().map(({ name, base, example }) => `
445
+ ${docComment([
446
+ `Brands a value from outside the API, like a URL segment, as \`${namespaceName}.${name}\`. A value from a response is branded already.`,
447
+ "",
448
+ "@example",
449
+ `const id = to${name}(${example});`
450
+ ])}export const to${name} = (value: ${base}): ${namespaceName}.${name} => value as ${namespaceName}.${name};
451
+ `).join("");
390
452
  const generateFetchClient = (contract, options = {}) => {
391
453
  const { namespace: namespaceName = "API", runtimeModule = "@kizunajs/fetch", regenerateCommand = "kizuna generate", source } = options;
392
454
  const collector = new TypeCollector();
393
455
  const tree = emitTree(listedRoutes(contract), "", collector, namespaceName);
456
+ const requestContext = emitRequestContext(contract, collector);
394
457
  const api = [...collector.all().map((model) => {
395
458
  return `${model.description ? `/**\n * ${model.description}\n */\n` : ""}export type ${model.name} = ${model.body};`;
396
459
  }), ...tree.declarations].join("\n\n");
397
- const requestContext = emitRequestContext(contract, collector);
460
+ const brands = emitBrandConstructors(collector, namespaceName);
398
461
  return `/* eslint-disable */
399
462
  /**
400
463
  ${[
@@ -404,7 +467,7 @@ ${[
404
467
  ` * Regenerate: ${regenerateCommand}`
405
468
  ].join("\n")}
406
469
  */
407
- import { createGeneratedClient, type ClientConfig, type ClientMethod, type GeneratedRoutes } from '${runtimeModule}';
470
+ import { createGeneratedClient, ${collector.usesBrand() ? "type $brand, " : ""}type ClientConfig, type ClientMethod, type GeneratedRoutes } from '${runtimeModule}';
408
471
 
409
472
  export namespace ${namespaceName} {
410
473
  ${indent(api)}
@@ -417,7 +480,7 @@ ${indent(tree.signatures)}
417
480
  const routes: GeneratedRoutes = {
418
481
  ${indent(tree.table)}
419
482
  };
420
-
483
+ ${brands}
421
484
  ${requestContext === void 0 ? `/**
422
485
  * A client for this API. Pass the base URL and anything else the runtime takes.
423
486
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kizunajs/fetch",
3
- "version": "2.0.0-beta.11",
3
+ "version": "2.0.0-beta.13",
4
4
  "description": "Typed fetch client for Kizuna.js APIs",
5
5
  "keywords": [
6
6
  "kizunajs",
@@ -68,7 +68,7 @@
68
68
  },
69
69
  "peerDependencies": {
70
70
  "zod": "^4.0.0",
71
- "kizunajs": "2.0.0-beta.11"
71
+ "kizunajs": "2.0.0-beta.13"
72
72
  },
73
73
  "devDependencies": {
74
74
  "@types/express": "^5.0.0",
@@ -76,8 +76,8 @@
76
76
  "tsdown": "^0.21.0",
77
77
  "typescript": "^6.0.3",
78
78
  "zod": "^4.0.0",
79
- "kizunajs": "2.0.0-beta.11",
80
- "@kizunajs/express": "2.0.0-beta.11"
79
+ "kizunajs": "2.0.0-beta.13",
80
+ "@kizunajs/express": "2.0.0-beta.13"
81
81
  },
82
82
  "scripts": {
83
83
  "build": "tsdown src/index.ts src/server.ts --format esm,cjs --dts --clean --external kizunajs --external zod",