@gesslar/bedoc 1.7.0 → 1.10.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/dist/bedoc.action.json +50 -0
- package/package.json +2 -1
- package/src/core/ActionManager.js +12 -2
- package/src/core/Configuration.js +31 -3
- package/src/core/ConfigurationParameters.js +8 -0
- package/src/core/ContractManager.js +112 -0
- package/src/core/Conveyor.js +3 -0
- package/src/core/Core.js +12 -7
- package/src/core/Discovery.js +94 -67
- package/src/core/contract/ParseContract.js +7 -0
- package/src/core/contract/PrintContract.js +7 -0
- package/src/core/util/ContractUtil.js +63 -0
- package/src/core/util/DataUtil.js +59 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://bedoc.gesslar.dev/schemas/v1/bedoc.action.json",
|
|
4
|
+
"title": "BeDoc Action Schema",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"accepts": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"type": {
|
|
11
|
+
"const": "object"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"required": [
|
|
15
|
+
"type"
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"provides": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"properties": {
|
|
21
|
+
"type": {
|
|
22
|
+
"const": "object"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"required": [
|
|
26
|
+
"type"
|
|
27
|
+
],
|
|
28
|
+
"not": {
|
|
29
|
+
"properties": {
|
|
30
|
+
"required": {}
|
|
31
|
+
},
|
|
32
|
+
"required": [
|
|
33
|
+
"required"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"oneOf": [
|
|
39
|
+
{
|
|
40
|
+
"required": [
|
|
41
|
+
"accepts"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"required": [
|
|
46
|
+
"provides"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gesslar/bedoc",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Pluggable documentation engine for any language and format",
|
|
5
5
|
"publisher": "gesslar",
|
|
6
6
|
"author": "gesslar",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"lint:fix": "npx eslint . --fix"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
+
"ajv": "^8.17.1",
|
|
40
41
|
"commander": "^13.0.0",
|
|
41
42
|
"dotenv": "^16.4.7",
|
|
42
43
|
"error-stack-parser": "^2.1.4",
|
|
@@ -7,10 +7,12 @@ export default class ActionManager {
|
|
|
7
7
|
#log
|
|
8
8
|
#debug
|
|
9
9
|
#file
|
|
10
|
+
#variables
|
|
10
11
|
|
|
11
|
-
constructor(actionDefinition, logger) {
|
|
12
|
+
constructor({actionDefinition, logger, variables}) {
|
|
12
13
|
this.#log = logger
|
|
13
14
|
this.#debug = this.#log.newDebug()
|
|
15
|
+
this.#variables = variables
|
|
14
16
|
|
|
15
17
|
this.#initialize(actionDefinition)
|
|
16
18
|
}
|
|
@@ -65,6 +67,10 @@ export default class ActionManager {
|
|
|
65
67
|
return this.#log
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
get variables() {
|
|
71
|
+
return this.#variables
|
|
72
|
+
}
|
|
73
|
+
|
|
68
74
|
async #setupAction() {
|
|
69
75
|
const setup = this.action?.setup
|
|
70
76
|
|
|
@@ -90,7 +96,11 @@ export default class ActionManager {
|
|
|
90
96
|
return
|
|
91
97
|
|
|
92
98
|
await this.hookManager.setup.call(
|
|
93
|
-
this.hookManager.hooks, {
|
|
99
|
+
this.hookManager.hooks, {
|
|
100
|
+
action: this.action,
|
|
101
|
+
variables: this.#variables,
|
|
102
|
+
log: this.#log
|
|
103
|
+
}
|
|
94
104
|
)
|
|
95
105
|
}
|
|
96
106
|
|
|
@@ -246,9 +246,22 @@ export default class Configuration {
|
|
|
246
246
|
if(!configFile)
|
|
247
247
|
throw new Error("No config file specified")
|
|
248
248
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
249
|
+
const configObject = loadJson(configFile)
|
|
250
|
+
const subConfigName =
|
|
251
|
+
entryOptions?.sub ||
|
|
252
|
+
packageJson?.sub ||
|
|
253
|
+
environmentVariables?.sub
|
|
254
|
+
|
|
255
|
+
// If we didn't specify a subconfiguration, let's just remove
|
|
256
|
+
// it so it doesn't pollute anything.
|
|
257
|
+
if(!subConfigName)
|
|
258
|
+
delete configObject.sub
|
|
259
|
+
|
|
260
|
+
const finalConfig = subConfigName?.value
|
|
261
|
+
? this.#resolveSubconfigs(configObject, subConfigName.value)
|
|
262
|
+
: configObject
|
|
263
|
+
|
|
264
|
+
allOptions.push({source: "config", options: finalConfig})
|
|
252
265
|
}
|
|
253
266
|
|
|
254
267
|
allOptions.push({source: "entry", options: entryOptions})
|
|
@@ -256,6 +269,21 @@ export default class Configuration {
|
|
|
256
269
|
return allOptions
|
|
257
270
|
}
|
|
258
271
|
|
|
272
|
+
#resolveSubconfigs(configObject, subConfigName) {
|
|
273
|
+
const subConfig = configObject.sub?.find(sub => sub.name === subConfigName)
|
|
274
|
+
|
|
275
|
+
if(!subConfig)
|
|
276
|
+
throw new Error(`No such subconfiguration \`${subConfigName}\``)
|
|
277
|
+
|
|
278
|
+
// We don't need this anymore
|
|
279
|
+
delete subConfig.name
|
|
280
|
+
|
|
281
|
+
for(const [key,val] of Object.entries(subConfig))
|
|
282
|
+
configObject[key] = val
|
|
283
|
+
|
|
284
|
+
return configObject
|
|
285
|
+
}
|
|
286
|
+
|
|
259
287
|
/**
|
|
260
288
|
* Get environment variables
|
|
261
289
|
*
|
|
@@ -120,6 +120,14 @@ const ConfigurationParameters = Object.freeze({
|
|
|
120
120
|
mustExist: true,
|
|
121
121
|
},
|
|
122
122
|
},
|
|
123
|
+
sub: {
|
|
124
|
+
short: "s",
|
|
125
|
+
param: "name",
|
|
126
|
+
description: "Specify a subconfiguration",
|
|
127
|
+
type: newTypeSpec("string"),
|
|
128
|
+
required: false,
|
|
129
|
+
dependent: "config",
|
|
130
|
+
},
|
|
123
131
|
debug: {
|
|
124
132
|
short: "d",
|
|
125
133
|
description: "Enable debug mode",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import yaml from "yaml"
|
|
2
|
+
import JSON5 from "json5"
|
|
3
|
+
|
|
4
|
+
import * as FDUtil from "./util/FDUtil.js"
|
|
5
|
+
import * as ContractUtil from "./util/ContractUtil.js"
|
|
6
|
+
import * as DataUtil from "./util/DataUtil.js"
|
|
7
|
+
|
|
8
|
+
const {resolveFilename, readFile} = FDUtil
|
|
9
|
+
const {loadSchema, getValidator} = ContractUtil
|
|
10
|
+
const {findClosestMatch} = DataUtil
|
|
11
|
+
|
|
12
|
+
const refex = /^ref:\/\/(.*)$/
|
|
13
|
+
|
|
14
|
+
export default class ContractManager {
|
|
15
|
+
static async newContract(actionType, terms) {
|
|
16
|
+
// Load and validate against the BeDoc contract schema
|
|
17
|
+
const schema = await loadSchema()
|
|
18
|
+
const validator = getValidator(schema)
|
|
19
|
+
const valid = validator(terms)
|
|
20
|
+
|
|
21
|
+
if(!valid) {
|
|
22
|
+
const error = ContractManager.reportValidationErrors(validator.errors)
|
|
23
|
+
|
|
24
|
+
throw new Error(`Invalid contract terms:\n${error}`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const dataValidator = getValidator({
|
|
28
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
29
|
+
"$id": `${actionType} Schema`,
|
|
30
|
+
title: `${actionType} Schema`,
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: terms,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return new Contract(dataValidator)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
static parse(contractData, directoryObject) {
|
|
39
|
+
if(typeof contractData === "string") {
|
|
40
|
+
const match = refex.exec(contractData)
|
|
41
|
+
|
|
42
|
+
if(match)
|
|
43
|
+
contractData = readFile(resolveFilename(match[1], directoryObject))
|
|
44
|
+
|
|
45
|
+
return yaml.parse(String(contractData))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw new Error(`Invalid contract data: ${JSON5.stringify(contractData)}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static reportValidationErrors(errors) {
|
|
52
|
+
return errors.reduce((acc, error) => {
|
|
53
|
+
let msg = `- "${error.instancePath || "(root)"}" ${error.message}`
|
|
54
|
+
|
|
55
|
+
if(error.params) {
|
|
56
|
+
const details = []
|
|
57
|
+
|
|
58
|
+
if(error.params.type)
|
|
59
|
+
details.push(` ➜ Expected type: ${error.params.type}`)
|
|
60
|
+
|
|
61
|
+
if(error.params.missingProperty)
|
|
62
|
+
details.push(` ➜ Missing required field: ${error.params.missingProperty}`)
|
|
63
|
+
|
|
64
|
+
if(error.params.allowedValues) {
|
|
65
|
+
details.push(` ➜ Allowed values: "${error.params.allowedValues.join('", "')}"`)
|
|
66
|
+
details.push(` ➜ Received value: "${error.data}"`)
|
|
67
|
+
const closestMatch =
|
|
68
|
+
findClosestMatch(error.data, error.params.allowedValues)
|
|
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 acc ? `${acc}\n${msg}` : msg
|
|
87
|
+
}, "")
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
class Contract {
|
|
92
|
+
#validator = null
|
|
93
|
+
|
|
94
|
+
constructor(validator) {
|
|
95
|
+
this.#validator = validator
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get validator() {
|
|
99
|
+
return this.#validator
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
validate(data) {
|
|
103
|
+
const validator = this.validator
|
|
104
|
+
const valid = validator(data)
|
|
105
|
+
|
|
106
|
+
if(!valid) {
|
|
107
|
+
const error = ContractManager.reportValidationErrors(validator.errors)
|
|
108
|
+
|
|
109
|
+
throw new Error(`This document violates the agreed upon contract:\n${error}`)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
package/src/core/Conveyor.js
CHANGED
|
@@ -107,6 +107,9 @@ export default class Conveyor {
|
|
|
107
107
|
return {status: "warning", file, warning: mess}
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
parse.contract.validate(parseResult)
|
|
111
|
+
print.contract.validate(parseResult)
|
|
112
|
+
|
|
110
113
|
// Step 3: Print file
|
|
111
114
|
const printResult = await print.runAction({
|
|
112
115
|
file,
|
package/src/core/Core.js
CHANGED
|
@@ -42,7 +42,7 @@ export default class Core {
|
|
|
42
42
|
const instance = new Core({...validConfig, name: "BeDoc"})
|
|
43
43
|
const debug = instance.logger.newDebug()
|
|
44
44
|
|
|
45
|
-
debug("Creating new BeDoc instance with options: `%o`",
|
|
45
|
+
debug("Creating new BeDoc instance with options: `%o`", 3, validConfig)
|
|
46
46
|
|
|
47
47
|
const discovery = new Discovery(instance)
|
|
48
48
|
const actionDefs = await discovery.discoverActions({
|
|
@@ -52,7 +52,7 @@ export default class Core {
|
|
|
52
52
|
|
|
53
53
|
const validCrit = discovery.satisfyCriteria(actionDefs, validConfig)
|
|
54
54
|
|
|
55
|
-
debug("Actions that met criteria: `%o`",
|
|
55
|
+
debug("Actions that met criteria: `%o`", 3, validCrit)
|
|
56
56
|
|
|
57
57
|
if(Object.values(validCrit).some(arr => arr.length === 0))
|
|
58
58
|
throw new Error("No found matching parser and printer")
|
|
@@ -63,6 +63,7 @@ export default class Core {
|
|
|
63
63
|
const printer = validCrit.print[printers]
|
|
64
64
|
const printerSchema = printer.contract
|
|
65
65
|
const satisfied = []
|
|
66
|
+
|
|
66
67
|
for(const parser of validCrit.parse) {
|
|
67
68
|
const parserSchema = parser.contract
|
|
68
69
|
const result = schemaCompare(parserSchema, printerSchema)
|
|
@@ -91,14 +92,18 @@ export default class Core {
|
|
|
91
92
|
|
|
92
93
|
// Adding to instance
|
|
93
94
|
instance.actions = {}
|
|
95
|
+
const {variables} = validConfig
|
|
94
96
|
const managers = {print: PrintManager, parse: ParseManager}
|
|
95
|
-
for(const [,
|
|
96
|
-
const {action: actionType} =
|
|
97
|
+
for(const [, actionDefinition] of Object.entries(finalActions)) {
|
|
98
|
+
const {action: actionType} = actionDefinition.action.meta
|
|
97
99
|
|
|
98
100
|
debug("Attaching %o action to instance", 2, actionType)
|
|
99
|
-
instance.actions[actionType] =
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
instance.actions[actionType] =
|
|
102
|
+
new managers[actionType] ({
|
|
103
|
+
actionDefinition,
|
|
104
|
+
logger: instance.logger,
|
|
105
|
+
variables
|
|
106
|
+
})
|
|
102
107
|
|
|
103
108
|
if(validConfig.hooks) {
|
|
104
109
|
const hookManager = await HookManager.new({
|
package/src/core/Discovery.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
// import {process} from "node:process"
|
|
2
|
-
import yaml from "yaml"
|
|
3
1
|
import {execSync} from "child_process"
|
|
2
|
+
import path from "node:path"
|
|
4
3
|
|
|
4
|
+
import ContractManager from "./ContractManager.js"
|
|
5
5
|
import * as FDUtil from "./util/FDUtil.js"
|
|
6
6
|
import * as ActionUtil from "./util/ActionUtil.js"
|
|
7
7
|
import * as DataUtil from "./util/DataUtil.js"
|
|
8
|
-
import {composeDirectory,directoryExists} from "./util/FDUtil.js"
|
|
9
8
|
|
|
10
|
-
const {
|
|
9
|
+
const {composeDirectory,directoryExists,resolveDirectory} = FDUtil
|
|
10
|
+
const {newContract,parse} = ContractManager
|
|
11
|
+
const {ls,fileExists,composeFilename,getFiles} = FDUtil
|
|
11
12
|
const {actionTypes, actionMetaRequirements, loadJson} = ActionUtil
|
|
12
13
|
const {isType} = DataUtil
|
|
13
14
|
|
|
@@ -34,15 +35,16 @@ export default class Discovery {
|
|
|
34
35
|
|
|
35
36
|
debug("Discovering actions", 2)
|
|
36
37
|
|
|
37
|
-
debug("Specific modules provided: %o", 2, specific)
|
|
38
|
+
debug("Specific modules provided: %o", 2, Object.values(specific).filter(Boolean).length)
|
|
39
|
+
debug("Specific modules provided: %o", 3, specific)
|
|
38
40
|
|
|
39
|
-
const
|
|
41
|
+
const files = []
|
|
40
42
|
const options = this.core.options ?? {}
|
|
41
43
|
|
|
42
44
|
if(options?.mockPath) {
|
|
43
45
|
debug("Discovering mock actions in `%s`", 2, options.mockPath)
|
|
44
46
|
|
|
45
|
-
|
|
47
|
+
files.push(
|
|
46
48
|
...(await getFiles([
|
|
47
49
|
`${options.mockPath}/bedoc-*-printer.js`,
|
|
48
50
|
`${options.mockPath}/bedoc-*-parser.js`,
|
|
@@ -52,76 +54,94 @@ export default class Discovery {
|
|
|
52
54
|
debug("Mock path not set, discovering actions in node_modules", 2)
|
|
53
55
|
|
|
54
56
|
debug("Looking for actions in project's package.json", 2)
|
|
55
|
-
if(this.core.packageJson
|
|
56
|
-
const
|
|
57
|
+
if(this.core.packageJson) {
|
|
58
|
+
const exported = (this.core.packageJson.modules || [])
|
|
59
|
+
.map(m => composeFilename(options.basePath, m))
|
|
60
|
+
.flat()
|
|
57
61
|
|
|
58
|
-
debug("Found %o
|
|
59
|
-
debug("
|
|
62
|
+
debug("Found %o modules in project's package.json", 2, exported.length)
|
|
63
|
+
debug("Found modules in project's package.json: %o", 2, exported)
|
|
60
64
|
|
|
61
|
-
|
|
62
|
-
bucket.push(...actions)
|
|
63
|
-
else
|
|
64
|
-
debug("No actions found in package.json", 3)
|
|
65
|
+
files.push(...exported)
|
|
65
66
|
} else {
|
|
66
|
-
debug("No
|
|
67
|
+
debug("No modules found in project's package.json", 2)
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
debug("Looking for
|
|
70
|
+
debug("Looking for modules in node_modules (global and locally installed)", 2)
|
|
70
71
|
const directories = [
|
|
71
|
-
"
|
|
72
|
+
execSync("npm root").toString().trim(),
|
|
72
73
|
execSync("npm root -g").toString().trim(),
|
|
73
|
-
]
|
|
74
|
+
].filter(Boolean)
|
|
75
|
+
|
|
76
|
+
const nodeModulesDirs = directories
|
|
77
|
+
.map(composeDirectory)
|
|
78
|
+
.filter(directoryExists)
|
|
74
79
|
|
|
75
80
|
debug("Found %o directories to search for actions", 2, directories.length)
|
|
76
81
|
debug("Directories to search for actions: %o", 3, directories)
|
|
77
82
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
.
|
|
81
|
-
for(const moduleDirectory of moduleDirectories) {
|
|
82
|
-
const {directories: dirs} = await ls(moduleDirectory.absolutePath)
|
|
83
|
+
for(const nodeModulesDir of nodeModulesDirs) {
|
|
84
|
+
const dirsToSearch = []
|
|
85
|
+
const {directories: moduleDirs} = await ls(nodeModulesDir.absolutePath)
|
|
83
86
|
|
|
84
|
-
debug("Found %o directories in
|
|
85
|
-
dirs.length, moduleDirectory.absolutePath
|
|
86
|
-
)
|
|
87
|
+
debug("Found %o directories in %o", 2, moduleDirs.length, nodeModulesDir.absolutePath)
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
// Handle scoped packages (e.g., @bedoc/something)
|
|
90
|
+
const scopedDirs = moduleDirs.filter(d => d.name.startsWith("@"))
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
debug("Found %o module exports under %s", 2, exports.length, moduleDirectory.absolutePath)
|
|
92
|
+
dirsToSearch.push(...moduleDirs)
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
94
|
+
// If we find a scope (e.g., "@bedoc"), look inside it for bedoc modules
|
|
95
|
+
for(const scopedDir of scopedDirs) {
|
|
96
|
+
const {directories: scopedPackages} = await ls(scopedDir.absolutePath)
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
debug("Found %o directories under scoped package %o", 2, directories.length, scopedDir.name)
|
|
99
|
+
debug("Found directories under scoped package %o\n%o", 2, scopedDir.absolutePath, scopedPackages.map(d => d.absolutePath))
|
|
99
100
|
|
|
100
|
-
|
|
101
|
-
|
|
101
|
+
dirsToSearch.push(...scopedPackages)
|
|
102
|
+
}
|
|
102
103
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
*
|
|
106
|
-
* @param {object} dirMap The directory map object
|
|
107
|
-
* @returns {object[]} The discovered module exports
|
|
108
|
-
*/
|
|
109
|
-
#getModuleExports(dirMap) {
|
|
110
|
-
const debug = this.#debug
|
|
111
|
-
debug("Getting module exports from `%s`", 3, dirMap.absolutePath)
|
|
104
|
+
debug("1 Found %o directories to search for actions", 2, dirsToSearch.length)
|
|
105
|
+
debug("2 Found directories to search for actions: %o", 3, dirsToSearch)
|
|
112
106
|
|
|
113
|
-
|
|
114
|
-
debug("Loading package.json from `%s`", 3, packageJsonFile.absolutePath)
|
|
107
|
+
const visibleDirs = dirsToSearch.filter(d => !d.name.startsWith("."))
|
|
115
108
|
|
|
116
|
-
|
|
117
|
-
|
|
109
|
+
for(const dir of visibleDirs) {
|
|
110
|
+
const packageJsonFile = composeFilename(dir, "package.json")
|
|
118
111
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
resolveFilename(file, dirMap)
|
|
122
|
-
)
|
|
112
|
+
if(!fileExists(packageJsonFile))
|
|
113
|
+
continue
|
|
123
114
|
|
|
124
|
-
|
|
115
|
+
const packageJson = loadJson(packageJsonFile)
|
|
116
|
+
if(!packageJson.bedoc)
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
const {modules} = packageJson.bedoc ?? null
|
|
120
|
+
if(!modules || !Array.isArray(modules))
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
const moduleFiles = modules
|
|
124
|
+
.map(f => composeFilename(dir, f))
|
|
125
|
+
.filter(f => fileExists(f))
|
|
126
|
+
|
|
127
|
+
debug("Discovered %d modules from package.json file: %o", 2,
|
|
128
|
+
modules.length,
|
|
129
|
+
packageJsonFile.absolutePath
|
|
130
|
+
)
|
|
131
|
+
debug("Discovered from package.json files: %o", 3, modules)
|
|
132
|
+
|
|
133
|
+
files.push(...moduleFiles)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
debug("Discovered %d modules", 2, files.length)
|
|
139
|
+
debug("Discovered modules", 2, files.map(f => f.path))
|
|
140
|
+
debug("Discovered modules %o", 3, files)
|
|
141
|
+
|
|
142
|
+
// const available = files.map(f => this.#getModuleExports(f))
|
|
143
|
+
|
|
144
|
+
return await this.#loadActionsAndContracts(files, specific)
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
/**
|
|
@@ -137,7 +157,7 @@ export default class Discovery {
|
|
|
137
157
|
|
|
138
158
|
debug("Loading actions and contracts", 2)
|
|
139
159
|
debug("Loading %d module files", 2, moduleFiles.length)
|
|
140
|
-
debug("Specific modules to load: %o",
|
|
160
|
+
debug("Specific modules to load: %o", 3, specificModules)
|
|
141
161
|
|
|
142
162
|
const resultActions = {}
|
|
143
163
|
actionTypes.forEach(actionType => (resultActions[actionType] = []))
|
|
@@ -164,12 +184,19 @@ export default class Discovery {
|
|
|
164
184
|
debug("Loading module `%s`", 2, file.absoluteUri)
|
|
165
185
|
|
|
166
186
|
const loading = await this.#loadModule(file)
|
|
167
|
-
|
|
168
|
-
const
|
|
187
|
+
for(let index = 0; index < loading.actions.length; index++) {
|
|
188
|
+
const action = loading.actions[index]
|
|
169
189
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
190
|
+
if(!file.directory)
|
|
191
|
+
file.directory = resolveDirectory(path.dirname(file.path))
|
|
192
|
+
|
|
193
|
+
debug(`Loading %o contract from %o`, 2, action.meta.action, file.path)
|
|
194
|
+
|
|
195
|
+
const terms = parse(loading.contracts[index], file.directory)
|
|
196
|
+
const contract = await newContract(action.meta.action, terms)
|
|
197
|
+
|
|
198
|
+
loadedActions.push({file, action, contract})
|
|
199
|
+
}
|
|
173
200
|
}
|
|
174
201
|
|
|
175
202
|
debug("Loaded %d actions", 2, loadedActions.length)
|
|
@@ -225,7 +252,7 @@ export default class Discovery {
|
|
|
225
252
|
JSON.stringify(moduleFile, null, 2) + "\n" +
|
|
226
253
|
JSON.stringify(action, null, 2))
|
|
227
254
|
|
|
228
|
-
debug("Checking action
|
|
255
|
+
debug("Checking action %o", 2, metaAction)
|
|
229
256
|
|
|
230
257
|
const isValid = this.#validMeta(metaAction, {action, contract})
|
|
231
258
|
|
|
@@ -246,7 +273,7 @@ export default class Discovery {
|
|
|
246
273
|
|
|
247
274
|
for(const actionType of actionTypes) {
|
|
248
275
|
const total = resultActions[actionType].length
|
|
249
|
-
debug("Found %o
|
|
276
|
+
debug("Found %o %o actions", 2, total, actionType)
|
|
250
277
|
}
|
|
251
278
|
|
|
252
279
|
const total = Object.keys(resultActions).reduce((acc, curr) => {
|
|
@@ -280,7 +307,7 @@ export default class Discovery {
|
|
|
280
307
|
|
|
281
308
|
// First let's check if we wanted something specific
|
|
282
309
|
if(validatedConfig[config]) {
|
|
283
|
-
debug("Checking for specific
|
|
310
|
+
debug("Checking for specific %o action", 3, actionType)
|
|
284
311
|
const found = actions[actionType].find(
|
|
285
312
|
a => a.file.specificType.includes(actionType)
|
|
286
313
|
)
|
|
@@ -294,7 +321,7 @@ export default class Discovery {
|
|
|
294
321
|
}
|
|
295
322
|
|
|
296
323
|
// Hmm! We didn't find anything specific. Let's check the criterion
|
|
297
|
-
debug("Checking for
|
|
324
|
+
debug("Checking for %o actions with criterion %o", 3, actionType, criterion)
|
|
298
325
|
debug("Validated config to check against: %o", 3, validatedConfig)
|
|
299
326
|
const found = actions[actionType].filter(a => {
|
|
300
327
|
debug("Meta criterion value: %o", 4, a.action.meta[criterion])
|
|
@@ -324,7 +351,7 @@ export default class Discovery {
|
|
|
324
351
|
async #loadModule(module) {
|
|
325
352
|
const debug = this.#debug
|
|
326
353
|
|
|
327
|
-
debug("
|
|
354
|
+
debug("Loading module `%j`", 2, module)
|
|
328
355
|
|
|
329
356
|
const {absoluteUri} = module
|
|
330
357
|
const moduleExports = await import(absoluteUri)
|
|
@@ -341,7 +368,7 @@ export default class Discovery {
|
|
|
341
368
|
*/
|
|
342
369
|
#validMeta(actionType, toValidate) {
|
|
343
370
|
const debug = this.#debug
|
|
344
|
-
debug("Checking meta requirements for
|
|
371
|
+
debug("Checking meta requirements for %o", 3, actionType)
|
|
345
372
|
|
|
346
373
|
const requirements = actionMetaRequirements[actionType]
|
|
347
374
|
if(!requirements)
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import fetch from "node-fetch"
|
|
2
|
+
import JSON5 from "json5"
|
|
3
|
+
import Ajv from "ajv"
|
|
4
|
+
import {fileURLToPath,URL} from "node:url"
|
|
5
|
+
|
|
6
|
+
import * as FDUtil from "./FDUtil.js"
|
|
7
|
+
|
|
8
|
+
const {composeFilename,fileExists,readFile,writeFile} = FDUtil
|
|
9
|
+
|
|
10
|
+
const schemaUrl = "https://bedoc.gesslar.dev/schemas/v1/bedoc.action.json"
|
|
11
|
+
const localSchema = "./dist/bedoc.action.json"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Takes a schema and returns a validator function
|
|
15
|
+
*
|
|
16
|
+
* @param {object} schema The schema to compile
|
|
17
|
+
* @returns {Function} The schema validator function
|
|
18
|
+
*/
|
|
19
|
+
function getValidator(schema) {
|
|
20
|
+
const ajv = new Ajv({allErrors: true, verbose: true})
|
|
21
|
+
const f = ajv.compile(schema)
|
|
22
|
+
|
|
23
|
+
return f
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Downloads and preserves a copy of the action schema
|
|
28
|
+
* within the dist/ folder.
|
|
29
|
+
*
|
|
30
|
+
* @returns {object} The schema validator
|
|
31
|
+
*/
|
|
32
|
+
async function fetchSchema() {
|
|
33
|
+
const response = await fetch(schemaUrl)
|
|
34
|
+
const schema = await response.text()
|
|
35
|
+
|
|
36
|
+
const output = composeFilename(fileURLToPath(new URL("../../../", import.meta.url)), localSchema)
|
|
37
|
+
writeFile(output, schema)
|
|
38
|
+
|
|
39
|
+
return JSON5.parse(schema)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Loads a schema from file or fetches it if it is missing.
|
|
44
|
+
*
|
|
45
|
+
* @returns {object} The schema object
|
|
46
|
+
*/
|
|
47
|
+
async function loadSchema() {
|
|
48
|
+
const schemaFile = composeFilename(fileURLToPath(new URL("../../../", import.meta.url)), localSchema)
|
|
49
|
+
|
|
50
|
+
if(fileExists(schemaFile)) {
|
|
51
|
+
const schema = readFile(schemaFile)
|
|
52
|
+
|
|
53
|
+
return JSON5.parse(schema)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return await fetchSchema()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
fetchSchema,
|
|
61
|
+
getValidator,
|
|
62
|
+
loadSchema,
|
|
63
|
+
}
|
|
@@ -383,6 +383,9 @@ function deepFreezeObject(obj) {
|
|
|
383
383
|
/**
|
|
384
384
|
* Validates that a schema matches the expected structure.
|
|
385
385
|
*
|
|
386
|
+
* TODO get rid of this and all of its uses. We have a new
|
|
387
|
+
* contract system now.
|
|
388
|
+
*
|
|
386
389
|
* @param {object} schema - The schema to validate.
|
|
387
390
|
* @param {object} definition - The expected structure.
|
|
388
391
|
* @param {Array} stack - The stack trace for nested validation.
|
|
@@ -452,6 +455,60 @@ function schemaCompare(schema, definition, stack = [], logger = new Logger()) {
|
|
|
452
455
|
return {status: errors.length === 0 ? "success" : "error", errors}
|
|
453
456
|
}
|
|
454
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Determine the Levenshtein distance between two string values
|
|
460
|
+
*
|
|
461
|
+
* @param {string} a The first value for comparison.
|
|
462
|
+
* @param {string} b The second value for comparison.
|
|
463
|
+
* @returns {number} The Levenshtein distance
|
|
464
|
+
*/
|
|
465
|
+
function levenshteinDistance(a, b) {
|
|
466
|
+
const matrix = Array.from({length: a.length + 1}, (_, i) =>
|
|
467
|
+
Array.from({length: b.length + 1}, (_, j) =>
|
|
468
|
+
(i === 0 ? j : j === 0 ? i : 0)
|
|
469
|
+
)
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
for(let i = 1; i <= a.length; i++) {
|
|
473
|
+
for(let j = 1; j <= b.length; j++) {
|
|
474
|
+
matrix[i][j] =
|
|
475
|
+
a[i - 1] === b[j - 1]
|
|
476
|
+
? matrix[i - 1][j - 1]
|
|
477
|
+
: 1 + Math.min(
|
|
478
|
+
matrix[i - 1][j], matrix[i][j - 1],
|
|
479
|
+
matrix[i - 1][j - 1]
|
|
480
|
+
)
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return matrix[a.length][b.length]
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Determine the closest match between a string and allowed values
|
|
489
|
+
* from the Levenshtein distance.
|
|
490
|
+
*
|
|
491
|
+
* @param {string} input The input string to resolve
|
|
492
|
+
* @param {Array<string>} allowedValues The values which are permitted
|
|
493
|
+
* @returns {string} Suggested, probable match.
|
|
494
|
+
*/
|
|
495
|
+
function findClosestMatch(input, allowedValues) {
|
|
496
|
+
const threshold = 2 // Max edit distance for a "close match"
|
|
497
|
+
let closestMatch = null
|
|
498
|
+
let closestDistance = Infinity
|
|
499
|
+
|
|
500
|
+
for(const value of allowedValues) {
|
|
501
|
+
const distance = levenshteinDistance(input, value)
|
|
502
|
+
if(distance < closestDistance && distance <= threshold) {
|
|
503
|
+
closestMatch = value
|
|
504
|
+
closestDistance = distance
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return closestMatch
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
455
512
|
export {
|
|
456
513
|
// Classes
|
|
457
514
|
TypeSpec,
|
|
@@ -465,6 +522,7 @@ export {
|
|
|
465
522
|
arrayPad,
|
|
466
523
|
cloneObject,
|
|
467
524
|
deepFreezeObject,
|
|
525
|
+
findClosestMatch,
|
|
468
526
|
isArrayUniform,
|
|
469
527
|
isArrayUnique,
|
|
470
528
|
isBaseType,
|
|
@@ -473,6 +531,7 @@ export {
|
|
|
473
531
|
isObjectEmpty,
|
|
474
532
|
isType,
|
|
475
533
|
isValidType,
|
|
534
|
+
levenshteinDistance,
|
|
476
535
|
mapObject,
|
|
477
536
|
newTypeSpec,
|
|
478
537
|
prependString,
|