@mailwoman/geographic-model 0.0.0

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 (51) hide show
  1. package/README.md +155 -0
  2. package/artifact.ts +198 -0
  3. package/compile.ts +350 -0
  4. package/data/geographic-model.json +172 -0
  5. package/data/model/concepts.json +114 -0
  6. package/data/model/mappings.json +18 -0
  7. package/data/model/model.json +3 -0
  8. package/data/model/relations.json +14 -0
  9. package/index.ts +51 -0
  10. package/load.ts +396 -0
  11. package/lookup.ts +129 -0
  12. package/out/artifact.d.ts +106 -0
  13. package/out/artifact.d.ts.map +1 -0
  14. package/out/artifact.js +135 -0
  15. package/out/artifact.js.map +1 -0
  16. package/out/compile.d.ts +84 -0
  17. package/out/compile.d.ts.map +1 -0
  18. package/out/compile.js +259 -0
  19. package/out/compile.js.map +1 -0
  20. package/out/index.d.ts +51 -0
  21. package/out/index.d.ts.map +1 -0
  22. package/out/index.js +51 -0
  23. package/out/index.js.map +1 -0
  24. package/out/load.d.ts +122 -0
  25. package/out/load.d.ts.map +1 -0
  26. package/out/load.js +269 -0
  27. package/out/load.js.map +1 -0
  28. package/out/lookup.d.ts +64 -0
  29. package/out/lookup.d.ts.map +1 -0
  30. package/out/lookup.js +68 -0
  31. package/out/lookup.js.map +1 -0
  32. package/out/schema.d.ts +366 -0
  33. package/out/schema.d.ts.map +1 -0
  34. package/out/schema.js +166 -0
  35. package/out/schema.js.map +1 -0
  36. package/out/scripts/build-artifact.d.ts +51 -0
  37. package/out/scripts/build-artifact.d.ts.map +1 -0
  38. package/out/scripts/build-artifact.js +78 -0
  39. package/out/scripts/build-artifact.js.map +1 -0
  40. package/out/validate.d.ts +67 -0
  41. package/out/validate.d.ts.map +1 -0
  42. package/out/validate.js +465 -0
  43. package/out/validate.js.map +1 -0
  44. package/out/validation-issues.d.ts +84 -0
  45. package/out/validation-issues.d.ts.map +1 -0
  46. package/out/validation-issues.js +190 -0
  47. package/out/validation-issues.js.map +1 -0
  48. package/package.json +120 -0
  49. package/schema.ts +399 -0
  50. package/validate.ts +845 -0
  51. package/validation-issues.ts +305 -0
