@prosary/schema 0.0.0-stage → 0.0.1

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,123 @@
1
+ import { MessageFormatElement } from "@formatjs/icu-messageformat-parser";
2
+ /**
3
+ * The named formats every contract has without declaring them — FormatJS's
4
+ * defaults, less `currency`, which names no currency and so cannot format.
5
+ * A contract's own formats are declared in `prosary.config.ts`, and may
6
+ * redefine these.
7
+ */
8
+ export declare const FORMATS: {
9
+ readonly number: readonly ["integer", "percent"];
10
+ readonly date: readonly ["short", "medium", "long", "full"];
11
+ readonly time: readonly ["short", "medium", "long", "full"];
12
+ };
13
+ /**
14
+ * One compiled message: the FormatJS AST `@prosary/kit` parsed an ICU message
15
+ * to, which the runtime formats without a parser.
16
+ */
17
+ export type Ast = MessageFormatElement[];
18
+ /** The kinds of value a named format applies to. */
19
+ export type Kind = "number" | "date" | "time";
20
+ /**
21
+ * Named formats, by kind: what a message refers to by name
22
+ * (`{amount, number, price}`) and a helper takes in place of options.
23
+ */
24
+ export interface Formats {
25
+ readonly number?: Readonly<Record<string, Intl.NumberFormatOptions>>;
26
+ readonly date?: Readonly<Record<string, Intl.DateTimeFormatOptions>>;
27
+ readonly time?: Readonly<Record<string, Intl.DateTimeFormatOptions>>;
28
+ }
29
+ /**
30
+ * A contract as `@prosary/kit` builds it: the source locale, every locale a
31
+ * bundle exists for, and every message key. Plain data — no message text.
32
+ *
33
+ * `arguments` is a type carrier only: the kit declares it with the values
34
+ * each message takes and never emits it, so a definition infers a typed
35
+ * resolver without holding anything at runtime.
36
+ */
37
+ export interface Definition {
38
+ /** The locale the source messages are written in. */
39
+ readonly locale: string;
40
+ /** Every locale a bundle is built for, the source locale first. */
41
+ readonly locales: readonly string[];
42
+ /** Every message key, in source order. */
43
+ readonly messages: readonly string[];
44
+ /** The named formats `prosary.config.ts` declares, by kind. */
45
+ readonly formats?: Formats;
46
+ /** Never present at runtime; carries each message's values in the types. */
47
+ readonly arguments?: object;
48
+ }
49
+ /** The locales of a contract. */
50
+ export type Locale<D extends Definition> = D["locales"][number];
51
+ /** The message keys of a contract: what each message goes by. */
52
+ export type Key<D extends Definition> = D["messages"][number];
53
+ /**
54
+ * The names a format of one kind goes by: the built-in ones, and the ones the
55
+ * contract declares.
56
+ */
57
+ export type Named<D extends Definition, K extends Kind> = (typeof FORMATS)[K][number] | (D["formats"] extends Readonly<Record<K, infer F>> ? keyof F & string : never);
58
+ /**
59
+ * The values one message takes: read off the contract's `arguments` carrier,
60
+ * or any record when the definition carries none.
61
+ */
62
+ export type Values<D extends Definition, K extends Key<D>> = K extends keyof NonNullable<D["arguments"]> ? NonNullable<D["arguments"]>[K] : Record<string, unknown>;
63
+ /**
64
+ * Messages of one locale, compiled: message key to AST. Partial by design —
65
+ * an app may hold the whole locale or only the part it has loaded.
66
+ */
67
+ export type Bundle<D extends Definition> = { [K in Key<D>]?: Ast; };
68
+ /** One reason a value is not what the contract asks for. */
69
+ export interface Issue {
70
+ /** What is wrong. */
71
+ message: string;
72
+ /** Where in the value, when it is not the value itself. */
73
+ path?: string[];
74
+ }
75
+ /**
76
+ * The validation bundle for a contract: `check` answers as a type guard,
77
+ * `assert` throws a `SchemaError` carrying every issue.
78
+ */
79
+ export interface Schema<D extends Definition> {
80
+ /** The contract the schema was derived from. */
81
+ definition: D;
82
+ check: {
83
+ locale: (value: unknown) => value is Locale<D>;
84
+ key: (value: unknown) => value is Key<D>;
85
+ bundle: (value: unknown) => value is Bundle<D>;
86
+ timeZone: (value: unknown) => value is string;
87
+ convention: (value: unknown) => value is string;
88
+ };
89
+ assert: {
90
+ locale: (value: unknown) => asserts value is Locale<D>;
91
+ key: (value: unknown) => asserts value is Key<D>;
92
+ bundle: (value: unknown) => asserts value is Bundle<D>;
93
+ timeZone: (value: unknown) => asserts value is string;
94
+ convention: (value: unknown) => asserts value is string;
95
+ };
96
+ }
97
+ /**
98
+ * Raised when an assertion rejects a value. Carries the concrete
99
+ * {@link Issue}s — each with its own message and path — rather than
100
+ * collapsing them into a single string, so callers can react to every failure
101
+ * individually.
102
+ */
103
+ export declare class SchemaError extends Error {
104
+ readonly issues: Issue[];
105
+ constructor(issues: Issue[]);
106
+ }
107
+ /**
108
+ * Derives the validation bundle for a contract — what proves a locale, a
109
+ * message key, or a bundle that arrives at runtime (fetched, imported,
110
+ * restored from storage) before the service adopts it.
111
+ *
112
+ * A bundle may hold any part of the contract's messages, but nothing outside
113
+ * it: every key must be a message the contract declares, mapped to a compiled
114
+ * message.
115
+ *
116
+ * A time zone and a convention — the locale values are formatted by — are
117
+ * proven against the runtime rather than the contract: any IANA zone, any
118
+ * well-formed locale tag.
119
+ *
120
+ * @param definition - The contract `@prosary/kit` built.
121
+ * @returns The {@link Schema} for the contract.
122
+ */
123
+ export declare const defineSchema: <D extends Definition>(definition: D) => Schema<D>;
@@ -0,0 +1,96 @@
1
+ import { record } from "objectively";
2
+ const FORMATS = {
3
+ number: ["integer", "percent"],
4
+ date: [
5
+ "short",
6
+ "medium",
7
+ "long",
8
+ "full"
9
+ ],
10
+ time: [
11
+ "short",
12
+ "medium",
13
+ "long",
14
+ "full"
15
+ ]
16
+ };
17
+ const summarize = (issues) => issues.map((issue) => issue.path?.length ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("\n");
18
+ var SchemaError = class extends Error {
19
+ issues;
20
+ constructor(issues) {
21
+ super(summarize(issues));
22
+ this.name = "SchemaError";
23
+ this.issues = issues;
24
+ }
25
+ };
26
+ const isAst = (value) => {
27
+ return Array.isArray(value) && value.every((element) => record(element) && typeof element.type === "number");
28
+ };
29
+ const defineSchema = (definition) => {
30
+ const locales = new Set(definition.locales);
31
+ const messages = new Set(definition.messages);
32
+ const inspect = {
33
+ locale: (value) => {
34
+ if (locales.has(value)) return [];
35
+ return [{ message: `${JSON.stringify(value)} is not a locale of the contract` }];
36
+ },
37
+ key: (value) => {
38
+ if (messages.has(value)) return [];
39
+ return [{ message: `${JSON.stringify(value)} is not a message of the contract` }];
40
+ },
41
+ bundle: (value) => {
42
+ if (!record(value)) return [{ message: "a bundle must be a record of compiled messages" }];
43
+ const issues = [];
44
+ for (const [key, ast] of Object.entries(value)) if (!messages.has(key)) issues.push({
45
+ message: "is not a message of the contract",
46
+ path: [key]
47
+ });
48
+ else if (!isAst(ast)) issues.push({
49
+ message: "is not a compiled message",
50
+ path: [key]
51
+ });
52
+ return issues;
53
+ }
54
+ };
55
+ const settings = {
56
+ timeZone: (value) => {
57
+ try {
58
+ if (typeof value === "string") {
59
+ new Intl.DateTimeFormat(void 0, { timeZone: value });
60
+ return [];
61
+ }
62
+ } catch {}
63
+ return [{ message: `${JSON.stringify(value)} is not an IANA time zone` }];
64
+ },
65
+ convention: (value) => {
66
+ try {
67
+ if (typeof value === "string" && value !== "") {
68
+ Intl.getCanonicalLocales(value);
69
+ return [];
70
+ }
71
+ } catch {}
72
+ return [{ message: `${JSON.stringify(value)} is not a BCP 47 locale tag` }];
73
+ }
74
+ };
75
+ const assert = (issues) => {
76
+ if (issues.length > 0) throw new SchemaError(issues);
77
+ };
78
+ return {
79
+ definition,
80
+ check: {
81
+ locale: (value) => inspect.locale(value).length === 0,
82
+ key: (value) => inspect.key(value).length === 0,
83
+ bundle: (value) => inspect.bundle(value).length === 0,
84
+ timeZone: (value) => settings.timeZone(value).length === 0,
85
+ convention: (value) => settings.convention(value).length === 0
86
+ },
87
+ assert: {
88
+ locale: (value) => assert(inspect.locale(value)),
89
+ key: (value) => assert(inspect.key(value)),
90
+ bundle: (value) => assert(inspect.bundle(value)),
91
+ timeZone: (value) => assert(settings.timeZone(value)),
92
+ convention: (value) => assert(settings.convention(value))
93
+ }
94
+ };
95
+ };
96
+ export { FORMATS, SchemaError, defineSchema };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alexander Thorwaldson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,3 @@
1
- # Temporary Holding Version
1
+ # @prosary/schema
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Message contract types and runtime validation.
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@prosary/schema",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.1",
4
+ "description": "Message contract types and runtime validation",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/zoobzio/prosary.git",
9
+ "directory": "packages/schema"
10
+ },
11
+ "type": "module",
12
+ "main": "./.dist/index.mjs",
13
+ "types": "./.dist/index.d.mts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./.dist/index.d.mts",
17
+ "import": "./.dist/index.mjs"
18
+ }
19
+ },
20
+ "files": [
21
+ ".dist"
22
+ ],
23
+ "dependencies": {
24
+ "@formatjs/icu-messageformat-parser": "^3.5.20",
25
+ "objectively": "^0.3.0"
26
+ },
27
+ "scripts": {
28
+ "build": "obuild",
29
+ "stub": "obuild --stub",
30
+ "lint": "oxlint",
31
+ "typecheck": "tsc --noEmit",
32
+ "test": "vitest run"
33
+ }
6
34
  }