@xxxaz/json-rpc-schema-typing 0.10.20 → 0.10.22

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.
@@ -0,0 +1,121 @@
1
+ import { LazyResolvers } from "@xxxaz/stream-api-json/utility";
2
+ import { JsonRpcSchema } from "../router/JsonRpcRouter.js";
3
+ import { GenereteId, JsonRpcClient } from "./JsonRpcClient.js";
4
+ import { isRpcResponse, readStreamAll } from "../utility.js";
5
+ import { JsonRpcRequest, JsonRpcResponse, MessageInput, MessageOutput } from "../types.js";
6
+
7
+ type JsonRpcMessagePortClientOptions<Sch extends JsonRpcSchema> = {
8
+ schema: Sch;
9
+ generateId?: GenereteId;
10
+ targetOrigin?: string;
11
+ } & ({
12
+ input: MessageInput;
13
+ output: MessageOutput;
14
+ port?: undefined;
15
+ }|{
16
+ input?: undefined;
17
+ output?: undefined;
18
+ port: MessageInput & MessageOutput;
19
+ });
20
+
21
+ type ProccessedMessage = {
22
+ request: JsonRpcRequest;
23
+ resolver: LazyResolvers<JsonRpcResponse<any>>;
24
+ };
25
+
26
+ export class JsonRpcMessagePortClient<Sch extends JsonRpcSchema> extends JsonRpcClient<Sch> {
27
+ readonly #targetOrigin: string;
28
+ readonly #input: MessageInput;
29
+ readonly #output: MessageOutput;
30
+
31
+ constructor(options: JsonRpcMessagePortClientOptions<Sch>) {
32
+ const { schema, generateId } = options;
33
+ super({
34
+ schema,
35
+ generateId,
36
+ post: (request) => this.#request(request),
37
+ });
38
+
39
+ this.#input = options.port ?? options.input;
40
+ this.#output = options.port ?? options.output;
41
+ this.#targetOrigin = options.targetOrigin ?? globalThis.origin ?? '*';
42
+
43
+ this.#input.addEventListener('message', this.#receive.bind(this));
44
+ }
45
+
46
+ async #request(stream: ReadableStream<string>) {
47
+ const blob = await readStreamAll(stream);
48
+ const request = JSON.parse(await blob.text());
49
+ if (request instanceof Array) {
50
+ return this.#batch(request);
51
+ }
52
+
53
+ const response = await this.#post(request);
54
+ return new ReadableStream({
55
+ async start(controller) {
56
+ if (response) {
57
+ controller.enqueue(JSON.stringify(response));
58
+ }
59
+ controller.close();
60
+ }
61
+ });
62
+ }
63
+
64
+ async #batch(requests: JsonRpcRequest[]) {
65
+ const entries = requests
66
+ .map(req => [ req.id!, this.#post(req)! ] as const)
67
+ .filter(([ id, promise ]) => promise)
68
+ const promises = new Map(entries);
69
+
70
+ return new ReadableStream({
71
+ async start(controller) {
72
+ controller.enqueue('[');
73
+ while(promises.size > 0) {
74
+ const response = await Promise.race(promises.values());
75
+ controller.enqueue(JSON.stringify(response));
76
+ promises.delete(response.id!);
77
+ }
78
+ controller.enqueue(']');
79
+ controller.close();
80
+ }
81
+ });
82
+ };
83
+
84
+ readonly #pool = new Map<number|string, ProccessedMessage>();
85
+ #post(request: JsonRpcRequest) {
86
+ let resolver: LazyResolvers<JsonRpcResponse<any>>|null = null;
87
+ if (request.id != null) {
88
+ resolver = new LazyResolvers<JsonRpcResponse<any>>();
89
+ this.#pool.set(request.id, { request, resolver });
90
+ }
91
+
92
+ if(this.#output instanceof Window) {
93
+ this.#output.postMessage(request, this.#targetOrigin);
94
+ } else {
95
+ this.#output.postMessage(request);
96
+ }
97
+
98
+ return resolver?.promise ?? null;
99
+ }
100
+
101
+ async #receive(event: MessageEvent) {
102
+ if (event.source && event.source !== this.#output) return;
103
+ const data = JSON.parse(event.data);
104
+ if (!isRpcResponse(data)) {
105
+ console.debug('message is not JsonRpcResponse', data);
106
+ return;
107
+ }
108
+ if (data.id == null) {
109
+ console.debug('Reponse message is not Unidentifiable', data);
110
+ return;
111
+ }
112
+ const pooled = this.#pool.get(data.id);
113
+ if (!pooled) {
114
+ console.warn('Orphan rpc response.', data)
115
+ return;
116
+ }
117
+ pooled.resolver.resolve(data);
118
+ this.#pool.delete(data.id);
119
+ }
120
+
121
+ }
@@ -0,0 +1,143 @@
1
+ import { LazyResolvers } from "@xxxaz/stream-api-json/utility";
2
+ import { type JsonSerializable } from "@xxxaz/stream-api-json";
3
+ import { JsonRpcSchema } from "../router/JsonRpcRouter.js";
4
+ import { GenereteId, JsonRpcClient } from "./JsonRpcClient.js";
5
+ import { isRpcResponse, readStreamAll } from "../utility.js";
6
+ import { JsonRpcError, JsonRpcRequest, JsonRpcResponse } from "../types.js";
7
+ import { WebSocketWrapper, WrapableWebSocket, wrapWebSocket } from '../WebSocketWrapper.js';
8
+
9
+ type JsonRpcWebSocketClientOptions<Sch extends JsonRpcSchema, Skt extends WrapableWebSocket> = {
10
+ schema: Sch;
11
+ socket: Skt;
12
+ generateId?: GenereteId;
13
+ };
14
+
15
+ type ProccessedMessage = {
16
+ request: JsonRpcRequest;
17
+ resolver: LazyResolvers<JsonRpcResponse<any>>;
18
+ };
19
+
20
+ export class JsonRpcWebSocketClient<Sch extends JsonRpcSchema, Skt extends WrapableWebSocket> extends JsonRpcClient<Sch> {
21
+ readonly #socket: WebSocketWrapper;
22
+
23
+ constructor(options: JsonRpcWebSocketClientOptions<Sch, Skt>) {
24
+ const { schema, generateId } = options;
25
+ super({
26
+ schema,
27
+ generateId,
28
+ post: (request) => this.#request(request),
29
+ });
30
+ this.#socket = wrapWebSocket(
31
+ options.socket,
32
+ this.#receive.bind(this),
33
+ (close) => {
34
+ console.debug('socket closed', close);
35
+ const { code, reason, wasClean } = close;
36
+ this.rejectAll({
37
+ code: -32000,
38
+ message: 'Connection closed',
39
+ data: { code, reason, wasClean } as JsonSerializable,
40
+ });
41
+ }
42
+ );
43
+ }
44
+
45
+ get available() {
46
+ const { CLOSING, CLOSED } = WebSocket;
47
+ const state = this.#socket.readyState;
48
+ return state !== CLOSING && state !== CLOSED;
49
+ }
50
+
51
+ async #request(stream: ReadableStream<string>) {
52
+ const blob = await readStreamAll(stream);
53
+ const request = JSON.parse(await blob.text());
54
+ if (request instanceof Array) {
55
+ return this.#batch(request);
56
+ }
57
+
58
+ const response = await this.#post(request);
59
+ return new ReadableStream({
60
+ async start(controller) {
61
+ if (response) {
62
+ controller.enqueue(JSON.stringify(response));
63
+ }
64
+ controller.close();
65
+ }
66
+ });
67
+ }
68
+
69
+ async #batch(requests: JsonRpcRequest[]) {
70
+ const entries = requests
71
+ .map(req => [ req.id!, this.#post(req)! ] as const)
72
+ .filter(([ id, promise ]) => promise)
73
+ const promises = new Map(entries);
74
+
75
+ return new ReadableStream({
76
+ async start(controller) {
77
+ const initialCount = promises.size;
78
+ controller.enqueue('[');
79
+ while(promises.size > 0) {
80
+ if (promises.size < initialCount) {
81
+ controller.enqueue(',');
82
+ }
83
+ const response = await Promise.race(promises.values());
84
+ controller.enqueue(JSON.stringify(response));
85
+ promises.delete(response.id!);
86
+ }
87
+ controller.enqueue(']');
88
+ controller.close();
89
+ }
90
+ });
91
+ };
92
+
93
+ readonly #pool = new Map<number|string, ProccessedMessage>();
94
+ #post(request: JsonRpcRequest) {
95
+ if (!this.available) {
96
+ return Promise.resolve<JsonRpcResponse<any>>({
97
+ jsonrpc: '2.0',
98
+ id: request.id!,
99
+ error: {
100
+ code: -32000,
101
+ message: 'Connection is not available',
102
+ data: null,
103
+ },
104
+ });
105
+ }
106
+ let resolver: LazyResolvers<JsonRpcResponse<any>>|null = null;
107
+ if (request.id != null) {
108
+ resolver = new LazyResolvers<JsonRpcResponse<any>>();
109
+ this.#pool.set(request.id, { request, resolver });
110
+ }
111
+ this.#socket.send(request);
112
+ return resolver?.promise ?? null;
113
+ }
114
+
115
+ async #receive(data: JsonSerializable) {
116
+ if (!isRpcResponse(data)) {
117
+ console.debug('message is not JsonRpcResponse', data);
118
+ return;
119
+ }
120
+ if (data.id == null) {
121
+ console.debug('Reponse message is not Unidentifiable', data);
122
+ return;
123
+ }
124
+ const pooled = this.#pool.get(data.id);
125
+ if (!pooled) {
126
+ console.warn('Orphan rpc response.', data)
127
+ return;
128
+ }
129
+ pooled.resolver.resolve(data);
130
+ this.#pool.delete(data.id);
131
+ }
132
+
133
+ rejectAll(reason: JsonRpcError) {
134
+ for (const { request: { id }, resolver } of this.#pool.values()) {
135
+ resolver.resolve({
136
+ jsonrpc: '2.0',
137
+ id: id!,
138
+ error: reason,
139
+ });
140
+ }
141
+ this.#pool.clear();
142
+ }
143
+ }
@@ -0,0 +1,3 @@
1
+ export * from './JsonRpcClient.js';
2
+ export * from './JsonRpcHttpClient.js';
3
+ export * from './JsonRpcHttp2Client.js';
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+
2
+ export * from './JsonRpcException.js';
3
+ export * from './JsonRpcMethod.js';
4
+ export * from './JsonSchemaValidator.js';
5
+
6
+ export * from './types.js';
7
+
8
+ export * from './client/index.js';
9
+ export * from './schemas/index.js';
10
+ export * from './router/index.js';
11
+ export * from './server/index.js';
@@ -0,0 +1,78 @@
1
+ import { lstatSync, readdirSync, existsSync } from 'fs';
2
+ import { JsonRpcMethodDefinition } from "../JsonRpcMethod.js";
3
+ import { JsonRpcRouter } from "./JsonRpcRouter.js";
4
+
5
+ type RouteCache<Ctx> = {
6
+ [path: string]: FileSystemRouter<Ctx>|JsonRpcMethodDefinition<Ctx, any, any>|null;
7
+ };
8
+
9
+ export class FileSystemRouter<Ctx> extends JsonRpcRouter<Ctx> {
10
+ constructor(private readonly rootDir: string) {
11
+ super();
12
+ if (!existsSync(rootDir)) throw new Error(`Not found: ${rootDir}`);
13
+ const stat = lstatSync(rootDir);
14
+ if (!stat.isDirectory()) throw new Error(`Not a directory: ${rootDir}`);
15
+ }
16
+
17
+ // instanceof JsonRpcMethodDefinition だと参照先モジュールが複数存在した際に一致しなくなる
18
+ static #isDefinition(obj: any): obj is JsonRpcMethodDefinition<any, any, any> {
19
+ const key = obj?.constructor?.method;
20
+ return typeof key === 'symbol' && obj[key] instanceof Function;
21
+ }
22
+
23
+ #cache = {} as RouteCache<Ctx>;
24
+ async resolveChild(methodPath: string) {
25
+ if (methodPath in this.#cache) return this.#cache[methodPath];
26
+ const path = `${this.rootDir}/${methodPath}`;
27
+ if (existsSync(path)) {
28
+ if (lstatSync(path).isDirectory()) {
29
+ return this.#cache[methodPath] = new FileSystemRouter<Ctx>(path);
30
+ }
31
+ }
32
+
33
+ const filePath
34
+ = existsSync(`${path}.js`)
35
+ ? `${path}.js`
36
+ : existsSync(`${path}.ts`)
37
+ ? `${path}.ts`
38
+ : null;
39
+ if (!filePath) return this.#cache[methodPath] = null;
40
+
41
+ try {
42
+ const module = await import(filePath);
43
+ if (FileSystemRouter.#isDefinition(module.default)) {
44
+ return this.#cache[methodPath] = module.default;
45
+ }
46
+ } catch(e) {
47
+ console.error(`Invalid routing: ${path}`, e);
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ async resolve(methodPath: string|string[]): Promise<JsonRpcRouter<Ctx>|JsonRpcMethodDefinition<Ctx, any, any>|null> {
54
+ if (!methodPath || methodPath.length === 0) return this;
55
+ if (typeof methodPath === 'string') methodPath = methodPath.split('.');
56
+ const [head, ...tail] = methodPath;
57
+
58
+ const child = await this.resolveChild(head);
59
+ if (tail.length === 0) return child;
60
+ if (!child) {
61
+ console.warn(`Route not found: ${head}`);
62
+ return null;
63
+ }
64
+
65
+ if (FileSystemRouter.#isDefinition(child)) {
66
+ console.warn(`Method appear middle in route : ${head}`);
67
+ return null;
68
+ }
69
+
70
+ return child.resolve(tail);
71
+ }
72
+
73
+ async * enumerate(): AsyncIterable<string> {
74
+ for(const name of readdirSync(this.rootDir)) {
75
+ yield name.replace(/\.(js|ts)$/, '');
76
+ }
77
+ }
78
+ }
@@ -0,0 +1,62 @@
1
+ import { type JsonSerializable } from "@xxxaz/stream-api-json";
2
+ import { JsonRpcMethodDefinition, JsonRpcMethodSchema, ParameterSchema, Params, Return } from "../JsonRpcMethod.js";
3
+ import { hashObject } from "./hashObject.js";
4
+
5
+ type AsyncFunction<P, R> = P extends ParameterSchema ? (...params: Params<P>) => Promise<Return<R>> : never;
6
+ type NoticeFunction<P> = P extends ParameterSchema ? (...params: Params<P>) => void : never;
7
+
8
+ export abstract class JsonRpcRouter<Context = {}> {
9
+ abstract resolve(methodPath: string): Promise<JsonRpcRouter<Context>|JsonRpcMethodDefinition<Context, any, any>|null>;
10
+ abstract resolveChild(methodPath: string): Promise<JsonRpcRouter<Context>|JsonRpcMethodDefinition<Context, any, any>|null>;
11
+ abstract enumerate(): AsyncIterable<string>;
12
+
13
+ async schema(): Promise<JsonRpcSchema> {
14
+ const entries = [] as [string, JsonRpcMethodSchema<any, any>|JsonRpcSchema][];
15
+ for await (const key of this.enumerate()) {
16
+ const child = await this.resolveChild(key);
17
+ if (!child) continue;
18
+ if (child instanceof JsonRpcRouter) {
19
+ const schema = await child.schema();
20
+ if (Object.keys(schema).length > 0) {
21
+ entries.push([key, schema]);
22
+ }
23
+ continue;
24
+ }
25
+ const { $params, $return } = child;
26
+ entries.push([key, { $params, $return }]);
27
+ }
28
+ return Object.fromEntries(entries) as JsonRpcSchema;
29
+ }
30
+
31
+
32
+ async schemaTypeScript() {
33
+ const schema = await this.schema();
34
+ const hash = await hashObject(schema);
35
+ return [
36
+ `export default ${JSON.stringify(schema)} as const;`,
37
+ `export const hash = ${JSON.stringify(hash)} as const;`,
38
+ ].join('\n');
39
+ }
40
+ }
41
+
42
+ export type JsonRpcSchema = JsonSerializable & {
43
+ readonly [path: string]: JsonRpcMethodSchema<any, any>|JsonRpcSchema;
44
+ };
45
+
46
+ export type JsonRpcAccessor<Router extends JsonRpcRouter|JsonRpcSchema> = {
47
+ readonly [Key in keyof Router]
48
+ : Router[Key] extends JsonRpcRouter|JsonRpcSchema
49
+ ? JsonRpcAccessor<Router[Key]>
50
+ : Router[Key] extends JsonRpcMethodSchema<any, any>
51
+ ? AsyncFunction<Router[Key]['$params'], Router[Key]['$return']>
52
+ : never;
53
+ }
54
+
55
+ export type JsonRpcNotice<Router extends JsonRpcRouter|JsonRpcSchema> = {
56
+ readonly [Key in keyof Router]
57
+ : Router[Key] extends JsonRpcRouter|JsonRpcSchema
58
+ ? JsonRpcNotice<Router[Key]>
59
+ : Router[Key] extends JsonRpcMethodSchema<any, any>
60
+ ? NoticeFunction<Router[Key]['$params']>
61
+ : never;
62
+ }
@@ -0,0 +1,51 @@
1
+ import { JsonRpcMethodDefinition, JsonRpcMethodSchema } from "../JsonRpcMethod.js";
2
+ import { JsonRpcRouter, JsonRpcSchema } from "./JsonRpcRouter.js";
3
+
4
+ type RouteMap<Ctx> = {
5
+ readonly [path: string]: RouteMap<Ctx>|JsonRpcMethodDefinition<Ctx, any, any>|Promise<RouteMap<Ctx>|JsonRpcMethodDefinition<Ctx, any, any>|undefined>;
6
+ };
7
+
8
+ type RouteCache<Ctx> = {
9
+ [path: string]: StaticRouter<Ctx>|JsonRpcMethodDefinition<Ctx, any, any>|null;
10
+ };
11
+
12
+ export class StaticRouter<Ctx> extends JsonRpcRouter<Ctx> {
13
+ constructor(private readonly routeMap: RouteMap<Ctx>) {
14
+ super();
15
+ }
16
+
17
+ #cache = {} as RouteCache<Ctx>;
18
+ async resolveChild(methodPath: string) {
19
+ if (methodPath in this.#cache) return this.#cache[methodPath];
20
+ const child = await this.routeMap[methodPath];
21
+ if (!child) return this.#cache[methodPath] = null;
22
+ if (child instanceof JsonRpcMethodDefinition) return this.#cache[methodPath] = child;
23
+ return this.#cache[methodPath] = new StaticRouter<Ctx>(child);
24
+ }
25
+
26
+ async resolve(methodPath: string|string[]): Promise<JsonRpcRouter<Ctx>|JsonRpcMethodDefinition<Ctx, any, any>|null> {
27
+ if (!methodPath || methodPath.length === 0) return this;
28
+ if (typeof methodPath === 'string') methodPath = methodPath.split('.');
29
+ const [head, ...tail] = methodPath;
30
+
31
+ const child = await this.resolveChild(head);
32
+ if (tail.length === 0) return child;
33
+ if (!child) {
34
+ console.warn(`Route not found: ${head}`);
35
+ return null;
36
+ }
37
+
38
+ if (child instanceof JsonRpcMethodDefinition) {
39
+ console.warn(`Method appear middle in route : ${head}`);
40
+ return null;
41
+ }
42
+
43
+ return child.resolve(tail);
44
+ }
45
+
46
+ async * enumerate(): AsyncIterable<string> {
47
+ for(const key in this.routeMap) {
48
+ yield key;
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,31 @@
1
+ import { type JsonSerializable } from "@xxxaz/stream-api-json";
2
+
3
+ export async function hashObject(src: JsonSerializable): Promise<string> {
4
+ const json = orderGuaranteeJson(src);
5
+ const srcBuffer = await new Blob([json]).arrayBuffer();
6
+ const hashBuffer = await crypto.subtle.digest("SHA-256", srcBuffer);
7
+ return [...new Uint8Array(hashBuffer)].map(b => b.toString(16).padStart(2, '0')).join('');
8
+ }
9
+
10
+ function orderGuaranteeJson(src: JsonSerializable): string {
11
+ if (src === undefined) return 'null';
12
+ if (src === null) return 'null';
13
+ switch (typeof src) {
14
+ case 'string':
15
+ case 'number':
16
+ case 'boolean':
17
+ return JSON.stringify(src);
18
+ case 'object':
19
+ if (Array.isArray(src)) {
20
+ const stringfied = src.map(orderGuaranteeJson);
21
+ return `[${stringfied.join(',')}]`;
22
+ }
23
+ const stringfied = Object.entries(src)
24
+ .filter(([k, v]) => v !== undefined)
25
+ .sort(([k1], [k2]) => k1 < k2 ? -1 : 1)
26
+ .map(([k, v]) => JSON.stringify(k) + ':' + orderGuaranteeJson(v));
27
+ return `{${stringfied.join(',')}}`;
28
+ default:
29
+ throw new TypeError('Unexpected type');
30
+ }
31
+ }
@@ -0,0 +1,3 @@
1
+ export * from './FileSystemRouter.js';
2
+ export * from './JsonRpcRouter.js';
3
+ export * from './StaticRouter.js';
@@ -0,0 +1,92 @@
1
+ import { type Primitive } from "@xxxaz/stream-api-json";
2
+ import { JSONSchema } from "../types.js";
3
+
4
+ export function $Enum<T extends (null|boolean|number|string)[]>(...elements: T) {
5
+ return {
6
+ enum: elements
7
+ } as const;
8
+ }
9
+
10
+ export function $EnumKeys<T extends object>(obj: T) {
11
+ return {
12
+ enum: Object.keys(obj) as [(keyof T)]
13
+ } as const;
14
+ }
15
+
16
+ export function $EnumValues<T extends { [key: string]: Primitive }>(obj: T) {
17
+ return {
18
+ enum: Object.values(obj) as [T[keyof T]],
19
+ } as const;
20
+ };
21
+
22
+ export function $Expand<Src extends JSONSchema, Ex extends Partial<JSONSchema>>(source: Src, expand: Ex) {
23
+ return {
24
+ ...source,
25
+ ...expand,
26
+ } as const;
27
+ }
28
+
29
+ type Optional<T> = { oneOf: (T|false)[] } | { anyOf: (T|false)[] };
30
+ export function $Optional<T extends JSONSchema>(item: T) {
31
+ return {
32
+ oneOf: [item, false]
33
+ } as const;
34
+ }
35
+
36
+ $Optional.is = (item: unknown): item is Optional<any> => {
37
+ const lits = (item as any)?.oneOf ?? (item as any)?.anyOf ?? null;
38
+ if (!lits) return false;
39
+ return (lits.some((i: any) => i === false));
40
+ };
41
+
42
+ type Unwrapped<T> = T extends Optional<infer U> ? U : T;
43
+ $Optional.unwrap = <T extends JSONSchema|undefined>(item: T): Unwrapped<T> => {
44
+ const optionalList = item?.oneOf ?? item?.anyOf ?? null;
45
+ if (!optionalList) return item as Unwrapped<T>;
46
+ const list = optionalList.filter(i => i !== false);
47
+ if (list.length === 1) return list[0] as Unwrapped<T>;
48
+ return item?.oneOf
49
+ ? { oneOf: list } as Unwrapped<T>
50
+ : { anyOf: list } as Unwrapped<T>;
51
+ };
52
+
53
+ export function $And<Schemas extends JSONSchema[]>(...allOf: Schemas) {
54
+ return {
55
+ allOf
56
+ } as const;
57
+ }
58
+
59
+ export function $Or<Schemas extends JSONSchema[]>(...anyOf: Schemas) {
60
+ return {
61
+ anyOf
62
+ } as const;
63
+ }
64
+
65
+ export function $Xor<Schemas extends JSONSchema[]>(...oneOf: Schemas) {
66
+ return {
67
+ oneOf
68
+ } as const;
69
+ }
70
+
71
+ export function $Omit<
72
+ T extends JSONSchema & { type: 'object' },
73
+ K extends keyof T['properties'] & string,
74
+ >(schema: T, keys: readonly K[]) {
75
+ const properties = Object.fromEntries(
76
+ Object.entries(schema.properties ?? {}).filter(
77
+ ([key]) => !keys.includes(key as K)
78
+ )
79
+ );
80
+ const required = (schema.required ?? []).filter(
81
+ (k) => !keys.includes(k as K)
82
+ );
83
+ return {
84
+ type: 'object',
85
+ properties: properties as Omit<T['properties'], K>,
86
+ required: required as T['required'] extends (infer R)[]
87
+ ? [Exclude<R, K>]
88
+ : never,
89
+ additionalProperties:
90
+ schema.additionalProperties as T['additionalProperties'],
91
+ } as const;
92
+ }
@@ -0,0 +1,21 @@
1
+
2
+ export const $Null = {
3
+ type: 'null',
4
+ nullable: true,
5
+ } as const;
6
+
7
+ export const $Boolean = {
8
+ type: 'boolean',
9
+ } as const;
10
+
11
+ export const $Number = {
12
+ type: 'number',
13
+ } as const;
14
+
15
+ export const $Integer = {
16
+ type: 'integer',
17
+ } as const;
18
+
19
+ export const $String = {
20
+ type: 'string',
21
+ } as const;
@@ -0,0 +1,42 @@
1
+ import { IsOptionalSchema, JSONSchema, Max, RequiredKeys } from "../types.js";
2
+ import { $Optional } from "./Complex.js";
3
+
4
+ export function $Array<T extends JSONSchema>(items: T) {
5
+ return {
6
+ type: 'array',
7
+ items
8
+ } as const;
9
+ }
10
+
11
+ export type ExcludeOptional<T extends readonly any[]>
12
+ = T extends [...infer A, infer L]
13
+ ? IsOptionalSchema<L, ExcludeOptional<A>, T>
14
+ : T;
15
+
16
+ export function $Tuple<T extends readonly JSONSchema[]>(...items: T) {
17
+ // NOTE: Optionalを含む場合、Ajvが以下のような通知を吐きます
18
+ // strict mode: "items" is 2-tuple, but minItems or maxItems/additionalItems are not specified or different at path "#"
19
+ const optionalNumber = [...items].reverse().findIndex((i: JSONSchema) => $Optional.is(i)) + 1;
20
+ return {
21
+ type: 'array',
22
+ items: items.map(item => $Optional.unwrap(item)!) as any as T,
23
+ minItems: items.length - optionalNumber as ExcludeOptional<T>['length'],
24
+ maxItems: items.length as Max<T['length']>,
25
+ additionalItems: false,
26
+ } as const;
27
+ }
28
+
29
+ type ObjectDefine = {
30
+ [key: string]: JSONSchema;
31
+ };
32
+ export function $Object<T extends ObjectDefine>(properties: T) {
33
+ const required = Object.entries(properties)
34
+ .filter(([key, value]) => !$Optional.is(value))
35
+ .map(([key]) => key) as [RequiredKeys<T>];
36
+ return {
37
+ type: 'object',
38
+ additionalProperties: false,
39
+ required,
40
+ properties: properties as T
41
+ } as const;
42
+ }
@@ -0,0 +1,3 @@
1
+ export * from './Primitive.js';
2
+ export * from './Complex.js';
3
+ export * from './Structure.js';