Skip to content

Commit c7b6504

Browse files
committed
fix: align public api and docs
1 parent eb41354 commit c7b6504

15 files changed

Lines changed: 387 additions & 86 deletions
Lines changed: 110 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,58 @@
11
# Implementation Examples
22

3-
This page provides practical examples of how to implement the core interfaces of `neuron-js` to build a custom rules engine tailored to your domain.
3+
This page shows how to implement the core extension points of `neuron-js` with the current public API.
4+
5+
The script boundary stays JSON-friendly: rules, actions, conditions, and parameters are arrays with explicit `options` objects. For plugin ergonomics, the abstract action and condition base classes transform the parameter array into a name-keyed `Map`, so plugin code can use `this.params.get('name')`.
46

57
## 1. Condition Implementation
6-
Conditions are logical predicates. They should be stateless and only depend on their parameters and the current `ExecutionContext`.
8+
9+
Conditions are logical predicates. They should be stateless and depend only on their parameters and the current `ExecutionContext`.
710

811
```typescript
9-
import { AbstractCondition, ExecutionResult, ExecutionContext } from '@sebasoft/neuron-js';
12+
import { AbstractCondition, ExecutionResult, type ExecutionContext } from '@sebasoft/neuron-js';
1013

1114
/**
12-
* Checks if the current hour in the context state matches a target.
15+
* Checks if the current hour in the context state matches a target period.
1316
*/
1417
export class TimeOfDayCondition extends AbstractCondition {
15-
static TYPE = 'is_time_of_day';
18+
static readonly TYPE = 'is_time_of_day';
1619

17-
async execute(context: ExecutionContext): Promise<ExecutionResult<boolean>> {
18-
// 1. Resolve parameters (e.g., "morning", "afternoon")
19-
const targetPeriod = this.params.get('period').getValue(context);
20-
21-
// 2. Access state from context
20+
execute(context: ExecutionContext): ExecutionResult<boolean> {
21+
const targetPeriod = this.params.get('period')?.getValue(context);
2222
const currentHour = context.state.systemTime.getHours();
23-
23+
2424
let isMatch = false;
2525
if (targetPeriod === 'morning') isMatch = currentHour >= 6 && currentHour < 12;
2626
if (targetPeriod === 'afternoon') isMatch = currentHour >= 12 && currentHour < 18;
2727

28-
// 3. Return result (Condition value is boolean)
29-
return new ExecutionResult(isMatch, context, isMatch);
28+
return new ExecutionResult(true, context, isMatch);
3029
}
3130
}
3231
```
3332

3433
## 2. Action Implementation
35-
Actions perform side effects or mutate the context state. They are only triggered if the Rule's conditions pass.
34+
35+
Actions perform side effects or return an updated context. They are only triggered if the rule's conditions pass.
3636

3737
```typescript
38-
import { AbstractAction, ExecutionResult, ExecutionContext } from '@sebasoft/neuron-js';
38+
import { AbstractAction, ExecutionResult, MessageType, type ExecutionContext } from '@sebasoft/neuron-js';
3939

4040
/**
4141
* Adds a customized audit message to the context.
4242
*/
4343
export class AuditLogAction extends AbstractAction {
44-
static TYPE = 'audit_log';
44+
static readonly TYPE = 'audit_log';
4545

46-
async execute(context: ExecutionContext): Promise<ExecutionResult<void>> {
47-
const messageTemplate = this.params.get('template').getValue(context);
46+
execute(context: ExecutionContext): ExecutionResult<void> {
47+
const messageTemplate = this.params.get('template')?.getValue(context);
4848
const user = context.state.user.name;
4949

50-
// Mutate the context by adding a new message
5150
const nextContext = {
5251
...context,
5352
messages: [
5453
...context.messages,
55-
{ type: 'info', text: `${user}: ${messageTemplate}` }
56-
]
54+
{ type: MessageType.INFO, text: `${user}: ${messageTemplate}` },
55+
],
5756
};
5857

5958
return new ExecutionResult(true, nextContext);
@@ -62,126 +61,165 @@ export class AuditLogAction extends AbstractAction {
6261
```
6362

6463
## 3. Parameter Implementation
65-
Parameters resolve values. They can be static (hardcoded in JSON) or dynamic (resolving paths in state or environment variables).
64+
65+
Parameters resolve values. They can be static, dynamic from context, or backed by another source.
6666

