Skip to content

Commit 7d1330e

Browse files
authored
chore: update lichess api schema (#40)
* chore: update lichess api schema * fix: update `servers` generation * chore: update dependencies, fix lint warnings
1 parent 0fc90a5 commit 7d1330e

24 files changed

Lines changed: 239 additions & 170 deletions

bun.lock

Lines changed: 64 additions & 64 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,21 +31,21 @@
3131
"gen": "bun run --sequential gen:schemas gen:client format",
3232
"test": "bun test",
3333
"typecheck": "tsgo --noEmit",
34-
"lint": "biome check ./src",
34+
"lint": "biome check",
3535
"format": "biome check --write --linter-enabled=false",
3636
"build": "bunx --bun tsdown",
3737
"prepublishOnly": "bun run build"
3838
},
3939
"dependencies": {
40-
"minizod": "^0.0.3"
40+
"minizod": "0.0.3"
4141
},
4242
"devDependencies": {
43-
"@biomejs/biome": "2.4.12",
44-
"@gameroman/config": "^0.1.0",
45-
"@types/bun": "^1.3.11",
46-
"@typescript/native-preview": "^7.0.0-dev.20260401.1",
47-
"tsdown": "^0.21.7",
48-
"zod": "^4.3.6"
43+
"@biomejs/biome": "2.4.15",
44+
"@gameroman/config": "0.1.0",
45+
"@types/bun": "1.3.14",
46+
"@typescript/native-preview": "7.0.0-dev.20260517.1",
47+
"tsdown": "0.22.0",
48+
"zod": "4.4.3"
4949
},
5050
"engines": {
5151
"node": ">=24"

scripts/gen-client.ts

Lines changed: 44 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
OperationParameterBase,
77
OperationQueryParameterSchema,
88
type QueryParamSchemaSchema,
9+
recordToObject,
10+
refToName,
911
type Schema,
1012
SchemaSchema,
1113
SchemaSchemaBoolean,
@@ -38,15 +40,17 @@ const OpenApiSchemaPaths = z.record(OpenApiSchemaPath, SchemaSchemaRef);
3840

3941
const OpenApiSchemaComponents = z.object();
4042

43+
const OpenApiServersSchema = z.tuple([
44+
z.object({ url: z.literal("https://lichess.org") }),
45+
z.object({ url: z.literal("https://lichess.dev") }),
46+
z.object({ url: z.literal("http://localhost:{port}") }),
47+
z.object({ url: z.literal("http://l.org") }),
48+
]);
49+
4150
const OpenApiSchemaSchema = z.object({
4251
openapi: z.literal("3.1.0"),
4352
info: OpenApiSchemaInfo,
44-
servers: z.tuple([
45-
z.object({ url: z.literal("https://lichess.org") }),
46-
z.object({ url: z.literal("https://lichess.dev") }),
47-
z.object({ url: z.literal("http://localhost:{port}") }),
48-
z.object({ url: z.literal("http://l.org") }),
49-
]),
53+
servers: OpenApiServersSchema,
5054
tags: z.array(z.object({ name: z.string(), description: z.string() })),
5155
paths: OpenApiSchemaPaths,
5256
components: OpenApiSchemaComponents,
@@ -248,10 +252,30 @@ const ResponseStatus = z.string();
248252

249253
const OperationResponses = z.record(ResponseStatus, ResponseSchema);
250254

255+
const LichessServerSchema = z.union([
256+
z.object({
257+
url: z.literal([
258+
"https://engine.lichess.ovh",
259+
"https://explorer.lichess.org",
260+
"https://tablebase.lichess.org",
261+
]),
262+
}),
263+
z.object({
264+
url: z.literal("http://localhost:{port}"),
265+
variables: z.object({ port: z.object({ default: z.string() }) }),
266+
}),
267+
]);
268+
269+
const LichessServersSchema = z
270+
.tuple([LichessServerSchema])
271+
.rest(LichessServerSchema)
272+
.transform((s) => ({ url: s[0].url, __id: "__servers" as const }));
273+
251274
const BaseTagSchemaOperation = z.object({
252275
operationId: z.string(),
253276
summary: z.string(),
254277
description: z.string(),
278+
servers: LichessServersSchema.optional(),
255279
tags: z.array(z.string()),
256280
security: SecuritySchema,
257281
parameters: OperationParameters,
@@ -312,18 +336,7 @@ const TagSchemaSchemaPut = BaseTagSchemaOperation.extend({
312336

313337
const TagSchemaSchema = z
314338
.object({
315-
servers: z
316-
.tuple([
317-
z.object({
318-
url: z.literal([
319-
"https://engine.lichess.ovh",
320-
"https://explorer.lichess.org",
321-
"https://tablebase.lichess.org",
322-
]),
323-
}),
324-
])
325-
.transform((s) => ({ url: s[0].url, __id: "__servers" as const }))
326-
.optional(),
339+
servers: LichessServersSchema.optional(),
327340
parameters: z
328341
.array(OperationPathParameterSchema)
329342
.transform((s) => ({ parameters: s, __id: "__parameters" as const }))
@@ -453,13 +466,13 @@ function schemaToTypescriptTypes(
453466
case "$ref":
454467
case "notverified:reftoprimitive": {
455468
const ref = schema.$ref;
456-
const name = ref.split("/").pop()!.replace(".yaml", "");
469+
const name = refToName(ref);
457470
const typescriptSchema = `schemas.${name}` as const;
458471
return typescriptSchema;
459472
}
460473
case "notverified:reftoprimitive:nullable": {
461474
const ref = schema.allOf[0].$ref;
462-
const name = ref.split("/").pop()!.replace(".yaml", "");
475+
const name = refToName(ref);
463476
const typescriptSchema = `schemas.${name} | null` as const;
464477
return typescriptSchema;
465478
}
@@ -474,15 +487,7 @@ function schemaToTypescriptTypes(
474487
: (`?: ${typescriptSchema}` as const);
475488
objectRecord[k] = propStr;
476489
}
477-
const entries = Object.entries(objectRecord);
478-
if (entries.length === 1) {
479-
return `{ "${entries[0]![0]}" ${entries[0]![1]} }` as const;
480-
}
481-
return (
482-
"{\n" +
483-
entries.map(([k, v]) => ` "${k}" ${v},` as const).join("\n") +
484-
"\n}"
485-
);
490+
return recordToObject(objectRecord, { colon: false });
486491
}
487492
case "boolean":
488493
case "boolean-like": {
@@ -524,7 +529,7 @@ function schemaToTypescriptTypes(
524529
}
525530
case "array:notverified:reftoprimitive": {
526531
const ref = schema.items.$ref;
527-
const name = ref.split("/").pop()!.replace(".yaml", "");
532+
const name = refToName(ref);
528533
const typescriptSchema = `schemas.${name}` as const;
529534
return `(${typescriptSchema})[]` as const;
530535
}
@@ -552,32 +557,15 @@ function extractQueryParams(queryParams: OperationQueryParameter[]) {
552557
? `: ${typescriptSchema}`
553558
: `?: ${typescriptSchema}`;
554559
}
555-
const entries = Object.entries(params);
556-
if (entries.length === 1) {
557-
return `{ "${entries[0]![0]}" ${entries[0]![1]} }` as const;
558-
}
559-
return (
560-
"{\n" +
561-
entries.map(([k, v]) => ` "${k}" ${v},` as const).join("\n") +
562-
"\n}"
563-
);
560+
return recordToObject(params, { colon: false });
564561
}
565562

566563
function extractPathParams(pathParams: OperationPathParameter[]) {
567564
const params: Record<string, string> = {};
568565
for (const param of pathParams) {
569-
const typescriptSchema = schemaToTypescriptTypes(param.schema);
570-
params[param.name] = `: ${typescriptSchema}` as const;
571-
}
572-
const entries = Object.entries(params);
573-
if (entries.length === 1) {
574-
return `{ "${entries[0]![0]}" ${entries[0]![1]} }` as const;
566+
params[param.name] = schemaToTypescriptTypes(param.schema);
575567
}
576-
return (
577-
"{\n" +
578-
entries.map(([k, v]) => ` "${k}" ${v},` as const).join("\n") +
579-
"\n}"
580-
);
568+
return recordToObject(params);
581569
}
582570

583571
function extractBodyTypes(bodySchema: Schema) {
@@ -665,10 +653,12 @@ function processOperation(
665653
requestObjCode = `${requestPieces.join(", ")}`;
666654
}
667655

656+
const baseUrl = options?.baseUrl || operation.servers?.url;
657+
668658
let baseUrlLine = "";
669659
let baseUrlArg = "";
670-
if (options?.baseUrl) {
671-
baseUrlLine = ` const baseUrl = "${options.baseUrl}";\n`;
660+
if (baseUrl) {
661+
baseUrlLine = ` const baseUrl = "${baseUrl}";\n`;
672662
baseUrlArg = ", baseUrl";
673663
}
674664

@@ -692,8 +682,8 @@ ${baseUrlLine}\
692682

693683
function processTag(tagSchema: TagSchema, rawApiPath: string) {
694684
const methodsCode: string[] = [];
695-
let sharedPathParams: OperationPathParameter[] | undefined = undefined;
696-
let baseUrl: string | undefined = undefined;
685+
let sharedPathParams: OperationPathParameter[] | undefined;
686+
let baseUrl: string | undefined;
697687

698688
for (const operation of Object.values(tagSchema)) {
699689
const processedOperation = processOperation(operation, rawApiPath, {

scripts/gen-schemas.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import * as fs from "node:fs/promises";
33
import * as path from "node:path";
44
import { ZodError } from "zod";
55
import { prettifyError } from "zod/mini";
6-
import { convertToZod, SchemaSchema } from "./shared";
6+
import { convertToZod, refToName, SchemaSchema, StringYamlRef } from "./shared";
77

88
async function processFile(filePath: string) {
99
const normalizedFilePath = filePath.replaceAll("\\", "/");
10-
const fileName = normalizedFilePath.split("/").pop()!.replace(".yaml", "");
10+
const fileName = refToName(StringYamlRef.parse(normalizedFilePath));
1111
const yamlStr = await Bun.file(normalizedFilePath).text();
1212
const yamlContent = Bun.YAML.parse(yamlStr);
1313
const parsedSchema = SchemaSchema.safeParse(yamlContent);

scripts/shared.ts

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as path from "node:path";
12
import * as z from "zod";
23

34
const SchemaUnparsed = z
@@ -29,6 +30,8 @@ const StringYamlRef = z
2930
.templateLiteral([z.string(), ".yaml"])
3031
.brand("StringYamlRef");
3132

33+
type StringYamlRef = z.infer<typeof StringYamlRef>;
34+
3235
const SchemaSchemaRef = BaseSchema.extend({
3336
type: z.literal("object").optional(),
3437
$ref: StringYamlRef,
@@ -240,6 +243,33 @@ function parseUnparsed(schema: Unparsed) {
240243

241244
type ConvertResult = { readonly zodSchema: string; readonly refs: string[] };
242245

246+
function refToName(ref: StringYamlRef) {
247+
return path.basename(ref).replace(".yaml", "");
248+
}
249+
250+
function recordToObject(
251+
object: Record<string, string>,
252+
options = { colon: true },
253+
) {
254+
const entries = Object.entries(object);
255+
256+
const colon = options.colon ? ":" : "";
257+
258+
if (entries.length <= 1) {
259+
const firstEntry = entries[0];
260+
if (firstEntry) {
261+
return `{ "${firstEntry[0]}"${colon} ${firstEntry[1]} }`;
262+
}
263+
return "Record<string, never>";
264+
}
265+
266+
const entriesKv = entries
267+
.map(([k, v]) => ` "${k}"${colon} ${v},`)
268+
.join("\n");
269+
270+
return `{\n${entriesKv}\n}`;
271+
}
272+
243273
function convertToZod(schema: Schema, prefix: string = ""): ConvertResult {
244274
if (schema.const !== undefined) {
245275
return {
@@ -252,7 +282,7 @@ function convertToZod(schema: Schema, prefix: string = ""): ConvertResult {
252282
switch (schema.__schema) {
253283
case "$ref": {
254284
const ref = schema.$ref;
255-
const name = ref.split("/").pop()!.replace(".yaml", "");
285+
const name = refToName(ref);
256286
const prefixedName = `${prefix}${name}` as const;
257287
return { zodSchema: prefixedName, refs: prefix ? [] : [name] } as const;
258288
}
@@ -262,7 +292,9 @@ function convertToZod(schema: Schema, prefix: string = ""): ConvertResult {
262292
);
263293
const zodSchemas = subResults.map((r) => r.zodSchema);
264294
const allRefs = new Set<string>();
265-
subResults.forEach((r) => r.refs.forEach((ref) => allRefs.add(ref)));
295+
subResults.forEach(
296+
(r) => void r.refs.forEach((ref) => void allRefs.add(ref)),
297+
);
266298
return {
267299
zodSchema: `z.union([${zodSchemas.join(", ")}])`,
268300
refs: Array.from(allRefs),
@@ -286,10 +318,8 @@ function convertToZod(schema: Schema, prefix: string = ""): ConvertResult {
286318
case "anyOf": {
287319
const refNames: string[] = [];
288320
const allRefs = new Set<string>();
289-
for (const [_, refYaml] of Object.entries(
290-
schema.discriminator.mapping,
291-
)) {
292-
const name = refYaml.split("/").pop()!.replace(".yaml", "");
321+
for (const [_, ref] of Object.entries(schema.discriminator.mapping)) {
322+
const name = refToName(ref);
293323
refNames.push(prefix + name);
294324
if (!prefix) allRefs.add(name);
295325
}
@@ -387,20 +417,14 @@ function convertToZod(schema: Schema, prefix: string = ""): ConvertResult {
387417
parseUnparsed(v),
388418
prefix,
389419
);
390-
propRefs.forEach((r) => allRefs.add(r));
420+
propRefs.forEach((r) => void allRefs.add(r));
391421
let propStr = sch;
392422
if (!required.has(k)) {
393423
propStr = `z.optional(${propStr})`;
394424
}
395425
zodProps[k] = propStr;
396426
}
397-
const entries = Object.entries(zodProps);
398-
const inner =
399-
entries.length === 1
400-
? `{ "${entries[0]![0]}": ${entries[0]![1]} }`
401-
: "{\n" +
402-
entries.map(([k, v]) => ` "${k}": ${v},`).join("\n") +
403-
"\n}";
427+
const inner = recordToObject(zodProps);
404428
return {
405429
zodSchema: `z.object(${inner})`,
406430
refs: Array.from(allRefs),
@@ -471,6 +495,9 @@ export {
471495
assertNever,
472496
convertToZod,
473497
QueryParamSchemaSchema,
498+
recordToObject,
499+
refToName,
474500
SchemaSchema,
475501
SchemaSchemaRef,
502+
StringYamlRef,
476503
};

specs/lichess-api.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
openapi: "3.1.0"
22
info:
3-
version: 2.0.139
3+
version: 2.0.143
44
title: Lichess.org API reference
55
contact:
66
name: "Lichess.org API"
@@ -219,7 +219,8 @@ tags:
219219
220220
Runs <https://github.com/lichess-org/lila-openingexplorer>.
221221
222-
**The endpoint hostname is not lichess.org but explorer.lichess.org.**
222+
> [!important]
223+
> The hostname for these endpoints is `explorer.lichess.org` and not `lichess.org`.
223224
- name: Puzzles
224225
description: |
225226
Fetch and solve [puzzles](https://lichess.org/training), view your puzzle history and dashboard.
@@ -241,7 +242,8 @@ tags:
241242
description: |
242243
Lookup positions from the [Lichess tablebase server](https://lichess.org/blog/W3WeMyQAACQAdfAL/7-piece-syzygy-tablebases-are-complete).
243244
244-
**The endpoint hostname is not lichess.org but tablebase.lichess.org.**
245+
> [!important]
246+
> The hostname for these endpoints is `tablebase.lichess.org` and not `lichess.org`.
245247
- name: Teams
246248
description: |
247249
Access and manage Lichess teams and their members.

specs/schemas/BroadcastGameEntry.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ properties:
2020
description: The change in rating for the player as a result of this game
2121
fideTC:
2222
$ref: "./FideTimeControl.yaml"
23+
ongoing:
24+
type: boolean
2325

2426
required:
2527
- round

specs/schemas/BroadcastTour.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ properties:
6262
description: "Full tournament description in markdown format, or in HTML if the html=1 query parameter is set."
6363
teamTable:
6464
type: boolean
65+
showTeamScores:
66+
type: boolean
6567
url:
6668
type: string
6769
format: uri

0 commit comments

Comments
 (0)