@gesslar/toolkit 0.4.0 → 0.6.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.
- package/package.json +2 -1
- package/src/index.js +3 -0
- package/src/lib/Contract.js +257 -0
- package/src/lib/Data.js +24 -15
- package/src/lib/Glog.js +21 -5
- package/src/lib/Logger.js +3 -0
- package/src/lib/Schemer.js +89 -0
- package/src/lib/Terms.js +74 -0
- package/src/lib/Util.js +21 -0
- package/src/types/Collection.d.ts +6 -1
- package/src/types/Contract.d.ts +162 -0
- package/src/types/Data.d.ts +23 -23
- package/src/types/FS.d.ts +3 -3
- package/src/types/Glog.d.ts +302 -49
- package/src/types/Sass.d.ts +1 -1
- package/src/types/Schemer.d.ts +179 -0
- package/src/types/Tantrum.d.ts +10 -10
- package/src/types/Term.d.ts +1 -1
- package/src/types/Terms.d.ts +145 -0
- package/src/types/Type.d.ts +1 -1
- package/src/types/Util.d.ts +20 -2
- package/src/types/index.d.ts +3 -0
- package/src/lib/ActionBuilder.js +0 -144
- package/src/lib/ActionRunner.js +0 -109
- package/src/lib/BaseActionManager.js +0 -246
- package/src/lib/BaseHookManager.js +0 -209
- package/src/lib/Piper.js +0 -181
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gesslar/toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Get in, bitches, we're going toolkitting.",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"homepage": "https://github.com/gesslar/toolkit#readme",
|
|
52
52
|
"dependencies": {
|
|
53
53
|
"@gesslar/colours": "^0.0.1",
|
|
54
|
+
"ajv": "^8.17.1",
|
|
54
55
|
"globby": "^15.0.0",
|
|
55
56
|
"json5": "^2.2.3",
|
|
56
57
|
"yaml": "^2.8.1"
|
package/src/index.js
CHANGED
|
@@ -6,11 +6,14 @@ export {default as FS} from "./lib/FS.js"
|
|
|
6
6
|
// Utility classes
|
|
7
7
|
export {default as Cache} from "./lib/Cache.js"
|
|
8
8
|
export {default as Collection} from "./lib/Collection.js"
|
|
9
|
+
export {default as Contract} from "./lib/Contract.js"
|
|
9
10
|
export {default as Data} from "./lib/Data.js"
|
|
10
11
|
export {default as Glog} from "./lib/Glog.js"
|
|
11
12
|
export {default as Sass} from "./lib/Sass.js"
|
|
13
|
+
export {default as Schemer} from "./lib/Schemer.js"
|
|
12
14
|
export {default as Tantrum} from "./lib/Tantrum.js"
|
|
13
15
|
export {default as Term} from "./lib/Term.js"
|
|
16
|
+
export {default as Terms} from "./lib/Terms.js"
|
|
14
17
|
export {default as Type} from "./lib/TypeSpec.js"
|
|
15
18
|
export {default as Util} from "./lib/Util.js"
|
|
16
19
|
export {default as Valid} from "./lib/Valid.js"
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import Sass from "./Sass.js"
|
|
2
|
+
import Schemer from "./Schemer.js"
|
|
3
|
+
import Terms from "./Terms.js"
|
|
4
|
+
import Data from "./Data.js"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Contract represents a successful negotiation between Terms.
|
|
8
|
+
* It handles validation and compatibility checking between what
|
|
9
|
+
* one action provides and what another accepts.
|
|
10
|
+
*/
|
|
11
|
+
export default class Contract {
|
|
12
|
+
#providerTerms = null
|
|
13
|
+
#consumerTerms = null
|
|
14
|
+
#validator = null
|
|
15
|
+
#debug = null
|
|
16
|
+
#isNegotiated = false
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates a contract by negotiating between provider and consumer terms
|
|
20
|
+
*
|
|
21
|
+
* @param {Terms} providerTerms - What the provider offers
|
|
22
|
+
* @param {Terms} consumerTerms - What the consumer expects
|
|
23
|
+
* @param {object} options - Configuration options
|
|
24
|
+
* @param {import('../types.js').DebugFunction} [options.debug] - Debug function
|
|
25
|
+
*/
|
|
26
|
+
constructor(providerTerms, consumerTerms, {debug = null} = {}) {
|
|
27
|
+
this.#providerTerms = providerTerms
|
|
28
|
+
this.#consumerTerms = consumerTerms
|
|
29
|
+
this.#debug = debug
|
|
30
|
+
|
|
31
|
+
// Perform the negotiation
|
|
32
|
+
this.#negotiate()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extracts the actual schema from a terms definition
|
|
37
|
+
*
|
|
38
|
+
* @param {object} definition - Terms definition with TLD descriptor
|
|
39
|
+
* @returns {object} Extracted schema content
|
|
40
|
+
* @throws {Sass} If definition structure is invalid
|
|
41
|
+
* @private
|
|
42
|
+
*/
|
|
43
|
+
static #extractSchemaFromTerms(definition) {
|
|
44
|
+
// Must be a plain object
|
|
45
|
+
if(!Data.isPlainObject(definition)) {
|
|
46
|
+
throw Sass.new("Terms definition must be a plain object")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Must have exactly one key (the TLD/descriptor)
|
|
50
|
+
const keys = Object.keys(definition)
|
|
51
|
+
if(keys.length !== 1) {
|
|
52
|
+
throw Sass.new("Terms definition must have exactly one top-level key (descriptor)")
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Extract the content under the TLD
|
|
56
|
+
const [key] = keys
|
|
57
|
+
|
|
58
|
+
return definition[key]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Creates a contract from terms with schema validation
|
|
63
|
+
*
|
|
64
|
+
* @param {string} name - Contract identifier
|
|
65
|
+
* @param {object} termsDefinition - The terms definition
|
|
66
|
+
* @param {import('ajv').ValidateFunction|null} [validator] - Optional AJV schema validator function with .errors property
|
|
67
|
+
* @param {import('../types.js').DebugFunction} [debug] - Debug function
|
|
68
|
+
* @returns {Contract} New contract instance
|
|
69
|
+
*/
|
|
70
|
+
static fromTerms(name, termsDefinition, validator = null, debug = null) {
|
|
71
|
+
// Validate the terms definition if validator provided
|
|
72
|
+
if(validator) {
|
|
73
|
+
const valid = validator(termsDefinition)
|
|
74
|
+
|
|
75
|
+
if(!valid) {
|
|
76
|
+
const error = Schemer.reportValidationErrors(validator.errors)
|
|
77
|
+
throw Sass.new(`Invalid terms definition for ${name}:\n${error}`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Extract schema from terms definition for validation
|
|
82
|
+
const schemaDefinition = Contract.#extractSchemaFromTerms(termsDefinition)
|
|
83
|
+
const termsSchemaValidator = Schemer.getValidator(schemaDefinition)
|
|
84
|
+
|
|
85
|
+
const contract = new Contract(null, null, {debug})
|
|
86
|
+
contract.#validator = termsSchemaValidator
|
|
87
|
+
contract.#isNegotiated = true // Single-party contract is automatically negotiated
|
|
88
|
+
|
|
89
|
+
return contract
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Performs negotiation between provider and consumer terms
|
|
94
|
+
*
|
|
95
|
+
* @private
|
|
96
|
+
*/
|
|
97
|
+
#negotiate() {
|
|
98
|
+
if(!this.#providerTerms || !this.#consumerTerms) {
|
|
99
|
+
// Single-party contract scenario
|
|
100
|
+
this.#isNegotiated = true
|
|
101
|
+
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Extract content for comparison (ignore TLD metadata)
|
|
106
|
+
const providerContent = Contract.#extractSchemaFromTerms(
|
|
107
|
+
this.#providerTerms.definition
|
|
108
|
+
)
|
|
109
|
+
const consumerContent = Contract.#extractSchemaFromTerms(
|
|
110
|
+
this.#consumerTerms.definition
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
// Compare terms for compatibility
|
|
114
|
+
const compatibility = this.#compareTerms(providerContent, consumerContent)
|
|
115
|
+
|
|
116
|
+
if(compatibility.status === "error") {
|
|
117
|
+
throw Sass.new(
|
|
118
|
+
`Contract negotiation failed: ${compatibility.errors.map(e => e.message).join(", ")}`
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.#isNegotiated = true
|
|
123
|
+
this.#debug?.(`Contract negotiated successfully`, 3)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Validates data against this contract
|
|
128
|
+
*
|
|
129
|
+
* @param {object} data - Data to validate
|
|
130
|
+
* @returns {boolean} True if valid
|
|
131
|
+
* @throws {Sass} If validation fails or contract not negotiated
|
|
132
|
+
*/
|
|
133
|
+
validate(data) {
|
|
134
|
+
const debug = this.#debug
|
|
135
|
+
|
|
136
|
+
if(!this.#isNegotiated)
|
|
137
|
+
throw Sass.new("Cannot validate against unnegotiated contract")
|
|
138
|
+
|
|
139
|
+
if(!this.#validator)
|
|
140
|
+
throw Sass.new("No validator available for this contract")
|
|
141
|
+
|
|
142
|
+
debug?.("Validating data %o", 4, data)
|
|
143
|
+
|
|
144
|
+
const valid = this.#validator(data)
|
|
145
|
+
|
|
146
|
+
if(!valid) {
|
|
147
|
+
const error = Schemer.reportValidationErrors(this.#validator.errors)
|
|
148
|
+
throw Sass.new(`Contract validation failed:\n${error}`)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Compares terms for compatibility
|
|
156
|
+
*
|
|
157
|
+
* @param {object} providerTerms - Terms offered by provider
|
|
158
|
+
* @param {object} consumerTerms - Terms expected by consumer
|
|
159
|
+
* @param {Array} stack - Stack trace for nested validation
|
|
160
|
+
* @returns {object} Result with status and errors
|
|
161
|
+
* @private
|
|
162
|
+
*/
|
|
163
|
+
#compareTerms(providerTerms, consumerTerms, stack = []) {
|
|
164
|
+
const debug = this.#debug
|
|
165
|
+
const breadcrumb = key => (stack.length ? `@${stack.join(".")}` : key)
|
|
166
|
+
const errors = []
|
|
167
|
+
|
|
168
|
+
if(!providerTerms || !consumerTerms) {
|
|
169
|
+
return {
|
|
170
|
+
status: "error",
|
|
171
|
+
errors: [Sass.new("Both provider and consumer terms are required")]
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
debug?.("Comparing provider keys:%o with consumer keys:%o", 3,
|
|
176
|
+
Object.keys(providerTerms), Object.keys(consumerTerms))
|
|
177
|
+
|
|
178
|
+
// Check that consumer requirements are met by provider
|
|
179
|
+
for(const [key, consumerRequirement] of Object.entries(consumerTerms)) {
|
|
180
|
+
debug?.("Checking consumer requirement: %o [required = %o]", 3,
|
|
181
|
+
key, consumerRequirement.required ?? false)
|
|
182
|
+
|
|
183
|
+
if(consumerRequirement.required && !(key in providerTerms)) {
|
|
184
|
+
debug?.("Provider missing required capability: %o", 2, key)
|
|
185
|
+
errors.push(
|
|
186
|
+
Sass.new(`Provider missing required capability: ${key} ${breadcrumb(key)}`)
|
|
187
|
+
)
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if(key in providerTerms) {
|
|
192
|
+
const expectedType = consumerRequirement.dataType
|
|
193
|
+
const providedType = providerTerms[key]?.dataType
|
|
194
|
+
|
|
195
|
+
if(expectedType && providedType && expectedType !== providedType) {
|
|
196
|
+
errors.push(
|
|
197
|
+
Sass.new(
|
|
198
|
+
`Type mismatch for ${key}: Consumer expects ${expectedType}, provider offers ${providedType} ${breadcrumb(key)}`
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Recursive validation for nested requirements
|
|
204
|
+
if(consumerRequirement.contains) {
|
|
205
|
+
debug?.("Recursing into nested requirement: %o", 3, key)
|
|
206
|
+
const nestedResult = this.#compareTerms(
|
|
207
|
+
providerTerms[key]?.contains,
|
|
208
|
+
consumerRequirement.contains,
|
|
209
|
+
[...stack, key]
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if(nestedResult.errors.length) {
|
|
213
|
+
errors.push(...nestedResult.errors)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {status: errors.length === 0 ? "success" : "error", errors}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Check if contract negotiation was successful
|
|
224
|
+
*
|
|
225
|
+
* @returns {boolean} True if negotiated
|
|
226
|
+
*/
|
|
227
|
+
get isNegotiated() {
|
|
228
|
+
return this.#isNegotiated
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get the provider terms (if any)
|
|
233
|
+
*
|
|
234
|
+
* @returns {Terms|null} Provider terms
|
|
235
|
+
*/
|
|
236
|
+
get providerTerms() {
|
|
237
|
+
return this.#providerTerms
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get the consumer terms (if any)
|
|
242
|
+
*
|
|
243
|
+
* @returns {Terms|null} Consumer terms
|
|
244
|
+
*/
|
|
245
|
+
get consumerTerms() {
|
|
246
|
+
return this.#consumerTerms
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Get the contract validator
|
|
251
|
+
*
|
|
252
|
+
* @returns {(data: object) => boolean|null} The contract validator function
|
|
253
|
+
*/
|
|
254
|
+
get validator() {
|
|
255
|
+
return this.#validator
|
|
256
|
+
}
|
|
257
|
+
}
|
package/src/lib/Data.js
CHANGED
|
@@ -17,17 +17,18 @@ export default class Data {
|
|
|
17
17
|
*/
|
|
18
18
|
static primitives = Object.freeze([
|
|
19
19
|
// Primitives
|
|
20
|
-
"
|
|
21
|
-
"Null",
|
|
20
|
+
"Bigint",
|
|
22
21
|
"Boolean",
|
|
22
|
+
"Class",
|
|
23
|
+
"Null",
|
|
23
24
|
"Number",
|
|
24
|
-
"Bigint",
|
|
25
25
|
"String",
|
|
26
26
|
"Symbol",
|
|
27
|
+
"Undefined",
|
|
27
28
|
|
|
28
29
|
// Object Categories from typeof
|
|
29
|
-
"Object",
|
|
30
30
|
"Function",
|
|
31
|
+
"Object",
|
|
31
32
|
])
|
|
32
33
|
|
|
33
34
|
/**
|
|
@@ -38,21 +39,21 @@ export default class Data {
|
|
|
38
39
|
*/
|
|
39
40
|
static constructors = Object.freeze([
|
|
40
41
|
// Object Constructors
|
|
41
|
-
"Object",
|
|
42
42
|
"Array",
|
|
43
|
-
"Function",
|
|
44
43
|
"Date",
|
|
45
|
-
"RegExp",
|
|
46
44
|
"Error",
|
|
45
|
+
"Float32Array",
|
|
46
|
+
"Float64Array",
|
|
47
|
+
"Function",
|
|
48
|
+
"Int8Array",
|
|
47
49
|
"Map",
|
|
50
|
+
"Object",
|
|
51
|
+
"Promise",
|
|
52
|
+
"RegExp",
|
|
48
53
|
"Set",
|
|
54
|
+
"Uint8Array",
|
|
49
55
|
"WeakMap",
|
|
50
56
|
"WeakSet",
|
|
51
|
-
"Promise",
|
|
52
|
-
"Int8Array",
|
|
53
|
-
"Uint8Array",
|
|
54
|
-
"Float32Array",
|
|
55
|
-
"Float64Array",
|
|
56
57
|
])
|
|
57
58
|
|
|
58
59
|
/**
|
|
@@ -134,9 +135,8 @@ export default class Data {
|
|
|
134
135
|
*/
|
|
135
136
|
static isValidType(type) {
|
|
136
137
|
// Allow built-in types
|
|
137
|
-
if(Data.dataTypes.includes(type))
|
|
138
|
+
if(Data.dataTypes.includes(type))
|
|
138
139
|
return true
|
|
139
|
-
}
|
|
140
140
|
|
|
141
141
|
// Allow custom classes (PascalCase starting with capital letter)
|
|
142
142
|
return /^[A-Z][a-zA-Z0-9]*$/.test(type)
|
|
@@ -155,12 +155,21 @@ export default class Data {
|
|
|
155
155
|
if(!Data.isValidType(type))
|
|
156
156
|
return false
|
|
157
157
|
|
|
158
|
+
// We gotta do classes up front. Ugh.
|
|
159
|
+
if(/^[Cc]lass$/.test(type)) {
|
|
160
|
+
if(typeof value === "function" &&
|
|
161
|
+
value.prototype &&
|
|
162
|
+
value.prototype.constructor === value)
|
|
163
|
+
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
|
|
158
167
|
const valueType = Data.typeOf(value)
|
|
159
168
|
|
|
160
169
|
// Special cases that need extra validation
|
|
161
170
|
switch(valueType) {
|
|
162
171
|
case "Number":
|
|
163
|
-
return
|
|
172
|
+
return type === "Number" && !isNaN(value) // Excludes NaN
|
|
164
173
|
default:
|
|
165
174
|
return valueType === type
|
|
166
175
|
}
|
package/src/lib/Glog.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import Data from "./Data.js"
|
|
2
|
-
import Util from "./Util.js"
|
|
3
1
|
import c from "@gesslar/colours"
|
|
2
|
+
|
|
3
|
+
import Data from "./Data.js"
|
|
4
4
|
import Term from "./Term.js"
|
|
5
|
+
import Util from "./Util.js"
|
|
5
6
|
// ErrorStackParser will be dynamically imported when needed
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -237,10 +238,24 @@ class Glog {
|
|
|
237
238
|
}
|
|
238
239
|
|
|
239
240
|
// Traditional logger methods
|
|
240
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Log a debug message with specified verbosity level.
|
|
243
|
+
* Level 0 means debug OFF - use levels 1-4 for actual debug output.
|
|
244
|
+
* Debug messages only show when logLevel > 0.
|
|
245
|
+
*
|
|
246
|
+
* @param {string} message - Debug message to log
|
|
247
|
+
* @param {number} level - Debug verbosity level (1-4, default: 1)
|
|
248
|
+
* @param {...unknown} arg - Additional arguments to log
|
|
249
|
+
* @throws {Error} If level < 1 (level 0 = debug OFF)
|
|
250
|
+
*/
|
|
251
|
+
debug(message, level = 1, ...arg) {
|
|
252
|
+
if(level < 1) {
|
|
253
|
+
throw new Error("Debug level must be >= 1 (level 0 = debug OFF)")
|
|
254
|
+
}
|
|
255
|
+
|
|
241
256
|
const currentLevel = this.#logLevel || Glog.logLevel
|
|
242
257
|
|
|
243
|
-
if(level <= currentLevel) {
|
|
258
|
+
if(currentLevel > 0 && level <= currentLevel) {
|
|
244
259
|
Term.debug(this.#compose("debug", message, level), ...arg)
|
|
245
260
|
}
|
|
246
261
|
}
|
|
@@ -369,10 +384,11 @@ export default new Proxy(Glog, {
|
|
|
369
384
|
return new target(...argumentsList)
|
|
370
385
|
},
|
|
371
386
|
get(target, prop) {
|
|
387
|
+
// Hide execute method from public API
|
|
372
388
|
if(prop === "execute") {
|
|
373
389
|
return undefined
|
|
374
390
|
}
|
|
375
391
|
|
|
376
|
-
return target
|
|
392
|
+
return Reflect.get(target, prop)
|
|
377
393
|
}
|
|
378
394
|
})
|
package/src/lib/Logger.js
CHANGED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import Ajv from "ajv"
|
|
2
|
+
|
|
3
|
+
import Data from "./Data.js"
|
|
4
|
+
import Util from "./Util.js"
|
|
5
|
+
import Valid from "./Valid.js"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Schemer provides utilities for compiling and validating JSON schemas using AJV.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* - Use Schemer.fromFile(file, options) to create a validator from a file.
|
|
12
|
+
* - Use Schemer.from(schemaData, options) to create a validator from a schema object.
|
|
13
|
+
* - Use Schemer.getValidator(schema, options) to get a raw AJV validator function.
|
|
14
|
+
* - Use Schemer.reportValidationErrors(errors) to format AJV validation errors.
|
|
15
|
+
*/
|
|
16
|
+
export default class Schemer {
|
|
17
|
+
static async fromFile(file, options={}) {
|
|
18
|
+
Valid.type(file, "FileObject")
|
|
19
|
+
Valid.assert(Data.isPlainObject(options), "Options must be a plain object.")
|
|
20
|
+
|
|
21
|
+
const schemaData = await file.loadData()
|
|
22
|
+
|
|
23
|
+
return Schemer.getValidator(schemaData, options)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static async from(schemaData={}, options={}) {
|
|
27
|
+
Valid.assert(Data.isPlainObject(schemaData), "Schema data must be a plain object.")
|
|
28
|
+
Valid.assert(Data.isPlainObject(options), "Options must be a plain object.")
|
|
29
|
+
|
|
30
|
+
return Schemer.getValidator(schemaData, options)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Creates a validator function from a schema object
|
|
35
|
+
*
|
|
36
|
+
* @param {object} schema - The schema to compile
|
|
37
|
+
* @param {object} [options] - AJV options
|
|
38
|
+
* @returns {(data: unknown) => boolean} The AJV validator function, which may have additional properties (e.g., `.errors`)
|
|
39
|
+
*/
|
|
40
|
+
static getValidator(schema, options = {allErrors: true, verbose: true}) {
|
|
41
|
+
const ajv = new Ajv(options)
|
|
42
|
+
|
|
43
|
+
return ajv.compile(schema)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static reportValidationErrors(errors) {
|
|
47
|
+
if(!errors) {
|
|
48
|
+
return ""
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return errors.reduce((errorMessages, error) => {
|
|
52
|
+
let msg = `- "${error.instancePath || "(root)"}" ${error.message}`
|
|
53
|
+
|
|
54
|
+
if(error.params) {
|
|
55
|
+
const details = []
|
|
56
|
+
|
|
57
|
+
if(error.params.type)
|
|
58
|
+
details.push(` ➜ Expected type: ${error.params.type}`)
|
|
59
|
+
|
|
60
|
+
if(error.params.missingProperty)
|
|
61
|
+
details.push(` ➜ Missing required field: ${error.params.missingProperty}`)
|
|
62
|
+
|
|
63
|
+
if(error.params.allowedValues) {
|
|
64
|
+
details.push(` ➜ Allowed values: "${error.params.allowedValues.join('", "')}"`)
|
|
65
|
+
details.push(` ➜ Received value: "${error.data}"`)
|
|
66
|
+
const closestMatch =
|
|
67
|
+
Util.findClosestMatch(error.data, error.params.allowedValues)
|
|
68
|
+
|
|
69
|
+
if(closestMatch)
|
|
70
|
+
details.push(` ➜ Did you mean: "${closestMatch}"?`)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if(error.params.pattern)
|
|
74
|
+
details.push(` ➜ Expected pattern: ${error.params.pattern}`)
|
|
75
|
+
|
|
76
|
+
if(error.params.format)
|
|
77
|
+
details.push(` ➜ Expected format: ${error.params.format}`)
|
|
78
|
+
|
|
79
|
+
if(error.params.additionalProperty)
|
|
80
|
+
details.push(` ➜ Unexpected property: ${error.params.additionalProperty}`)
|
|
81
|
+
|
|
82
|
+
if(details.length)
|
|
83
|
+
msg += `\n${details.join("\n")}`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return errorMessages ? `${errorMessages}\n${msg}` : msg
|
|
87
|
+
}, "")
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/lib/Terms.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import JSON5 from "json5"
|
|
2
|
+
import yaml from "yaml"
|
|
3
|
+
|
|
4
|
+
import Data from "./Data.js"
|
|
5
|
+
import DirectoryObject from "./DirectoryObject.js"
|
|
6
|
+
import FileObject from "./FileObject.js"
|
|
7
|
+
import Sass from "./Sass.js"
|
|
8
|
+
import Valid from "./Valid.js"
|
|
9
|
+
|
|
10
|
+
const refex = /^ref:\/\/(?<file>.*)$/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Terms represents an interface definition - what an action promises to provide or accept.
|
|
14
|
+
* It's just the specification, not the negotiation. Contract handles the negotiation.
|
|
15
|
+
*/
|
|
16
|
+
export default class Terms {
|
|
17
|
+
#definition = null
|
|
18
|
+
|
|
19
|
+
constructor(definition) {
|
|
20
|
+
this.#definition = definition
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parses terms data, handling file references
|
|
25
|
+
*
|
|
26
|
+
* @param {string|object} termsData - Terms data or reference
|
|
27
|
+
* @param {DirectoryObject?} directoryObject - Directory context for file resolution
|
|
28
|
+
* @returns {object} Parsed terms data
|
|
29
|
+
*/
|
|
30
|
+
static async parse(termsData, directoryObject) {
|
|
31
|
+
if(Data.isBaseType(termsData, "String")) {
|
|
32
|
+
const match = refex.exec(termsData)
|
|
33
|
+
|
|
34
|
+
if(match?.groups?.file) {
|
|
35
|
+
Valid.type(directoryObject, "DirectoryObject")
|
|
36
|
+
|
|
37
|
+
const file = new FileObject(match.groups.file, directoryObject)
|
|
38
|
+
|
|
39
|
+
return await file.loadData()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Try parsing as YAML/JSON
|
|
43
|
+
try {
|
|
44
|
+
const result = JSON5.parse(termsData)
|
|
45
|
+
|
|
46
|
+
return result
|
|
47
|
+
} catch {
|
|
48
|
+
try {
|
|
49
|
+
const result = yaml.parse(termsData)
|
|
50
|
+
|
|
51
|
+
return result
|
|
52
|
+
} catch {
|
|
53
|
+
throw Sass.new(`Could not parse terms data as YAML or JSON: ${termsData}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if(Data.isBaseType(termsData, "Object")) {
|
|
59
|
+
return termsData
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
throw Sass.new(`Invalid terms data type: ${typeof termsData}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get the terms definition
|
|
67
|
+
*
|
|
68
|
+
* @returns {object} The terms definition
|
|
69
|
+
*/
|
|
70
|
+
get definition() {
|
|
71
|
+
return this.#definition
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
}
|
package/src/lib/Util.js
CHANGED
|
@@ -62,6 +62,27 @@ export default class Util {
|
|
|
62
62
|
return `${" ".repeat(diff)}${work}`
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Centre-align a string inside a fixed width (pad with spaces on left).
|
|
67
|
+
* If the string exceeds width it is returned unchanged.
|
|
68
|
+
*
|
|
69
|
+
* @param {string|number} text - Text to align.
|
|
70
|
+
* @param {number} width - Target field width (default 80).
|
|
71
|
+
* @returns {string} Padded string with text centred.
|
|
72
|
+
*/
|
|
73
|
+
static centreAlignText(text, width=80) {
|
|
74
|
+
const work = String(text)
|
|
75
|
+
|
|
76
|
+
if(work.length >= width)
|
|
77
|
+
return work
|
|
78
|
+
|
|
79
|
+
const totalPadding = width - work.length
|
|
80
|
+
const leftPadding = Math.floor(totalPadding / 2)
|
|
81
|
+
const rightPadding = totalPadding - leftPadding
|
|
82
|
+
|
|
83
|
+
return `${" ".repeat(leftPadding)}${work}${" ".repeat(rightPadding)}`
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
/**
|
|
66
87
|
* Compute sha256 hash (hex) of the provided string.
|
|
67
88
|
*
|
|
@@ -211,7 +211,12 @@ export default class Collection {
|
|
|
211
211
|
): Promise<Record<string, R>>
|
|
212
212
|
|
|
213
213
|
/** Allocate an object from a source array and spec */
|
|
214
|
-
static allocateObject(
|
|
214
|
+
static allocateObject(
|
|
215
|
+
source: Array<unknown>,
|
|
216
|
+
spec:
|
|
217
|
+
| Array<unknown>
|
|
218
|
+
| ((source: Array<unknown>) => Promise<Array<unknown>> | Array<unknown>)
|
|
219
|
+
): Promise<Record<string, unknown>>
|
|
215
220
|
|
|
216
221
|
/**
|
|
217
222
|
* Flattens one level of an array of plain objects, transposing values so each
|