@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gesslar/toolkit",
3
- "version": "0.4.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
- "Undefined",
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 valueType === "Number" && !isNaN(value) // Excludes NaN
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
- debug(message, level = 0, ...arg) {
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[prop]
392
+ return Reflect.get(target, prop)
377
393
  }
378
394
  })
package/src/lib/Logger.js CHANGED
@@ -180,3 +180,6 @@ export default class Logger {
180
180
  this.vscodeError?.(JSON.stringify(message))
181
181
  }
182
182
  }
183
+
184
+ // NOTE: This is an artifact file kept for reference during Glog development.
185
+ // Not exported from toolkit. Has broken imports to ./Core.js (actions package).
@@ -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
+ }
@@ -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(source: Array<unknown>, spec: Array<unknown> | ((source: Array<unknown>) => Promise<Array<unknown>> | Array<unknown>)): Promise<Record<string, unknown>>
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