@benrogmans/lemma-engine 0.8.1 → 0.8.3

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/README.md CHANGED
@@ -1,299 +1,84 @@
1
1
  # @benrogmans/lemma-engine
2
2
 
3
- Lemma is a declarative programming language for expressing business rules. This WebAssembly build for JavaScript and TypeScript provides an embeddable runtime, so that Lemma code can be evaluated anywhere.
3
+ Embeddable Lemma engine (WebAssembly).
4
4
 
5
- **New to Lemma?** Check out the [language introduction](https://github.com/benrogmans/lemma#quick-start) and [examples](https://github.com/benrogmans/lemma/tree/main/documentation/examples).
5
+ npm `description` / `keywords` / `homepage`: edit **`NPM_BRANDING`** in `build.js` (not Cargo).
6
6
 
7
- ## Installation
7
+ ## Install
8
8
 
9
9
  ```bash
10
10
  npm install @benrogmans/lemma-engine
11
11
  ```
12
12
 
13
- ## JavaScript API Reference
13
+ ## Browser / bundler
14
14
 
15
- ### Initialization
16
-
17
- #### Browser
18
15
  ```javascript
19
- import init, { WasmEngine } from '@benrogmans/lemma-engine';
16
+ import { Lemma } from '@benrogmans/lemma-engine';
20
17
 
21
- // Async initialization (recommended for browsers)
22
- await init();
23
- const engine = new WasmEngine();
18
+ const engine = await Lemma();
24
19
  ```
25
20
 
26
- #### Node.js
27
- ```javascript
28
- import { readFileSync } from 'fs';
29
- import { WasmEngine, initSync } from '@benrogmans/lemma-engine';
21
+ `Lemma()` initializes WASM once and returns an `Engine`. Serve over **http(s)** (not `file://`). For manual init: `init()` then `new Engine()`.
30
22
 
31
- // Synchronous initialization for Node.js
32
- const wasmBytes = readFileSync('node_modules/@benrogmans/lemma-engine/lemma_bg.wasm');
33
- initSync(wasmBytes);
34
- const engine = new WasmEngine();
35
- ```
23
+ If your bundler outputs IIFE (or otherwise breaks `import.meta.url`), use:
36
24
 
37
- #### Bundlers (Webpack, Vite, etc.)
38
25
  ```javascript
39
- import init, { WasmEngine } from '@benrogmans/lemma-engine';
40
- import wasmUrl from '@benrogmans/lemma-engine/lemma_bg.wasm?url';
26
+ import { Lemma } from '@benrogmans/lemma-engine/iife';
41
27
 
42
- // Initialize with URL
43
- await init(wasmUrl);
44
- const engine = new WasmEngine();
28
+ const engine = await Lemma();
45
29
  ```
46
30
 
47
- ### Core Methods
31
+ This entry embeds WASM bytes and avoids external wasm URL handling.
48
32
 
49
- #### `addLemmaFile(code: string, filename: string): string`
33
+ ## esbuild auto-handler
50
34
 
51
- Adds a Lemma spec to the engine.
35
+ If you use esbuild JS API, plugin rewrites root import to `/iife` automatically:
52
36
 
53
37
  ```javascript
54
- const result = engine.addLemmaFile(`
55
- spec employee_contract
56
-
57
- fact salary: 5000 eur
58
- fact start_date: 2024-01-15
59
- fact vacation_days: 25
60
-
61
- rule annual_salary: salary * 12
62
- rule daily_rate: salary / 21
63
- `, 'employee.lemma');
64
-
65
- const response = JSON.parse(result);
66
- if (response.success) {
67
- console.log('Spec loaded:', response.data);
68
- } else {
69
- console.error('Error:', response.error);
70
- }
38
+ import { lemmaEngineEsbuildPlugin } from '@benrogmans/lemma-engine/esbuild';
71
39
  ```
72
40
 
73
- #### `evaluate(docName: string, facts: string): string`
74
-
75
- Evaluates a spec with optional runtime facts.
41
+ ## Node
76
42
 
77
43
  ```javascript
78
- // Evaluate with default facts
79
- const result1 = engine.evaluate('employee_contract', '{}');
80
-
81
- // Evaluate with runtime fact values (as JSON object)
82
- const result2 = engine.evaluate('employee_contract', JSON.stringify({
83
- salary: 6000,
84
- vacation_days: 30
85
- }));
86
-
87
- const response = JSON.parse(result2);
88
- if (response.success) {
89
- console.log('Spec:', response.data.spec);
90
- console.log('Rules:', response.data.rules);
91
- // Access specific rule results directly:
92
- // response.data.rules.annual_salary.value
93
- }
94
- ```
95
-
96
- #### `listSpecs(): string`
97
-
98
- Lists all loaded specs in the engine.
99
-
100
- ```javascript
101
- const result = engine.listSpecs();
102
- const response = JSON.parse(result);
103
-
104
- if (response.success) {
105
- console.log('Loaded specs:', response.data);
106
- // response.data is an array of spec names
107
- }
108
- ```
109
-
110
- ### Response Format
111
-
112
- All methods return a JSON string with this structure:
113
-
114
- ```typescript
115
- interface WasmResponse {
116
- success: boolean;
117
- data?: any; // Success data (varies by method)
118
- error?: string; // Error message if success is false
119
- warnings?: string[]; // Optional warnings
120
- }
121
- ```
122
-
123
- ### Complete Example
124
-
125
- ```javascript
126
- import init, { WasmEngine } from '@benrogmans/lemma-engine';
127
-
128
- async function calculatePricing() {
129
- // Initialize WASM
130
- await init();
131
- const engine = new WasmEngine();
44
+ import { Lemma } from '@benrogmans/lemma-engine';
132
45
 
133
- // Define pricing rules
134
- const pricingDoc = `
135
- spec product_pricing
136
-
137
- fact base_price: 100 usd
138
- fact quantity: 1
139
- fact is_member: false
140
- fact promo_code: ""
141
-
142
- rule bulk_discount: 0%
143
- unless quantity >= 10 then 5%
144
- unless quantity >= 50 then 10%
145
- unless quantity >= 100 then 15%
146
-
147
- rule member_discount: 0%
148
- unless is_member then 10%
149
-
150
- rule promo_discount: 0%
151
- unless promo_code == "SAVE10" then 10%
152
- unless promo_code == "SAVE20" then 20%
153
-
154
- rule best_discount: bulk_discount
155
- unless member_discount >= bulk_discount then member_discount
156
- unless promo_discount >= bulk_discount then promo_discount
157
- unless member_discount >= promo_discount then member_discount
158
- unless promo_discount >= member_discount then promo_discount
159
-
160
- rule final_price: base_price * quantity * (1 - best_discount)
161
- `;
162
-
163
- // Load the spec
164
- const loadResult = JSON.parse(
165
- engine.addLemmaFile(pricingDoc, 'pricing.lemma')
166
- );
167
-
168
- if (!loadResult.success) {
169
- throw new Error(loadResult.error);
170
- }
171
-
172
- // Calculate for different scenarios
173
- const scenarios = [
174
- { quantity: 25, is_member: false, promo_code: "" },
175
- { quantity: 10, is_member: true, promo_code: "" },
176
- { quantity: 5, is_member: false, promo_code: "SAVE20" }
177
- ];
178
-
179
- for (const scenario of scenarios) {
180
- const result = JSON.parse(
181
- engine.evaluate('product_pricing', JSON.stringify(scenario))
182
- );
183
-
184
- if (result.success) {
185
- console.log(`Scenario:`, scenario);
186
- // Access rule results directly by name
187
- const finalPrice = result.data.rules.final_price.value;
188
- const bestDiscount = result.data.rules.best_discount.value;
189
- console.log(`Final price:`, finalPrice);
190
- console.log(`Discount applied:`, bestDiscount);
191
- console.log('---');
192
- }
193
- }
194
- }
195
-
196
- calculatePricing().catch(console.error);
46
+ const engine = await Lemma();
197
47
  ```
198
48
 
199
- ### TypeScript Support
200
-
201
- The package includes TypeScript definitions. For better type safety:
49
+ Or with a preloaded buffer (e.g. no async fetch): `initSync({ module })` then `new Engine()`.
202
50
 
203
- ```typescript
204
- import init, { WasmEngine } from '@benrogmans/lemma-engine';
51
+ ## LSP (browser streams)
205
52
 
206
- interface PricingFacts {
207
- quantity: number;
208
- is_member: boolean;
209
- promo_code: string;
210
- }
211
-
212
- interface PricingResults {
213
- base_price: string;
214
- final_price: string;
215
- best_discount: string;
216
- // ... other fields
217
- }
218
-
219
- interface EvaluationResponse {
220
- success: boolean;
221
- data?: {
222
- spec: string;
223
- rules: {
224
- [ruleName: string]: {
225
- value: any; // The computed value (e.g., {Number: "100"})
226
- veto?: string; // Present if rule was vetoed
227
- missing_facts?: string[]; // Present if rule couldn't be evaluated
228
- operations?: Array<{ // Operation records (always present if rule was evaluated)
229
- type: string; // "fact_used", "operation_executed", "final_result", etc.
230
- // Additional fields depend on type
231
- }>;
232
- };
233
- };
234
- };
235
- error?: string;
236
- warnings?: string[]; // Spec-level warnings
237
- }
238
-
239
- async function typedExample() {
240
- await init();
241
- const engine = new WasmEngine();
242
-
243
- // ... load spec
244
-
245
- const facts = {
246
- quantity: 10,
247
- is_member: true,
248
- promo_code: "SAVE10"
249
- };
250
-
251
- const result: EvaluationResponse = JSON.parse(
252
- engine.evaluate('product_pricing', JSON.stringify(facts))
253
- );
254
-
255
- if (result.success && result.data) {
256
- const price = result.data.rules.final_price?.value;
257
- }
258
- }
259
- ```
260
-
261
- ### Error Handling
53
+ Call `init()` first. Use `LspClient`; `start()` uses the bundled LSP (no need to pass `serve`/`ServerConfig`). Optional: `start(serve, ServerConfig)` to override.
262
54
 
263
55
  ```javascript
264
- try {
265
- const result = JSON.parse(
266
- engine.addLemmaFile('invalid syntax', 'bad.lemma')
267
- );
56
+ import { init } from '@benrogmans/lemma-engine';
57
+ import { LspClient } from '@benrogmans/lemma-engine/lsp-client';
268
58
 
269
- if (!result.success) {
270
- console.error('Lemma error:', result.error);
271
- // Handle parse/semantic errors
272
- }
273
- } catch (e) {
274
- // Handle JSON parse errors or WASM exceptions
275
- console.error('System error:', e);
276
- }
59
+ await init();
60
+ const client = new LspClient(monaco);
61
+ await client.start();
62
+ await client.initialize();
63
+ client.onDiagnostics((uri, diagnostics) => { /* ... */ });
64
+ client.didOpen(uri, 'lemma', 1, documentText);
277
65
  ```
278
66
 
279
- ### Performance Tips
67
+ ## API (`Engine`)
280
68
 
281
- 1. **Initialize once**: The WASM module should be initialized once per application
282
- 2. **Reuse engines**: Create one `WasmEngine` and load multiple specs
283
- 3. **Batch operations**: Load all specs before evaluating
284
- 4. **Cache results**: Evaluation results can be cached if facts don't change
69
+ | Method | |
70
+ |--------|--|
71
+ | `load(code, attribute)` | Promise; reject `string[]` |
72
+ | `list()` | Spec entries |
73
+ | `schema(spec, effective?)` | `SpecSchema` |
74
+ | `run(spec, rules, facts, effective?)` | `Response` |
75
+ | `format(code, attribute?)` | string or throw |
76
+ | `invert(...)` | throws (N/A) |
285
77
 
286
- ### Compatibility
78
+ ## Build (maintainers)
287
79
 
288
- Works in modern browsers with WebAssembly support and Node.js with ES module support.
80
+ `node build.js`: wasm-pack `lemma.bindings.js` + `lemma_bg.wasm` copy checked-in `lemma-entry.js` / `lsp-entry.js` / `*.d.ts` into `dist/`. Do not edit generated bindings by hand.
289
81
 
290
82
  ## License
291
83
 
292
84
  Apache-2.0
293
-
294
- ## Links
295
-
296
- - [GitHub Repository](https://github.com/benrogmans/lemma)
297
- - [Lemma Language Guide](https://github.com/benrogmans/lemma/tree/main/documentation)
298
- - [Examples](https://github.com/benrogmans/lemma/tree/main/documentation/examples)
299
- - [Report Issues](https://github.com/benrogmans/lemma/issues)
package/esbuild.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * esbuild plugin for @benrogmans/lemma-engine.
3
+ *
4
+ * Rewrites root imports to the IIFE-safe entry so users do not have
5
+ * to handle WASM asset paths manually in esbuild-based builds.
6
+ */
7
+ import { createRequire } from "node:module";
8
+
9
+ export function lemmaEngineEsbuildPlugin() {
10
+ const require = createRequire(import.meta.url);
11
+
12
+ return {
13
+ name: "lemma-engine",
14
+ setup(build) {
15
+ build.onResolve({ filter: /^@benrogmans\/lemma-engine$/ }, () => {
16
+ const iifePath = require.resolve("@benrogmans/lemma-engine/iife");
17
+ return { path: iifePath };
18
+ });
19
+ },
20
+ };
21
+ }
@@ -0,0 +1,138 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ /**
4
+ * The `ReadableStreamType` enum.
5
+ *
6
+ * *This API requires the following crate features to be activated: `ReadableStreamType`*
7
+ */
8
+
9
+ type ReadableStreamType = "bytes";
10
+
11
+ export class Engine {
12
+ free(): void;
13
+ [Symbol.dispose](): void;
14
+ /**
15
+ * Returns formatted source string on success; throws with error message on failure.
16
+ */
17
+ format(code: string, attribute?: string | null): any;
18
+ invert(_spec_name: string, _rule_name: string, _target_json: string, _provided_values_json: string): any;
19
+ /**
20
+ * Spec names from the engine (same order as [`Engine::list_specs`]).
21
+ */
22
+ list(): any;
23
+ /**
24
+ * Load Lemma source. Resolves with `undefined` on success; rejects with an array of error strings.
25
+ */
26
+ load(code: string, attribute: string): Promise<any>;
27
+ constructor();
28
+ /**
29
+ * Evaluate spec. Returns [`crate::evaluation::Response`] as a JS object. Throws on planning/runtime error.
30
+ */
31
+ run(spec: string, rule_names: any, fact_values: any, effective?: string | null): any;
32
+ /**
33
+ * Planning schema for the spec ([`crate::planning::execution_plan::SpecSchema`]). Throws on error.
34
+ */
35
+ schema(spec: string, effective?: string | null): any;
36
+ }
37
+
38
+ export class IntoUnderlyingByteSource {
39
+ private constructor();
40
+ free(): void;
41
+ [Symbol.dispose](): void;
42
+ cancel(): void;
43
+ pull(controller: ReadableByteStreamController): Promise<any>;
44
+ start(controller: ReadableByteStreamController): void;
45
+ readonly autoAllocateChunkSize: number;
46
+ readonly type: ReadableStreamType;
47
+ }
48
+
49
+ export class IntoUnderlyingSink {
50
+ private constructor();
51
+ free(): void;
52
+ [Symbol.dispose](): void;
53
+ abort(reason: any): Promise<any>;
54
+ close(): Promise<any>;
55
+ write(chunk: any): Promise<any>;
56
+ }
57
+
58
+ export class IntoUnderlyingSource {
59
+ private constructor();
60
+ free(): void;
61
+ [Symbol.dispose](): void;
62
+ cancel(): void;
63
+ pull(controller: ReadableStreamDefaultController): Promise<any>;
64
+ }
65
+
66
+ export class ServerConfig {
67
+ free(): void;
68
+ [Symbol.dispose](): void;
69
+ constructor(into_server: AsyncIterator<any>, from_server: WritableStream);
70
+ }
71
+
72
+ /**
73
+ * Run the Lemma LSP over the given streams. Call from JS after creating
74
+ * an AsyncIterator (client → server messages) and a WritableStream (server → client).
75
+ */
76
+ export function serve(config: ServerConfig): Promise<void>;
77
+
78
+ export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
79
+
80
+ export interface InitOutput {
81
+ readonly memory: WebAssembly.Memory;
82
+ readonly __wbg_serverconfig_free: (a: number, b: number) => void;
83
+ readonly serve: (a: number) => number;
84
+ readonly serverconfig_new: (a: number, b: number) => number;
85
+ readonly __wbg_intounderlyingbytesource_free: (a: number, b: number) => void;
86
+ readonly __wbg_intounderlyingsink_free: (a: number, b: number) => void;
87
+ readonly __wbg_intounderlyingsource_free: (a: number, b: number) => void;
88
+ readonly intounderlyingbytesource_autoAllocateChunkSize: (a: number) => number;
89
+ readonly intounderlyingbytesource_cancel: (a: number) => void;
90
+ readonly intounderlyingbytesource_pull: (a: number, b: number) => number;
91
+ readonly intounderlyingbytesource_start: (a: number, b: number) => void;
92
+ readonly intounderlyingbytesource_type: (a: number) => number;
93
+ readonly intounderlyingsink_abort: (a: number, b: number) => number;
94
+ readonly intounderlyingsink_close: (a: number) => number;
95
+ readonly intounderlyingsink_write: (a: number, b: number) => number;
96
+ readonly intounderlyingsource_cancel: (a: number) => void;
97
+ readonly intounderlyingsource_pull: (a: number, b: number) => number;
98
+ readonly __wbg_engine_free: (a: number, b: number) => void;
99
+ readonly wasmengine_format: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
100
+ readonly wasmengine_invert: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number, j: number) => void;
101
+ readonly wasmengine_list: (a: number, b: number) => void;
102
+ readonly wasmengine_load: (a: number, b: number, c: number, d: number, e: number) => number;
103
+ readonly wasmengine_new: () => number;
104
+ readonly wasmengine_run: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => void;
105
+ readonly wasmengine_schema: (a: number, b: number, c: number, d: number, e: number, f: number) => void;
106
+ readonly __wasm_bindgen_func_elem_9731: (a: number, b: number) => void;
107
+ readonly __wasm_bindgen_func_elem_385: (a: number, b: number) => void;
108
+ readonly __wasm_bindgen_func_elem_10982: (a: number, b: number, c: number, d: number) => void;
109
+ readonly __wasm_bindgen_func_elem_2764: (a: number, b: number, c: number, d: number) => void;
110
+ readonly __wasm_bindgen_func_elem_10985: (a: number, b: number, c: number, d: number) => void;
111
+ readonly __wbindgen_export: (a: number, b: number) => number;
112
+ readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
113
+ readonly __wbindgen_export3: (a: number) => void;
114
+ readonly __wbindgen_export4: (a: number, b: number, c: number) => void;
115
+ readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
116
+ }
117
+
118
+ export type SyncInitInput = BufferSource | WebAssembly.Module;
119
+
120
+ /**
121
+ * Instantiates the given `module`, which can either be bytes or
122
+ * a precompiled `WebAssembly.Module`.
123
+ *
124
+ * @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
125
+ *
126
+ * @returns {InitOutput}
127
+ */
128
+ export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
129
+
130
+ /**
131
+ * If `module_or_path` is {RequestInfo} or {URL}, makes a request and
132
+ * for everything else, calls `WebAssembly.instantiate` directly.
133
+ *
134
+ * @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
135
+ *
136
+ * @returns {Promise<InitOutput>}
137
+ */
138
+ export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;