@gesslar/bedoc 1.9.0 → 1.11.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/cli.js +2 -2
- package/src/core/Configuration.js +3 -3
- package/src/core/ContractManager.js +112 -0
- package/src/core/Conveyor.js +3 -0
- package/src/core/Core.js +3 -2
- package/src/core/Discovery.js +63 -62
- package/src/core/contract/ParseContract.js +7 -0
- package/src/core/contract/PrintContract.js +7 -0
- package/src/core/util/ActionUtil.js +18 -9
- package/src/core/util/ContractUtil.js +63 -0
- package/src/core/util/DataUtil.js +59 -0
- package/src/core/util/ModuleUtil.js +0 -40
|
@@ -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.11.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",
|
package/src/cli.js
CHANGED
|
@@ -11,7 +11,7 @@ import {ConfigurationParameters} from "./core/ConfigurationParameters.js"
|
|
|
11
11
|
import * as ActionUtil from "./core/util/ActionUtil.js"
|
|
12
12
|
import * as FDUtil from "./core/util/FDUtil.js"
|
|
13
13
|
|
|
14
|
-
const {
|
|
14
|
+
const {loadDataFile} = ActionUtil
|
|
15
15
|
const {resolveFilename,resolveDirectory} = FDUtil
|
|
16
16
|
|
|
17
17
|
// Main entry point
|
|
@@ -20,7 +20,7 @@ const {resolveFilename,resolveDirectory} = FDUtil
|
|
|
20
20
|
// Get package info
|
|
21
21
|
const basePath = resolveDirectory(process.cwd())
|
|
22
22
|
const thisPath = resolveDirectory(fileURLToPath(new URL("..", import.meta.url)))
|
|
23
|
-
const bedocPackageJson =
|
|
23
|
+
const bedocPackageJson = loadDataFile(resolveFilename("package.json", thisPath))
|
|
24
24
|
|
|
25
25
|
// Setup program
|
|
26
26
|
program
|
|
@@ -11,7 +11,7 @@ import * as ActionUtil from "./util/ActionUtil.js"
|
|
|
11
11
|
import * as DataUtil from "./util/DataUtil.js"
|
|
12
12
|
import * as FDUtil from "./util/FDUtil.js"
|
|
13
13
|
|
|
14
|
-
const {
|
|
14
|
+
const {loadDataFile} = ActionUtil
|
|
15
15
|
const {isNothing, isType, mapObject} = DataUtil
|
|
16
16
|
const {getFiles, composeFilename, fileExists} = FDUtil
|
|
17
17
|
const {resolveDirectory, resolveFilename} = FDUtil
|
|
@@ -222,7 +222,7 @@ export default class Configuration {
|
|
|
222
222
|
} else {
|
|
223
223
|
const packageJsonFile = composeFilename(process.cwd(), "package.json")
|
|
224
224
|
if(fileExists(packageJsonFile)) {
|
|
225
|
-
const packageJson =
|
|
225
|
+
const packageJson = loadDataFile(packageJsonFile)
|
|
226
226
|
|
|
227
227
|
if(packageJson.bedoc)
|
|
228
228
|
allOptions.push({source: "packageJson", options: packageJson.bedoc})
|
|
@@ -246,7 +246,7 @@ export default class Configuration {
|
|
|
246
246
|
if(!configFile)
|
|
247
247
|
throw new Error("No config file specified")
|
|
248
248
|
|
|
249
|
-
const configObject =
|
|
249
|
+
const configObject = loadDataFile(configFile)
|
|
250
250
|
const subConfigName =
|
|
251
251
|
entryOptions?.sub ||
|
|
252
252
|
packageJson?.sub ||
|
|
@@ -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)
|
package/src/core/Discovery.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
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
|
|
|
9
|
+
const {composeDirectory,directoryExists,resolveDirectory} = FDUtil
|
|
10
|
+
const {newContract,parse} = ContractManager
|
|
10
11
|
const {ls,fileExists,composeFilename,getFiles} = FDUtil
|
|
11
|
-
const {actionTypes, actionMetaRequirements,
|
|
12
|
+
const {actionTypes, actionMetaRequirements,loadDataFile} = ActionUtil
|
|
12
13
|
const {isType} = DataUtil
|
|
13
14
|
|
|
14
15
|
export default class Discovery {
|
|
@@ -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,22 +54,20 @@ 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)
|
|
57
|
+
if(this.core.packageJson) {
|
|
58
|
+
const exported = (this.core.packageJson.modules || [])
|
|
59
|
+
.map(m => composeFilename(options.basePath, m))
|
|
60
|
+
.flat()
|
|
55
61
|
|
|
56
|
-
|
|
57
|
-
|
|
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)
|
|
58
64
|
|
|
59
|
-
|
|
60
|
-
debug("Actions found in package.json action in package.json: %o", 3, actions)
|
|
61
|
-
|
|
62
|
-
if(actions && typeof(actions) === "object")
|
|
63
|
-
bucket.push(...actions)
|
|
64
|
-
else
|
|
65
|
-
debug("No actions found in package.json", 3)
|
|
65
|
+
files.push(...exported)
|
|
66
66
|
} else {
|
|
67
|
-
debug("No
|
|
67
|
+
debug("No modules found in project's package.json", 2)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
debug("Looking for
|
|
70
|
+
debug("Looking for modules in node_modules (global and locally installed)", 2)
|
|
71
71
|
const directories = [
|
|
72
72
|
execSync("npm root").toString().trim(),
|
|
73
73
|
execSync("npm root -g").toString().trim(),
|
|
@@ -96,58 +96,52 @@ export default class Discovery {
|
|
|
96
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
|
dirsToSearch.push(...scopedPackages)
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
debug("Found %o directories to search for actions", 2, dirsToSearch.length)
|
|
104
|
-
|
|
105
|
-
const exports = dirsToSearch
|
|
106
|
-
.filter(d => !d.name.startsWith("."))
|
|
107
|
-
.map(d => this.#getModuleExports(d))
|
|
108
|
-
|
|
109
|
-
debug("Found %o module exports under %o", 2, exports.length, nodeModulesDir.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)
|
|
110
106
|
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
}
|
|
107
|
+
const visibleDirs = dirsToSearch.filter(d => !d.name.startsWith("."))
|
|
114
108
|
|
|
115
|
-
|
|
109
|
+
for(const dir of visibleDirs) {
|
|
110
|
+
const packageJsonFile = composeFilename(dir, "package.json")
|
|
116
111
|
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
if(!fileExists(packageJsonFile))
|
|
113
|
+
continue
|
|
119
114
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
* @param {object} dirMap The directory map object
|
|
124
|
-
* @returns {object[]} The discovered module exports
|
|
125
|
-
*/
|
|
126
|
-
#getModuleExports(dirMap) {
|
|
127
|
-
const debug = this.#debug
|
|
128
|
-
debug("Getting module exports from %o", 3, dirMap.absolutePath)
|
|
115
|
+
const packageJson = loadDataFile(packageJsonFile)
|
|
116
|
+
if(!packageJson.bedoc)
|
|
117
|
+
continue
|
|
129
118
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
119
|
+
const {modules} = packageJson.bedoc ?? null
|
|
120
|
+
if(!modules || !Array.isArray(modules))
|
|
121
|
+
continue
|
|
133
122
|
|
|
134
|
-
|
|
123
|
+
const moduleFiles = modules
|
|
124
|
+
.map(f => composeFilename(dir, f))
|
|
125
|
+
.filter(f => fileExists(f))
|
|
135
126
|
|
|
136
|
-
|
|
137
|
-
|
|
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)
|
|
138
132
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
133
|
+
files.push(...moduleFiles)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
143
137
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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)
|
|
147
141
|
|
|
148
|
-
|
|
142
|
+
// const available = files.map(f => this.#getModuleExports(f))
|
|
149
143
|
|
|
150
|
-
return
|
|
144
|
+
return await this.#loadActionsAndContracts(files, specific)
|
|
151
145
|
}
|
|
152
146
|
|
|
153
147
|
/**
|
|
@@ -163,7 +157,7 @@ export default class Discovery {
|
|
|
163
157
|
|
|
164
158
|
debug("Loading actions and contracts", 2)
|
|
165
159
|
debug("Loading %d module files", 2, moduleFiles.length)
|
|
166
|
-
debug("Specific modules to load: %o",
|
|
160
|
+
debug("Specific modules to load: %o", 3, specificModules)
|
|
167
161
|
|
|
168
162
|
const resultActions = {}
|
|
169
163
|
actionTypes.forEach(actionType => (resultActions[actionType] = []))
|
|
@@ -190,12 +184,19 @@ export default class Discovery {
|
|
|
190
184
|
debug("Loading module `%s`", 2, file.absoluteUri)
|
|
191
185
|
|
|
192
186
|
const loading = await this.#loadModule(file)
|
|
193
|
-
|
|
194
|
-
const
|
|
187
|
+
for(let index = 0; index < loading.actions.length; index++) {
|
|
188
|
+
const action = loading.actions[index]
|
|
195
189
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
+
}
|
|
199
200
|
}
|
|
200
201
|
|
|
201
202
|
debug("Loaded %d actions", 2, loadedActions.length)
|
|
@@ -350,7 +351,7 @@ export default class Discovery {
|
|
|
350
351
|
async #loadModule(module) {
|
|
351
352
|
const debug = this.#debug
|
|
352
353
|
|
|
353
|
-
debug("
|
|
354
|
+
debug("Loading module `%j`", 2, module)
|
|
354
355
|
|
|
355
356
|
const {absoluteUri} = module
|
|
356
357
|
const moduleExports = await import(absoluteUri)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as FDUtil from "./FDUtil.js"
|
|
2
2
|
import process from "node:process"
|
|
3
3
|
import JSON5 from "json5"
|
|
4
|
+
import YAML from "yaml"
|
|
4
5
|
|
|
5
6
|
const {readFile, fileExists, composeFilename} = FDUtil
|
|
6
7
|
|
|
@@ -14,16 +15,24 @@ const actionMetaRequirements = freeze({
|
|
|
14
15
|
})
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
|
-
* Loads
|
|
18
|
+
* Loads an object from JSON or YAML provided a fileMap
|
|
18
19
|
*
|
|
19
|
-
* @param {object}
|
|
20
|
-
*
|
|
20
|
+
* @param {object} fileMap - The FileObj file to load containing
|
|
21
|
+
* JSON or YAML text.
|
|
22
|
+
* @returns {object} The parsed data object.
|
|
21
23
|
*/
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
function loadDataFile(fileMap) {
|
|
25
|
+
const content = readFile(fileMap)
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
return JSON5.parse(content)
|
|
29
|
+
} catch{
|
|
30
|
+
try {
|
|
31
|
+
return YAML.parse(content)
|
|
32
|
+
} catch{
|
|
33
|
+
throw new Error("Content is neither valid JSON nor valid YAML")
|
|
34
|
+
}
|
|
35
|
+
}
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
/**
|
|
@@ -48,6 +57,6 @@ export {
|
|
|
48
57
|
actionMetaRequirements,
|
|
49
58
|
actionTypes,
|
|
50
59
|
// Functions
|
|
51
|
-
|
|
60
|
+
loadDataFile,
|
|
52
61
|
loadPackageJson,
|
|
53
62
|
}
|
|
@@ -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,
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import {createRequire} from "module"
|
|
2
|
-
import FDUtil from "./FDUtil.js"
|
|
3
|
-
import JSON5 from "json5"
|
|
4
|
-
|
|
5
|
-
export default class ModuleUtil {
|
|
6
|
-
/**
|
|
7
|
-
* Requires a module synchronously
|
|
8
|
-
*
|
|
9
|
-
* @param {object} fileObject - The file to require
|
|
10
|
-
* @returns {object} The required module
|
|
11
|
-
*/
|
|
12
|
-
static require(fileObject) {
|
|
13
|
-
return createRequire(import.meta.url)(fileObject.absolutePath)
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Loads a JSON file asynchronously
|
|
18
|
-
*
|
|
19
|
-
* @param {object} jsonFileObject - The JSON file to load
|
|
20
|
-
* @returns {Promise<object>} The parsed JSON content
|
|
21
|
-
*/
|
|
22
|
-
static async loadJson(jsonFileObject) {
|
|
23
|
-
// Read the file
|
|
24
|
-
const jsonContent = await FDUtil.readFile(jsonFileObject)
|
|
25
|
-
const json = JSON5.parse(jsonContent)
|
|
26
|
-
return json
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Loads the package.json file asynchronously
|
|
31
|
-
*
|
|
32
|
-
* @returns {Promise<object>} The parsed package.json content
|
|
33
|
-
*/
|
|
34
|
-
static async loadPackageJson() {
|
|
35
|
-
const packageJsonFileObject = FDUtil.resolveFilename("./package.json")
|
|
36
|
-
const jsonContent = await FDUtil.readFile(packageJsonFileObject)
|
|
37
|
-
const json = JSON5.parse(jsonContent)
|
|
38
|
-
return json
|
|
39
|
-
}
|
|
40
|
-
}
|