@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.
@@ -0,0 +1,145 @@
1
+ // Implementation: ../lib/Terms.js
2
+
3
+ /**
4
+ * Terms represents an interface definition - what an action promises to provide or accept.
5
+ * It's just the specification, not the negotiation. Contract handles the negotiation.
6
+ *
7
+ * Terms can be created from objects, strings (YAML/JSON), or file references.
8
+ * File references use the format "ref://path/to/file" for loading external definitions.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * // Create terms from object definition
13
+ * const terms = new Terms({
14
+ * provides: {
15
+ * type: "object",
16
+ * properties: {
17
+ * userId: { type: "string" },
18
+ * userName: { type: "string" }
19
+ * },
20
+ * required: ["userId"]
21
+ * }
22
+ * })
23
+ * ```
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * // Parse terms from YAML string
28
+ * const yamlData = `
29
+ * accepts:
30
+ * type: object
31
+ * properties:
32
+ * input:
33
+ * type: string
34
+ * minLength: 1
35
+ * `
36
+ * const parsedTerms = await Terms.parse(yamlData)
37
+ * ```
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * // Parse terms from file reference
42
+ * const directory = new DirectoryObject("/path/to/schemas")
43
+ * const parsedTerms = await Terms.parse("ref://user-schema.json", directory)
44
+ * ```
45
+ */
46
+ declare class Terms {
47
+ /**
48
+ * Creates a new Terms instance with the given definition
49
+ *
50
+ * @param definition - The terms definition object describing what is provided or accepted
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const terms = new Terms({
55
+ * provides: {
56
+ * type: "object",
57
+ * properties: {
58
+ * data: { type: "array", items: { type: "string" } },
59
+ * metadata: {
60
+ * type: "object",
61
+ * properties: {
62
+ * timestamp: { type: "string", format: "date-time" }
63
+ * }
64
+ * }
65
+ * }
66
+ * }
67
+ * })
68
+ * ```
69
+ */
70
+ constructor(definition: object)
71
+
72
+ /**
73
+ * Parses terms data from various sources, handling file references
74
+ *
75
+ * @param termsData - Terms data as string (YAML/JSON/file reference) or object
76
+ * @param directoryObject - Directory context for resolving file references (required for ref:// URLs)
77
+ * @returns Promise resolving to parsed terms data object
78
+ *
79
+ * @throws {Sass} If termsData is not a string or object
80
+ * @throws {Sass} If string data cannot be parsed as YAML or JSON
81
+ * @throws {Sass} If file reference cannot be loaded (missing directory or file not found)
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * // Parse from YAML string
86
+ * const yamlTerms = await Terms.parse(`
87
+ * provides:
88
+ * type: string
89
+ * pattern: "^[A-Z][a-z]+"
90
+ * `)
91
+ * ```
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * // Parse from JSON string
96
+ * const jsonTerms = await Terms.parse(`{
97
+ * "accepts": {
98
+ * "type": "number",
99
+ * "minimum": 0,
100
+ * "maximum": 100
101
+ * }
102
+ * }`)
103
+ * ```
104
+ *
105
+ * @example
106
+ * ```typescript
107
+ * // Parse from file reference
108
+ * const directory = new DirectoryObject("./schemas")
109
+ * const fileTerms = await Terms.parse("ref://api-contract.yaml", directory)
110
+ * ```
111
+ *
112
+ * @example
113
+ * ```typescript
114
+ * // Parse from object (returns as-is)
115
+ * const objectTerms = await Terms.parse({
116
+ * provides: { type: "boolean" }
117
+ * })
118
+ * ```
119
+ */
120
+ static parse(
121
+ termsData: string | object,
122
+ directoryObject?: import('./DirectoryObject.js').default
123
+ ): Promise<object>
124
+
125
+ /**
126
+ * Get the terms definition object
127
+ *
128
+ * @returns The complete terms definition as provided to the constructor
129
+ *
130
+ * @example
131
+ * ```typescript
132
+ * const terms = new Terms({
133
+ * accepts: { type: "string" },
134
+ * provides: { type: "number" }
135
+ * })
136
+ *
137
+ * const definition = terms.definition
138
+ * console.log(definition.accepts) // { type: "string" }
139
+ * console.log(definition.provides) // { type: "number" }
140
+ * ```
141
+ */
142
+ get definition(): object
143
+ }
144
+
145
+ export default Terms
@@ -23,4 +23,4 @@ export default class TypeSpec {
23
23
  reduce<T>(callback: (acc: T, spec: TypeSpecDefinition) => T, initialValue: T): T
24
24
  find(callback: (spec: TypeSpecDefinition) => boolean): TypeSpecDefinition | undefined
25
25
  match(value: unknown, options?: { allowEmpty?: boolean }): boolean
26
- }
26
+ }
@@ -54,6 +54,16 @@ declare class Util {
54
54
  */
55
55
  static rightAlignText(text: string | number, width?: number): string
56
56
 
57
+ /**
58
+ * Centre-align a string inside a fixed width (pad with spaces on left).
59
+ * If the string exceeds width it is returned unchanged.
60
+ *
61
+ * @param text - Text to align.
62
+ * @param width - Target field width (default 80).
63
+ * @returns Padded string with text centred.
64
+ */
65
+ static centreAlignText(text: string | number, width?: number): string
66
+
57
67
  /**
58
68
  * Compute sha256 hash (hex) of the provided string.
59
69
  *
@@ -174,7 +184,15 @@ declare class Util {
174
184
  * @param args - Arguments to pass to event listeners
175
185
  * @returns Resolves when all listeners have completed
176
186
  */
177
- static asyncEmitAnon(emitter: { listeners(event: string): Function[], on(event: string, listener: Function): any, emit(event: string, ...args: unknown[]): any }, event: string, ...args: unknown[]): Promise<void>
187
+ static asyncEmitAnon(
188
+ emitter: {
189
+ listeners(event: string): Function[],
190
+ on(event: string, listener: Function): any,
191
+ emit(event: string, ...args: unknown[]): any
192
+ },
193
+ event: string,
194
+ ...args: unknown[]
195
+ ): Promise<void>
178
196
 
179
197
  /**
180
198
  * Determine the Levenshtein distance between two string values.
@@ -228,7 +246,7 @@ declare class Util {
228
246
  * @param trim - Whether to trim whitespace from each line (default: true)
229
247
  * @param flags - Array of regex flags to apply (default: [])
230
248
  * @returns A new RegExp object with the processed pattern
231
- *
249
+ *
232
250
  * @throws Will throw if input is not a string
233
251
  * @throws Will throw if trim is not a boolean
234
252
  * @throws Will throw if flags is not an array
@@ -7,11 +7,14 @@ export { default as FS } from './FS.js'
7
7
  // Utility classes
8
8
  export { default as Cache } from './Cache.js'
9
9
  export { default as Collection } from './Collection.js'
10
+ export { default as Contract } from './Contract.js'
10
11
  export { default as Data } from './Data.js'
11
12
  export { default as Glog } from './Glog.js'
12
13
  export { default as Sass } from './Sass.js'
14
+ export { default as Schemer } from './Schemer.js'
13
15
  export { default as Tantrum } from './Tantrum.js'
14
16
  export { default as Term } from './Term.js'
17
+ export { default as Terms } from './Terms.js'
15
18
  export { default as Type } from './Type.js'
16
19
  export { default as Util } from './Util.js'
17
20
  export { default as Valid } from './Valid.js'
@@ -1,144 +0,0 @@
1
- import Valid from "./Valid.js"
2
-
3
- /** @typedef {import("./ActionRunner.js").default} ActionRunner */
4
-
5
- /**
6
- * Activity bit flags recognised by {@link ActionBuilder#act}. The flag decides
7
- * how results are accumulated for each activity.
8
- *
9
- * @readonly
10
- * @enum {number}
11
- */
12
- export const ACTIVITY = Object.freeze({
13
- ONCE: 1<<1,
14
- MANY: 1<<2,
15
- PARALLEL: 1<<3,
16
- })
17
-
18
- /**
19
- * Fluent builder for describing how an action should process the context that
20
- * flows through the {@link ActionRunner}. Consumers register named activities,
21
- * optional hook pairs, and nested parallel pipelines before handing the
22
- * builder back to the runner for execution.
23
- *
24
- * Typical usage:
25
- *
26
- * ```js
27
- * const pipeline = new ActionBuilder(myAction)
28
- * .act("prepare", ACTIVITY.ONCE, ctx => ctx.initialise())
29
- * .parallel(parallel => parallel
30
- * .act("step", ACTIVITY.MANY, ctx => ctx.consume())
31
- * )
32
- * .act("finalise", ACTIVITY.ONCE, ctx => ctx.complete())
33
- * .build()
34
- * ```
35
- *
36
- * @class ActionBuilder
37
- */
38
- export default class ActionBuilder {
39
- #action = null
40
- #activities = new Map([])
41
-
42
- /**
43
- * Creates a new ActionBuilder instance with the provided action callback.
44
- *
45
- * @param {(ctx: object) => void} action Base action invoked by the runner when a block
46
- * satisfies the configured structure.
47
- */
48
- constructor(action) {
49
- this.#action = action
50
- }
51
-
52
- /**
53
- * Returns the underlying action that will receive the extracted context.
54
- *
55
- * @returns {(ctx: object) => void} The action callback function that processes the extracted context.
56
- */
57
- get action() {
58
- return this.#action
59
- }
60
-
61
- /**
62
- * Returns the registered activities keyed by their name.
63
- *
64
- * @returns {Map<string | symbol, {op: (context: object) => unknown, kind: number, hooks: {before: ((context: object) => void)|null, after: ((context: object) => void)|null}}>} Map of registered activities and their metadata.
65
- */
66
- get activities() {
67
- return this.#activities
68
- }
69
-
70
- /**
71
- * Registers a named activity that will run for each matching block.
72
- *
73
- * @param {string} name Unique activity identifier.
74
- * @param {number} kind Activity accumulation strategy (see {@link ACTIVITY}).
75
- * @param {(context: object) => unknown} op Work function executed with the runner context.
76
- * @param {{before?: (context: object) => void, after?: (context: object) => void}} [hooks] Optional hooks to run before or after the activity operation.
77
- * @returns {ActionBuilder} Builder instance for chaining.
78
- */
79
- act(name, kind, op, hooks={}) {
80
- this.#validActivityKind(kind)
81
- this.#dupeActivityCheck(name)
82
-
83
- hooks = this.#normalizeHooks(hooks)
84
-
85
- this.#activities.set(name, {op, kind, hooks})
86
-
87
- return this
88
- }
89
-
90
- #normalizeHooks({before=null, after=null}) {
91
- return {before, after}
92
- }
93
-
94
- /**
95
- * Defines a nested pipeline that will run with the {@link ACTIVITY} flag PARALLEL.
96
- *
97
- * The callback receives a fresh {@link ActionBuilder} scoped to the current
98
- * action. The callback must return the configured builder so the runner can
99
- * execute the nested pipeline.
100
- *
101
- * @param {(builder: ActionBuilder) => ActionBuilder} func Callback configuring a nested builder.
102
- * @returns {ActionBuilder} Builder instance for chaining.
103
- */
104
- parallel(func) {
105
- const activityName = Symbol(performance.now())
106
-
107
- this.#activities.set(activityName, {
108
- op: func.call(this.action, new ActionBuilder(this.action)),
109
- kind: ACTIVITY.PARALLEL
110
- })
111
-
112
- return this
113
- }
114
-
115
- #validActivityKind(kind) {
116
- Valid.assert(
117
- Object.values(ACTIVITY).includes(kind),
118
- "Invalid activity kind."
119
- )
120
- }
121
-
122
- /**
123
- * Validates that an activity name has not been reused.
124
- *
125
- * @private
126
- * @param {string|symbol} name Activity identifier.
127
- */
128
- #dupeActivityCheck(name) {
129
- Valid.assert(
130
- !this.#activities.has(name),
131
- `Activity '${String(name)}' has already been registered.`
132
- )
133
- }
134
-
135
- /**
136
- * Finalises the builder and returns a payload that can be consumed by the
137
- * runner.
138
- *
139
- * @returns {{action: (context: object) => unknown, build: ActionBuilder}} Payload consumed by the {@link ActionRunner} constructor.
140
- */
141
- build() {
142
- return {action: this.#action, build: this}
143
- }
144
- }
@@ -1,109 +0,0 @@
1
- import ActionBuilder, {ACTIVITY} from "./ActionBuilder.js"
2
- import Data from "./Data.js"
3
- import Piper from "./Piper.js"
4
- import Sass from "./Sass.js"
5
- import Glog from "./Glog.js"
6
-
7
- /**
8
- * Orchestrates execution of {@link ActionBuilder}-produced pipelines.
9
- *
10
- * Activities run in insertion order, with support for once-off work, repeated
11
- * loops, and nested parallel pipelines. Each activity receives a mutable
12
- * context object under `result.value` that can be replaced or enriched.
13
- */
14
- export default class ActionRunner {
15
- #action = null
16
- #build = null
17
- #logger = null
18
-
19
- constructor({action, build, logger}) {
20
- this.#action = action
21
- this.#build = build
22
- this.#logger = logger ?? {newDebug: () => () => {}}
23
- }
24
-
25
- /**
26
- * Executes the configured action pipeline.
27
- *
28
- * @param {unknown} content Seed value passed to the first activity.
29
- * @returns {Promise<unknown>} Final value produced by the pipeline, or null when a parallel stage reports failures.
30
- * @throws {Sass} When no activities are registered or required parallel builders are missing.
31
- */
32
- async run(content) {
33
- const AR = ActionRunner
34
- const result = {value: content}
35
- const action = this.#action
36
- const activities = this.#build.activities
37
-
38
- if(!activities.size)
39
- throw Sass.new("No activities defined in action.")
40
-
41
- for(const [_,activity] of activities) {
42
- const {op} = activity
43
-
44
- if(activity.kind === ACTIVITY.ONCE) {
45
-
46
- if(Data.typeOf(activity.hooks?.before) === "Function")
47
- await activity.hooks.before.call(action, result)
48
-
49
- const activityResult = await op.call(action, result)
50
-
51
- if(!activityResult)
52
- break
53
-
54
- if(Data.typeOf(activity.hooks?.after) === "Function")
55
- await activity.hooks.after.call(action, result)
56
-
57
- } else if(activity.kind == ACTIVITY.MANY) {
58
- for(;;) {
59
-
60
- if(Data.typeOf(activity.hooks?.before) === "Function")
61
- await activity.hooks.before.call(action, result)
62
-
63
- const activityResult = await op.call(action, result)
64
-
65
- if(!activityResult)
66
- break
67
-
68
- if(Data.typeOf(activity.hooks?.after) === "Function")
69
- await activity.hooks.after.call(action, result)
70
- }
71
- } else if(activity.kind === ACTIVITY.PARALLEL) {
72
- if(op === undefined)
73
- throw Sass.new("Missing action builder. Did you return the builder?")
74
-
75
- if(!op)
76
- throw Sass.new("Okay, cheeky monkey, you need to return the builder for this to work.")
77
-
78
- const piper = new Piper({logger: this.#logger})
79
- .addStep(c => new AR(op.build()).run(c))
80
-
81
- result.value = await piper.pipe()
82
- Glog(result)
83
- throw Sass.new("Nope")
84
-
85
- // // wheeeeeeeeeeeeee! ZOOMZOOM!
86
- // const settled = await Util.settleAll(
87
- // result.value.map()
88
- // )
89
-
90
- // const rejected = settled
91
- // .filter(r => r.status === "rejected")
92
- // .map(r => {
93
- // return r.reason instanceof Sass
94
- // ? r.reason
95
- // : Sass.new("Running structured parsing.", r.reason)
96
- // })
97
- // .map(r => r.report(true))
98
-
99
- // if(rejected.length)
100
- // return null
101
-
102
- // result.value = settled.map(s => s.value)
103
- // .sort((a,b) => a.index-b.index)
104
- }
105
- }
106
-
107
- return result.value
108
- }
109
- }
@@ -1,246 +0,0 @@
1
- import Data from "./Data.js"
2
- import Sass from "./Sass.js"
3
- import ActionBuilder from "./ActionBuilder.js"
4
- import ActionRunner from "./ActionRunner.js"
5
-
6
- /**
7
- * Generic base class for managing actions with lifecycle hooks.
8
- * Provides common functionality for action setup, execution, and cleanup.
9
- * Designed to be extended by specific implementations.
10
- */
11
- export default class BaseActionManager {
12
- #action = null
13
- #hookManager = null
14
- #contract = null
15
- #log = null
16
- #debug = null
17
- #file = null
18
- #variables = null
19
- #runner = null
20
- #id = null
21
-
22
- /**
23
- * @param {object} config - Configuration object
24
- * @param {object} config.actionDefinition - Action definition with action, file, and contract
25
- * @param {object} config.logger - Logger instance
26
- * @param {object} [config.variables] - Variables to pass to action
27
- */
28
- constructor({actionDefinition, logger, variables}) {
29
- this.#id = Symbol(performance.now())
30
- this.#log = logger
31
- this.#debug = this.#log.newDebug()
32
- this.#variables = variables || {}
33
-
34
- this.#initialize(actionDefinition)
35
- }
36
-
37
- get id() {
38
- return this.#id
39
- }
40
-
41
- get action() {
42
- return this.#action
43
- }
44
-
45
- get hookManager() {
46
- return this.#hookManager
47
- }
48
-
49
- set hookManager(hookManager) {
50
- if(this.hookManager)
51
- throw new Error("Hook manager already set")
52
-
53
- this.#hookManager = hookManager
54
- this.#attachHooksToAction(hookManager)
55
- }
56
-
57
- get contract() {
58
- return this.#contract
59
- }
60
-
61
- get meta() {
62
- return this.#action?.meta
63
- }
64
-
65
- get log() {
66
- return this.#log
67
- }
68
-
69
- get variables() {
70
- return this.#variables
71
- }
72
-
73
- get runner() {
74
- return this.#runner
75
- }
76
-
77
- get file() {
78
- return this.#file
79
- }
80
-
81
- /**
82
- * Initialize the action manager with the provided definition.
83
- * Override in subclasses to add specific validation or setup.
84
- *
85
- * @param {object} actionDefinition - Action definition
86
- * @protected
87
- */
88
- #initialize(actionDefinition) {
89
- const debug = this.#debug
90
-
91
- debug("Setting up action", 2)
92
-
93
- const {action, file, contract} = actionDefinition
94
-
95
- if(!action)
96
- throw new Error("Action is required")
97
-
98
- if(!contract)
99
- throw new Error("Contract is required")
100
-
101
- this.#action = action
102
- this.#contract = contract
103
- this.#file = file
104
-
105
- debug("Action initialization complete", 2)
106
- }
107
-
108
- /**
109
- * Attach hooks to the action instance.
110
- * Override in subclasses to customize hook attachment.
111
- *
112
- * @param {object} hookManager - Hook manager instance
113
- * @protected
114
- */
115
- #attachHooksToAction(hookManager) {
116
- // Basic hook attachment - can be overridden by subclasses
117
- this.action.hook = hookManager.on?.bind(hookManager)
118
- this.action.hooks = hookManager.hooks
119
- }
120
-
121
- /**
122
- * Setup the action by creating and configuring the runner.
123
- * Override setupActionInstance() in subclasses for custom setup logic.
124
- *
125
- * @returns {Promise<void>}
126
- */
127
- async setupAction() {
128
- this.#debug("Setting up action for %s on %s", 2, this.action.meta?.kind, this.id)
129
-
130
- await this.#setupHooks()
131
- await this.#setupActionInstance()
132
- }
133
-
134
- /**
135
- * Setup the action instance and create the runner.
136
- * Override in subclasses to customize action setup.
137
- *
138
- * @protected
139
- */
140
- async #setupActionInstance() {
141
- const actionInstance = new this.action()
142
- const setup = actionInstance?.setup
143
-
144
- // Setup is required for actions.
145
- if(Data.typeOf(setup) === "Function") {
146
- const builder = new ActionBuilder(actionInstance)
147
- const configuredBuilder = setup(builder)
148
- const buildResult = configuredBuilder.build()
149
- const runner = new ActionRunner({
150
- action: buildResult.action,
151
- build: buildResult.build,
152
- logger: this.#log
153
- })
154
-
155
- this.#runner = runner
156
- } else {
157
- throw Sass.new("Action setup must be a function.")
158
- }
159
- }
160
-
161
- /**
162
- * Run the action with the provided input.
163
- *
164
- * @param {unknown} result - Input to pass to the action
165
- * @returns {Promise<unknown>} Action result
166
- */
167
- async runAction(result) {
168
- if(!this.#runner)
169
- throw new Error("Action not set up. Call setupAction() first.")
170
-
171
- return await this.#runner.run(result)
172
- }
173
-
174
- /**
175
- * Cleanup the action and hooks.
176
- *
177
- * @returns {Promise<void>}
178
- */
179
- async cleanupAction() {
180
- this.#debug("Cleaning up action for %s on %s", 2, this.action.meta?.kind, this.id)
181
-
182
- await this.#cleanupHooks()
183
- await this.#cleanupActionInstance()
184
- }
185
-
186
- /**
187
- * Setup hooks if hook manager is present.
188
- * Override in subclasses to customize hook setup.
189
- *
190
- * @protected
191
- */
192
- async #setupHooks() {
193
- const setup = this.#hookManager?.setup
194
-
195
- const type = Data.typeOf(setup)
196
-
197
- // No hooks attached.
198
- if(type === "Null" || type === "Undefined")
199
- return
200
-
201
- if(type !== "Function")
202
- throw Sass.new("Hook setup must be a function.")
203
-
204
- await setup.call(
205
- this.hookManager.hooks, {
206
- action: this.action,
207
- variables: this.#variables,
208
- log: this.#log
209
- }
210
- )
211
- }
212
-
213
- /**
214
- * Cleanup hooks if hook manager is present.
215
- * Override in subclasses to customize hook cleanup.
216
- *
217
- * @protected
218
- */
219
- async #cleanupHooks() {
220
- const cleanup = this.hookManager?.cleanup
221
-
222
- if(!cleanup)
223
- return
224
-
225
- await cleanup.call(this.hookManager.hooks)
226
- }
227
-
228
- /**
229
- * Cleanup the action instance.
230
- * Override in subclasses to add custom cleanup logic.
231
- *
232
- * @protected
233
- */
234
- async #cleanupActionInstance() {
235
- const cleanup = this.action?.cleanup
236
-
237
- if(!cleanup)
238
- return
239
-
240
- await cleanup.call(this.action)
241
- }
242
-
243
- toString() {
244
- return `${this.#file?.module || "UNDEFINED"} (${this.meta?.action || "UNDEFINED"})`
245
- }
246
- }