@modulify/validator 0.1.0 → 0.2.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +324 -107
  3. package/dist/assert.cjs +66 -0
  4. package/dist/assert.d.ts +16 -0
  5. package/dist/assert.mjs +66 -0
  6. package/dist/assertions.cjs +190 -92
  7. package/dist/assertions.d.ts +58 -2
  8. package/dist/assertions.mjs +191 -93
  9. package/dist/checkers.d.ts +8 -0
  10. package/dist/combinators.cjs +341 -0
  11. package/dist/combinators.d.ts +17 -0
  12. package/dist/combinators.mjs +341 -0
  13. package/dist/constraints.d.ts +4 -0
  14. package/dist/extractors.d.ts +2 -0
  15. package/dist/index.cjs +172 -61
  16. package/dist/index.d.ts +10 -4
  17. package/dist/index.mjs +176 -64
  18. package/dist/json-schema.cjs +514 -0
  19. package/dist/json-schema.d.ts +14 -0
  20. package/dist/json-schema.mjs +514 -0
  21. package/dist/metadata.cjs +8 -0
  22. package/dist/metadata.cjs.js +130 -0
  23. package/dist/metadata.d.ts +8 -0
  24. package/dist/metadata.es.js +131 -0
  25. package/dist/metadata.mjs +8 -0
  26. package/dist/predicates.cjs +40 -5
  27. package/dist/predicates.d.ts +25 -3
  28. package/dist/predicates.mjs +40 -5
  29. package/dist/violations.d.ts +29 -0
  30. package/docs/en/00-index.md +14 -0
  31. package/docs/en/01-shape-api.md +348 -0
  32. package/docs/en/02-metadata-and-introspection.md +276 -0
  33. package/docs/en/03-violations.md +267 -0
  34. package/docs/en/04-json-schema-export.md +264 -0
  35. package/docs/en/05-public-api.md +123 -0
  36. package/docs/en/06-common-recipes.md +273 -0
  37. package/docs/en/07-ai-reference.md +215 -0
  38. package/docs/en/08-violation-code-types.md +241 -0
  39. package/docs/ru/00-index.md +15 -0
  40. package/docs/ru/01-shape-api.md +348 -0
  41. package/docs/ru/02-metadata-and-introspection.md +276 -0
  42. package/docs/ru/03-violations.md +267 -0
  43. package/docs/ru/04-json-schema-export.md +264 -0
  44. package/docs/ru/05-public-api.md +123 -0
  45. package/docs/ru/06-common-recipes.md +273 -0
  46. package/docs/ru/07-ai-reference.md +215 -0
  47. package/docs/ru/08-violation-code-types.md +241 -0
  48. package/docs/ru/README.md +371 -0
  49. package/package.json +51 -33
  50. package/types/index.d.ts +789 -30
  51. package/types/json-schema.d.ts +75 -0
  52. package/dist/assertions/Assert.d.ts +0 -2
  53. package/dist/assertions/HasLength.d.ts +0 -7
  54. package/dist/assertions/check.d.ts +0 -3
  55. package/dist/assertions/index.d.ts +0 -16
  56. package/dist/runners/Each.d.ts +0 -3
  57. package/dist/runners/HasProperties.d.ts +0 -6
  58. package/dist/runners/index.d.ts +0 -2
  59. package/dist/runners.cjs +0 -32
  60. package/dist/runners.d.ts +0 -2
  61. package/dist/runners.mjs +0 -32
