diff --git a/package.json b/package.json
index e7cb3ec..57f730f 100644
--- a/package.json
+++ b/package.json
@@ -44,11 +44,14 @@
"dependencies": {
"@reduxjs/toolkit": "==2.0.0-alpha.5 ",
"@yanick/updeep-remeda": "^2.2.0",
+ "ajv": "^8.12.0",
"beercss": "^3.1.3",
"git-describe": "^4.1.1",
"git-repo-version": "^1.0.2",
"histoire": "^0.16.1",
"jsdom": "^21.1.1",
+ "json-schema-shorthand": "^3.0.0",
+ "json-schema-to-ts": "^2.9.1",
"lodash": "^4.17.21",
"mdsvex": "^0.10.6",
"memoize-one": "^6.0.0",
diff --git a/src/lib/jsc.ts b/src/lib/jsc.ts
new file mode 100644
index 0000000..4e7be41
--- /dev/null
+++ b/src/lib/jsc.ts
@@ -0,0 +1,13 @@
+export default {
+ string: (args = {}) => ({ type: "string" as const, ...args }),
+ object:
(properties: P = {}, args: A = {}) => ({
+ type: "object" as const,
+ properties,
+ ...args,
+ }),
+ boolean: (args = {}) => ({ type: "boolean" as const, ...args }),
+ number: (args = {}) => ({ type: "number" as const, ...args }),
+ integer: (args = {}) => ({ type: "integer" as const, ...args }),
+ type: (type: S, args = {}) => ({ type, ...args }),
+ ref: (ref: S, args = {}) => ({ $ref: ref, ...args }),
+};
diff --git a/src/lib/store/ship.ts b/src/lib/store/ship.ts
index 70d9980..bec0c03 100644
--- a/src/lib/store/ship.ts
+++ b/src/lib/store/ship.ts
@@ -2,6 +2,7 @@ import { createSelector } from "@reduxjs/toolkit";
import Updux, { createPayloadAction } from "updux";
import * as R from "remeda";
import memoize from "memoize-one";
+import Ajv from "ajv";
import identification from "./ship/identification";
import ftl, { calcFtlReqs } from "./ship/propulsion/ftl";
@@ -18,6 +19,7 @@ import { fireconsDux } from "./ship/weaponry/firecons";
import { adfcDux } from "./ship/weaponry/adfc";
import { weaponsDux } from "./ship/weaponry/weapons";
import { weaponryDux } from "./ship/weaponry";
+import schema from "./ship/schema";
if (typeof process !== "undefined") {
process.env.UPDEEP_MODE = "dangerously_never_freeze";
@@ -64,6 +66,31 @@ const shipDux = new Updux({
},
});
+shipDux.addEffect((api) => {
+ const ajv = new Ajv();
+
+ const validate = ajv.compile(schema);
+
+ return (next) => (action) => {
+ next(action);
+
+ if (!validate(api.getState())) {
+ console.error(
+ JSON.stringify(
+ {
+ errors: validate.errors,
+ state: api.getState(),
+ action,
+ },
+ undefined,
+ 2
+ )
+ );
+ throw new Error("validation failed");
+ }
+ };
+});
+
shipDux.addMutation(restore, (state) => () => state);
shipDux.addMutation(importShip, (state) => () => state);
diff --git a/src/lib/store/ship/identification.ts b/src/lib/store/ship/identification.ts
index bdb65f0..c726c68 100644
--- a/src/lib/store/ship/identification.ts
+++ b/src/lib/store/ship/identification.ts
@@ -1,17 +1,15 @@
import Updux, { createAction, withPayload } from "updux";
import u from "@yanick/updeep-remeda";
import { carrierDux } from "./carrier";
+import type { FromSchema } from "json-schema-to-ts";
+import * as j from "json-schema-shorthand";
-const initialState = {
- shipType: "",
- shipClass: "",
- isCarrier: false,
- reqs: {
- mass: 10,
- cost: 0,
- usedMass: 0,
- },
-};
+import {
+ identificationSchema,
+ type Identification,
+} from "./identification/schema.js";
+
+const initialState: Identification = identificationSchema.default;
const setShipClass = createAction("setShipClass", withPayload());
const updateIdentification = createAction("updateIdentification");
diff --git a/src/lib/store/ship/identification/schema.ts b/src/lib/store/ship/identification/schema.ts
new file mode 100644
index 0000000..4c99cd9
--- /dev/null
+++ b/src/lib/store/ship/identification/schema.ts
@@ -0,0 +1,33 @@
+import type { FromSchema } from "json-schema-to-ts";
+import * as j from "json-schema-shorthand";
+
+import { reqsSchema } from "../reqs";
+
+export const identificationSchema = j.object(
+ {
+ shipType: "string",
+ shipClass: "string",
+ isCarrier: { type: "boolean", default: false },
+ reqs: {
+ allOf: ["#/$defs/reqs", { object: { usedMass: "number" } }],
+ },
+ } as const,
+ {
+ additionalProperties: false,
+ $defs: {
+ reqs: reqsSchema,
+ },
+ default: {
+ shipType: "",
+ shipClass: "",
+ isCarrier: false,
+ reqs: {
+ mass: 10,
+ cost: 0,
+ usedMass: 0,
+ },
+ },
+ } as const
+);
+
+export type Identification = FromSchema;
diff --git a/src/lib/store/ship/reqs.ts b/src/lib/store/ship/reqs.ts
new file mode 100644
index 0000000..bbb0ce7
--- /dev/null
+++ b/src/lib/store/ship/reqs.ts
@@ -0,0 +1,11 @@
+import type { FromSchema, JSONSchema } from "json-schema-to-ts";
+import * as j from "json-schema-shorthand";
+
+import jsc from "$lib/jsc";
+
+export const reqsSchema = j.object({
+ cost: "integer",
+ mass: "integer",
+} as const);
+
+export type Reqs = FromSchema;
diff --git a/src/lib/store/ship/schema.test.ts b/src/lib/store/ship/schema.test.ts
new file mode 100644
index 0000000..2ced854
--- /dev/null
+++ b/src/lib/store/ship/schema.test.ts
@@ -0,0 +1,28 @@
+import schema from "./schema";
+import shipDux from "../ship";
+import Ajv from "ajv";
+
+const ajv = new Ajv();
+
+test("initial value is valid", () => {
+ const validate = ajv.compile(schema);
+ console.log(shipDux.initialState);
+
+ try {
+ expect(validate(shipDux.initialState)).toBeTruthy();
+ } catch (e) {
+ if (validate.errors) {
+ console.warn(
+ JSON.stringify(
+ {
+ state: shipDux.initialState,
+ error: validate.errors,
+ },
+ undefined,
+ 2
+ )
+ );
+ }
+ throw new Error("validation failed");
+ }
+});
diff --git a/src/lib/store/ship/schema.ts b/src/lib/store/ship/schema.ts
new file mode 100644
index 0000000..1aeebb0
--- /dev/null
+++ b/src/lib/store/ship/schema.ts
@@ -0,0 +1,29 @@
+const todo = {
+ type: "object",
+ additionalProperties: true,
+};
+
+const shipSchema = {
+ type: "object",
+ properties: {
+ schemaVersion: { const: "1" },
+ identification: { $ref: "#/$defs/identification" },
+ structure: { $ref: "#/$defs/todo" },
+ propulsion: { $ref: "#/$defs/todo" },
+ carrier: { $ref: "#/$defs/todo" },
+ weaponry: { $ref: "#/$defs/todo" },
+ },
+ additionalProperties: false,
+ $defs: {
+ identification: identificationSchema,
+ todo,
+ ...identificationSchema.$defs,
+ },
+} as const;
+
+import type { FromSchema } from "json-schema-to-ts";
+import { identificationSchema } from "./identification/schema";
+
+type Ship = FromSchema;
+
+export default shipSchema;
diff --git a/vite.config.js b/vite.config.js
index e2e67f3..3e5d975 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,4 +1,3 @@
-// vite.config.js
import { sveltekit } from "@sveltejs/kit/vite";
import packageJson from "./package.json";
import getVersion from "git-repo-version";
@@ -6,17 +5,17 @@ import git from "git-describe";
/** @type {import('vite').UserConfig} */
const config = {
- plugins: [sveltekit()],
- publicDir: "./static",
- ssr: {},
- optimizeDeps: {},
- define: {
- "import.meta.env.PACKAGE_VERSION": JSON.stringify(
- git.gitDescribeSync().semverString
- ),
- "import.meta.env.HOMEPAGE": JSON.stringify(packageJson.homepage),
- "import.meta.env.BUGS": JSON.stringify(packageJson.bugs.url),
- },
+ plugins: [sveltekit()],
+ publicDir: "./static",
+ ssr: {},
+ optimizeDeps: {},
+ define: {
+ "import.meta.env.PACKAGE_VERSION": JSON.stringify(
+ git.gitDescribeSync().semverString
+ ),
+ "import.meta.env.HOMEPAGE": JSON.stringify(packageJson.homepage),
+ "import.meta.env.BUGS": JSON.stringify(packageJson.bugs.url),
+ },
};
export default config;