6767
```typescript
68-
import { AbstractParameter, ExecutionContext } from '@sebasoft/neuron-js';
68+
import { AbstractParameter, type ExecutionContext } from '@sebasoft/neuron-js';
6969

7070
/**
7171
* Resolves a value from an environment variable.
7272
*/
7373
export class EnvVarParameter extends AbstractParameter<string> {
74-
static TYPE = 'env_var';
74+
static readonly TYPE = 'env_var';
7575

76-
getValue(context: ExecutionContext): string | null {
77-
// Parameters can use their 'value' property as the key
78-
const envKey = this.value;
79-
return process.env[envKey] || this.defaultValue || null;
76+
getValue(_context: ExecutionContext): string | null {
77+
const envKey = this.value;
78+
return envKey ? process.env[envKey] ?? this.defaultValue ?? null : this.defaultValue ?? null;
8079
}
8180
}
8281
```
8382

8483
## 4. Context Implementation
85-
While `ExecutionContext` is a flexible interface, you should define a strict schema for your application's state to ensure type safety in your plugins.
84+
85+
`ExecutionContext` is intentionally flexible. Define a stricter application-level context for your own plugins.
8686

8787
```typescript
88+
import type { ExecutionContext } from '@sebasoft/neuron-js';
89+
8890
export interface MyAppContext extends ExecutionContext {
8991
state: {
9092
user: { id: string; name: string; loyalty: number };
91-
cart: { items: any[]; total: number };
93+
cart: { items: unknown[]; total: number };
9294
systemTime: Date;
9395
};
9496
}
9597
```
9698

9799
## 5. Putting It All Together
98-
The following example shows how to register these custom plugins and execute a script that uses them.
100+
101+
The following example registers custom plugins and executes a JSON-serializable script.
99102

100103
```typescript
101104
import { Neuron, Synapse } from '@sebasoft/neuron-js';
102105

103-
// 1. Initialize Registry and register plugins
104106
const registry = new Neuron();
105107
registry.registerCondition(TimeOfDayCondition.TYPE, TimeOfDayCondition);
106108
registry.registerAction(AuditLogAction.TYPE, AuditLogAction);
107109
registry.registerParameter(EnvVarParameter.TYPE, EnvVarParameter);
108110

109-
// 2. Create the Engine
110111
const engine = new Synapse(registry);
111112

112-
// 3. Define Logic (JSON)
113113
const script = {
114114
id: 'morning-greeting',
115-
rules: [{
116-
id: 'greet-rule',
117-
type: 'simple_rule',
118-
conditions: [
119-
{
120-
type: 'is_time_of_day',
121-
params: [{ name: 'period', type: 'simple_string', value: 'morning' }]
122-
}
123-
],
124-
actions: [
125-
{
126-
type: 'audit_log',
127-
params: [{ name: 'template', type: 'simple_string', value: 'Good morning!' }]
128-
}
129-
]
130-
}]
115+
rules: [
116+
{
117+
id: 'greet-rule',
118+
type: 'simple_rule',
119+
options: {},
120+
conditions: [
121+
{
122+
id: 'morning-condition',
123+
type: 'is_time_of_day',
124+
options: {},
125+
params: [
126+
{
127+
id: 'period-param',
128+
name: 'period',
129+
type: 'simple_string',
130+
value: 'morning',
131+
options: {},
132+
},
133+
],
134+
},
135+
],
136+
actions: [
137+
{
138+
id: 'audit-action',
139+
type: 'audit_log',
140+
options: {},
141+
params: [
142+
{
143+
id: 'template-param',
144+
name: 'template',
145+
type: 'simple_string',
146+
value: 'Good morning!',
147+
options: {},
148+
},
149+
],
150+
},
151+
],
152+
},
153+
],
131154
};
132155

133-
// 4. Run it
134156
const context: MyAppContext = {
135157
state: {
136158
user: { id: '1', name: 'Alice', loyalty: 10 },
137159
cart: { items: [], total: 0 },
138-
systemTime: new Date() // Suppose it's 9:00 AM
160+
systemTime: new Date(),
139161
},
140-
messages: []
162+
messages: [],
141163
};
142164

143165
const result = engine.execute(script, context);
144-
console.log(result.context.messages); // [{ type: 'info', text: 'Alice: Good morning!' }]
166+
console.log(result.context.messages);
145167
```
146168

147169
## 6. Logical Grouping (AND/OR)
170+
148171
`neuron-js` uses a "Sum of Products" approach to logic. Conditions are grouped into blocks.
149-
- **AND**: All conditions within a block must be true.
150-
- **OR**: If any block is true, the entire rule evaluates to true.
172+
173+
- **AND**: all conditions within a block must be true.
174+
- **OR**: if any block is true, the entire rule evaluates to true.
151175

152176
### How to trigger OR
153-
Setting `orCondition: true` on a condition starts a **new block**. This new block is joined to the previous block with an `OR` operator.
177+
178+
Setting `orCondition: true` on a condition's `options` starts a new block. This new block is joined to the previous block with an `OR` operator.
154179

