@flexiberry/berrycore 0.1.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/LICENSE +21 -0
- package/dist/adapter/cli-adapter.d.ts +37 -0
- package/dist/adapter/cli-adapter.js +119 -0
- package/dist/berry-core.d.ts +108 -0
- package/dist/berry-core.js +258 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +18 -0
- package/dist/interpreter/environment.d.ts +45 -0
- package/dist/interpreter/environment.js +96 -0
- package/dist/interpreter/errors.d.ts +16 -0
- package/dist/interpreter/errors.js +27 -0
- package/dist/interpreter/interpreter.d.ts +111 -0
- package/dist/interpreter/interpreter.js +682 -0
- package/dist/interpreter/interpreter.types.d.ts +182 -0
- package/dist/interpreter/interpreter.types.js +73 -0
- package/dist/parser/ast/ast.engine.d.ts +103 -0
- package/dist/parser/ast/ast.engine.js +526 -0
- package/dist/parser/ast/ast.types.d.ts +242 -0
- package/dist/parser/ast/ast.types.js +37 -0
- package/dist/parser/formatter/formatter.d.ts +44 -0
- package/dist/parser/formatter/formatter.js +214 -0
- package/dist/parser/tokenizer/reader/grammer/api.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/api.grammer.js +102 -0
- package/dist/parser/tokenizer/reader/grammer/capture.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/capture.grammer.js +21 -0
- package/dist/parser/tokenizer/reader/grammer/check.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/check.grammer.js +21 -0
- package/dist/parser/tokenizer/reader/grammer/comment.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/comment.grammer.js +13 -0
- package/dist/parser/tokenizer/reader/grammer/conditions.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/conditions.grammer.js +68 -0
- package/dist/parser/tokenizer/reader/grammer/input.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/input.grammer.js +17 -0
- package/dist/parser/tokenizer/reader/grammer/keyvalue.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/keyvalue.grammer.js +240 -0
- package/dist/parser/tokenizer/reader/grammer/link.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/link.grammer.js +17 -0
- package/dist/parser/tokenizer/reader/grammer/params.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/params.grammer.js +21 -0
- package/dist/parser/tokenizer/reader/grammer/step.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/step.grammer.js +25 -0
- package/dist/parser/tokenizer/reader/grammer/task.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/task.grammer.js +17 -0
- package/dist/parser/tokenizer/reader/grammer/var.grammer.d.ts +2 -0
- package/dist/parser/tokenizer/reader/grammer/var.grammer.js +47 -0
- package/dist/parser/tokenizer/reader/lexer.engine.d.ts +43 -0
- package/dist/parser/tokenizer/reader/lexer.engine.js +178 -0
- package/dist/parser/tokenizer/reader/lexer.types.d.ts +18 -0
- package/dist/parser/tokenizer/reader/lexer.types.js +1 -0
- package/dist/parser/tokenizer/token.d.ts +13 -0
- package/dist/parser/tokenizer/token.js +13 -0
- package/dist/parser/tokenizer/tokenType.d.ts +58 -0
- package/dist/parser/tokenizer/tokenType.js +64 -0
- package/dist/script/format-util.d.ts +33 -0
- package/dist/script/format-util.js +94 -0
- package/dist/script/postman.util.d.ts +88 -0
- package/dist/script/postman.util.js +176 -0
- package/dist/script/swagger.util.d.ts +80 -0
- package/dist/script/swagger.util.js +202 -0
- package/dist/util/store-util.d.ts +5 -0
- package/dist/util/store-util.js +22 -0
- package/package.json +25 -0
- package/readme.md +107 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 flexiberry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Adapter
|
|
3
|
+
*
|
|
4
|
+
* Default IOAdapter implementation using Node readline + stdout.
|
|
5
|
+
* Handles input, output, and execution control for CLI execution.
|
|
6
|
+
*/
|
|
7
|
+
import { IOAdapter, LogLevel, ExecutionCommand } from "../interpreter/interpreter.types.js";
|
|
8
|
+
export declare class CliAdapter implements IOAdapter {
|
|
9
|
+
private rl;
|
|
10
|
+
private commandHandler;
|
|
11
|
+
private readonly enableLogging;
|
|
12
|
+
private readonly logLevels;
|
|
13
|
+
private readonly enableCommands;
|
|
14
|
+
constructor(options?: {
|
|
15
|
+
/** Enable log output to stdout (default: true) */
|
|
16
|
+
enableLogging?: boolean;
|
|
17
|
+
/** Which log levels to show (default: all except debug) */
|
|
18
|
+
logLevels?: LogLevel[];
|
|
19
|
+
/** Enable keyboard commands during execution (default: false) */
|
|
20
|
+
enableCommands?: boolean;
|
|
21
|
+
});
|
|
22
|
+
private getReadline;
|
|
23
|
+
/** Prompt the user for input via stdin */
|
|
24
|
+
prompt(message: string): Promise<string>;
|
|
25
|
+
/** Ask a yes/no question */
|
|
26
|
+
confirm(message: string): Promise<boolean>;
|
|
27
|
+
/** Push a log line to stdout */
|
|
28
|
+
log(level: LogLevel, message: string): void;
|
|
29
|
+
/**
|
|
30
|
+
* Register a command handler.
|
|
31
|
+
* Called by the interpreter to wire up command sending.
|
|
32
|
+
*/
|
|
33
|
+
onCommand(handler: (command: ExecutionCommand) => void): void;
|
|
34
|
+
/** Clean up readline interface */
|
|
35
|
+
dispose(): void;
|
|
36
|
+
private handleKeyboardInput;
|
|
37
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Adapter
|
|
3
|
+
*
|
|
4
|
+
* Default IOAdapter implementation using Node readline + stdout.
|
|
5
|
+
* Handles input, output, and execution control for CLI execution.
|
|
6
|
+
*/
|
|
7
|
+
import * as readline from "readline";
|
|
8
|
+
import { ExecutionCommand } from "../interpreter/interpreter.types.js";
|
|
9
|
+
// ─── Log Level Icons ────────────────────────────────────────────────────────
|
|
10
|
+
const LEVEL_ICONS = {
|
|
11
|
+
info: "ℹ️ ",
|
|
12
|
+
warn: "⚠️ ",
|
|
13
|
+
error: "❌",
|
|
14
|
+
debug: "🔍",
|
|
15
|
+
step: "▶ ",
|
|
16
|
+
task: "📋",
|
|
17
|
+
api: "🌐",
|
|
18
|
+
};
|
|
19
|
+
// ─── CLI Adapter ────────────────────────────────────────────────────────────
|
|
20
|
+
export class CliAdapter {
|
|
21
|
+
rl = null;
|
|
22
|
+
commandHandler = null;
|
|
23
|
+
enableLogging;
|
|
24
|
+
logLevels;
|
|
25
|
+
enableCommands;
|
|
26
|
+
constructor(options = {}) {
|
|
27
|
+
this.enableLogging = options.enableLogging ?? true;
|
|
28
|
+
this.logLevels = new Set(options.logLevels ?? ["info", "warn", "error", "step", "task", "api"]);
|
|
29
|
+
this.enableCommands = options.enableCommands ?? false;
|
|
30
|
+
}
|
|
31
|
+
getReadline() {
|
|
32
|
+
if (!this.rl) {
|
|
33
|
+
this.rl = readline.createInterface({
|
|
34
|
+
input: process.stdin,
|
|
35
|
+
output: process.stdout,
|
|
36
|
+
});
|
|
37
|
+
// Listen for keyboard commands during execution
|
|
38
|
+
if (this.enableCommands) {
|
|
39
|
+
this.rl.on("line", (input) => {
|
|
40
|
+
this.handleKeyboardInput(input.trim().toLowerCase());
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return this.rl;
|
|
45
|
+
}
|
|
46
|
+
/** Prompt the user for input via stdin */
|
|
47
|
+
prompt(message) {
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
this.getReadline().question(`🔹 ${message}: `, (answer) => {
|
|
50
|
+
resolve(answer.trim());
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** Ask a yes/no question */
|
|
55
|
+
async confirm(message) {
|
|
56
|
+
const answer = await this.prompt(`${message} (y/n)`);
|
|
57
|
+
return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";
|
|
58
|
+
}
|
|
59
|
+
/** Push a log line to stdout */
|
|
60
|
+
log(level, message) {
|
|
61
|
+
if (!this.enableLogging)
|
|
62
|
+
return;
|
|
63
|
+
if (!this.logLevels.has(level))
|
|
64
|
+
return;
|
|
65
|
+
const icon = LEVEL_ICONS[level];
|
|
66
|
+
console.log(`${icon} ${message}`);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Register a command handler.
|
|
70
|
+
* Called by the interpreter to wire up command sending.
|
|
71
|
+
*/
|
|
72
|
+
onCommand(handler) {
|
|
73
|
+
this.commandHandler = handler;
|
|
74
|
+
// Initialize readline so keyboard listening starts
|
|
75
|
+
if (this.enableCommands) {
|
|
76
|
+
this.getReadline();
|
|
77
|
+
console.log("─── Commands: [p]ause [c]ontinue [s]kip [t]stop [k]ill ───");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Clean up readline interface */
|
|
81
|
+
dispose() {
|
|
82
|
+
if (this.rl) {
|
|
83
|
+
this.rl.close();
|
|
84
|
+
this.rl = null;
|
|
85
|
+
}
|
|
86
|
+
this.commandHandler = null;
|
|
87
|
+
}
|
|
88
|
+
// ── Keyboard Input Handling ─────────────────────────────────────────────
|
|
89
|
+
handleKeyboardInput(input) {
|
|
90
|
+
if (!this.commandHandler)
|
|
91
|
+
return;
|
|
92
|
+
switch (input) {
|
|
93
|
+
case "p":
|
|
94
|
+
case "pause":
|
|
95
|
+
this.commandHandler(ExecutionCommand.Pause);
|
|
96
|
+
break;
|
|
97
|
+
case "c":
|
|
98
|
+
case "continue":
|
|
99
|
+
case "resume":
|
|
100
|
+
this.commandHandler(ExecutionCommand.Continue);
|
|
101
|
+
break;
|
|
102
|
+
case "s":
|
|
103
|
+
case "skip":
|
|
104
|
+
this.commandHandler(ExecutionCommand.Skip);
|
|
105
|
+
break;
|
|
106
|
+
case "t":
|
|
107
|
+
case "stop":
|
|
108
|
+
this.commandHandler(ExecutionCommand.Stop);
|
|
109
|
+
break;
|
|
110
|
+
case "k":
|
|
111
|
+
case "kill":
|
|
112
|
+
this.commandHandler(ExecutionCommand.Kill);
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
// Ignore unknown input
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BerryCore — Runtime Facade
|
|
3
|
+
*
|
|
4
|
+
* The single entry-point for running a .berry script programmatically.
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* 1. Tokenise the source code (LexerEngine)
|
|
8
|
+
* 2. Build the AST (AstEngine)
|
|
9
|
+
* 3. Execute the program (Interpreter)
|
|
10
|
+
* 4. Relay every interpreter event to registered outside listeners
|
|
11
|
+
*
|
|
12
|
+
* Usage (CLI consumer):
|
|
13
|
+
* const core = new BerryCore(source, { adapter: new CliAdapter() });
|
|
14
|
+
* core.on(InterpreterEvent.Completed, payload => { ... });
|
|
15
|
+
* await core.run();
|
|
16
|
+
*
|
|
17
|
+
* Usage (bare / no adapter):
|
|
18
|
+
* const core = new BerryCore(source);
|
|
19
|
+
* core.on(InterpreterEvent.Log, ({ message }) => console.log(message));
|
|
20
|
+
* await core.run();
|
|
21
|
+
*/
|
|
22
|
+
import { InterpreterOptions } from "./interpreter/interpreter.js";
|
|
23
|
+
import { InterpreterEvent, EventListener, ExecutionCommand, ExecutionState, IOAdapter, TaskResult } from "./interpreter/interpreter.types.js";
|
|
24
|
+
export interface BerryCoreOptions {
|
|
25
|
+
/**
|
|
26
|
+
* IO adapter used for user-input prompts, log output, and keyboard commands.
|
|
27
|
+
* Pass a `CliAdapter` for terminal use, or a custom adapter for a UI/WS layer.
|
|
28
|
+
* When omitted, the interpreter runs silently (events are still emitted).
|
|
29
|
+
*/
|
|
30
|
+
readonly adapter?: IOAdapter;
|
|
31
|
+
/**
|
|
32
|
+
* Fine-grained interpreter execution options forwarded as-is to the
|
|
33
|
+
* `Interpreter` constructor (timeout, continueOnError, dryRun, …).
|
|
34
|
+
*/
|
|
35
|
+
readonly interpreterOptions?: Partial<InterpreterOptions>;
|
|
36
|
+
/**
|
|
37
|
+
* Optional base path used by the default link resolver to resolve relative local file paths.
|
|
38
|
+
* Typically the directory of the entry .berry file.
|
|
39
|
+
*/
|
|
40
|
+
readonly basePath?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Optional resolver for `Link` statements.
|
|
43
|
+
* Receives the path/url and should return the string content of that file.
|
|
44
|
+
* Required if the source code contains `Link` statements.
|
|
45
|
+
*/
|
|
46
|
+
readonly linkResolver?: (path: string) => Promise<string>;
|
|
47
|
+
/**
|
|
48
|
+
* Optional decryption provider for `Decrypt` flagged `Var` entries.
|
|
49
|
+
* If omitted, base64 decryption is used by default.
|
|
50
|
+
*/
|
|
51
|
+
readonly decryptionProvider?: (encrypted: string) => Promise<string> | string;
|
|
52
|
+
}
|
|
53
|
+
export declare class BerryCore {
|
|
54
|
+
private readonly source;
|
|
55
|
+
private readonly options;
|
|
56
|
+
/**
|
|
57
|
+
* The interpreter instance is created fresh on every `run()` call so that
|
|
58
|
+
* BerryCore remains reusable across multiple sequential runs.
|
|
59
|
+
*/
|
|
60
|
+
private interpreter;
|
|
61
|
+
/**
|
|
62
|
+
* Listeners registered via `.on()` before `run()` is called.
|
|
63
|
+
* They are forwarded to the interpreter after it is created.
|
|
64
|
+
*/
|
|
65
|
+
private readonly pendingListeners;
|
|
66
|
+
constructor(source: string, options?: BerryCoreOptions);
|
|
67
|
+
/**
|
|
68
|
+
* Register a listener for an interpreter lifecycle event.
|
|
69
|
+
*
|
|
70
|
+
* Listeners can be registered before or after calling `run()`.
|
|
71
|
+
* Listeners registered before `run()` are forwarded to the newly
|
|
72
|
+
* created Interpreter instance automatically.
|
|
73
|
+
*/
|
|
74
|
+
on<E extends InterpreterEvent>(event: E, listener: EventListener<E>): this;
|
|
75
|
+
/**
|
|
76
|
+
* Send an execution command to the running interpreter.
|
|
77
|
+
* Has no effect if `run()` has not been called yet.
|
|
78
|
+
*/
|
|
79
|
+
sendCommand(command: ExecutionCommand): void;
|
|
80
|
+
/**
|
|
81
|
+
* Immediately kill the running execution and clean up resources.
|
|
82
|
+
*/
|
|
83
|
+
kill(): void;
|
|
84
|
+
/**
|
|
85
|
+
* Returns the current execution state, or `Idle` if not yet started.
|
|
86
|
+
*/
|
|
87
|
+
getState(): ExecutionState;
|
|
88
|
+
private resolveLinks;
|
|
89
|
+
private defaultLinkResolver;
|
|
90
|
+
private parseData;
|
|
91
|
+
/**
|
|
92
|
+
* Execute the source code end-to-end.
|
|
93
|
+
*
|
|
94
|
+
* Pipeline:
|
|
95
|
+
* source → LexerEngine.tokenize() → Token[]
|
|
96
|
+
* → AstEngine.build() → ProgramNode
|
|
97
|
+
* → Interpreter.execute() → TaskResult[]
|
|
98
|
+
*
|
|
99
|
+
* Throws:
|
|
100
|
+
* - LexerError — if the source contains illegal characters
|
|
101
|
+
* - ParserError — if the source has a grammar error
|
|
102
|
+
* - RuntimeError — if execution encounters an unrecoverable state
|
|
103
|
+
*
|
|
104
|
+
* @param inputRow Optional data row to inject into the execution for `Input` mappings
|
|
105
|
+
* @returns The array of `TaskResult`s produced by the interpreter.
|
|
106
|
+
*/
|
|
107
|
+
run(inputRow?: Record<string, string>): Promise<TaskResult[]>;
|
|
108
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BerryCore — Runtime Facade
|
|
3
|
+
*
|
|
4
|
+
* The single entry-point for running a .berry script programmatically.
|
|
5
|
+
*
|
|
6
|
+
* Responsibilities:
|
|
7
|
+
* 1. Tokenise the source code (LexerEngine)
|
|
8
|
+
* 2. Build the AST (AstEngine)
|
|
9
|
+
* 3. Execute the program (Interpreter)
|
|
10
|
+
* 4. Relay every interpreter event to registered outside listeners
|
|
11
|
+
*
|
|
12
|
+
* Usage (CLI consumer):
|
|
13
|
+
* const core = new BerryCore(source, { adapter: new CliAdapter() });
|
|
14
|
+
* core.on(InterpreterEvent.Completed, payload => { ... });
|
|
15
|
+
* await core.run();
|
|
16
|
+
*
|
|
17
|
+
* Usage (bare / no adapter):
|
|
18
|
+
* const core = new BerryCore(source);
|
|
19
|
+
* core.on(InterpreterEvent.Log, ({ message }) => console.log(message));
|
|
20
|
+
* await core.run();
|
|
21
|
+
*/
|
|
22
|
+
import { LexerEngine } from "./parser/tokenizer/reader/lexer.engine.js";
|
|
23
|
+
import { AstEngine } from "./parser/ast/ast.engine.js";
|
|
24
|
+
import { NodeType } from "./parser/ast/ast.types.js";
|
|
25
|
+
import { Interpreter } from "./interpreter/interpreter.js";
|
|
26
|
+
import { InterpreterEvent, ExecutionState, } from "./interpreter/interpreter.types.js";
|
|
27
|
+
// ─── BerryCore ───────────────────────────────────────────────────────────────
|
|
28
|
+
export class BerryCore {
|
|
29
|
+
source;
|
|
30
|
+
options;
|
|
31
|
+
/**
|
|
32
|
+
* The interpreter instance is created fresh on every `run()` call so that
|
|
33
|
+
* BerryCore remains reusable across multiple sequential runs.
|
|
34
|
+
*/
|
|
35
|
+
interpreter = null;
|
|
36
|
+
/**
|
|
37
|
+
* Listeners registered via `.on()` before `run()` is called.
|
|
38
|
+
* They are forwarded to the interpreter after it is created.
|
|
39
|
+
*/
|
|
40
|
+
pendingListeners = [];
|
|
41
|
+
// ── Constructor ──────────────────────────────────────────────────────────
|
|
42
|
+
constructor(source, options = {}) {
|
|
43
|
+
this.source = source;
|
|
44
|
+
this.options = options;
|
|
45
|
+
}
|
|
46
|
+
// ── Public Event API ────────────────────────────────────────────────────
|
|
47
|
+
/**
|
|
48
|
+
* Register a listener for an interpreter lifecycle event.
|
|
49
|
+
*
|
|
50
|
+
* Listeners can be registered before or after calling `run()`.
|
|
51
|
+
* Listeners registered before `run()` are forwarded to the newly
|
|
52
|
+
* created Interpreter instance automatically.
|
|
53
|
+
*/
|
|
54
|
+
on(event, listener) {
|
|
55
|
+
if (this.interpreter) {
|
|
56
|
+
// Already running — wire directly
|
|
57
|
+
this.interpreter.on(event, listener);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Queue for when run() creates the interpreter
|
|
61
|
+
this.pendingListeners.push({
|
|
62
|
+
event,
|
|
63
|
+
listener: listener,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
// ── Execution Control API ───────────────────────────────────────────────
|
|
69
|
+
/**
|
|
70
|
+
* Send an execution command to the running interpreter.
|
|
71
|
+
* Has no effect if `run()` has not been called yet.
|
|
72
|
+
*/
|
|
73
|
+
sendCommand(command) {
|
|
74
|
+
this.interpreter?.sendCommand(command);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Immediately kill the running execution and clean up resources.
|
|
78
|
+
*/
|
|
79
|
+
kill() {
|
|
80
|
+
this.interpreter?.kill();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Returns the current execution state, or `Idle` if not yet started.
|
|
84
|
+
*/
|
|
85
|
+
getState() {
|
|
86
|
+
return this.interpreter?.getState() ?? ExecutionState.Idle;
|
|
87
|
+
}
|
|
88
|
+
// ── Main Run ────────────────────────────────────────────────────────────
|
|
89
|
+
async resolveLinks(ast, visited = new Set()) {
|
|
90
|
+
const resolvedBody = [];
|
|
91
|
+
for (const stmt of ast.body) {
|
|
92
|
+
if (stmt.type === NodeType.LinkStatement) {
|
|
93
|
+
// Prevent infinite loops in circular links
|
|
94
|
+
if (visited.has(stmt.path)) {
|
|
95
|
+
continue; // Already processed this link
|
|
96
|
+
}
|
|
97
|
+
visited.add(stmt.path);
|
|
98
|
+
let content;
|
|
99
|
+
if (this.options.linkResolver) {
|
|
100
|
+
content = await this.options.linkResolver(stmt.path);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
content = await this.defaultLinkResolver(stmt.path);
|
|
104
|
+
}
|
|
105
|
+
const subTokens = new LexerEngine(content).tokenize();
|
|
106
|
+
const subAst = new AstEngine(subTokens).build();
|
|
107
|
+
// Recursively resolve links in the sub-AST
|
|
108
|
+
const resolvedSubAst = await this.resolveLinks(subAst, visited);
|
|
109
|
+
// Add the resolved sub-body instead of the link statement
|
|
110
|
+
resolvedBody.push(...resolvedSubAst.body);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
resolvedBody.push(stmt);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
...ast,
|
|
118
|
+
body: resolvedBody,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
async defaultLinkResolver(path) {
|
|
122
|
+
// 1. Check if it's an HTTP URL
|
|
123
|
+
if (path.startsWith("http://") || path.startsWith("https://")) {
|
|
124
|
+
try {
|
|
125
|
+
const response = await fetch(path);
|
|
126
|
+
if (!response.ok) {
|
|
127
|
+
throw new Error(`Failed to fetch URL ${path}: ${response.statusText}`);
|
|
128
|
+
}
|
|
129
|
+
return await response.text();
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
throw new Error(`Failed to fetch URL ${path}: ${error}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// 2. Check if we are in Node.js
|
|
136
|
+
const isNode = typeof process !== "undefined" && process.versions != null && process.versions.node != null;
|
|
137
|
+
if (isNode) {
|
|
138
|
+
try {
|
|
139
|
+
// Use dynamic import to avoid bundler issues in browser
|
|
140
|
+
const fs = await import("fs/promises");
|
|
141
|
+
const pathModule = await import("path");
|
|
142
|
+
const resolvedPath = this.options.basePath
|
|
143
|
+
? pathModule.resolve(this.options.basePath, path)
|
|
144
|
+
: pathModule.resolve(process.cwd(), path);
|
|
145
|
+
return await fs.readFile(resolvedPath, "utf-8");
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
throw new Error(`Failed to read local file ${path}: ${e}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// 3. Fallback for browser (relative URLs)
|
|
152
|
+
if (typeof window !== "undefined" && typeof fetch !== "undefined") {
|
|
153
|
+
try {
|
|
154
|
+
const response = await fetch(path);
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
throw new Error(`Failed to fetch relative URL ${path}: ${response.statusText}`);
|
|
157
|
+
}
|
|
158
|
+
return await response.text();
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
throw new Error(`Failed to fetch relative URL ${path}: ${e}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
throw new Error(`Cannot resolve link '${path}': Environment does not support file reading and path is not an absolute URL.`);
|
|
165
|
+
}
|
|
166
|
+
parseData(content, path) {
|
|
167
|
+
if (path.endsWith('.json')) {
|
|
168
|
+
return JSON.parse(content);
|
|
169
|
+
}
|
|
170
|
+
// Basic CSV parser
|
|
171
|
+
const lines = content.split(/\r?\n/).filter(line => line.trim().length > 0);
|
|
172
|
+
if (lines.length === 0)
|
|
173
|
+
return [];
|
|
174
|
+
const headers = lines[0].split(',').map(h => h.trim());
|
|
175
|
+
const rows = [];
|
|
176
|
+
for (let i = 1; i < lines.length; i++) {
|
|
177
|
+
const values = lines[i].split(',').map(v => v.trim());
|
|
178
|
+
const row = {};
|
|
179
|
+
for (let j = 0; j < headers.length; j++) {
|
|
180
|
+
row[headers[j]] = values[j] ?? "";
|
|
181
|
+
}
|
|
182
|
+
rows.push(row);
|
|
183
|
+
}
|
|
184
|
+
return rows;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Execute the source code end-to-end.
|
|
188
|
+
*
|
|
189
|
+
* Pipeline:
|
|
190
|
+
* source → LexerEngine.tokenize() → Token[]
|
|
191
|
+
* → AstEngine.build() → ProgramNode
|
|
192
|
+
* → Interpreter.execute() → TaskResult[]
|
|
193
|
+
*
|
|
194
|
+
* Throws:
|
|
195
|
+
* - LexerError — if the source contains illegal characters
|
|
196
|
+
* - ParserError — if the source has a grammar error
|
|
197
|
+
* - RuntimeError — if execution encounters an unrecoverable state
|
|
198
|
+
*
|
|
199
|
+
* @param inputRow Optional data row to inject into the execution for `Input` mappings
|
|
200
|
+
* @returns The array of `TaskResult`s produced by the interpreter.
|
|
201
|
+
*/
|
|
202
|
+
async run(inputRow) {
|
|
203
|
+
// ── Phase 1: Tokenise ──────────────────────────────────────────────
|
|
204
|
+
const tokens = new LexerEngine(this.source).tokenize();
|
|
205
|
+
// ── Phase 2: Build AST ────────────────────────────────────────────
|
|
206
|
+
let ast = new AstEngine(tokens).build();
|
|
207
|
+
// ── Phase 2.5: Resolve Links ──────────────────────────────────────
|
|
208
|
+
ast = await this.resolveLinks(ast);
|
|
209
|
+
// ── Phase 2.6: Handle Input ───────────────────────────────────────
|
|
210
|
+
if (!inputRow) {
|
|
211
|
+
const inputNode = ast.body.find(node => node.type === NodeType.InputStatement);
|
|
212
|
+
if (inputNode) {
|
|
213
|
+
let content;
|
|
214
|
+
if (this.options.linkResolver) {
|
|
215
|
+
content = await this.options.linkResolver(inputNode.path);
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
content = await this.defaultLinkResolver(inputNode.path);
|
|
219
|
+
}
|
|
220
|
+
const rows = this.parseData(content, inputNode.path);
|
|
221
|
+
// Fire DataLoaded event to pending listeners
|
|
222
|
+
for (const { event, listener } of this.pendingListeners) {
|
|
223
|
+
if (event === InterpreterEvent.DataLoaded) {
|
|
224
|
+
await listener({ rows });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Return early, adapter is expected to loop and call run(row)
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// ── Phase 3: Create Interpreter ───────────────────────────────────
|
|
232
|
+
this.interpreter = new Interpreter(ast, {
|
|
233
|
+
...this.options.interpreterOptions,
|
|
234
|
+
inputRow,
|
|
235
|
+
decryptionProvider: this.options.decryptionProvider
|
|
236
|
+
});
|
|
237
|
+
// Wire the IO adapter (if provided)
|
|
238
|
+
if (this.options.adapter) {
|
|
239
|
+
this.interpreter.setIOAdapter(this.options.adapter);
|
|
240
|
+
}
|
|
241
|
+
// Forward any listeners that were registered before run() was called
|
|
242
|
+
for (const { event, listener } of this.pendingListeners) {
|
|
243
|
+
this.interpreter.on(event, listener);
|
|
244
|
+
}
|
|
245
|
+
// ── Phase 4: Execute ──────────────────────────────────────────────
|
|
246
|
+
try {
|
|
247
|
+
return await this.interpreter.execute();
|
|
248
|
+
}
|
|
249
|
+
finally {
|
|
250
|
+
// Only destroy if the interpreter actually finished
|
|
251
|
+
const state = this.interpreter?.getState();
|
|
252
|
+
const isFinished = state === ExecutionState.Completed || state === ExecutionState.Killed || state === ExecutionState.Stopped;
|
|
253
|
+
if (!this.options.interpreterOptions?.dryRun && isFinished) {
|
|
254
|
+
this.interpreter = null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { AstEngine, Ast, ParserError } from "./parser/ast/ast.engine.js";
|
|
2
|
+
import { LexerEngine, LexerError } from "./parser/tokenizer/reader/lexer.engine.js";
|
|
3
|
+
import { BerryFormatter } from "./parser/formatter/formatter.js";
|
|
4
|
+
import { Interpreter } from "./interpreter/interpreter.js";
|
|
5
|
+
import { CliAdapter } from "./adapter/cli-adapter.js";
|
|
6
|
+
import { FormatUtil } from "./script/format-util.js";
|
|
7
|
+
import { PostmanUtil } from "./script/postman.util.js";
|
|
8
|
+
import { SwaggerUtil } from "./script/swagger.util.js";
|
|
9
|
+
import { BerryCore } from "./berry-core.js";
|
|
10
|
+
import { InterpreterEvent, type CompletedPayload, type TaskResult, type StepResult, type IOAdapter, ExecutionCommand, ExecutionState, ExecutionStatus } from "./interpreter/interpreter.types.js";
|
|
11
|
+
import { type BerryCoreOptions } from "./berry-core.js";
|
|
12
|
+
import { NodeType, type BaseNode, type TaskBlockNode, type StepBlockNode, type ParamsBlockNode, type CaptureBlockNode, type CheckBlockNode, type KeyValuePairNode, type ConditionNode, ProgramNode } from "./parser/ast/ast.types.js";
|
|
13
|
+
export { BerryCore, type BerryCoreOptions, AstEngine, Ast, ParserError, LexerEngine, LexerError, BerryFormatter, Interpreter, CliAdapter, FormatUtil, PostmanUtil, SwaggerUtil, InterpreterEvent, ExecutionCommand, ExecutionState, ExecutionStatus, NodeType, type IOAdapter, type CompletedPayload, type TaskResult, type StepResult, type BaseNode, type TaskBlockNode, type StepBlockNode, type ParamsBlockNode, type CaptureBlockNode, type CheckBlockNode, type KeyValuePairNode, type ConditionNode, type ProgramNode };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AstEngine, Ast, ParserError } from "./parser/ast/ast.engine.js";
|
|
2
|
+
import { LexerEngine, LexerError } from "./parser/tokenizer/reader/lexer.engine.js";
|
|
3
|
+
import { BerryFormatter } from "./parser/formatter/formatter.js";
|
|
4
|
+
import { Interpreter } from "./interpreter/interpreter.js";
|
|
5
|
+
import { CliAdapter } from "./adapter/cli-adapter.js";
|
|
6
|
+
import { FormatUtil } from "./script/format-util.js";
|
|
7
|
+
import { PostmanUtil } from "./script/postman.util.js";
|
|
8
|
+
import { SwaggerUtil } from "./script/swagger.util.js";
|
|
9
|
+
import { BerryCore } from "./berry-core.js";
|
|
10
|
+
import { InterpreterEvent, ExecutionCommand, ExecutionState, ExecutionStatus, } from "./interpreter/interpreter.types.js";
|
|
11
|
+
import { NodeType } from "./parser/ast/ast.types.js";
|
|
12
|
+
export {
|
|
13
|
+
// ── High-level facade (recommended entry-point) ──────────────────────
|
|
14
|
+
BerryCore,
|
|
15
|
+
// ── Low-level building blocks (advanced consumers) ───────────────────
|
|
16
|
+
AstEngine, Ast, ParserError, LexerEngine, LexerError, BerryFormatter, Interpreter, CliAdapter, FormatUtil, PostmanUtil, SwaggerUtil,
|
|
17
|
+
// ── Types / enums needed by consumers ────────────────────────────────
|
|
18
|
+
InterpreterEvent, ExecutionCommand, ExecutionState, ExecutionStatus, NodeType };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment — Scoped Variable Store
|
|
3
|
+
*
|
|
4
|
+
* Maintains a chain of scopes: global → task → step.
|
|
5
|
+
* Each scope is a Map<string, RuntimeValue>.
|
|
6
|
+
* Lookup walks up the parent chain.
|
|
7
|
+
*/
|
|
8
|
+
import { RuntimeValue } from "./interpreter.types.js";
|
|
9
|
+
export declare class Environment {
|
|
10
|
+
private readonly store;
|
|
11
|
+
private readonly parent;
|
|
12
|
+
constructor(parent?: Environment | null);
|
|
13
|
+
/**
|
|
14
|
+
* Declare a new variable in this scope.
|
|
15
|
+
* Overwrites if it already exists in this scope.
|
|
16
|
+
*/
|
|
17
|
+
declare(name: string, value: RuntimeValue): void;
|
|
18
|
+
/**
|
|
19
|
+
* Look up a variable, walking up the scope chain.
|
|
20
|
+
* Throws VariableNotFoundError if not found in any scope.
|
|
21
|
+
*/
|
|
22
|
+
lookup(name: string): RuntimeValue;
|
|
23
|
+
/**
|
|
24
|
+
* Try to look up a variable without throwing.
|
|
25
|
+
* Returns undefined if not found.
|
|
26
|
+
*/
|
|
27
|
+
tryLookup(name: string): RuntimeValue | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Assign a value to an existing variable in the nearest scope.
|
|
30
|
+
* If not found, declares it in this scope.
|
|
31
|
+
*/
|
|
32
|
+
assign(name: string, value: RuntimeValue): void;
|
|
33
|
+
/**
|
|
34
|
+
* Create a child scope inheriting from this environment.
|
|
35
|
+
*/
|
|
36
|
+
createChild(): Environment;
|
|
37
|
+
/**
|
|
38
|
+
* Get all variables in this scope (not parents).
|
|
39
|
+
*/
|
|
40
|
+
getOwnEntries(): ReadonlyMap<string, RuntimeValue>;
|
|
41
|
+
/**
|
|
42
|
+
* Get all variables including parent scopes (closest wins).
|
|
43
|
+
*/
|
|
44
|
+
getAllEntries(): Map<string, RuntimeValue>;
|
|
45
|
+
}
|