Skip to content

Commit 60bcb35

Browse files
authored
Embed json schema using a script (#71)
* Embed JSON schema into config.ts using a script * Bump version * Split func
1 parent 5829c33 commit 60bcb35

File tree

10 files changed

+331
-30
lines changed

10 files changed

+331
-30
lines changed

apps/class-solid/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "class-solid",
33
"private": true,
4-
"version": "0.0.8",
4+
"version": "0.0.9",
55
"type": "module",
66
"scripts": {
77
"dev": "vinxi dev",

apps/class-solid/src/components/NamedConfig.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
import {
2-
type PartialConfig,
3-
ajv,
4-
jsonSchemaOfConfig,
5-
} from "@classmodel/class/validate";
1+
import { jsonSchemaOfConfig } from "@classmodel/class/config";
2+
import { type PartialConfig, ajv } from "@classmodel/class/validate";
63
import type { JSONSchemaType } from "ajv";
74

85
type NamedAndDescription = {

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"private": true,
3-
"version": "0.0.8",
3+
"version": "0.0.9",
44
"scripts": {
55
"build": "turbo build",
66
"dev": "turbo dev",

packages/class/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,10 @@ This package is part of a [monorepo](https://github.com/classmodel/class-web) wi
6868

6969
The Class model uses a JSON schema to validate the input configuration. The schema is defined in the `@classmodel/class` package and can be found at [src/config.json](https://github.com/classmodel/class-web/blob/main/packages/class/src/config.json) (in [repo](./src/config.json)). The schema is used to validate the input configuration and to generate a form to input the configuration.
7070

71-
If any changes are made to the `src/config.json` file then the Typescript type need to be regenerated with the following command:
71+
The `src/config.ts` file contains the embedded JSON schema and its Typescript type definition.
72+
When runnning `pnpm dev` or `pnpm build` the `src/config.ts` file is generated from the `src/config.json` file.
73+
74+
To manually generate the `src/config.ts` file run the following command:
7275

7376
```shell
7477
pnpm json2ts

packages/class/package.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@classmodel/class",
33
"description": "Chemistry Land-surface Atmosphere Soil Slab (CLASS) model",
44
"type": "module",
5-
"version": "0.0.8",
5+
"version": "0.0.9",
66
"exports": {
77
"./package.json": "./package.json",
88
"./class": {
@@ -54,17 +54,20 @@
5454
"files": ["dist"],
5555
"license": "GPL-3.0-only",
5656
"scripts": {
57-
"dev": "tsc --watch",
58-
"build": "tsc",
57+
"dev": "concurrently -n \"tsc,json2ts\" -c \"blue,green\" \"pnpm dev:tsc\" \"pnpm json2ts --watch\"",
58+
"dev:tsc": "tsc --watch",
59+
"build:tsc": "tsc",
60+
"build": "pnpm json2ts && pnpm build:tsc",
5961
"prepack": "pnpm build",
6062
"test": "tsx --test src/*.test.ts",
6163
"typecheck": "tsc --noEmit",
62-
"json2ts": "json2ts src/config.json src/config.ts",
64+
"json2ts": "node scripts/json2ts.mjs",
6365
"docs": "typedoc"
6466
},
6567
"bin": "./dist/cli.js",
6668
"devDependencies": {
6769
"@types/node": "^20.13.1",
70+
"concurrently": "^9.0.1",
6871
"json-schema-to-typescript": "^15.0.2",
6972
"tsx": "^4.16.5",
7073
"typedoc": "^0.26.10",

packages/class/scripts/json2ts.mjs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* Convert JSON schema file to TypeScript file with embedded JSON schema and type definition.
3+
*
4+
* Some Javascript runtimes cannot import JSON files as ES module, so embedding the JSON schema is a workaround.
5+
*/
6+
import { exec as cbExec } from "node:child_process";
7+
import { watch } from "node:fs";
8+
import { readFile, writeFile } from "node:fs/promises";
9+
import { basename, extname, join } from "node:path";
10+
import { promisify } from "node:util";
11+
12+
import { Command } from "@commander-js/extra-typings";
13+
import { compileFromFile } from "json-schema-to-typescript";
14+
15+
async function format(schemaTsPath) {
16+
// Format the generated TypeScript file
17+
// Expect this script to be run from packages/class
18+
const exec = promisify(cbExec);
19+
const biomeHome = join(process.cwd(), "../..");
20+
const { stdout, stderr } = await exec(
21+
`pnpm biome format --write packages/class/${schemaTsPath}`,
22+
{
23+
cwd: biomeHome,
24+
},
25+
);
26+
console.log(stdout);
27+
console.error(stderr);
28+
}
29+
30+
async function readJsonSchema(jsonSchemaPath) {
31+
const jsonSchema = await readFile(jsonSchemaPath, "utf-8");
32+
const trimmedJsonSchema = jsonSchema.trim();
33+
return trimmedJsonSchema;
34+
}
35+
36+
function prefixOfJsonSchema(jsonSchemaPath) {
37+
const fn = basename(jsonSchemaPath, extname(jsonSchemaPath));
38+
// json-schema-to-typescript generates type names with base of filename with the first letter capitalized
39+
const prefix = fn.charAt(0).toUpperCase() + fn.slice(1);
40+
return prefix;
41+
}
42+
43+
/**
44+
*
45+
* @param {string} jsonSchemaPath
46+
* @param {string} schemaTsPath
47+
*/
48+
async function json2ts(jsonSchemaPath, schemaTsPath) {
49+
// Geneerate TypeScript type definition from JSON schema
50+
const tsOfJsonSchema = await compileFromFile(jsonSchemaPath, {
51+
format: false,
52+
bannerComment: "",
53+
});
54+
55+
const prefix = prefixOfJsonSchema(jsonSchemaPath);
56+
57+
// Read JSON schema file
58+
const trimmedJsonSchema = await readJsonSchema(jsonSchemaPath);
59+
60+
// Combine types and JSON schema into a single TypeScript file
61+
const body = `\
62+
/**
63+
* This file was automatically generated by "../scripts/json2ts.mjs" script.
64+
* DO NOT MODIFY IT BY HAND. Instead, modify the JSON schema file "${jsonSchemaPath}",
65+
* and run "pnpm json2ts" to regenerate this file.
66+
*/
67+
import type { JSONSchemaType } from "ajv/dist/2019.js";
68+
${tsOfJsonSchema}
69+
export type JsonSchemaOf${prefix} = JSONSchemaType<${prefix}>;
70+
/**
71+
* JSON schema of ${jsonSchemaPath} embedded in a TypeScript file.
72+
*/
73+
export const jsonSchemaOf${prefix} = ${trimmedJsonSchema} as unknown as JsonSchemaOf${prefix};
74+
`;
75+
await writeFile(schemaTsPath, body, { flag: "w" });
76+
77+
await format(schemaTsPath);
78+
}
79+
80+
function watchJsonSchema(jsonSchemaPath, schemaTsPath) {
81+
console.log(`Watching ${jsonSchemaPath} for changes`);
82+
watch(jsonSchemaPath, (event) => {
83+
if (event !== "change") {
84+
return;
85+
}
86+
console.log(
87+
`File ${jsonSchemaPath} has been changed, regenerating ${schemaTsPath}`,
88+
);
89+
json2ts(jsonSchemaPath, schemaTsPath).catch((err) => {
90+
console.error(err);
91+
});
92+
});
93+
}
94+
95+
function main() {
96+
const program = new Command()
97+
.name("json2ts")
98+
.description(
99+
"Convert JSON schema file to TypeScript file with embedded JSON schema and type definition",
100+
)
101+
.option(
102+
"-i, --input <jsonSchemaPath>",
103+
"JSON schema file",
104+
"src/config.json",
105+
)
106+
.option("-o, --output <schemaTsPath>", "Output file path", "src/config.ts")
107+
.option("--watch", "Watch mode")
108+
.parse();
109+
const options = program.opts();
110+
const jsonSchemaPath = options.input;
111+
const schemaTsPath = options.output;
112+
113+
if (options.watch) {
114+
watchJsonSchema(jsonSchemaPath, schemaTsPath);
115+
} else {
116+
console.log(`Generating ${schemaTsPath} from ${jsonSchemaPath}`);
117+
json2ts(jsonSchemaPath, schemaTsPath).catch((err) => {
118+
console.error(err);
119+
process.exit(1);
120+
});
121+
}
122+
}
123+
124+
main();

packages/class/src/config.ts

Lines changed: 145 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
/* eslint-disable */
21
/**
3-
* This file was automatically generated by json-schema-to-typescript.
4-
* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
5-
* and run json-schema-to-typescript to regenerate this file.
2+
* This file was automatically generated by "../scripts/json2ts.mjs" script.
3+
* DO NOT MODIFY IT BY HAND. Instead, modify the JSON schema file "src/config.json",
4+
* and run "pnpm json2ts" to regenerate this file.
65
*/
7-
6+
import type { JSONSchemaType } from "ajv/dist/2019.js";
87
export type ABLHeight = number;
98
/**
109
* The potential temperature of the mixed layer at the initial time.
@@ -50,3 +49,144 @@ export interface MixedLayer {
5049
divU: HorizontalLargeScaleDivergenceOfWind;
5150
beta: EntrainmentRatioForVirtualHeat;
5251
}
52+
53+
export type JsonSchemaOfConfig = JSONSchemaType<Config>;
54+
/**
55+
* JSON schema of src/config.json embedded in a TypeScript file.
56+
*/
57+
export const jsonSchemaOfConfig = {
58+
type: "object",
59+
properties: {
60+
initialState: {
61+
type: "object",
62+
properties: {
63+
h_0: {
64+
type: "number",
65+
default: 200,
66+
title: "ABL height",
67+
unit: "m",
68+
},
69+
theta_0: {
70+
type: "number",
71+
default: 288,
72+
title: "Mixed-layer potential temperature",
73+
minimum: 0,
74+
description:
75+
"The potential temperature of the mixed layer at the initial time.",
76+
unit: "K",
77+
},
78+
dtheta_0: {
79+
type: "number",
80+
default: 1,
81+
title: "Temperature jump at h",
82+
unit: "K",
83+
},
84+
q_0: {
85+
type: "number",
86+
default: 0.008,
87+
unit: "kg kg-1",
88+
title: "Mixed-layer specific humidity",
89+
},
90+
dq_0: {
91+
type: "number",
92+
default: -0.001,
93+
unit: "kg kg-1",
94+
title: "Specific humidity jump at h",
95+
},
96+
},
97+
additionalProperties: false,
98+
required: ["h_0", "theta_0", "dtheta_0", "q_0", "dq_0"],
99+
title: "Initial State",
100+
default: {},
101+
},
102+
timeControl: {
103+
type: "object",
104+
properties: {
105+
dt: {
106+
type: "number",
107+
default: 60,
108+
unit: "s",
109+
title: "Time step",
110+
},
111+
runtime: {
112+
type: "number",
113+
default: 43200,
114+
unit: "s",
115+
title: "Total run time",
116+
},
117+
},
118+
additionalProperties: false,
119+
required: ["dt", "runtime"],
120+
title: "Time control",
121+
default: {},
122+
},
123+
mixedLayer: {
124+
type: "object",
125+
properties: {
126+
wtheta: {
127+
type: "number",
128+
default: 0.1,
129+
unit: "K m s-1",
130+
title: "Surface kinematic heat flux",
131+
},
132+
advtheta: {
133+
type: "number",
134+
default: 0,
135+
unit: "K s-1",
136+
title: "Advection of heat",
137+
},
138+
gammatheta: {
139+
type: "number",
140+
default: 0.006,
141+
unit: "K m-1",
142+
title: "Free atmosphere potential temperature lapse rate",
143+
},
144+
wq: {
145+
type: "number",
146+
default: 0.0001,
147+
unit: "kg kg-1 m s-1",
148+
title: "Surface kinematic moisture flux",
149+
},
150+
advq: {
151+
type: "number",
152+
default: 0,
153+
unit: "kg kg-1 s-1",
154+
title: "Advection of moisture",
155+
},
156+
gammaq: {
157+
type: "number",
158+
default: 0,
159+
unit: "kg kg-1 m-1",
160+
title: "Free atmosphere specific humidity lapse rate",
161+
},
162+
divU: {
163+
type: "number",
164+
default: 0,
165+
unit: "s-1",
166+
title: "Horizontal large-scale divergence of wind",
167+
},
168+
beta: {
169+
type: "number",
170+
default: 0.2,
171+
title: "Entrainment ratio for virtual heat",
172+
},
173+
},
174+
additionalProperties: false,
175+
required: [
176+
"wtheta",
177+
"advtheta",
178+
"gammatheta",
179+
"wq",
180+
"advq",
181+
"gammaq",
182+
"divU",
183+
"beta",
184+
],
185+
title: "Mixed layer",
186+
default: {},
187+
},
188+
},
189+
additionalProperties: false,
190+
required: ["initialState", "timeControl", "mixedLayer"],
191+
$schema: "https://json-schema.org/draft/2019-09/schema",
192+
} as unknown as JsonSchemaOfConfig;

packages/class/src/validate.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import assert from "node:assert";
22
import test, { describe } from "node:test";
3-
import type { Config } from "./config.js";
3+
4+
import { type Config, jsonSchemaOfConfig } from "./config.js";
45
import {
56
type PartialConfig,
6-
jsonSchemaOfConfig,
77
overwriteDefaultsInJsonSchema,
88
parse,
99
pruneDefaults,

packages/class/src/validate.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,7 @@
55
*/
66
import { Ajv2019 } from "ajv/dist/2019.js";
77
import type { DefinedError, JSONSchemaType } from "ajv/dist/2019.js";
8-
9-
import type { Config } from "./config.js";
10-
import rawConfigJson from "./config.json";
11-
12-
/**
13-
* The JSON schema for the configuration object.
14-
*/
15-
export type JsonSchemaOfConfig = JSONSchemaType<Config>;
16-
export const jsonSchemaOfConfig =
17-
rawConfigJson as unknown as JsonSchemaOfConfig;
8+
import { type Config, jsonSchemaOfConfig } from "./config.js";
189

1910
export const ajv = new Ajv2019({
2011
coerceTypes: true,
@@ -36,7 +27,6 @@ ajv.addKeyword({
3627
*
3728
* @param input - The input to be validated.
3829
* @returns `true` if the input is valid, `false` otherwise.
39-
*
4030
*/
4131
export const validate = ajv.compile(jsonSchemaOfConfig);
4232

0 commit comments

Comments
 (0)