@ancplua/qyl-api-schema 7.2.0 → 7.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/README.md +22 -0
- package/generated/otel-keys.gen.tsp +21 -3
- package/generated/ts-runtime/api.js +1 -1
- package/generated/zod-runtime/index.d.ts +50 -0
- package/generated/zod-runtime/index.js +262 -0
- package/package.json +19 -5
package/README.md
CHANGED
|
@@ -59,6 +59,28 @@ rather than from prose here; this line moves faster than a README is revised.
|
|
|
59
59
|
`main.tsp` is the local compile entry point and includes emitter routing. `index.tsp`
|
|
60
60
|
is the published TypeSpec entry point and contains only the client-facing contract.
|
|
61
61
|
|
|
62
|
+
## Runtime validation
|
|
63
|
+
|
|
64
|
+
`@ancplua/qyl-api-schema/zod` builds Zod validators from the bundled JSON Schema at
|
|
65
|
+
runtime. It is a translation layer over `z.fromJSONSchema` — authored in `src/zod/`
|
|
66
|
+
and compiled to `generated/zod-runtime/` — not a second hand-written schema, so a
|
|
67
|
+
contract change cannot leave a validator describing the previous shape.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { publishedContractSchema } from "@ancplua/qyl-api-schema/zod";
|
|
71
|
+
import type { Span } from "@ancplua/qyl-api-schema/types";
|
|
72
|
+
|
|
73
|
+
const SpanSchema = publishedContractSchema<Span>("OTel.Traces.Span");
|
|
74
|
+
const span = SpanSchema.parse(await response.json());
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`zod` is an optional peer dependency (`>=4.5.0 <5`): a consumer that does not
|
|
78
|
+
install it simply never imports this subpath, and the other subpaths do not depend
|
|
79
|
+
on it. Definition names are the published `$defs` keys — the same names the OpenAPI
|
|
80
|
+
components and the generated C# and TypeScript DTOs carry — and
|
|
81
|
+
`contractDefinitionNames()` lists all of them. The type argument is not checked
|
|
82
|
+
against the schema, so pair it with the matching type from `./types`.
|
|
83
|
+
|
|
62
84
|
## Contract revision
|
|
63
85
|
|
|
64
86
|
`scripts/emit-contract-revision.mjs` stamps a deterministic revision — `sha256:` plus
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// <auto-generated/>
|
|
2
2
|
// Generated by qyl's Weaver pipeline (emit_typespec_keys.py) from:
|
|
3
3
|
// open-telemetry/semantic-conventions v1.44.0 (e10a930844c6951757a43b849d364f7d056ac32b)
|
|
4
|
-
// open-telemetry/semantic-conventions-genai
|
|
4
|
+
// open-telemetry/semantic-conventions-genai eaefa142a94cefe5d199d47e4a73727dfbd825df (eaefa142a94cefe5d199d47e4a73727dfbd825df)
|
|
5
5
|
// Schema: https://opentelemetry.io/schemas/1.44.0
|
|
6
6
|
// GenAI schema: https://opentelemetry.io/schemas/gen-ai-dev/1.42.0-dev
|
|
7
7
|
// Licensed under Apache-2.0 (inherited from OpenTelemetry upstream)
|
|
@@ -945,16 +945,34 @@ namespace Qyl.Telemetry.SemanticConventions.Keys.GenAi {
|
|
|
945
945
|
const ToolName: string = "gen_ai.tool.name";
|
|
946
946
|
/** Type of the tool utilized by the agent */
|
|
947
947
|
const ToolType: string = "gen_ai.tool.type";
|
|
948
|
-
/** The number of input tokens
|
|
949
|
-
const
|
|
948
|
+
/** The number of audio input tokens served from a provider-managed cache. */
|
|
949
|
+
const UsageAudioCacheReadInputTokens: string = "gen_ai.usage.audio.cache_read.input_tokens";
|
|
950
|
+
/** The number of audio input tokens. */
|
|
951
|
+
const UsageAudioInputTokens: string = "gen_ai.usage.audio.input_tokens";
|
|
952
|
+
/** The number of audio output tokens. */
|
|
953
|
+
const UsageAudioOutputTokens: string = "gen_ai.usage.audio.output_tokens";
|
|
950
954
|
/** The number of input tokens served from a provider-managed cache. */
|
|
951
955
|
const UsageCacheReadInputTokens: string = "gen_ai.usage.cache_read.input_tokens";
|
|
956
|
+
/** The number of input tokens written to a provider-managed cache. */
|
|
957
|
+
const UsageCacheWriteInputTokens: string = "gen_ai.usage.cache_write.input_tokens";
|
|
958
|
+
/** The number of image input tokens served from a provider-managed cache. */
|
|
959
|
+
const UsageImageCacheReadInputTokens: string = "gen_ai.usage.image.cache_read.input_tokens";
|
|
960
|
+
/** The number of image input tokens. */
|
|
961
|
+
const UsageImageInputTokens: string = "gen_ai.usage.image.input_tokens";
|
|
962
|
+
/** The number of image output tokens. */
|
|
963
|
+
const UsageImageOutputTokens: string = "gen_ai.usage.image.output_tokens";
|
|
952
964
|
/** The number of tokens used in the GenAI input (prompt). */
|
|
953
965
|
const UsageInputTokens: string = "gen_ai.usage.input_tokens";
|
|
954
966
|
/** The number of tokens used in the GenAI response (completion). */
|
|
955
967
|
const UsageOutputTokens: string = "gen_ai.usage.output_tokens";
|
|
956
968
|
/** The number of output tokens used for reasoning (e.g. chain-of-thought, extended thinking). */
|
|
957
969
|
const UsageReasoningOutputTokens: string = "gen_ai.usage.reasoning.output_tokens";
|
|
970
|
+
/** The number of text input tokens served from a provider-managed cache. */
|
|
971
|
+
const UsageTextCacheReadInputTokens: string = "gen_ai.usage.text.cache_read.input_tokens";
|
|
972
|
+
/** The number of text input tokens. */
|
|
973
|
+
const UsageTextInputTokens: string = "gen_ai.usage.text.input_tokens";
|
|
974
|
+
/** The number of text output tokens. */
|
|
975
|
+
const UsageTextOutputTokens: string = "gen_ai.usage.text.output_tokens";
|
|
958
976
|
/** Human-readable name of the GenAI workflow provided by the application. */
|
|
959
977
|
const WorkflowName: string = "gen_ai.workflow.name";
|
|
960
978
|
}
|
|
@@ -445,4 +445,4 @@ export const FetchTelemetryViewValues = {
|
|
|
445
445
|
};
|
|
446
446
|
// --- contract revision (scripts/emit-contract-revision.mjs) ---
|
|
447
447
|
/** Revision of the API contract these types were emitted from; compare against HealthReport.contract_revision. */
|
|
448
|
-
export const CONTRACT_REVISION = "sha256:
|
|
448
|
+
export const CONTRACT_REVISION = "sha256:a11cb761a9cb6534";
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* A published contract type as it looks *before* validation: branded identity
|
|
4
|
+
* scalars are still plain strings and arrays may be readonly, because branding
|
|
5
|
+
* is what `parse` produces rather than what a caller can construct.
|
|
6
|
+
*
|
|
7
|
+
* Annotating a projection with `satisfies ContractInput<T>` moves the wire-name
|
|
8
|
+
* check from a runtime `parse` throw to a compile error, so a property spelled
|
|
9
|
+
* in an internal camelCase instead of the contract's snake_case cannot reach a
|
|
10
|
+
* response at all.
|
|
11
|
+
*/
|
|
12
|
+
export type ContractInput<TContract> = TContract extends {
|
|
13
|
+
readonly __brand: string;
|
|
14
|
+
} ? string : TContract extends readonly (infer TElement)[] ? readonly ContractInput<TElement>[] : TContract extends (...args: never[]) => unknown ? TContract : TContract extends object ? {
|
|
15
|
+
readonly [K in keyof TContract]: ContractInput<TContract[K]>;
|
|
16
|
+
} : TContract;
|
|
17
|
+
/**
|
|
18
|
+
* Build a strict runtime validator for one published contract definition.
|
|
19
|
+
*
|
|
20
|
+
* `definitionName` is a key of the published schema's `$defs` — the same name
|
|
21
|
+
* the OpenAPI component and the generated C#/TypeScript DTOs carry, for example
|
|
22
|
+
* `"OTel.Traces.Span"` or `"Common.Errors.ProblemDetails"`. Pair it with the
|
|
23
|
+
* matching type from `@ancplua/qyl-api-schema/types`; the type argument is not
|
|
24
|
+
* checked against the schema, so an unrelated one produces a validator that
|
|
25
|
+
* lies about what it returns.
|
|
26
|
+
*
|
|
27
|
+
* Results are memoized per name: building a schema walks the whole definition,
|
|
28
|
+
* and the returned validator is stateless.
|
|
29
|
+
*
|
|
30
|
+
* @throws Error if no such definition is published.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { publishedContractSchema } from "@ancplua/qyl-api-schema/zod";
|
|
35
|
+
* import type { Span } from "@ancplua/qyl-api-schema/types";
|
|
36
|
+
*
|
|
37
|
+
* const SpanSchema = publishedContractSchema<Span>("OTel.Traces.Span");
|
|
38
|
+
* const span = SpanSchema.parse(await response.json());
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export declare function publishedContractSchema<TContract>(definitionName: string): z.ZodType<TContract>;
|
|
42
|
+
/**
|
|
43
|
+
* Every definition name {@link publishedContractSchema} accepts, sorted.
|
|
44
|
+
*
|
|
45
|
+
* Enumerating the published surface is how a consumer proves it covers all of
|
|
46
|
+
* it — a contract definition that gains no validator is otherwise invisible.
|
|
47
|
+
*/
|
|
48
|
+
export declare function contractDefinitionNames(): readonly string[];
|
|
49
|
+
/** The published schema's `$id`, for reporting which contract a validator came from. */
|
|
50
|
+
export declare const contractJsonSchemaId: string;
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
// Runtime validators for the contracts this package publishes.
|
|
2
|
+
//
|
|
3
|
+
// The JSON Schema in generated/json-schema/qyl-api-schema.json is the artifact a
|
|
4
|
+
// validator should be derived from, not a second hand-maintained copy of it: a
|
|
5
|
+
// contract change that a consumer forgets to mirror is exactly the failure this
|
|
6
|
+
// module exists to make impossible. Everything below is therefore a translation
|
|
7
|
+
// layer over `z.fromJSONSchema`, not a schema in its own right.
|
|
8
|
+
//
|
|
9
|
+
// The package's own schema is reached by self-reference through the "exports"
|
|
10
|
+
// map, so this file resolves the same artifact from source, from
|
|
11
|
+
// generated/zod-runtime, and from inside an installed node_modules copy.
|
|
12
|
+
import contractJsonSchema from "@ancplua/qyl-api-schema/json-schema" with { type: "json" };
|
|
13
|
+
import { z } from "zod";
|
|
14
|
+
/**
|
|
15
|
+
* Rewrite the published JSON Schema into the subset `z.fromJSONSchema` accepts,
|
|
16
|
+
* preserving the contract's meaning exactly. Three published constructs need it:
|
|
17
|
+
*
|
|
18
|
+
* - `unevaluatedProperties` — `z.fromJSONSchema` rejects the keyword outright.
|
|
19
|
+
* The emitted schemas only ever use it to seal an object, which
|
|
20
|
+
* `additionalProperties` expresses for the same shapes.
|
|
21
|
+
* - `allOf` inheritance — a derived object carries a single `$ref` base plus its
|
|
22
|
+
* own properties. Zod builds that as an intersection, and an intersection of a
|
|
23
|
+
* sealed base with a sealed extension accepts neither side's extra keys but
|
|
24
|
+
* also loses the base's own key set, so the derived object stops rejecting
|
|
25
|
+
* unknown keys. Flattening the base into the derivative keeps one sealed
|
|
26
|
+
* object with the full key set.
|
|
27
|
+
* - 64-bit integers — Zod's `int64` conversion adds a +/-2^53 range check. The
|
|
28
|
+
* contract has no such bound (Unix nanosecond timestamps exceed it), so the
|
|
29
|
+
* integer rule is kept as `multipleOf: 1` and the format is dropped.
|
|
30
|
+
*
|
|
31
|
+
* `format: "date-time"` is deliberately left untouched: Zod 4.5's conversion is
|
|
32
|
+
* already RFC 3339 offset-aware (it accepts `+02:00`, requires seconds, and
|
|
33
|
+
* rejects colon-less offsets). scripts/verify-zod-contracts.mjs pins that.
|
|
34
|
+
* One consequence for consumers that republish a validator through
|
|
35
|
+
* `z.toJSONSchema`: the output carries `format: "date-time"` next to the RFC
|
|
36
|
+
* 3339 pattern, so a pinned snapshot of such output changes when a consumer
|
|
37
|
+
* moves onto this module.
|
|
38
|
+
*/
|
|
39
|
+
function adaptPublishedSchemaForZod(schemaNode) {
|
|
40
|
+
if (typeof schemaNode !== "object" || schemaNode === null || Array.isArray(schemaNode)) {
|
|
41
|
+
throw new Error("Published Qyl JSON Schema root must be an object");
|
|
42
|
+
}
|
|
43
|
+
const root = schemaNode;
|
|
44
|
+
const sourceDefinitions = asSchemaRecord(root.$defs, "$defs");
|
|
45
|
+
const definitionCache = new Map();
|
|
46
|
+
const resolvingDefinitions = new Set();
|
|
47
|
+
const definition = (name) => {
|
|
48
|
+
const cached = definitionCache.get(name);
|
|
49
|
+
if (cached)
|
|
50
|
+
return cached;
|
|
51
|
+
// Inheritance is flattened by inlining the base, so a cycle would recurse
|
|
52
|
+
// forever rather than fail; a $ref cycle that is not inheritance is fine and
|
|
53
|
+
// never reaches here.
|
|
54
|
+
if (resolvingDefinitions.has(name)) {
|
|
55
|
+
throw new Error(`Published Qyl JSON Schema has an inheritance cycle at '${name}'`);
|
|
56
|
+
}
|
|
57
|
+
const source = sourceDefinitions[name];
|
|
58
|
+
if (source === undefined) {
|
|
59
|
+
throw new Error(`Published Qyl JSON Schema is missing definition '${name}'`);
|
|
60
|
+
}
|
|
61
|
+
resolvingDefinitions.add(name);
|
|
62
|
+
const adapted = adaptNode(source);
|
|
63
|
+
resolvingDefinitions.delete(name);
|
|
64
|
+
definitionCache.set(name, adapted);
|
|
65
|
+
return adapted;
|
|
66
|
+
};
|
|
67
|
+
const adaptNode = (node) => {
|
|
68
|
+
if (typeof node !== "object" || node === null || Array.isArray(node)) {
|
|
69
|
+
throw new Error("Expected a JSON Schema object node");
|
|
70
|
+
}
|
|
71
|
+
const source = node;
|
|
72
|
+
const inheritedDefinitions = inheritedObjectDefinitions(source, definition);
|
|
73
|
+
const adapted = {};
|
|
74
|
+
if (inheritedDefinitions.length > 0) {
|
|
75
|
+
let inheritedAdditionalProperties;
|
|
76
|
+
for (const inherited of inheritedDefinitions) {
|
|
77
|
+
const existingProperties = optionalSchemaRecord(adapted.properties);
|
|
78
|
+
const inheritedProperties = optionalSchemaRecord(inherited.properties);
|
|
79
|
+
const existingRequired = stringArray(adapted.required);
|
|
80
|
+
const inheritedRequired = stringArray(inherited.required);
|
|
81
|
+
Object.assign(adapted, inherited);
|
|
82
|
+
adapted.properties = { ...existingProperties, ...inheritedProperties };
|
|
83
|
+
adapted.required = [...new Set([...existingRequired, ...inheritedRequired])];
|
|
84
|
+
inheritedAdditionalProperties = inherited.additionalProperties;
|
|
85
|
+
}
|
|
86
|
+
// The derivative documents itself and seals itself; inheriting either
|
|
87
|
+
// would describe the wrong type and could reopen a sealed object.
|
|
88
|
+
delete adapted.description;
|
|
89
|
+
delete adapted.additionalProperties;
|
|
90
|
+
for (const [keyword, child] of Object.entries(source)) {
|
|
91
|
+
if (keyword === "allOf" ||
|
|
92
|
+
keyword === "properties" ||
|
|
93
|
+
keyword === "required" ||
|
|
94
|
+
keyword === "unevaluatedProperties") {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
adapted[keyword] = adaptValue(child);
|
|
98
|
+
}
|
|
99
|
+
adapted.properties = {
|
|
100
|
+
...optionalSchemaRecord(adapted.properties),
|
|
101
|
+
...adaptSchemaRecord(source.properties),
|
|
102
|
+
};
|
|
103
|
+
const required = [
|
|
104
|
+
...new Set([...stringArray(adapted.required), ...stringArray(source.required)]),
|
|
105
|
+
];
|
|
106
|
+
if (required.length > 0)
|
|
107
|
+
adapted.required = required;
|
|
108
|
+
else
|
|
109
|
+
delete adapted.required;
|
|
110
|
+
if ("unevaluatedProperties" in source) {
|
|
111
|
+
adapted.additionalProperties = adaptUnevaluatedProperties(source.unevaluatedProperties);
|
|
112
|
+
}
|
|
113
|
+
else if (inheritedAdditionalProperties !== undefined) {
|
|
114
|
+
adapted.additionalProperties = inheritedAdditionalProperties;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
for (const [keyword, child] of Object.entries(source)) {
|
|
119
|
+
if (keyword === "unevaluatedProperties")
|
|
120
|
+
continue;
|
|
121
|
+
adapted[keyword] = adaptValue(child);
|
|
122
|
+
}
|
|
123
|
+
if ("unevaluatedProperties" in source) {
|
|
124
|
+
adapted.additionalProperties = adaptUnevaluatedProperties(source.unevaluatedProperties);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (source.type === "integer" &&
|
|
128
|
+
(source.format === "int64" || source.format === "uint64")) {
|
|
129
|
+
adapted.type = "number";
|
|
130
|
+
adapted.multipleOf = 1;
|
|
131
|
+
delete adapted.format;
|
|
132
|
+
}
|
|
133
|
+
return adapted;
|
|
134
|
+
};
|
|
135
|
+
const adaptValue = (value) => {
|
|
136
|
+
if (Array.isArray(value))
|
|
137
|
+
return value.map(adaptValue);
|
|
138
|
+
if (typeof value === "object" && value !== null)
|
|
139
|
+
return adaptNode(value);
|
|
140
|
+
return value;
|
|
141
|
+
};
|
|
142
|
+
const adaptSchemaRecord = (value) => {
|
|
143
|
+
const record = optionalSchemaRecord(value);
|
|
144
|
+
return Object.fromEntries(Object.entries(record).map(([name, child]) => [name, adaptValue(child)]));
|
|
145
|
+
};
|
|
146
|
+
const adaptUnevaluatedProperties = (value) => isFalseSchema(value) ? false : adaptValue(value);
|
|
147
|
+
const rootWithoutDefinitions = { ...root };
|
|
148
|
+
delete rootWithoutDefinitions.$defs;
|
|
149
|
+
const adaptedRoot = adaptNode(rootWithoutDefinitions);
|
|
150
|
+
// Re-attached after adaptation so every `#/$defs/...` reference in an adapted
|
|
151
|
+
// node resolves against the adapted definitions rather than the source ones.
|
|
152
|
+
adaptedRoot.$defs = Object.fromEntries(Object.keys(sourceDefinitions).map((name) => [name, definition(name)]));
|
|
153
|
+
return adaptedRoot;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* The base definitions a node inherits from, or none if this is not the
|
|
157
|
+
* single-`$ref`-base object inheritance the emitter produces. Anything else —
|
|
158
|
+
* an `allOf` of inline schemas, a non-object base — is left for Zod to build as
|
|
159
|
+
* a real intersection.
|
|
160
|
+
*/
|
|
161
|
+
function inheritedObjectDefinitions(source, resolve) {
|
|
162
|
+
if (source.type !== "object" || !Array.isArray(source.allOf))
|
|
163
|
+
return [];
|
|
164
|
+
const names = source.allOf.map((entry) => definitionNameFromRef(entry));
|
|
165
|
+
if (names.some((name) => name === undefined))
|
|
166
|
+
return [];
|
|
167
|
+
const inherited = names.map((name) => resolve(name));
|
|
168
|
+
return inherited.every((definition) => definition.type === "object") ? inherited : [];
|
|
169
|
+
}
|
|
170
|
+
function definitionNameFromRef(value) {
|
|
171
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
172
|
+
return undefined;
|
|
173
|
+
const record = value;
|
|
174
|
+
if (Object.keys(record).length !== 1 || typeof record.$ref !== "string")
|
|
175
|
+
return undefined;
|
|
176
|
+
const prefix = "#/$defs/";
|
|
177
|
+
return record.$ref.startsWith(prefix) ? record.$ref.slice(prefix.length) : undefined;
|
|
178
|
+
}
|
|
179
|
+
function asSchemaRecord(value, context) {
|
|
180
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
181
|
+
throw new Error(`Published Qyl JSON Schema ${context} must be an object`);
|
|
182
|
+
}
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
function optionalSchemaRecord(value) {
|
|
186
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
187
|
+
? value
|
|
188
|
+
: {};
|
|
189
|
+
}
|
|
190
|
+
function stringArray(value) {
|
|
191
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
192
|
+
? value
|
|
193
|
+
: [];
|
|
194
|
+
}
|
|
195
|
+
/** `{ "not": {} }` — how the emitter spells "no further properties". */
|
|
196
|
+
function isFalseSchema(value) {
|
|
197
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
198
|
+
return false;
|
|
199
|
+
const record = value;
|
|
200
|
+
const not = record.not;
|
|
201
|
+
return Object.keys(record).length === 1 &&
|
|
202
|
+
typeof not === "object" &&
|
|
203
|
+
not !== null &&
|
|
204
|
+
!Array.isArray(not) &&
|
|
205
|
+
Object.keys(not).length === 0;
|
|
206
|
+
}
|
|
207
|
+
const zodCompatibleContractJsonSchema = adaptPublishedSchemaForZod(contractJsonSchema);
|
|
208
|
+
const zodCompatibleDefinitions = asSchemaRecord(zodCompatibleContractJsonSchema.$defs, "$defs");
|
|
209
|
+
const publishedContractSchemas = new Map();
|
|
210
|
+
/**
|
|
211
|
+
* Build a strict runtime validator for one published contract definition.
|
|
212
|
+
*
|
|
213
|
+
* `definitionName` is a key of the published schema's `$defs` — the same name
|
|
214
|
+
* the OpenAPI component and the generated C#/TypeScript DTOs carry, for example
|
|
215
|
+
* `"OTel.Traces.Span"` or `"Common.Errors.ProblemDetails"`. Pair it with the
|
|
216
|
+
* matching type from `@ancplua/qyl-api-schema/types`; the type argument is not
|
|
217
|
+
* checked against the schema, so an unrelated one produces a validator that
|
|
218
|
+
* lies about what it returns.
|
|
219
|
+
*
|
|
220
|
+
* Results are memoized per name: building a schema walks the whole definition,
|
|
221
|
+
* and the returned validator is stateless.
|
|
222
|
+
*
|
|
223
|
+
* @throws Error if no such definition is published.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* ```ts
|
|
227
|
+
* import { publishedContractSchema } from "@ancplua/qyl-api-schema/zod";
|
|
228
|
+
* import type { Span } from "@ancplua/qyl-api-schema/types";
|
|
229
|
+
*
|
|
230
|
+
* const SpanSchema = publishedContractSchema<Span>("OTel.Traces.Span");
|
|
231
|
+
* const span = SpanSchema.parse(await response.json());
|
|
232
|
+
* ```
|
|
233
|
+
*/
|
|
234
|
+
export function publishedContractSchema(definitionName) {
|
|
235
|
+
const cached = publishedContractSchemas.get(definitionName);
|
|
236
|
+
if (cached)
|
|
237
|
+
return cached;
|
|
238
|
+
if (!Object.hasOwn(zodCompatibleDefinitions, definitionName)) {
|
|
239
|
+
throw new Error(`Published Qyl JSON Schema has no '${definitionName}' definition`);
|
|
240
|
+
}
|
|
241
|
+
const schema = z.fromJSONSchema({
|
|
242
|
+
$schema: zodCompatibleContractJsonSchema.$schema,
|
|
243
|
+
$defs: zodCompatibleDefinitions,
|
|
244
|
+
$ref: `#/$defs/${definitionName}`,
|
|
245
|
+
});
|
|
246
|
+
publishedContractSchemas.set(definitionName, schema);
|
|
247
|
+
return schema;
|
|
248
|
+
}
|
|
249
|
+
const definitionNames = Object.freeze(Object.keys(zodCompatibleDefinitions).sort());
|
|
250
|
+
/**
|
|
251
|
+
* Every definition name {@link publishedContractSchema} accepts, sorted.
|
|
252
|
+
*
|
|
253
|
+
* Enumerating the published surface is how a consumer proves it covers all of
|
|
254
|
+
* it — a contract definition that gains no validator is otherwise invisible.
|
|
255
|
+
*/
|
|
256
|
+
export function contractDefinitionNames() {
|
|
257
|
+
return definitionNames;
|
|
258
|
+
}
|
|
259
|
+
/** The published schema's `$id`, for reporting which contract a validator came from. */
|
|
260
|
+
export const contractJsonSchemaId = typeof contractJsonSchema.$id === "string"
|
|
261
|
+
? contractJsonSchema.$id
|
|
262
|
+
: "";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ancplua/qyl-api-schema",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.4.0",
|
|
4
4
|
"description": "TypeSpec source of truth for qyl API contracts. Emits OpenAPI, JSON Schema, Qyl.Api.Contracts DTOs, and TypeScript contract types; not an OpenTelemetry package, storage schema, or server implementation.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -46,6 +46,11 @@
|
|
|
46
46
|
},
|
|
47
47
|
"./json-schema": {
|
|
48
48
|
"default": "./generated/json-schema/qyl-api-schema.json"
|
|
49
|
+
},
|
|
50
|
+
"./zod": {
|
|
51
|
+
"types": "./generated/zod-runtime/index.d.ts",
|
|
52
|
+
"import": "./generated/zod-runtime/index.js",
|
|
53
|
+
"default": "./generated/zod-runtime/index.js"
|
|
49
54
|
}
|
|
50
55
|
},
|
|
51
56
|
"files": [
|
|
@@ -75,6 +80,8 @@
|
|
|
75
80
|
"generated/ts-runtime/api.js",
|
|
76
81
|
"generated/openapi/qyl.openapi.json",
|
|
77
82
|
"generated/json-schema/qyl-api-schema.json",
|
|
83
|
+
"generated/zod-runtime/index.d.ts",
|
|
84
|
+
"generated/zod-runtime/index.js",
|
|
78
85
|
"README.md",
|
|
79
86
|
"LICENSE"
|
|
80
87
|
],
|
|
@@ -87,17 +94,19 @@
|
|
|
87
94
|
"clean:generated": "node scripts/clean-generated.mjs",
|
|
88
95
|
"build:emitters": "tsc -p emitters/csharp && tsc -p emitters/ts-types && tsc -p emitters/qyl-lint",
|
|
89
96
|
"build:ts-runtime": "tsc -p tsconfig.contracts.json",
|
|
97
|
+
"build:zod-runtime": "tsc -p tsconfig.zod.json",
|
|
90
98
|
"build:json-schema": "node scripts/openapi-to-json-schema.mjs",
|
|
91
99
|
"emit:mcp-tool-schemas": "node scripts/emit-mcp-tool-schemas.mjs",
|
|
92
100
|
"emit:contract-revision": "node scripts/emit-contract-revision.mjs",
|
|
93
101
|
"prepare": "npm run build:emitters",
|
|
94
102
|
"prepack": "npm run compile",
|
|
95
|
-
"compile": "npm run clean:generated && npm run build:emitters && tsp compile main.tsp && npm run emit:contract-revision && npm run build:json-schema && npm run emit:mcp-tool-schemas && npm run build:ts-runtime",
|
|
103
|
+
"compile": "npm run clean:generated && npm run build:emitters && tsp compile main.tsp && npm run emit:contract-revision && npm run build:json-schema && npm run emit:mcp-tool-schemas && npm run build:ts-runtime && npm run build:zod-runtime",
|
|
96
104
|
"format": "tsp format **/*.tsp",
|
|
97
105
|
"lint": "npm run build:emitters && tsp compile main.tsp --no-emit --warn-as-error",
|
|
98
106
|
"lint:public": "npm run build:emitters && tsp compile index.tsp --no-emit --warn-as-error",
|
|
99
107
|
"verify:routes": "node scripts/verify-route-contracts.mjs",
|
|
100
108
|
"verify:contracts": "node scripts/verify-contract-fixtures.mjs",
|
|
109
|
+
"verify:zod-contracts": "npm run build:zod-runtime && node scripts/verify-zod-contracts.mjs",
|
|
101
110
|
"verify:lint-rules": "npm run build:emitters && node scripts/verify-lint-rules.mjs"
|
|
102
111
|
},
|
|
103
112
|
"peerDependencies": {
|
|
@@ -106,7 +115,8 @@
|
|
|
106
115
|
"@typespec/http": "^1.13.0",
|
|
107
116
|
"@typespec/openapi": "^1.13.0",
|
|
108
117
|
"@typespec/openapi3": "^1.13.0",
|
|
109
|
-
"@typespec/sse": ">=0.83.0 <0.86.0"
|
|
118
|
+
"@typespec/sse": ">=0.83.0 <0.86.0",
|
|
119
|
+
"zod": ">=4.5.0 <5"
|
|
110
120
|
},
|
|
111
121
|
"peerDependenciesMeta": {
|
|
112
122
|
"@typespec/compiler": {
|
|
@@ -126,13 +136,16 @@
|
|
|
126
136
|
},
|
|
127
137
|
"@typespec/sse": {
|
|
128
138
|
"optional": true
|
|
139
|
+
},
|
|
140
|
+
"zod": {
|
|
141
|
+
"optional": true
|
|
129
142
|
}
|
|
130
143
|
},
|
|
131
144
|
"devDependencies": {
|
|
132
145
|
"@ancplua/typespec-emit-csharp": "file:./emitters/csharp",
|
|
133
146
|
"@ancplua/typespec-emit-ts-types": "file:./emitters/ts-types",
|
|
134
147
|
"@ancplua/typespec-qyl-lint": "file:./emitters/qyl-lint",
|
|
135
|
-
"@types/node": "26.
|
|
148
|
+
"@types/node": "26.3.0",
|
|
136
149
|
"@typespec/compiler": "1.15.0",
|
|
137
150
|
"@typespec/events": "0.85.0",
|
|
138
151
|
"@typespec/http": "1.15.0",
|
|
@@ -140,6 +153,7 @@
|
|
|
140
153
|
"@typespec/openapi3": "1.15.0",
|
|
141
154
|
"@typespec/sse": "0.85.0",
|
|
142
155
|
"ajv": "8.20.0",
|
|
143
|
-
"typescript": "7.0.2"
|
|
156
|
+
"typescript": "7.0.2",
|
|
157
|
+
"zod": "4.5.4"
|
|
144
158
|
}
|
|
145
159
|
}
|