@@ -0,0 +1,267 @@
1
+ # Violations
2
+
3
+ [Documentation index](./00-index.md)
4
+ [Russian translation](../ru/03-violations.md)
5
+
6
+ `@modulify/validator` returns structured violations instead of built-in human-readable error messages.
7
+
8
+ This part of the API has two layers:
9
+
10
+ - raw `Violation[]` results returned by `validate(...)` and `validate.sync(...)`;
11
+ - `ViolationCollection`, a thin utility wrapper built by `collection(...)`.
12
+
13
+ ## Quick Start
14
+
15
+ ```typescript
16
+ import {
17
+ collection,
18
+ isString,
19
+ shape,
20
+ validate,
21
+ } from '@modulify/validator'
22
+
23
+ const [ok, validated, violations] = validate.sync({
24
+ profile: {
25
+ email: 42,
26
+ },
27
+ }, shape({
28
+ profile: shape({
29
+ email: isString,
30
+ }),
31
+ }))
32
+
33
+ const errors = collection(violations)
34
+ const emailErrors = errors.at(['profile', 'email'])
35
+ ```
36
+
37
+ ## Why Violations Are Structured
38
+
39
+ The library intentionally returns data, not presentation.
40
+
41
+ That keeps message rendering outside the validation layer and makes the same result reusable for:
42
+
43
+ - localized UI messages;
44
+ - form error state;
45
+ - API payloads;
46
+ - analytics and diagnostics;
47
+ - custom adapters and tooling.
48
+
49
+ ## The `Violation` Shape
50
+
51
+ A violation contains:
52
+
53
+ - `value` - the value that failed validation;
54
+ - `path` - where the failure happened inside a nested object or array;
55
+ - `violates` - a machine-readable description of what failed.
56
+
57
+ Example:
58
+
59
+ ```typescript
60
+ import type { Violation } from '@modulify/validator'
61
+
62
+ const violation: Violation = {
63
+ value: '',
64
+ path: ['form', 'nickname'],
65
+ violates: {
66
+ kind: 'assertion',
67
+ name: 'hasLength',
68
+ code: 'length.min',
69
+ args: [4],
70
+ },
71
+ }
72
+ ```
73
+
74
+ ## `violates`
75
+
76
+ The `violates` field contains structured failure metadata.
77
+
78
+ Important parts:
79
+
80
+ - `kind` - which layer produced the failure;
81
+ - `name` - which assertion or validator produced it;
82
+ - `code` - a semantic failure code;
83
+ - `args` - structured payload for the failure.
84
+
85
+ This allows consumers to decide how to render or transform failures later.
86
+
87
+ ## `violates.kind`
88
+
89
+ `violates.kind` tells you which layer produced the failure:
90
+
91
+ - `'assertion'`
92
+ - `'validator'`
93
+ - `'runtime'`
94
+
95
+ Examples:
96
+
97
+ - `isString` failures are assertion-level;
98
+ - `shape.unknown-key` is validator-level;
99
+ - rejected async validations can surface as runtime-level failures.
100
+
101
+ ## Nested Paths
102
+
103
+ `path` is a regular `PropertyKey[]`.
104
+
105
+ That means:
106
+
107
+ - object properties stay as property keys;
108
+ - array positions stay as numeric indexes;
109
+ - nested failures keep their full absolute path from the root input.
110
+
111
+ Example:
112
+
113
+ - `['profile', 'email']`
114
+ - `['items', 0, 'title']`
115
+
116
+ This is one of the reasons the library can stay adapter-friendly without stringifying field paths.
117
+
118
+ ## Validation Results
119
+
120
+ `validate(...)` and `validate.sync(...)` return:
121
+
122
+ ```typescript
123
+ type ValidationTuple<T> =
124
+ | [ok: true, validated: T, violations: []]
125
+ | [ok: false, validated: unknown, violations: Violation[]]
126
+ ```
127
+
128
+ That means:
129
+
130
+ - success returns an empty violation list;
131
+ - failure returns the original input plus collected structured violations.
132
+
133
+ ## `ViolationCollection`
134
+
135
+ `ViolationCollection` is a thin convenience wrapper around `Violation[]`.
136
+
137
+ It keeps the raw list model, but adds a few operations for post-processing:
138
+
139
+ - `size`
140
+ - iteration with `for...of`
141
+ - `.forEach(...)`
142
+ - `.map(...)`
143
+ - `.at(path)`
144
+ - `.tree()`
145
+
146
+ The goal is convenience, not a second error model.
147
+
148
+ ## `collection(...)`
149
+
150
+ Use `collection(...)` to wrap a raw `Violation[]` result:
151
+
152
+ ```typescript
153
+ const errors = collection(violations)
154
+ ```
155
+
156
+ This is especially useful once validation has already happened and you want to:
157
+
158
+ - inspect one field;
159
+ - build a nested UI tree;
160
+ - group or map failure codes;
161
+ - reuse helper methods without changing the underlying data format.
162
+
163
+ ## Exact Path Lookup With `.at(path)`
164
+
165
+ `.at(path)` performs exact path matching.
166
+
167
+ ```typescript
168
+ const rootErrors = errors.at([])
169
+ const emailErrors = errors.at(['profile', 'email'])
170
+ ```
171
+
172
+ Important semantics:
173
+
174
+ - `at([])` means root-level violations;
175
+ - violations without a path are treated as root-level by the collection utilities;
176
+ - `at(['profile'])` does not include `['profile', 'email']`;
177
+ - the result is another `ViolationCollection`.
178
+
179
+ This makes path lookups predictable and easy to reason about.
180
+
181
+ ## Tree View With `.tree()`
182
+
183
+ `.tree()` builds a nested machine-readable tree from the current collection.
184
+
185
+ ```typescript
186
+ const tree = errors.tree()
187
+ ```
188
+
189
+ This is useful when a consumer wants:
190
+
191
+ - hierarchical traversal;
192
+ - nested error rendering;
193
+ - path-aware UI state;
194
+ - a structured debugging view.
195
+
196
+ Tree nodes expose:
197
+
198
+ - `path`
199
+ - `self`
200
+ - `subtree`
201
+ - `children`
202
+ - `.at(path)`
203
+
204
+ ## `ViolationTreeNode`
205
+
206
+ The tree view is shaped like this:
207
+
208
+ ```typescript
209
+ type ViolationTreeNode = {
210
+ path: readonly PropertyKey[]
211
+ self: ViolationCollection
212
+ subtree: ViolationCollection
213
+ children: ReadonlyMap<PropertyKey, ViolationTreeNode>
214
+ at(path: readonly PropertyKey[]): ViolationTreeNode | undefined
215
+ }
216
+ ```
217
+
218
+ Important differences:
219
+
220
+ - `self` contains only violations exactly on the current path;
221
+ - `subtree` contains current-path violations plus all descendant violations.
222
+
223
+ ## Tree Path Semantics
224
+
225
+ Tree nodes keep absolute paths.
226
+
227
+ Also:
228
+
229
+ - intermediate nodes may exist even when they have no own violations;
230
+ - they can still be useful because descendants may have failures;
231
+ - the tree is built from path arrays, not from dot-separated strings.
232
+
233
+ This keeps the structure aligned with the raw violation format.
234
+
235
+ ## Practical Example
236
+
237
+ ```typescript
238
+ const [ok, validated, violations] = validate.sync({
239
+ profile: {
240
+ email: '',
241
+ },
242
+ }, shape({
243
+ profile: shape({
244
+ email: [isString],
245
+ }),
246
+ }))
247
+
248
+ const errors = collection(violations)
249
+ const rootErrors = errors.at([])
250
+ const emailErrors = errors.at(['profile', 'email'])
251
+ const codes = emailErrors.map(violation => violation.violates.code)
252
+ const tree = errors.tree()
253
+ ```
254
+
255
+ From one raw violation list you can derive:
256
+
257
+ - exact-path collections;
258
+ - mapped codes;
259
+ - a nested traversal tree.
260
+
261
+ ## Practical Notes
262
+
263
+ - keep `Violation[]` as the canonical transport format;
264
+ - use `collection(...)` only when the helper API improves ergonomics;
265
+ - treat `code` and `args` as your main integration points;
266
+ - keep message rendering outside the validation layer;
267
+ - prefer path-array handling over string path serialization.
@@ -0,0 +1,264 @@
1
+ # JSON Schema Export
2
+
3
+ [Documentation index](./00-index.md)
4
+ [Russian translation](../ru/04-json-schema-export.md)
5
+
6
+ `@modulify/validator/json-schema` provides a thin JSON Schema export layer on top of the public descriptor contract.
7
+
8
+ It is intentionally a derivation layer, not a second schema model. The exporter reads public descriptors and builds a JSON Schema view from them.
9
+
10
+ ## Quick Start
11
+
12
+ ```typescript
13
+ import {
14
+ isNumber,
15
+ isString,
16
+ meta,
17
+ optional,
18
+ shape,
19
+ } from '@modulify/validator'
20
+ import { toJsonSchema } from '@modulify/validator/json-schema'
21
+
22
+ const profile = meta(shape({
23
+ email: meta(isString, {
24
+ title: 'Email',
25
+ format: 'email',
26
+ }),
27
+ age: optional(isNumber),
28
+ }).strict(), {
29
+ title: 'Profile',
30
+ })
31
+
32
+ const jsonSchema = toJsonSchema(profile)
33
+ ```
34
+
35
+ ## Mental Model
36
+
37
+ The export flow is intentionally small:
38
+
39
+ 1. validators expose public descriptors through `describe(...)`;
40
+ 2. `toJsonSchema(...)` derives a JSON Schema document from those descriptors;
41
+ 3. unsupported or opaque nodes are either dropped in best-effort mode or rejected in strict mode.
42
+
43
+ This separation matters because:
44
+
45
+ - runtime validation stays runtime-first;
46
+ - exporter behavior stays explicit;
47
+ - custom tooling can depend on one public introspection contract.
48
+
49
+ ## Entry Point
50
+
51
+ Import JSON Schema export from the dedicated subpath:
52
+
53
+ ```typescript
54
+ import {
55
+ JsonSchemaExportError,
56
+ toJsonSchema,
57
+ } from '@modulify/validator/json-schema'
58
+ ```
59
+
60
+ The root package intentionally does not re-export this API.
61
+
62
+ ## Supported Built-In Mappings
63
+
64
+ The exporter covers the built-in descriptor set that already participates in public introspection.
65
+
66
+ ### Leaf assertions
67
+
68
+ Supported practical mappings include:
69
+
70
+ - `isString` -> `type: 'string'`
71
+ - `isNumber` -> `type: 'number'`
72
+ - `isBoolean` -> `type: 'boolean'`
73
+ - `isNull` -> `type: 'null'`
74
+ - `isEmail` -> `type: 'string'` plus `format: 'email'`
75
+ - `exact(...)` -> `const`
76
+ - `oneOf(...)` -> `enum`
77
+ - `hasLength(...)` -> string/array length constraints
78
+
79
+ ### Wrappers
80
+
81
+ Supported wrappers:
82
+
83
+ - `optional(...)`
84
+ - `nullable(...)`
85
+ - `nullish(...)`
86
+
87
+ `optional(...)` affects object `required` calculation. `nullable(...)` and `nullish(...)` are exported as unions with `null`.
88
+
89
+ ### Structural combinators
90
+
91
+ Supported structural mappings:
92
+
93
+ - `shape(...)`
94
+ - `each(...)`
95
+ - `tuple(...)`
96
+ - `record(...)`
97
+
98
+ ### Branching combinators
99
+
100
+ Supported branching mappings:
101
+
102
+ - `union(...)`
103
+ - `discriminatedUnion(...)`
104
+
105
+ ### Sequential slots
106
+
107
+ Array slots with multiple constraints are exported as JSON Schema `allOf`.
108
+
109
+ ## Object Schema Mapping
110
+
111
+ `shape(...)` is exported as a JSON Schema object with:
112
+
113
+ - `type: 'object'`
114
+ - `properties`
115
+ - `required`
116
+ - `additionalProperties`
117
+
118
+ Unknown-key handling maps like this:
119
+
120
+ - `.strict()` -> `additionalProperties: false`
121
+ - default or `.passthrough()` -> `additionalProperties: true`
122
+
123
+ ## `required` And `undefined`
124
+
125
+ One important boundary is object field presence.
126
+
127
+ The runtime layer treats:
128
+
129
+ - a missing key;
130
+ - and a key whose value is `undefined`;
131
+
132
+ as the same case in several practical scenarios, especially around `optional(...)` and derived helpers such as `partial()`.
133
+
134
+ JSON Schema does not model that behavior the same way. Because of that, the exporter approximates field presence through JSON Schema `required` rather than claiming exact semantic parity.
135
+
136
+ That approximation is intentional and should be treated as an interoperability layer, not as a promise that runtime and JSON Schema semantics are identical.
137
+
138
+ ## Metadata Mapping
139
+
140
+ The exporter does not blindly copy all metadata into JSON Schema.
141
+
142
+ Instead, it maps a small explicit whitelist:
143
+
144
+ - `title`
145
+ - `description`
146
+ - `format`
147
+ - `default`
148
+ - `examples`
149
+ - `deprecated`
150
+ - `readOnly`
151
+ - `writeOnly`
152
+
153
+ Other metadata stays library-specific and is left out of the exported schema automatically.
154
+
155
+ This keeps the metadata layer explicit and avoids turning it into implicit export magic.
156
+
157
+ ## Best-Effort Mode
158
+
159
+ Best-effort mode is the default:
160
+
161
+ ```typescript
162
+ const schema = toJsonSchema(profile)
163
+ ```
164
+
165
+ In this mode:
166
+
167
+ - unsupported or opaque nodes become permissive `{}` schema nodes;
168
+ - unsupported object-level shape rules are dropped;
169
+ - dropped shape rules are marked with a `$comment` when practical.
170
+
171
+ This mode is useful when you want the broadest possible external schema even if some runtime semantics cannot be expressed faithfully.
172
+
173
+ ## Strict Mode
174
+
175
+ Strict mode rejects unsupported nodes:
176
+
177
+ ```typescript
178
+ const schema = toJsonSchema(profile, { mode: 'strict' })
179
+ ```
180
+
181
+ When export cannot be represented faithfully, the exporter throws `JsonSchemaExportError`.
182
+
183
+ That error exposes:
184
+
185
+ - the descriptor that failed;
186
+ - the machine-readable reason;
187
+ - the descriptor path where the failure happened.
188
+
189
+ Strict mode is useful when silent fallback would be misleading.
190
+
191
+ ## Unsupported And Opaque Cases
192
+
193
+ Some runtime behavior does not have a faithful JSON Schema representation.
194
+
195
+ Important examples:
196
+
197
+ - custom validators without a supported public descriptor;
198
+ - unknown custom descriptor kinds;
199
+ - object-level `refine(...)` rules;
200
+ - descriptor nodes whose semantics depend on runtime-only behavior;
201
+ - values that are not representable as practical JSON Schema constants or enums.
202
+
203
+ The exporter keeps these boundaries explicit instead of guessing.
204
+
205
+ ## Relationship To `describe(...)`
206
+
207
+ `toJsonSchema(...)` is downstream from `describe(...)`.
208
+
209
+ That means:
210
+
211
+ - JSON Schema export should rely on public descriptors;
212
+ - exporter logic should not depend on private runtime shape internals;
213
+ - custom validators participate by exposing a public descriptor contract first.
214
+
215
+ This keeps the architecture thin and stable for external tooling.
216
+
217
+ ## Example: Metadata-Aware Shape Export
218
+
219
+ ```typescript
220
+ const profile = meta(shape({
221
+ email: meta(isString, {
222
+ title: 'Email',
223
+ format: 'email',
224
+ }),
225
+ age: optional(isNumber),
226
+ }).strict(), {
227
+ title: 'Profile',
228
+ })
229
+
230
+ const schema = toJsonSchema(profile)
231
+ ```
232
+
233
+ Practical result:
234
+
235
+ - shape metadata can become schema metadata;
236
+ - field metadata can become property metadata;
237
+ - strict object behavior becomes `additionalProperties: false`.
238
+
239
+ ## Example: Strict Failure
240
+
241
+ ```typescript
242
+ const schema = shape({
243
+ publishedAt: custom({
244
+ check(value: unknown): value is string {
245
+ return typeof value === 'string'
246
+ },
247
+ run() {
248
+ return []
249
+ },
250
+ }),
251
+ })
252
+
253
+ toJsonSchema(schema, { mode: 'strict' })
254
+ ```
255
+
256
+ This throws because the custom validator stays opaque to the exporter.
257
+
258
+ ## Practical Notes
259
+
260
+ - treat JSON Schema export as an interoperability layer;
261
+ - use best-effort mode when partial export is acceptable;
262
+ - use strict mode when unsupported semantics must fail loudly;
263
+ - keep custom descriptors compact and public if they are meant to participate in export;
264
+ - do not assume JSON Schema export replaces runtime validation semantics.
@@ -0,0 +1,123 @@
1
+ # Public API
2
+
3
+ [Documentation index](./00-index.md)
4
+ [Russian translation](../ru/05-public-api.md)
5
+
6
+ This guide summarizes the package surface of `@modulify/validator` and its supported subpath exports.
7
+
8
+ It is meant as a navigation document:
9
+
10
+ - where each public entrypoint lives;
11
+ - which groups of functions are exported together;
12
+ - what the validation result looks like;
13
+ - which APIs belong to dedicated subpaths such as predicates and JSON Schema export.
14
+
15
+ ## Root Package
16
+
17
+ The root package exports:
18
+
19
+ - `validate`
20
+ - `validate.sync`
21
+ - `matches.sync`
22
+ - `meta`
23
+ - `describe`
24
+ - `custom`
25
+ - `collection`
26
+ - `ViolationCollection`
27
+ - all exports from `./assertions`
28
+ - all exports from `./combinators`
29
+
30
+ Use the root package when you need the main validation API, composed validators, metadata/introspection, and violation utilities.
31
+
32
+ ## Assertions And Combinators
33
+
34
+ The root package includes:
35
+
36
+ - low-level assertion construction through `assert(...)`
37
+ - built-in assertions such as `isString`, `isNumber`, `isBoolean`, `isNull`, `isEmail`, `hasLength(...)`, `oneOf(...)`
38
+ - structural combinators such as `shape(...)`, `each(...)`, `tuple(...)`, `record(...)`
39
+ - wrapper combinators such as `optional(...)`, `nullable(...)`, `nullish(...)`
40
+ - branching combinators such as `union(...)` and `discriminatedUnion(...)`
41
+ - exact-value matching through `exact(...)`
42
+
43
+ This is the main runtime-facing API surface of the library.
44
+
45
+ ## Metadata And Introspection
46
+
47
+ The same root package also includes:
48
+
49
+ - `meta(...)`
50
+ - `describe(...)`
51
+ - `custom(...)`
52
+
53
+ Together these provide the public machine-readable descriptor contract and the metadata layer built on top of it.
54
+
55
+ ## Violation Utilities
56
+
57
+ The root package also includes:
58
+
59
+ - raw violation results returned by `validate(...)`
60
+ - `collection(...)`
61
+ - `ViolationCollection`
62
+
63
+ These are the main utilities for post-processing machine-readable validation failures.
64
+
65
+ ## Validation Result
66
+
67
+ `validate(...)` and `validate.sync(...)` return:
68
+
69
+ ```typescript
70
+ type ValidationTuple<T> =
71
+ | [ok: true, validated: T, violations: []]
72
+ | [ok: false, validated: unknown, violations: Violation[]]
73
+ ```
74
+
75
+ Practical meaning:
76
+
77
+ - `ok` tells whether validation succeeded;
78
+ - `validated` becomes strongly typed only in the success branch;
79
+ - `violations` is empty on success and contains structured failures on error.
80
+
81
+ ## Predicates Subpath
82
+
83
+ Predicates are available from:
84
+
85
+ ```typescript
86
+ @modulify/validator/predicates
87
+ ```
88
+
89
+ This subpath contains reusable runtime/type-guard helpers and predicate combinators such as:
90
+
91
+ - `isString`
92
+ - `isNumber`
93
+ - `isRecord`
94
+ - `isArray`
95
+ - `isShape`
96
+ - `And`, `Or`, `Not`
97
+
98
+ Use this subpath when you want guard-style runtime checks without pulling in the higher-level validation layer.
99
+
100
+ ## JSON Schema Export Subpath
101
+
102
+ JSON Schema export is available from:
103
+
104
+ ```typescript
105
+ @modulify/validator/json-schema
106
+ ```
107
+
108
+ This subpath contains:
109
+
110
+ - `toJsonSchema(...)`
111
+ - `JsonSchemaExportError`
112
+
113
+ It is intentionally kept separate from the root package so that export-specific concerns stay isolated from the main validation entrypoint.
114
+
115
+ ## How To Read The Package Surface
116
+
117
+ At a high level:
118
+
119
+ - root package = validation, combinators, metadata, violations;
120
+ - `./predicates` = standalone runtime/type-guard helpers;
121
+ - `./json-schema` = derived JSON Schema export layer.
122
+
123
+ This split keeps the main API discoverable while still allowing specialized subpaths where needed.