155180
### Example: (A AND B) OR (C AND D)
181+
156182
```json
157183
{
158-
"id": "complex-logic",
184+
"id": "complex-rule",
185+
"type": "simple_rule",
186+
"options": {},
187+
"actions": [],
159188
"conditions": [
160-
{ "id": "A", "type": "is_gold_member" },
161-
{ "id": "B", "type": "has_high_balance" },
162-
{
163-
"id": "C",
164-
"type": "is_new_user",
165-
"options": { "orCondition": true }
189+
{ "id": "A", "type": "is_gold_member", "options": {}, "params": [] },
190+
{ "id": "B", "type": "has_high_balance", "options": {}, "params": [] },
191+
{
192+
"id": "C",
193+
"type": "is_new_user",
194+
"options": { "orCondition": true },
195+
"params": []
166196
},
167-
{ "id": "D", "type": "has_promo_code" }
197+
{ "id": "D", "type": "has_promo_code", "options": {}, "params": [] }
168198
]
169199
}
170200
```
171-
**Evaluation Logic**: `(A && B) || (C && D)`
201+
202+
Evaluation logic: `(A && B) || (C && D)`
172203

173204
## 7. Condition Inversion (NOT)
174-
Any condition can be negated by setting `inverted: true` in its options. This is handled by the runtime, so your plugin implementation doesn't need to worry about it.
205+
206+
Any condition can be negated by setting `inverted: true` in its `options`. This is handled by the runtime, so plugin implementations do not need special inversion logic.
175207

176208
```json
177209
{
178-
"id": "not-blocked",
210+
"id": "not-blocked-rule",
211+
"type": "simple_rule",
212+
"options": {},
213+
"actions": [],
179214
"conditions": [
180-
{
181-
"type": "is_user_blocked",
182-
"options": { "inverted": true }
215+
{
216+
"id": "not-blocked",
217+
"type": "is_user_blocked",
218+
"options": { "inverted": true },
219+
"params": []
183220
}
184221
]
185222
}
186223
```
187-
**Evaluation Logic**: `!is_user_blocked`
224+
225+
Evaluation logic: `!is_user_blocked`

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"node": ">=24.0.0"
1414
},
1515
"scripts": {
16-
"build": "tshy",
16+
"build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && tshy",
1717
"dev": "vitest",
1818
"test": "vitest run",
1919
"lint": "biome check .",

src/abstracts/AbstractAction.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { Neuron } from "../index.js";
2+
import type { ActionOptions } from "../interfaces/Action.js";
3+
import type { ParameterInterface } from "../interfaces/Parameter.js";
4+
import type { ExecutionContext } from "../types/ExecutionContext.js";
5+
import { AbstractElement } from "./AbstractElement.js";
6+
7+
export abstract class AbstractAction<
8+
TOptions extends ActionOptions = ActionOptions,
9+
> extends AbstractElement<TOptions> {
10+
public readonly params: Map<
11+
string,
12+
{ getValue(context: ExecutionContext): unknown | null }
13+
>;
14+
15+
constructor(
16+
id: string,
17+
type: string,
18+
public readonly rawParams: ParameterInterface[],
19+
options: TOptions,
20+
protected readonly neuron: Neuron,
21+
) {
22+
super(id, type, options);
23+
this.params = neuron.createParameterMap(rawParams);
24+
}
25+
26+
toJSON(): object {
27+
return {
28+
id: this.id,
29+
type: this.type,
30+
params: this.rawParams,
31+
options: this.options,
32+
};
33+
}
34+
}

src/abstracts/AbstractCondition.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import type { Neuron } from "../index.js";
2+
import type { ConditionOptions } from "../interfaces/Condition.js";
3+
import type { ParameterInterface } from "../interfaces/Parameter.js";
4+
import type { ExecutionContext } from "../types/ExecutionContext.js";
5+
import { AbstractElement } from "./AbstractElement.js";
6+
7+
export abstract class AbstractCondition<
8+
TOptions extends ConditionOptions = ConditionOptions,
9+
> extends AbstractElement<TOptions> {
10+
public readonly params: Map<
11+
string,
12+
{ getValue(context: ExecutionContext): unknown | null }
13+
>;
14+
15+
constructor(
16+
id: string,
17+
type: string,
18+
public readonly rawParams: ParameterInterface[],
19+
options: TOptions,
20+
protected readonly neuron: Neuron,
21+
) {
22+
super(id, type, options);
23+
this.params = neuron.createParameterMap(rawParams);
24+
}
25+
26+
toJSON(): object {
27+
return {
28+
id: this.id,
29+
type: this.type,
30+
params: this.rawParams,
31+
options: this.options,
32+
};
33+
}
34+
}

0 commit comments

Comments
 (0)