@gesslar/toolkit 0.4.0 → 0.5.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/Action.js +283 -0
- package/src/lib/ActionRunner.js +21 -51
- package/src/lib/Contract.js +257 -0
- package/src/lib/Data.js +1 -1
- package/src/lib/Glog.js +3 -2
- package/src/lib/Hooks.js +194 -0
- package/src/lib/Piper.js +74 -100
- package/src/lib/Schemer.js +89 -0
- package/src/lib/Terms.js +74 -0
- package/src/types/Contract.d.ts +162 -0
- package/src/types/Schemer.d.ts +179 -0
- package/src/types/Terms.d.ts +145 -0
- package/src/types/index.d.ts +3 -0
- package/src/lib/BaseActionManager.js +0 -246
- package/src/lib/BaseHookManager.js +0 -209
package/src/lib/Glog.js
CHANGED
package/src/lib/Hooks.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import {setTimeout as timeout} from "timers/promises"
|
|
2
|
+
|
|
3
|
+
import FileObject from "./FileObject.js"
|
|
4
|
+
import Sass from "./Sass.js"
|
|
5
|
+
import Util from "./Util.js"
|
|
6
|
+
import Valid from "./Valid.js"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generic base class for managing hooks with configurable event types.
|
|
10
|
+
* Provides common functionality for hook registration, execution, and lifecycle management.
|
|
11
|
+
* Designed to be extended by specific implementations.
|
|
12
|
+
*/
|
|
13
|
+
export default class Hooks {
|
|
14
|
+
#hooksFile = null
|
|
15
|
+
#hooks = null
|
|
16
|
+
#actionKind = null
|
|
17
|
+
#timeout = 1000 // Default 1 second timeout
|
|
18
|
+
#debug = null
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a new BaseHookManager instance.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} config - Configuration object
|
|
24
|
+
* @param {string} config.actionKind - Action identifier
|
|
25
|
+
* @param {FileObject} config.hooksFile - File object containing hooks with uri property
|
|
26
|
+
* @param {number} [config.hookTimeout] - Hook execution timeout in milliseconds
|
|
27
|
+
* @param {unknown} [config.hooks] - The hooks object
|
|
28
|
+
* @param {import('../types.js').DebugFunction} debug - Debug function from Glog.
|
|
29
|
+
*/
|
|
30
|
+
constructor({actionKind, hooksFile, hooks, hookTimeout = 1000}, debug) {
|
|
31
|
+
this.#actionKind = actionKind
|
|
32
|
+
this.#hooksFile = hooksFile
|
|
33
|
+
this.#hooks = hooks
|
|
34
|
+
this.#timeout = hookTimeout
|
|
35
|
+
this.#debug = debug
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets the action identifier.
|
|
40
|
+
*
|
|
41
|
+
* @returns {string} Action identifier or instance
|
|
42
|
+
*/
|
|
43
|
+
get actionKind() {
|
|
44
|
+
return this.#actionKind
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Gets the hooks file object.
|
|
49
|
+
*
|
|
50
|
+
* @returns {FileObject} File object containing hooks
|
|
51
|
+
*/
|
|
52
|
+
get hooksFile() {
|
|
53
|
+
return this.#hooksFile
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Gets the loaded hooks object.
|
|
58
|
+
*
|
|
59
|
+
* @returns {object|null} Hooks object or null if not loaded
|
|
60
|
+
*/
|
|
61
|
+
get hooks() {
|
|
62
|
+
return this.#hooks
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Gets the hook execution timeout in milliseconds.
|
|
67
|
+
*
|
|
68
|
+
* @returns {number} Timeout in milliseconds
|
|
69
|
+
*/
|
|
70
|
+
get timeout() {
|
|
71
|
+
return this.#timeout
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Gets the setup hook function if available.
|
|
76
|
+
*
|
|
77
|
+
* @returns {(args: object) => unknown|null} Setup hook function or null
|
|
78
|
+
*/
|
|
79
|
+
get setup() {
|
|
80
|
+
return this.hooks?.setup || null
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Gets the cleanup hook function if available.
|
|
85
|
+
*
|
|
86
|
+
* @returns {(args: object) => unknown|null} Cleanup hook function or null
|
|
87
|
+
*/
|
|
88
|
+
get cleanup() {
|
|
89
|
+
return this.hooks?.cleanup || null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Static factory method to create and initialize a hook manager.
|
|
94
|
+
* Loads hooks from the specified file and returns an initialized instance.
|
|
95
|
+
* Override loadHooks() in subclasses to customize hook loading logic.
|
|
96
|
+
*
|
|
97
|
+
* @param {object} config - Same configuration object as constructor
|
|
98
|
+
* @param {string|object} config.actionKind - Action identifier or instance
|
|
99
|
+
* @param {FileObject} config.hooksFile - File object containing hooks with uri property
|
|
100
|
+
* @param {number} [config.timeOut] - Hook execution timeout in milliseconds
|
|
101
|
+
* @param {import('../types.js').DebugFunction} debug - The debug function.
|
|
102
|
+
* @returns {Promise<Hooks|null>} Initialized hook manager or null if no hooks found
|
|
103
|
+
*/
|
|
104
|
+
static async new(config, debug) {
|
|
105
|
+
debug("Creating new HookManager instance with args: %o", 2, config)
|
|
106
|
+
|
|
107
|
+
const instance = new this(config, debug)
|
|
108
|
+
const hooksFile = instance.hooksFile
|
|
109
|
+
|
|
110
|
+
debug("Loading hooks from %o", 2, hooksFile.uri)
|
|
111
|
+
|
|
112
|
+
debug("Checking hooks file exists: %o", 2, hooksFile.uri)
|
|
113
|
+
if(!await hooksFile.exists)
|
|
114
|
+
throw Sass.new(`No such hooks file, ${hooksFile.uri}`)
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const hooksImport = await hooksFile.import()
|
|
118
|
+
|
|
119
|
+
if(!hooksImport)
|
|
120
|
+
return null
|
|
121
|
+
|
|
122
|
+
debug("Hooks file imported successfully as a module", 2)
|
|
123
|
+
|
|
124
|
+
const actionKind = instance.actionKind
|
|
125
|
+
if(!hooksImport[actionKind])
|
|
126
|
+
return null
|
|
127
|
+
|
|
128
|
+
const hooks = new hooksImport[actionKind]({debug})
|
|
129
|
+
|
|
130
|
+
debug(hooks.constructor.name, 4)
|
|
131
|
+
|
|
132
|
+
// Attach common properties to hooks
|
|
133
|
+
instance.#hooks = hooks
|
|
134
|
+
|
|
135
|
+
debug("Hooks %o loaded successfully for %o", 2, hooksFile.uri, instance.actionKind)
|
|
136
|
+
|
|
137
|
+
return instance
|
|
138
|
+
} catch(error) {
|
|
139
|
+
debug("Failed to load hooks %o: %o", 1, hooksFile.uri, error.message)
|
|
140
|
+
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async callHook(kind, activityName, context) {
|
|
146
|
+
try {
|
|
147
|
+
const debug = this.#debug
|
|
148
|
+
const hooks = this.#hooks
|
|
149
|
+
|
|
150
|
+
if(!hooks)
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
const hookName = `${kind}$${activityName}`
|
|
154
|
+
|
|
155
|
+
debug("Looking for hook: %o", 4, hookName)
|
|
156
|
+
|
|
157
|
+
const hook = hooks[hookName]
|
|
158
|
+
if(!hook)
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
debug("Triggering hook: %o", 4, hookName)
|
|
162
|
+
Valid.type(hook, "Function", `Hook "${hookName}" is not a function`)
|
|
163
|
+
|
|
164
|
+
const hookFunction = async() => {
|
|
165
|
+
debug("Hook function starting execution: %o", 4, hookName)
|
|
166
|
+
|
|
167
|
+
const duration = (await Util.time(() => hook.call(this.#hooks, context))).cost
|
|
168
|
+
|
|
169
|
+
debug("Hook function completed successfully: %o, after %oms", 4, hookName, duration)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const hookTimeout = this.timeout
|
|
173
|
+
const expireAsync = (async() => {
|
|
174
|
+
await timeout(hookTimeout)
|
|
175
|
+
throw Sass.new(`Hook ${hookName} execution exceeded timeout of ${hookTimeout}ms`)
|
|
176
|
+
})()
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
debug("Starting Promise race for hook: %o", 4, hookName)
|
|
180
|
+
await Util.race([
|
|
181
|
+
hookFunction(),
|
|
182
|
+
expireAsync
|
|
183
|
+
])
|
|
184
|
+
} catch(error) {
|
|
185
|
+
throw Sass.new(`Processing hook ${kind}$${activityName}`, error)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
debug("We made it throoough the wildernessss", 4)
|
|
189
|
+
|
|
190
|
+
} catch(error) {
|
|
191
|
+
throw Sass.new(`Processing hook ${kind}$${activityName}`, error)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
package/src/lib/Piper.js
CHANGED
|
@@ -9,31 +9,36 @@
|
|
|
9
9
|
* - Error handling and reporting
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import Glog from "./Glog.js"
|
|
13
|
+
import Sass from "./Sass.js"
|
|
14
|
+
import Tantrum from "./Tantrum.js"
|
|
15
|
+
import Util from "./Util.js"
|
|
16
|
+
|
|
12
17
|
export default class Piper {
|
|
13
|
-
#
|
|
14
|
-
|
|
15
|
-
#
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
constructor(
|
|
22
|
-
this.#
|
|
18
|
+
#debug
|
|
19
|
+
|
|
20
|
+
#lifeCycle = new Map([
|
|
21
|
+
["setup", new Set()],
|
|
22
|
+
["process", new Set()],
|
|
23
|
+
["teardown", new Set()]
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
constructor(arg) {
|
|
27
|
+
this.#debug = arg?.debug ?? new Glog().newDebug("[PIPER]")
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
/**
|
|
26
31
|
* Add a processing step to the pipeline
|
|
27
32
|
*
|
|
28
|
-
* @param {(context: object) => Promise<object>}
|
|
33
|
+
* @param {(context: object) => Promise<object>} fn - Function that processes an item: (context) => Promise<result>
|
|
29
34
|
* @param {object} options - Step options (name, required, etc.)
|
|
30
35
|
* @returns {Piper} The pipeline instance (for chaining)
|
|
31
36
|
*/
|
|
32
|
-
addStep(
|
|
33
|
-
this.#
|
|
34
|
-
fn
|
|
35
|
-
name: options.name || `Step ${this.#
|
|
36
|
-
required: options.required
|
|
37
|
+
addStep(fn, options = {}) {
|
|
38
|
+
this.#lifeCycle.get("process").add({
|
|
39
|
+
fn,
|
|
40
|
+
name: options.name || `Step ${this.#lifeCycle.get("process").size + 1}`,
|
|
41
|
+
required: !!options.required, // Default to required
|
|
37
42
|
...options
|
|
38
43
|
})
|
|
39
44
|
|
|
@@ -43,11 +48,11 @@ export default class Piper {
|
|
|
43
48
|
/**
|
|
44
49
|
* Add setup hook that runs before processing starts
|
|
45
50
|
*
|
|
46
|
-
* @param {() => Promise<void>}
|
|
51
|
+
* @param {() => Promise<void>} fn - Setup function: () => Promise<void>
|
|
47
52
|
* @returns {Piper} The pipeline instance (for chaining)
|
|
48
53
|
*/
|
|
49
|
-
addSetup(
|
|
50
|
-
this.#
|
|
54
|
+
addSetup(fn) {
|
|
55
|
+
this.#lifeCycle.get("setup").add(fn)
|
|
51
56
|
|
|
52
57
|
return this
|
|
53
58
|
}
|
|
@@ -55,11 +60,11 @@ export default class Piper {
|
|
|
55
60
|
/**
|
|
56
61
|
* Add cleanup hook that runs after processing completes
|
|
57
62
|
*
|
|
58
|
-
* @param {() => Promise<void>}
|
|
63
|
+
* @param {() => Promise<void>} fn - Cleanup function: () => Promise<void>
|
|
59
64
|
* @returns {Piper} The pipeline instance (for chaining)
|
|
60
65
|
*/
|
|
61
|
-
addCleanup(
|
|
62
|
-
this.#
|
|
66
|
+
addCleanup(fn) {
|
|
67
|
+
this.#lifeCycle.get("teardown").add(fn)
|
|
63
68
|
|
|
64
69
|
return this
|
|
65
70
|
}
|
|
@@ -72,50 +77,54 @@ export default class Piper {
|
|
|
72
77
|
* @returns {Promise<object>} - Results object with succeeded, warned, errored arrays
|
|
73
78
|
*/
|
|
74
79
|
async pipe(items, maxConcurrent = 10) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
items.forEach(item => item.pipeStamp = Symbol(performance.now()))
|
|
81
|
+
|
|
82
|
+
let itemIndex = 0
|
|
83
|
+
const allResults = []
|
|
84
|
+
|
|
85
|
+
const processWorker = async() => {
|
|
86
|
+
while(true) {
|
|
87
|
+
const currentIndex = itemIndex++
|
|
88
|
+
if(currentIndex >= items.length)
|
|
89
|
+
break
|
|
90
|
+
|
|
91
|
+
const item = items[currentIndex]
|
|
92
|
+
try {
|
|
93
|
+
const result = await this.#processItem(item)
|
|
94
|
+
allResults.push(result)
|
|
95
|
+
} catch(error) {
|
|
96
|
+
throw Sass.new("Processing pipeline item.", error)
|
|
90
97
|
}
|
|
91
|
-
|
|
92
|
-
// Process next item if queue has items
|
|
93
|
-
if(itemQueue.length > 0) {
|
|
94
|
-
const nextItem = itemQueue.shift()
|
|
95
|
-
|
|
96
|
-
return processNextItem(nextItem)
|
|
97
|
-
}
|
|
98
|
-
})
|
|
98
|
+
}
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const item = itemQueue.shift()
|
|
101
|
+
const setupResult = await Util.settleAll([...this.#lifeCycle.get("setup")].map(e => e()))
|
|
102
|
+
this.#processResult("Setting up the pipeline.", setupResult)
|
|
104
103
|
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
// Start workers up to maxConcurrent limit
|
|
105
|
+
const workers = []
|
|
106
|
+
const workerCount = Math.min(maxConcurrent, items.length)
|
|
107
|
+
|
|
108
|
+
for(let i = 0; i < workerCount; i++)
|
|
109
|
+
workers.push(processWorker())
|
|
107
110
|
|
|
108
|
-
// Wait for all
|
|
109
|
-
await
|
|
111
|
+
// Wait for all workers to complete
|
|
112
|
+
const processResult = await Util.settleAll(workers)
|
|
113
|
+
this.#processResult("Processing pipeline.", processResult)
|
|
110
114
|
|
|
111
115
|
// Run cleanup hooks
|
|
112
|
-
await
|
|
116
|
+
const teardownResult = await Util.settleAll([...this.#lifeCycle.get("teardown")].map(e => e()))
|
|
117
|
+
this.#processResult("Tearing down the pipeline.", teardownResult)
|
|
113
118
|
|
|
114
|
-
return
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
return allResults
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
#processResult(message, settled) {
|
|
123
|
+
if(settled.some(r => r.status === "rejected"))
|
|
124
|
+
throw Tantrum.new(
|
|
125
|
+
message,
|
|
126
|
+
settled.filter(r => r.status==="rejected").map(r => r.reason)
|
|
127
|
+
)
|
|
119
128
|
}
|
|
120
129
|
|
|
121
130
|
/**
|
|
@@ -126,56 +135,21 @@ export default class Piper {
|
|
|
126
135
|
* @private
|
|
127
136
|
*/
|
|
128
137
|
async #processItem(item) {
|
|
129
|
-
const debug = this.#
|
|
130
|
-
const context = {item, data: {}}
|
|
138
|
+
const debug = this.#debug
|
|
131
139
|
|
|
132
140
|
try {
|
|
133
141
|
// Execute each step in sequence
|
|
134
|
-
|
|
135
|
-
debug(`Executing step: ${step.name}`, 2)
|
|
136
|
-
|
|
137
|
-
const result = await step.fn(context)
|
|
142
|
+
let result = item
|
|
138
143
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if(result.status === "error") {
|
|
142
|
-
return result
|
|
143
|
-
}
|
|
144
|
+
for(const step of this.#lifeCycle.get("process")) {
|
|
145
|
+
debug("Executing step: %o", 2, step.name)
|
|
144
146
|
|
|
145
|
-
|
|
146
|
-
return result
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Merge result data into context for next steps
|
|
150
|
-
context.data = {...context.data, ...result.data}
|
|
151
|
-
context.status = result.status || context.status
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
status: context.status || "success",
|
|
157
|
-
...context.data
|
|
147
|
+
result = await step.fn(result) ?? result
|
|
158
148
|
}
|
|
159
149
|
|
|
150
|
+
return result
|
|
160
151
|
} catch(error) {
|
|
161
|
-
|
|
162
|
-
status: "error",
|
|
163
|
-
error,
|
|
164
|
-
item
|
|
165
|
-
}
|
|
152
|
+
throw Sass.new("Processing an item.", error)
|
|
166
153
|
}
|
|
167
154
|
}
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* Clear results (useful for reusing pipeline instance)
|
|
171
|
-
*
|
|
172
|
-
* @returns {Piper} The pipeline instance (for chaining)
|
|
173
|
-
*/
|
|
174
|
-
clearResults() {
|
|
175
|
-
this.#succeeded = []
|
|
176
|
-
this.#warned = []
|
|
177
|
-
this.#errored = []
|
|
178
|
-
|
|
179
|
-
return this
|
|
180
|
-
}
|
|
181
155
|
}
|
|
@@ -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
|
+
}
|