@@ -0,0 +1,305 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The issue vocabulary and the primitive field readers `./validate.ts` is built from.
7
+ *
8
+ * Every reader takes the issue list it appends to and returns `undefined` when it could not read the
9
+ * field. That shape is deliberate: a reader that threw would end the pass at the first defect, and a
10
+ * reader that substituted a default would convert "I could not read this" into a value, which at a
11
+ * validation boundary is the same as inventing one. Returning nothing, having said why, lets the
12
+ * caller skip the checks that depended on the field and keep running the ones that did not.
13
+ *
14
+ * `./validate.ts` re-exports {@link ValidationIssueCode} and {@link ValidationIssue}; the readers
15
+ * stay internal to the package.
16
+ */
17
+
18
+ /**
19
+ * Every way a document can fail validation. Closed, so a consumer branches on the code rather than on message prose.
20
+ */
21
+ export const ValidationIssueCode = {
22
+ MissingField: "missing_field",
23
+ WrongType: "wrong_type",
24
+ /**
25
+ * A required string was present and blank.
26
+ */
27
+ EmptyValue: "empty_value",
28
+ /**
29
+ * A list that must carry entries was empty.
30
+ */
31
+ EmptyList: "empty_list",
32
+ UnknownField: "unknown_field",
33
+ /**
34
+ * A field whose name announces ranking policy. Refused wherever it appears, at any depth.
35
+ */
36
+ RankingField: "ranking_field",
37
+ UnknownConceptKind: "unknown_concept_kind",
38
+ UnknownModality: "unknown_modality",
39
+ UnknownRelationSemantics: "unknown_relation_semantics",
40
+ UnknownConceptStatus: "unknown_concept_status",
41
+ UnknownExternalVocabulary: "unknown_external_vocabulary",
42
+ UnknownDerivationInputKind: "unknown_derivation_input_kind",
43
+ DuplicateID: "duplicate_id",
44
+ /**
45
+ * A record names itself where it may only name another record.
46
+ */
47
+ SelfReference: "self_reference",
48
+ CyclicIsA: "cyclic_isa",
49
+ UnknownConcept: "unknown_concept",
50
+ UnknownRelation: "unknown_relation",
51
+ UnknownDerivationInput: "unknown_derivation_input",
52
+ DomainKindMismatch: "domain_kind_mismatch",
53
+ RangeKindMismatch: "range_kind_mismatch",
54
+ InverseNotReciprocal: "inverse_not_reciprocal",
55
+ InverseKindsMismatch: "inverse_kinds_mismatch",
56
+ TransitiveKindsDisjoint: "transitive_kinds_disjoint",
57
+ MalformedCountry: "malformed_country",
58
+ } as const
59
+
60
+ export type ValidationIssueCode = (typeof ValidationIssueCode)[keyof typeof ValidationIssueCode]
61
+
62
+ /**
63
+ * One violation, addressed by where it was found.
64
+ */
65
+ export interface ValidationIssue {
66
+ /**
67
+ * A JSONPath-style address into the validated input, e.g. `$.concepts[0].assertions[1].modality`. The document root
68
+ * is `$`.
69
+ */
70
+ path: string
71
+ code: ValidationIssueCode
72
+ message: string
73
+ }
74
+
75
+ /**
76
+ * Name fragments that announce ranking policy. Matched case-insensitively against every field name at every depth, so
77
+ * `score`, `boost`, `penalty`, `rankWeight`, `relevanceWeight`, and `affinityWeight` are refused by one rule rather
78
+ * than by an enumeration that a seventh spelling walks past.
79
+ */
80
+ const RANKING_FIELD_FRAGMENTS = [
81
+ "boost",
82
+ "penalt",
83
+ "weight",
84
+ "rank",
85
+ "score",
86
+ "relevance",
87
+ "affinity",
88
+ "priorit",
89
+ "ordering",
90
+ ] as const
91
+
92
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
93
+ return typeof value === "object" && value !== null && !Array.isArray(value)
94
+ }
95
+
96
+ export function add(issues: ValidationIssue[], path: string, code: ValidationIssueCode, message: string): void {
97
+ issues.push({ path, code, message })
98
+ }
99
+
100
+ export function listVocabulary(allowed: readonly string[]): string {
101
+ return allowed.map((candidate) => `\`${candidate}\``).join(", ")
102
+ }
103
+
104
+ /**
105
+ * Refuse any field the schema does not declare, naming the ranking-policy ones under their own code.
106
+ */
107
+ export function checkFieldNames(
108
+ issues: ValidationIssue[],
109
+ path: string,
110
+ value: Record<string, unknown>,
111
+ allowed: readonly string[]
112
+ ): void {
113
+ for (const key of Object.keys(value)) {
114
+ const lowered = key.toLowerCase()
115
+ const fragment = RANKING_FIELD_FRAGMENTS.find((candidate) => lowered.includes(candidate))
116
+
117
+ if (fragment) {
118
+ add(
119
+ issues,
120
+ `${path}.${key}`,
121
+ ValidationIssueCode.RankingField,
122
+ `field \`${key}\` names ranking policy (\`${fragment}\`) — authored records create observations, and ranking belongs to @mailwoman/resolver and @mailwoman/neural`
123
+ )
124
+
125
+ continue
126
+ }
127
+
128
+ if (!allowed.includes(key)) {
129
+ add(
130
+ issues,
131
+ `${path}.${key}`,
132
+ ValidationIssueCode.UnknownField,
133
+ `unknown field \`${key}\` — this record accepts ${listVocabulary(allowed)}`
134
+ )
135
+ }
136
+ }
137
+ }
138
+
139
+ export function readString(
140
+ issues: ValidationIssue[],
141
+ path: string,
142
+ container: Record<string, unknown>,
143
+ key: string,
144
+ required: boolean
145
+ ): string | undefined {
146
+ const value = container[key]
147
+ const fieldPath = `${path}.${key}`
148
+
149
+ if (value === undefined) {
150
+ if (required) {
151
+ add(issues, fieldPath, ValidationIssueCode.MissingField, `\`${key}\` is required`)
152
+ }
153
+
154
+ return undefined
155
+ }
156
+
157
+ if (typeof value !== "string") {
158
+ add(issues, fieldPath, ValidationIssueCode.WrongType, `\`${key}\` must be a string`)
159
+
160
+ return undefined
161
+ }
162
+
163
+ if (!value.trim().length) {
164
+ add(issues, fieldPath, ValidationIssueCode.EmptyValue, `\`${key}\` must not be blank`)
165
+
166
+ return undefined
167
+ }
168
+
169
+ return value
170
+ }
171
+
172
+ export function readBoolean(
173
+ issues: ValidationIssue[],
174
+ path: string,
175
+ container: Record<string, unknown>,
176
+ key: string
177
+ ): boolean | undefined {
178
+ const value = container[key]
179
+ const fieldPath = `${path}.${key}`
180
+
181
+ if (value === undefined) {
182
+ add(issues, fieldPath, ValidationIssueCode.MissingField, `\`${key}\` is required`)
183
+
184
+ return undefined
185
+ }
186
+
187
+ if (typeof value !== "boolean") {
188
+ add(issues, fieldPath, ValidationIssueCode.WrongType, `\`${key}\` must be a boolean`)
189
+
190
+ return undefined
191
+ }
192
+
193
+ return value
194
+ }
195
+
196
+ export function readArray(
197
+ issues: ValidationIssue[],
198
+ path: string,
199
+ container: Record<string, unknown>,
200
+ key: string,
201
+ required: boolean
202
+ ): unknown[] | undefined {
203
+ const value = container[key]
204
+ const fieldPath = `${path}.${key}`
205
+
206
+ if (value === undefined) {
207
+ if (required) {
208
+ add(issues, fieldPath, ValidationIssueCode.MissingField, `\`${key}\` is required`)
209
+ }
210
+
211
+ return undefined
212
+ }
213
+
214
+ if (!Array.isArray(value)) {
215
+ add(issues, fieldPath, ValidationIssueCode.WrongType, `\`${key}\` must be an array`)
216
+
217
+ return undefined
218
+ }
219
+
220
+ return value
221
+ }
222
+
223
+ export function readStringArray(
224
+ issues: ValidationIssue[],
225
+ path: string,
226
+ container: Record<string, unknown>,
227
+ key: string,
228
+ required: boolean
229
+ ): string[] | undefined {
230
+ const entries = readArray(issues, path, container, key, required)
231
+
232
+ if (!entries) return undefined
233
+
234
+ const values: string[] = []
235
+
236
+ for (const [index, entry] of entries.entries()) {
237
+ const entryPath = `${path}.${key}[${index}]`
238
+
239
+ if (typeof entry !== "string") {
240
+ add(issues, entryPath, ValidationIssueCode.WrongType, "must be a string")
241
+
242
+ continue
243
+ }
244
+
245
+ if (!entry.trim().length) {
246
+ add(issues, entryPath, ValidationIssueCode.EmptyValue, "must not be blank")
247
+
248
+ continue
249
+ }
250
+
251
+ values.push(entry)
252
+ }
253
+
254
+ return values
255
+ }
256
+
257
+ export function readVocabularyValue<T extends string>(
258
+ issues: ValidationIssue[],
259
+ path: string,
260
+ container: Record<string, unknown>,
261
+ key: string,
262
+ allowed: readonly T[],
263
+ code: ValidationIssueCode
264
+ ): T | undefined {
265
+ const value = readString(issues, path, container, key, true)
266
+
267
+ if (value === undefined) return undefined
268
+
269
+ const match = allowed.find((candidate) => candidate === value)
270
+
271
+ if (match === undefined) {
272
+ add(issues, `${path}.${key}`, code, `\`${value}\` is not one of ${listVocabulary(allowed)}`)
273
+ }
274
+
275
+ return match
276
+ }
277
+
278
+ export function readVocabularyArray<T extends string>(
279
+ issues: ValidationIssue[],
280
+ path: string,
281
+ container: Record<string, unknown>,
282
+ key: string,
283
+ allowed: readonly T[],
284
+ code: ValidationIssueCode
285
+ ): T[] | undefined {
286
+ const values = readStringArray(issues, path, container, key, true)
287
+
288
+ if (!values) return undefined
289
+
290
+ const matched: T[] = []
291
+
292
+ for (const [index, value] of values.entries()) {
293
+ const match = allowed.find((candidate) => candidate === value)
294
+
295
+ if (match === undefined) {
296
+ add(issues, `${path}.${key}[${index}]`, code, `\`${value}\` is not one of ${listVocabulary(allowed)}`)
297
+
298
+ continue
299
+ }
300
+
301
+ matched.push(match)
302
+ }
303
+
304
+ return matched
305
+ }