├── .github
└── workflows
│ ├── codequality.yml
│ ├── memory_test.yml
│ └── test.yml
├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── benchmark_test.go
├── cgo.go
├── custom_node.go
├── decision.go
├── decision_loader.go
├── decision_test.go
├── deps
├── darwin_amd64
│ ├── libzen_ffi.a
│ └── vendor.go
├── darwin_arm64
│ ├── libzen_ffi.a
│ └── vendor.go
├── linux_amd64
│ ├── libzen_ffi.a
│ └── vendor.go
├── linux_arm64
│ ├── libzen_ffi.a
│ └── vendor.go
└── windows_amd64
│ ├── libzen_ffi.lib
│ └── vendor.go
├── engine.go
├── engine_test.go
├── examples
└── custom-node
│ ├── main.go
│ ├── nodes
│ ├── add.go
│ ├── div.go
│ ├── interface.go
│ ├── mul.go
│ └── sub.go
│ └── rules
│ └── custom-node.json
├── expression.go
├── expression_test.go
├── go.mod
├── go.sum
├── memory.go
├── memory_test.go
├── test-data
├── custom-node.json
├── expression.json
├── function.json
├── large.json
└── table.json
├── util.go
├── zen.go
└── zen_engine.h
/.github/workflows/codequality.yml:
--------------------------------------------------------------------------------
1 | name: Code quality
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - master
8 | tags-ignore:
9 | - '**'
10 |
11 | jobs:
12 | codequality:
13 | name: Code quality
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v2
18 | - name: Format
19 | run: make fmt_check
--------------------------------------------------------------------------------
/.github/workflows/memory_test.yml:
--------------------------------------------------------------------------------
1 | name: Memory test
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - master
8 | tags-ignore:
9 | - '**'
10 |
11 | jobs:
12 | memory_test:
13 | name: Memory test
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v2
18 | - name: Run memory leak tests
19 | run: make memory_test
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - master
8 | tags-ignore:
9 | - '**'
10 |
11 | jobs:
12 | test:
13 | name: Test
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v2
18 | - name: Run tests
19 | run: make test
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.test
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2024 GoRules.io
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation
4 | files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy,
5 | modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software
6 | is furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9 |
10 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
11 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
13 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | test:
2 | @echo Running tests...
3 | go test
4 |
5 | memory_test:
6 | @echo Running memory tests...
7 | go test --tags memory_test
8 |
9 | fmt_check:
10 | @echo Checking format...
11 | @test -z $(shell go fmt ./...) || (echo "Code is not formatted according to gofmt. Please run 'go fmt ./...' to fix." && exit 1)
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://opensource.org/licenses/MIT)
2 |
3 | # Go Rules Engine
4 |
5 | ZEN Engine is a cross-platform, Open-Source Business Rules Engine (BRE). It is written in Rust and provides native
6 | bindings for **NodeJS**, **Python** and **Go**. ZEN Engine allows to load and execute JSON Decision Model (JDM) from JSON files.
7 |
8 |
9 |
10 | An open-source React editor is available on our [JDM Editor](https://github.com/gorules/jdm-editor) repo.
11 |
12 | ## Installation
13 |
14 | ```bash
15 | go get github.com/gorules/zen-go
16 | ```
17 |
18 | ## Usage
19 |
20 | ZEN Engine is built as embeddable BRE for your **Rust**, **NodeJS**, **Python** or **Go** applications.
21 | It parses JDM from JSON content. It is up to you to obtain the JSON content, e.g. from file system, database or service call.
22 |
23 | ### Load and Execute Rules
24 |
25 | ```go
26 | package main
27 |
28 | import (
29 | "fmt"
30 | "os"
31 | "path"
32 | )
33 |
34 | func readTestFile(key string) ([]byte, error) {
35 | filePath := path.Join("test-data", key)
36 | return os.ReadFile(filePath)
37 | }
38 |
39 | func main() {
40 | engine := zen.NewEngine(zen.EngineConfig{Loader: readTestFile})
41 | defer engine.Dispose() // Call to avoid leaks
42 |
43 | output, err := engine.Evaluate("rule.json", map[string]any{})
44 | if err != nil {
45 | fmt.Println(err)
46 | }
47 |
48 | fmt.Println(output)
49 | }
50 |
51 | ```
52 | For more details on rule format and advanced usage, take a look at the [Documentation](https://gorules.io/docs/developers/bre/engines/go).
53 |
54 | ### Supported Platforms
55 |
56 | List of platforms where Zen Engine is natively available:
57 |
58 | * **NodeJS** - [GitHub](https://github.com/gorules/zen/blob/master/bindings/nodejs/README.md) | [Documentation](https://gorules.io/docs/developers/bre/engines/nodejs) | [npmjs](https://www.npmjs.com/package/@gorules/zen-engine)
59 | * **Python** - [GitHub](https://github.com/gorules/zen/blob/master/bindings/python/README.md) | [Documentation](https://gorules.io/docs/developers/bre/engines/python) | [pypi](https://pypi.org/project/zen-engine/)
60 | * **Go** - [GitHub](https://github.com/gorules/zen-go) | [Documentation](https://gorules.io/docs/developers/bre/engines/go)
61 | * **Rust (Core)** - [GitHub](https://github.com/gorules/zen) | [Documentation](https://gorules.io/docs/developers/bre/engines/rust) | [crates.io](https://crates.io/crates/zen-engine)
62 |
63 | For a complete **Business Rules Management Systems (BRMS)** solution:
64 |
65 | * [Self-hosted BRMS](https://gorules.io)
66 | * [GoRules Cloud BRMS](https://gorules.io/signin/verify-email)
67 |
68 |
69 |
70 | ## JSON Decision Model (JDM)
71 |
72 | GoRules JDM (JSON Decision Model) is a modeling framework designed to streamline the representation and implementation
73 | of decision models.
74 |
75 | #### Understanding GoRules JDM
76 |
77 | At its core, GoRules JDM revolves around the concept of decision models as interconnected graphs stored in JSON format.
78 | These graphs capture the intricate relationships between various decision points, conditions, and outcomes in a GoRules
79 | Zen-Engine.
80 |
81 | Graphs are made by linking nodes with edges, which act like pathways for moving information from one node to another,
82 | usually from the left to the right.
83 |
84 | The Input node serves as an entry for all data relevant to the context, while the Output nodes produce the result of
85 | decision-making process. The progression of data follows a path from the Input Node to the Output Node, traversing all
86 | interconnected nodes in between. As the data flows through this network, it undergoes evaluation at each node, and
87 | connections determine where the data is passed along the graph.
88 |
89 | To see JDM Graph in action you can use [Free Online Editor](https://editor.gorules.io) with built in Simulator.
90 |
91 | There are 5 main node types in addition to a graph Input Node (Request) and Output Node (Response):
92 |
93 | * Decision Table Node
94 | * Switch Node
95 | * Function Node
96 | * Expression Node
97 | * Decision Node
98 |
99 | ### Decision Table Node
100 |
101 | #### Overview
102 |
103 | Tables provide a structured representation of decision-making processes, allowing developers and business users to
104 | express complex rules in a clear and concise manner.
105 |
106 |
107 |
108 | #### Structure
109 |
110 | At the core of the Decision Table is its schema, defining the structure with inputs and outputs. Inputs encompass
111 | business-friendly expressions using the ZEN Expression Language, accommodating a range of conditions such as equality,
112 | numeric comparisons, boolean values, date time functions, array functions and more. The schema's outputs dictate the
113 | form of results generated by the Decision Table.
114 | Inputs and outputs are expressed through a user-friendly interface, often resembling a spreadsheet. This facilitates
115 | easy modification and addition of rules, enabling business users to contribute to decision logic without delving into
116 | intricate code.
117 |
118 | #### Evaluation Process
119 |
120 | Decision Tables are evaluated row by row, from top to bottom, adhering to a specified hit policy.
121 | Single row is evaluated via Inputs columns, from left to right. Each input column represents `AND` operator. If cell is
122 | empty that column is evaluated **truthfully**, independently of the value.
123 |
124 | If a single cell within a row fails (due to error, or otherwise), the row is skipped.
125 |
126 | **HitPolicy**
127 |
128 | The hit policy determines the outcome calculation based on matching rules.
129 |
130 | The result of the evaluation is:
131 |
132 | * **an object** if the hit policy of the decision table is `first` and a rule matched. The structure is defined by the
133 | output fields. Qualified field names with a dot (.) inside lead to nested objects.
134 | * **`null`/`undefined`** if no rule matched in `first` hit policy
135 | * **an array of objects** if the hit policy of the decision table is `collect` (one array item for each matching rule)
136 | or empty array if no rules match
137 |
138 | #### Inputs
139 |
140 | In the assessment of rules or rows, input columns embody the `AND` operator. The values typically consist of (qualified)
141 | names, such as `customer.country` or `customer.age`.
142 |
143 | There are two types of evaluation of inputs, `Unary` and `Expression`.
144 |
145 | **Unary Evaluation**
146 |
147 | Unary evaluation is usually used when we would like to compare single fields from incoming context separately, for
148 | example `customer.country` and `cart.total` . It is activated when a column has `field` defined in its schema.
149 |
150 | ***Example***
151 |
152 | For the input:
153 |
154 | ```json
155 | {
156 | "customer": {
157 | "country": "US"
158 | },
159 | "cart": {
160 | "total": 1500
161 | }
162 | }
163 | ```
164 |
165 |
166 |
167 | This evaluation translates to
168 |
169 | ```
170 | IF customer.country == 'US' AND cart.total > 1000 THEN {"fees": {"percent": 2}}
171 | ELSE IF customer.country == 'US' THEN {"fees": {"flat": 30}}
172 | ELSE IF customer.country == 'CA' OR customer.country == 'MX' THEN {"fees": {"flat": 50}}
173 | ELSE {"fees": {"flat": 150}}
174 | ```
175 |
176 | List shows basic example of the unary tests in the Input Fields:
177 |
178 | | Input entry | Input Expression |
179 | |-------------|------------------------------------------------|
180 | | "A" | the field equals "A" |
181 | | "A", "B" | the field is either "A" or "B" |
182 | | 36 | the numeric value equals 36 |
183 | | < 36 | a value less than 36 |
184 | | > 36 | a value greater than 36 |
185 | | [20..39] | a value between 20 and 39 (inclusive) |
186 | | 20,39 | a value either 20 or 39 |
187 | | <20, >39 | a value either less than 20 or greater than 39 |
188 | | true | the boolean value true |
189 | | false | the boolean value false |
190 | | | any value, even null/undefined |
191 | | null | the value null or undefined |
192 |
193 | Note: For the full list please
194 | visit [ZEN Expression Language](https://gorules.io/docs/rules-engine/expression-language/).
195 |
196 | **Expression Evaluation**
197 |
198 | Expression evaluation is used when we would like to create more complex evaluation logic inside single cell. It allows
199 | us to compare multiple fields from the incoming context inside same cell.
200 |
201 | It can be used by providing an empty `Selector (field)` inside column configuration.
202 |
203 | ***Example***
204 |
205 | For the input:
206 |
207 | ```json
208 | {
209 | "transaction": {
210 | "country": "US",
211 | "createdAt": "2023-11-20T19:00:25Z",
212 | "amount": 10000
213 | }
214 | }
215 | ```
216 |
217 |
218 |
219 | ```
220 | IF time(transaction.createdAt) > time("17:00:00") AND transaction.amount > 1000 THEN {"status": "reject"}
221 | ELSE {"status": "approve"}
222 | ```
223 |
224 | Note: For the full list please
225 | visit [ZEN Expression Language](https://gorules.io/docs/rules-engine/expression-language/).
226 |
227 | **Outputs**
228 |
229 | Output columns serve as the blueprint for the data that the decision table will generate when the conditions are met
230 | during evaluation.
231 |
232 | When a row in the decision table satisfies its specified conditions, the output columns determine the nature and
233 | structure of the information that will be returned. Each output column represents a distinct field, and the collective
234 | set of these fields forms the output or result associated with the validated row. This mechanism allows decision tables
235 | to precisely define and control the data output.
236 |
237 | ***Example***
238 |
239 |
240 |
241 | And the result would be:
242 |
243 | ```json
244 | {
245 | "flatProperty": "A",
246 | "output": {
247 | "nested": {
248 | "property": "B"
249 | },
250 | "property": 36
251 | }
252 | }
253 | ```
254 |
255 | ### Switch Node (NEW)
256 |
257 | The Switch node in GoRules JDM introduces a dynamic branching mechanism to decision models, enabling the graph to
258 | diverge based on conditions.
259 |
260 | Conditions are written in a Zen Expression Language.
261 |
262 | By incorporating the Switch node, decision models become more flexible and context-aware. This capability is
263 | particularly valuable in scenarios where diverse decision logic is required based on varying inputs. The Switch node
264 | efficiently manages branching within the graph, enhancing the overall complexity and realism of decision models in
265 | GoRules JDM, making it a pivotal component for crafting intelligent and adaptive systems.
266 |
267 | The Switch node preserves the incoming data without modification; it forwards the entire context to the output branch(
268 | es).
269 |
270 |
271 |
272 | #### HitPolicy
273 |
274 | There are two HitPolicy options for the switch node, `first` and `collect`.
275 |
276 | In the context of a first hit policy, the graph branches to the initial matching condition, analogous to the behavior
277 | observed in a table. Conversely, under a collect hit policy, the graph extends to all branches where conditions hold
278 | true, allowing branching to multiple paths.
279 |
280 | Note: If there are multiple edges from the same condition, there is no guaranteed order of execution.
281 |
282 | *Available from:*
283 |
284 | * Python 0.16.0
285 | * NodeJS 0.13.0
286 | * Rust 0.16.0
287 | * Go 0.1.0
288 |
289 | ### Functions Node
290 |
291 | Function nodes are JavaScript snippets that allow for quick and easy parsing, re-mapping or otherwise modifying the data
292 | using JavaScript. Inputs of the node are provided as function's arguments. Functions are executed on top of QuickJS
293 | Engine that is bundled into the ZEN Engine.
294 |
295 | Function timeout is set to a 50ms.
296 |
297 | ```js
298 | const handler = (input, {dayjs, Big}) => {
299 | return {
300 | ...input,
301 | someField: 'hello'
302 | };
303 | };
304 | ```
305 |
306 | There are two built in libraries:
307 |
308 | * [dayjs](https://www.npmjs.com/package/dayjs) - for Date Manipulation
309 | * [big.js](https://www.npmjs.com/package/big.js) - for arbitrary-precision decimal arithmetic.
310 |
311 | ### Expression Node
312 |
313 | The Expression node serves as a tool for transforming input objects into alternative objects using the Zen Expression
314 | Language. When specifying the output properties, each property requires a separate row. These rows are defined by two
315 | fields:
316 |
317 | - Key - qualified name of the output property
318 | - Value - value expressed through the Zen Expression Language
319 |
320 | Note: Any errors within the Expression node will bring the graph to a halt.
321 |
322 |
323 |
324 | ### Decision Node
325 |
326 | The "Decision" node is designed to extend the capabilities of decision models. Its function is to invoke and reuse other
327 | decision models during execution.
328 |
329 | By incorporating the "Decision" node, developers can modularize decision logic, promoting reusability and
330 | maintainability in complex systems.
331 |
332 | ## Support matrix
333 |
334 | | Arch | Rust | NodeJS | Python | Go |
335 | |:----------------|:-------------------|:-------------------|:-------------------|:-------------------|
336 | | linux-x64-gnu | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
337 | | linux-arm64-gnu | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
338 | | darwin-x64 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
339 | | darwin-arm64 | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
340 | | win32-x64-msvc | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: |
341 |
342 | We do not support linux-musl currently.
343 |
344 | ## Contribution
345 |
346 | JDM standard is growing and we need to keep tight control over its development and roadmap as there are number of
347 | companies that are using GoRules Zen-Engine and GoRules BRMS.
348 | For this reason we can't accept any code contributions at this moment, apart from help with documentation and additional
349 | tests.
350 |
351 | ## License
352 |
353 | [MIT License]()
354 |
355 |
--------------------------------------------------------------------------------
/benchmark_test.go:
--------------------------------------------------------------------------------
1 | package zen_test
2 |
3 | import (
4 | "github.com/gorules/zen-go"
5 | "github.com/stretchr/testify/require"
6 | "testing"
7 | )
8 |
9 | func BenchmarkEngine(b *testing.B) {
10 | engine := zen.NewEngine(zen.EngineConfig{Loader: readTestFile, CustomNodeHandler: customNodeHandler})
11 | defer engine.Dispose()
12 |
13 | context := map[string]any{"input": 5}
14 |
15 | for i := 0; i < b.N; i++ {
16 | _, _ = engine.Evaluate("table.json", context)
17 | }
18 | }
19 |
20 | func BenchmarkDecision(b *testing.B) {
21 | engine := zen.NewEngine(zen.EngineConfig{Loader: readTestFile, CustomNodeHandler: customNodeHandler})
22 | defer engine.Dispose()
23 |
24 | decision, err := engine.GetDecision("table.json")
25 | require.NoError(b, err)
26 | defer decision.Dispose()
27 |
28 | context := map[string]any{"input": 5}
29 |
30 | for i := 0; i < b.N; i++ {
31 | _, _ = decision.Evaluate(context)
32 | }
33 | }
34 |
35 | func BenchmarkDecisionCustomNode(b *testing.B) {
36 | engine := zen.NewEngine(zen.EngineConfig{Loader: readTestFile, CustomNodeHandler: customNodeHandler})
37 | defer engine.Dispose()
38 |
39 | decision, err := engine.GetDecision("custom-node.json")
40 | require.NoError(b, err)
41 | defer decision.Dispose()
42 |
43 | context := map[string]any{"a": 5, "b": 10, "c": 15}
44 |
45 | for i := 0; i < b.N; i++ {
46 | _, _ = decision.Evaluate(context)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/cgo.go:
--------------------------------------------------------------------------------
1 | package zen
2 |
3 | /*
4 | #cgo LDFLAGS: -pthread -lzen_ffi
5 | #cgo darwin,amd64 LDFLAGS: -L${SRCDIR}/deps/darwin_amd64
6 | #cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/deps/darwin_arm64
7 | #cgo linux,amd64 LDFLAGS: -L${SRCDIR}/deps/linux_amd64 -lm -ldl
8 | #cgo linux,arm64 LDFLAGS: -L${SRCDIR}/deps/linux_arm64 -lm -ldl
9 | #cgo windows,amd64 LDFLAGS: -L${SRCDIR}/deps/windows_amd64 -lm -ldl
10 | */
11 | import "C"
12 |
13 | import (
14 | _ "github.com/gorules/zen-go/deps/darwin_amd64"
15 | _ "github.com/gorules/zen-go/deps/darwin_arm64"
16 | _ "github.com/gorules/zen-go/deps/linux_amd64"
17 | _ "github.com/gorules/zen-go/deps/linux_arm64"
18 | _ "github.com/gorules/zen-go/deps/windows_amd64"
19 | )
20 |
--------------------------------------------------------------------------------
/custom_node.go:
--------------------------------------------------------------------------------
1 | package zen
2 |
3 | // #include "zen_engine.h"
4 | import "C"
5 | import (
6 | "encoding/json"
7 | "errors"
8 | "github.com/tidwall/gjson"
9 | )
10 |
11 | type CustomNodeHandler func(request NodeRequest) (NodeResponse, error)
12 |
13 | type CustomNode struct {
14 | ID string `json:"id"`
15 | Name string `json:"name"`
16 | Kind string `json:"kind"`
17 | Config json.RawMessage `json:"config"`
18 | }
19 |
20 | type NodeRequest struct {
21 | Node CustomNode `json:"node"`
22 | Input json.RawMessage `json:"input"`
23 | }
24 |
25 | type NodeResponse struct {
26 | Output any `json:"output"`
27 | TraceData any `json:"traceData"`
28 | }
29 |
30 | func wrapCustomNodeHandler(customNodeHandler CustomNodeHandler) func(cRequest *C.char) C.ZenCustomNodeResult {
31 | return func(cRequest *C.char) C.ZenCustomNodeResult {
32 | strRequest := C.GoString(cRequest)
33 |
34 | var request NodeRequest
35 | if err := json.Unmarshal([]byte(strRequest), &request); err != nil {
36 | return C.ZenCustomNodeResult{
37 | content: nil,
38 | error: C.CString(err.Error()),
39 | }
40 | }
41 |
42 | response, err := customNodeHandler(request)
43 | if err != nil {
44 | return C.ZenCustomNodeResult{
45 | content: nil,
46 | error: C.CString(err.Error()),
47 | }
48 | }
49 |
50 | cResponse, err := json.Marshal(response)
51 | if err != nil {
52 | return C.ZenCustomNodeResult{
53 | content: nil,
54 | error: C.CString(err.Error()),
55 | }
56 | }
57 |
58 | return C.ZenCustomNodeResult{
59 | content: C.CString(string(cResponse)),
60 | error: nil,
61 | }
62 | }
63 | }
64 |
65 | func GetNodeFieldRaw[T any](request NodeRequest, path string) (T, error) {
66 | result := gjson.GetBytes(request.Node.Config, path)
67 | if !result.Exists() {
68 | return *new(T), errors.New("path does not exist")
69 | }
70 |
71 | var r T
72 | if err := json.Unmarshal([]byte(result.Raw), &r); err != nil {
73 | return *new(T), err
74 | }
75 |
76 | return r, nil
77 | }
78 |
79 | func GetNodeField[T any](request NodeRequest, path string) (T, error) {
80 | result := gjson.GetBytes(request.Node.Config, path)
81 | if !result.Exists() {
82 | return *new(T), errors.New("path does not exist")
83 | }
84 |
85 | if result.Type != gjson.String {
86 | var r T
87 | if err := json.Unmarshal([]byte(result.Raw), &r); err != nil {
88 | return *new(T), err
89 | }
90 |
91 | return r, nil
92 | }
93 |
94 | return RenderTemplate[T](result.Str, request.Input)
95 | }
96 |
--------------------------------------------------------------------------------
/decision.go:
--------------------------------------------------------------------------------
1 | package zen
2 |
3 | // #include "zen_engine.h"
4 | import "C"
5 | import (
6 | "encoding/json"
7 | "errors"
8 | "fmt"
9 | "unsafe"
10 | )
11 |
12 | type decision struct {
13 | decisionPtr *C.ZenDecisionStruct
14 | }
15 |
16 | // newDecision: called internally by zen_engine only, cleanup should still be fired however.
17 | func newDecision(decisionPtr *C.ZenDecisionStruct) Decision {
18 | return decision{
19 | decisionPtr: decisionPtr,
20 | }
21 | }
22 |
23 | func (decision decision) Evaluate(context any) (*EvaluationResponse, error) {
24 | return decision.EvaluateWithOpts(context, EvaluationOptions{})
25 | }
26 |
27 | func (decision decision) EvaluateWithOpts(context any, options EvaluationOptions) (*EvaluationResponse, error) {
28 | jsonData, err := extractJsonFromAny(context)
29 | if err != nil {
30 | return nil, err
31 | }
32 |
33 | cData := C.CString(string(jsonData))
34 | defer C.free(unsafe.Pointer(cData))
35 |
36 | maxDepth := options.MaxDepth
37 | if maxDepth == 0 {
38 | maxDepth = 1
39 | }
40 |
41 | resultPtr := C.zen_decision_evaluate(decision.decisionPtr, cData, C.ZenEngineEvaluationOptions{
42 | trace: C.bool(options.Trace),
43 | max_depth: C.uint8_t(maxDepth),
44 | })
45 | if resultPtr.error > 0 {
46 | var errorDetails string
47 | if resultPtr.details != nil {
48 | defer C.free(unsafe.Pointer(resultPtr.details))
49 | errorDetails = C.GoString(resultPtr.details)
50 | } else {
51 | errorDetails = fmt.Sprintf("Error code: %d", resultPtr.error)
52 | }
53 |
54 | return nil, errors.New(errorDetails)
55 | }
56 |
57 | defer C.free(unsafe.Pointer(resultPtr.result))
58 | result := C.GoString(resultPtr.result)
59 |
60 | var response EvaluationResponse
61 | if err := json.Unmarshal([]byte(result), &response); err != nil {
62 | return nil, err
63 | }
64 |
65 | return &response, nil
66 | }
67 |
68 | func (decision decision) Dispose() {
69 | C.zen_decision_free(decision.decisionPtr)
70 | }
71 |
--------------------------------------------------------------------------------
/decision_loader.go:
--------------------------------------------------------------------------------
1 | package zen
2 |
3 | // #include "zen_engine.h"
4 | import "C"
5 |
6 | type Loader func(key string) ([]byte, error)
7 |
8 | func wrapLoader(loader Loader) func(cKey *C.char) C.ZenDecisionLoaderResult {
9 | return func(cKey *C.char) C.ZenDecisionLoaderResult {
10 | key := C.GoString(cKey)
11 | content, err := loader(key)
12 | if err != nil {
13 | return C.ZenDecisionLoaderResult{
14 | content: nil,
15 | error: C.CString(err.Error()),
16 | }
17 | }
18 |
19 | return C.ZenDecisionLoaderResult{
20 | content: C.CString(string(content)),
21 | error: nil,
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/decision_test.go:
--------------------------------------------------------------------------------
1 | package zen_test
2 |
3 | import (
4 | "encoding/json"
5 | "github.com/gorules/zen-go"
6 | "github.com/stretchr/testify/assert"
7 | "sync"
8 | "testing"
9 | )
10 |
11 | func TestDecision_EvaluateWithOpts(t *testing.T) {
12 | engine := zen.NewEngine(zen.EngineConfig{Loader: readTestFile, CustomNodeHandler: customNodeHandler})
13 | defer engine.Dispose()
14 |
15 | testData := prepareEvaluationTestData()
16 | for _, data := range testData {
17 | decision, err := engine.GetDecision(data.file)
18 | assert.NoError(t, err)
19 |
20 | var inputJson any
21 | err = json.Unmarshal([]byte(data.inputJson), &inputJson)
22 | assert.NoError(t, err)
23 |
24 | output, err := decision.Evaluate(inputJson)
25 | assert.NoError(t, err)
26 | assert.Nil(t, output.Trace)
27 |
28 | result, err := output.Result.MarshalJSON()
29 | assert.NoError(t, err)
30 |
31 | assert.JSONEq(t, data.outputJson, string(result))
32 | decision.Dispose()
33 | }
34 | }
35 |
36 | func TestDecision_Evaluate(t *testing.T) {
37 | engine := zen.NewEngine(zen.EngineConfig{Loader: readTestFile, CustomNodeHandler: customNodeHandler})
38 | defer engine.Dispose()
39 |
40 | testData := prepareEvaluationTestData()
41 | for _, data := range testData {
42 | decision, err := engine.GetDecision(data.file)
43 | assert.NoError(t, err)
44 |
45 | var inputJson any
46 | err = json.Unmarshal([]byte(data.inputJson), &inputJson)
47 | assert.NoError(t, err)
48 |
49 | output, err := decision.EvaluateWithOpts(inputJson, zen.EvaluationOptions{
50 | Trace: true,
51 | MaxDepth: 10,
52 | })
53 | assert.NoError(t, err)
54 | assert.NotNil(t, output.Trace)
55 |
56 | result, err := output.Result.MarshalJSON()
57 | assert.NoError(t, err)
58 |
59 | assert.JSONEq(t, data.outputJson, string(result))
60 | decision.Dispose()
61 | }
62 | }
63 |
64 | func TestDecision_EvaluateParallel(t *testing.T) {
65 | engine := zen.NewEngine(zen.EngineConfig{Loader: readTestFile, CustomNodeHandler: customNodeHandler})
66 | defer engine.Dispose()
67 |
68 | type responseData struct {
69 | Output int `json:"output"`
70 | }
71 |
72 | var wg sync.WaitGroup
73 | for i := 0; i < 10; i++ {
74 | wg.Add(1)
75 | current := i
76 | go func() {
77 | defer wg.Done()
78 |
79 | decision, err := engine.GetDecision("function.json")
80 | assert.NoError(t, err)
81 | defer decision.Dispose()
82 |
83 | resp, err := decision.Evaluate(map[string]any{"input": current})
84 | assert.NoError(t, err)
85 |
86 | var respData responseData
87 | assert.NoError(t, json.Unmarshal(resp.Result, &respData))
88 | assert.Equal(t, current*2, respData.Output)
89 | }()
90 | }
91 |
92 | wg.Wait()
93 | }
94 |
--------------------------------------------------------------------------------
/deps/darwin_amd64/libzen_ffi.a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gorules/zen-go/e9b842c2f30154eda549dce606dc59a2f3dfa751/deps/darwin_amd64/libzen_ffi.a
--------------------------------------------------------------------------------
/deps/darwin_amd64/vendor.go:
--------------------------------------------------------------------------------
1 | // Package darwin_amd64 is required to provide support for vendoring modules
2 | // DO NOT REMOVE
3 | package darwin_amd64
4 |
--------------------------------------------------------------------------------
/deps/darwin_arm64/libzen_ffi.a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gorules/zen-go/e9b842c2f30154eda549dce606dc59a2f3dfa751/deps/darwin_arm64/libzen_ffi.a
--------------------------------------------------------------------------------
/deps/darwin_arm64/vendor.go:
--------------------------------------------------------------------------------
1 | // Package darwin_arm64 is required to provide support for vendoring modules
2 | // DO NOT REMOVE
3 | package darwin_arm64
4 |
--------------------------------------------------------------------------------
/deps/linux_amd64/libzen_ffi.a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gorules/zen-go/e9b842c2f30154eda549dce606dc59a2f3dfa751/deps/linux_amd64/libzen_ffi.a
--------------------------------------------------------------------------------
/deps/linux_amd64/vendor.go:
--------------------------------------------------------------------------------
1 | // Package linux_amd64 is required to provide support for vendoring modules
2 | // DO NOT REMOVE
3 | package linux_amd64
4 |
--------------------------------------------------------------------------------
/deps/linux_arm64/libzen_ffi.a:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gorules/zen-go/e9b842c2f30154eda549dce606dc59a2f3dfa751/deps/linux_arm64/libzen_ffi.a
--------------------------------------------------------------------------------
/deps/linux_arm64/vendor.go:
--------------------------------------------------------------------------------
1 | // Package linux_arm64 is required to provide support for vendoring modules
2 | // DO NOT REMOVE
3 | package linux_arm64
4 |
--------------------------------------------------------------------------------
/deps/windows_amd64/libzen_ffi.lib:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gorules/zen-go/e9b842c2f30154eda549dce606dc59a2f3dfa751/deps/windows_amd64/libzen_ffi.lib
--------------------------------------------------------------------------------
/deps/windows_amd64/vendor.go:
--------------------------------------------------------------------------------
1 | // Package windows_amd64 is required to provide support for vendoring modules
2 | // DO NOT REMOVE
3 | package windows_amd64
4 |
--------------------------------------------------------------------------------
/engine.go:
--------------------------------------------------------------------------------
1 | package zen
2 |
3 | // #include "zen_engine.h"
4 | import "C"
5 | import (
6 | "encoding/json"
7 | "errors"
8 | "fmt"
9 | "runtime/cgo"
10 | "unsafe"
11 | )
12 |
13 | type engine struct {
14 | loaderHandler cgo.Handle
15 | loaderHandlerIdPtr *C.uintptr_t
16 | customNodeHandler cgo.Handle
17 | customNodeHandlerIdPtr *C.uintptr_t
18 | enginePtr *C.ZenEngineStruct
19 | }
20 |
21 | type EngineConfig struct {
22 | Loader Loader
23 | CustomNodeHandler CustomNodeHandler
24 | }
25 |
26 | //export zen_engine_go_loader_callback
27 | func zen_engine_go_loader_callback(h C.uintptr_t, key *C.char) C.ZenDecisionLoaderResult {
28 | fn := cgo.Handle(h).Value().(func(*C.char) C.ZenDecisionLoaderResult)
29 | return fn(key)
30 | }
31 |
32 | //export zen_engine_go_custom_node_callback
33 | func zen_engine_go_custom_node_callback(h C.uintptr_t, request *C.char) C.ZenCustomNodeResult {
34 | fn := cgo.Handle(h).Value().(func(*C.char) C.ZenCustomNodeResult)
35 | return fn(request)
36 | }
37 |
38 | func NewEngine(config EngineConfig) Engine {
39 | var newEngine = engine{}
40 | var loaderHandlerIdPtr C.uintptr_t
41 | var customNodeHandlerIdPtr C.uintptr_t
42 |
43 | if config.Loader != nil {
44 | newEngine.loaderHandler = cgo.NewHandle(wrapLoader(config.Loader))
45 | loaderHandlerIdPtr = C.uintptr_t(newEngine.loaderHandler)
46 | newEngine.loaderHandlerIdPtr = &loaderHandlerIdPtr
47 | }
48 |
49 | if config.CustomNodeHandler != nil {
50 | newEngine.customNodeHandler = cgo.NewHandle(wrapCustomNodeHandler(config.CustomNodeHandler))
51 | customNodeHandlerIdPtr = C.uintptr_t(newEngine.customNodeHandler)
52 | newEngine.customNodeHandlerIdPtr = &customNodeHandlerIdPtr
53 | }
54 |
55 | newEngine.enginePtr = C.zen_engine_new_golang(&loaderHandlerIdPtr, &customNodeHandlerIdPtr)
56 | return newEngine
57 | }
58 |
59 | func (engine engine) Evaluate(key string, context any) (*EvaluationResponse, error) {
60 | return engine.EvaluateWithOpts(key, context, EvaluationOptions{})
61 | }
62 |
63 | func (engine engine) EvaluateWithOpts(key string, context any, options EvaluationOptions) (*EvaluationResponse, error) {
64 | jsonData, err := extractJsonFromAny(context)
65 | if err != nil {
66 | return nil, err
67 | }
68 |
69 | cKey := C.CString(key)
70 | defer C.free(unsafe.Pointer(cKey))
71 |
72 | cData := C.CString(string(jsonData))
73 | defer C.free(unsafe.Pointer(cData))
74 |
75 | maxDepth := options.MaxDepth
76 | if maxDepth == 0 {
77 | maxDepth = 1
78 | }
79 |
80 | resultPtr := C.zen_engine_evaluate(engine.enginePtr, cKey, cData, C.ZenEngineEvaluationOptions{
81 | trace: C.bool(options.Trace),
82 | max_depth: C.uint8_t(maxDepth),
83 | })
84 | if resultPtr.error > 0 {
85 | var errorDetails string
86 | if resultPtr.details != nil {
87 | defer C.free(unsafe.Pointer(resultPtr.details))
88 | errorDetails = C.GoString(resultPtr.details)
89 | } else {
90 | errorDetails = fmt.Sprintf("Error code: %d", resultPtr.error)
91 | }
92 |
93 | return nil, errors.New(errorDetails)
94 | }
95 |
96 | defer C.free(unsafe.Pointer(resultPtr.result))
97 | result := C.GoString(resultPtr.result)
98 |
99 | var response EvaluationResponse
100 | if err := json.Unmarshal([]byte(result), &response); err != nil {
101 | return nil, err
102 | }
103 |
104 | return &response, nil
105 | }
106 |
107 | func (engine engine) GetDecision(key string) (Decision, error) {
108 | cKey := C.CString(key)
109 | defer C.free(unsafe.Pointer(cKey))
110 |
111 | decisionPtr := C.zen_engine_get_decision(engine.enginePtr, cKey)
112 | if decisionPtr.error > 0 {
113 | var errorDetails string
114 | if decisionPtr.details != nil {
115 | defer C.free(unsafe.Pointer(decisionPtr.details))
116 | errorDetails = C.GoString(decisionPtr.details)
117 | } else {
118 | errorDetails = fmt.Sprintf("Error code: %d", decisionPtr.error)
119 | }
120 |
121 | return nil, errors.New(errorDetails)
122 | }
123 |
124 | return newDecision(decisionPtr.result), nil
125 | }
126 |
127 | func (engine engine) CreateDecision(data []byte) (Decision, error) {
128 | cData := C.CString(string(data))
129 | defer C.free(unsafe.Pointer(cData))
130 |
131 | decisionPtr := C.zen_engine_create_decision(engine.enginePtr, cData)
132 | if decisionPtr.error > 0 {
133 | var errorDetails string
134 | if decisionPtr.details != nil {
135 | defer C.free(unsafe.Pointer(decisionPtr.details))
136 | errorDetails = C.GoString(decisionPtr.details)
137 | } else {
138 | errorDetails = fmt.Sprintf("Error code: %d", decisionPtr.error)
139 | }
140 |
141 | return nil, errors.New(errorDetails)
142 | }
143 |
144 | return newDecision(decisionPtr.result), nil
145 | }
146 |
147 | func (engine engine) Dispose() {
148 | C.zen_engine_free(engine.enginePtr)
149 |
150 | if engine.loaderHandlerIdPtr != nil {
151 | engine.loaderHandler.Delete()
152 | }
153 |
154 | if engine.customNodeHandlerIdPtr != nil {
155 | engine.customNodeHandler.Delete()
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/engine_test.go:
--------------------------------------------------------------------------------
1 | package zen_test
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "github.com/stretchr/testify/assert"
7 | "os"
8 | "path"
9 | "sync"
10 | "testing"
11 |
12 | "github.com/gorules/zen-go"
13 | )
14 |
15 | func readTestFile(key string) ([]byte, error) {
16 | filePath := path.Join("test-data", key)
17 | return os.ReadFile(filePath)
18 | }
19 |
20 | func customNodeHandler(request zen.NodeRequest) (zen.NodeResponse, error) {
21 | if request.Node.Kind != "sum" {
22 | return zen.NodeResponse{}, errors.New("unknown component")
23 | }
24 |
25 | a, err := zen.GetNodeField[int](request, "a")
26 | if err != nil {
27 | return zen.NodeResponse{}, err
28 | }
29 |
30 | b, err := zen.GetNodeField[int](request, "b")
31 | if err != nil {
32 | return zen.NodeResponse{}, err
33 | }
34 |
35 | key, err := zen.GetNodeFieldRaw[string](request, "key")
36 | if err != nil {
37 | return zen.NodeResponse{}, err
38 | }
39 |
40 | output := make(map[string]any)
41 | output[key] = a + b
42 |
43 | return zen.NodeResponse{Output: output}, nil
44 | }
45 |
46 | type evaluateTestData struct {
47 | file string
48 | inputJson string
49 | outputJson string
50 | }
51 |
52 | func prepareEvaluationTestData() map[string]evaluateTestData {
53 | return map[string]evaluateTestData{
54 | "table < 10": {
55 | file: "table.json",
56 | inputJson: `{"input":5}`,
57 | outputJson: `{"output":0}`,
58 | },
59 | "table > 10": {
60 | file: "table.json",
61 | inputJson: `{"input":15}`,
62 | outputJson: `{"output":10}`,
63 | },
64 | "function = 1": {
65 | file: "function.json",
66 | inputJson: `{"input":1}`,
67 | outputJson: `{"output":2}`,
68 | },
69 | "function = 5": {
70 | file: "function.json",
71 | inputJson: `{"input":5}`,
72 | outputJson: `{"output":10}`,
73 | },
74 | "function = 15": {
75 | file: "function.json",
76 | inputJson: `{"input":15}`,
77 | outputJson: `{"output":30}`,
78 | },
79 | "expression": {
80 | file: "expression.json",
81 | inputJson: `{"numbers": [1, 5, 15, 25],"firstName": "John","lastName": "Doe"}`,
82 | outputJson: `{"deep":{"nested":{"sum":46}},"fullName":"John Doe","largeNumbers":[15,25],"smallNumbers":[1,5]}`,
83 | },
84 | "customNode": {
85 | file: "custom-node.json",
86 | inputJson: `{"a": 5, "b": 10, "c": 15}`,
87 | outputJson: `{"sum":30}`,
88 | },
89 | }
90 | }
91 |
92 | func TestEngine_NewEngine(t *testing.T) {
93 | engineWithLoader := zen.NewEngine(zen.EngineConfig{Loader: readTestFile, CustomNodeHandler: customNodeHandler})
94 | defer engineWithLoader.Dispose()
95 | assert.NotNil(t, engineWithLoader)
96 |
97 | engineWithoutLoader := zen.NewEngine(zen.EngineConfig{})
98 | defer engineWithoutLoader.Dispose()
99 | assert.NotNil(t, engineWithoutLoader)
100 | }
101 |
102 | func TestEngine_Evaluate(t *testing.T) {
103 | engine := zen.NewEngine(zen.EngineConfig{Loader: readTestFile, CustomNodeHandler: customNodeHandler})
104 | defer engine.Dispose()
105 |
106 | testData := prepareEvaluationTestData()
107 | for _, data := range testData {
108 | var inputJson any
109 | err := json.Unmarshal([]byte(data.inputJson), &inputJson)
110 | assert.NoError(t, err)
111 |
112 | output, err := engine.Evaluate(data.file, inputJson)
113 | assert.NoError(t, err)
114 | assert.Nil(t, output.Trace)
115 |
116 | result, err := output.Result.MarshalJSON()
117 | assert.NoError(t, err)
118 |
119 | assert.JSONEq(t, data.outputJson, string(result))
120 | }
121 | }
122 |
123 | func TestEngine_EvaluateWithOpts(t *testing.T) {
124 | engine := zen.NewEngine(zen.EngineConfig{Loader: readTestFile, CustomNodeHandler: customNodeHandler})
125 | defer engine.Dispose()
126 |
127 | testData := prepareEvaluationTestData()
128 | for _, data := range testData {
129 | var inputJson any
130 | err := json.Unmarshal([]byte(data.inputJson), &inputJson)
131 | assert.NoError(t, err)
132 |
133 | output, err := engine.EvaluateWithOpts(data.file, inputJson, zen.EvaluationOptions{
134 | Trace: true,
135 | MaxDepth: 10,
136 | })
137 | assert.NoError(t, err)
138 | assert.NotNil(t, output.Trace)
139 |
140 | result, err := output.Result.MarshalJSON()
141 | assert.NoError(t, err)
142 |
143 | assert.JSONEq(t, data.outputJson, string(result))
144 | }
145 | }
146 |
147 | func TestEngine_GetDecision(t *testing.T) {
148 | engine := zen.NewEngine(zen.EngineConfig{Loader: readTestFile, CustomNodeHandler: customNodeHandler})
149 | defer engine.Dispose()
150 |
151 | testData := prepareEvaluationTestData()
152 | for _, data := range testData {
153 | decision, err := engine.GetDecision(data.file)
154 | assert.NotNil(t, decision)
155 | assert.NoError(t, err)
156 |
157 | decision.Dispose()
158 | }
159 | }
160 |
161 | func TestEngine_CreateDecision(t *testing.T) {
162 | engine := zen.NewEngine(zen.EngineConfig{Loader: readTestFile, CustomNodeHandler: customNodeHandler})
163 | defer engine.Dispose()
164 |
165 | fileData, err := readTestFile("large.json")
166 | assert.NoError(t, err)
167 |
168 | decision, err := engine.CreateDecision(fileData)
169 | assert.NotNil(t, decision)
170 | assert.NoError(t, err)
171 |
172 | decision.Dispose()
173 | }
174 |
175 | func TestEngine_ErrorTransparency(t *testing.T) {
176 | errorStr := "Custom error"
177 | engine := zen.NewEngine(zen.EngineConfig{
178 | Loader: func(key string) ([]byte, error) {
179 | return nil, errors.New(errorStr)
180 | },
181 | })
182 | defer engine.Dispose()
183 |
184 | _, err := engine.Evaluate("myKey", nil)
185 | assert.Error(t, err)
186 | assert.ErrorContains(t, err, "myKey")
187 | assert.ErrorContains(t, err, errorStr)
188 | }
189 |
190 | func TestEngine_EvaluateParallel(t *testing.T) {
191 | engine := zen.NewEngine(zen.EngineConfig{Loader: readTestFile, CustomNodeHandler: customNodeHandler})
192 | defer engine.Dispose()
193 |
194 | type responseData struct {
195 | Output int `json:"output"`
196 | }
197 |
198 | var wg sync.WaitGroup
199 | for i := 0; i < 10; i++ {
200 | wg.Add(1)
201 | current := i
202 | go func() {
203 | defer wg.Done()
204 |
205 | resp, err := engine.Evaluate("function.json", map[string]any{"input": current})
206 | assert.NoError(t, err)
207 |
208 | var respData responseData
209 | assert.NoError(t, json.Unmarshal(resp.Result, &respData))
210 | assert.Equal(t, current*2, respData.Output)
211 | }()
212 | }
213 |
214 | wg.Wait()
215 | }
216 |
--------------------------------------------------------------------------------
/examples/custom-node/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "embed"
5 | "fmt"
6 | "github.com/gorules/zen-go"
7 | "github.com/gorules/zen-go/examples/custom-node/nodes"
8 | "path"
9 | )
10 |
11 | //go:embed rules
12 | var rulesFS embed.FS
13 |
14 | func readTestFile(key string) ([]byte, error) {
15 | data, err := rulesFS.ReadFile(path.Join("rules", key))
16 | if err != nil {
17 | return nil, err
18 | }
19 |
20 | return data, nil
21 | }
22 |
23 | func main() {
24 | engine := zen.NewEngine(zen.EngineConfig{Loader: readTestFile, CustomNodeHandler: nodes.CustomNodeHandler})
25 | context := map[string]any{"a": 10}
26 | r, _ := engine.Evaluate("custom-node.json", context)
27 |
28 | fmt.Printf("[%s] Your result is: %s.\n", r.Performance, r.Result)
29 | }
30 |
--------------------------------------------------------------------------------
/examples/custom-node/nodes/add.go:
--------------------------------------------------------------------------------
1 | package nodes
2 |
3 | import (
4 | "github.com/gorules/zen-go"
5 | )
6 |
7 | type addNode struct {
8 | }
9 |
10 | func (a addNode) Handle(request zen.NodeRequest) (zen.NodeResponse, error) {
11 | left, err := zen.GetNodeField[float64](request, "left")
12 | if err != nil {
13 | return zen.NodeResponse{}, err
14 | }
15 |
16 | right, err := zen.GetNodeField[float64](request, "right")
17 | if err != nil {
18 | return zen.NodeResponse{}, err
19 | }
20 |
21 | key, err := zen.GetNodeFieldRaw[string](request, "key")
22 | if err != nil {
23 | return zen.NodeResponse{}, err
24 | }
25 |
26 | output := make(map[string]any)
27 | output[key] = left + right
28 |
29 | return zen.NodeResponse{Output: output}, nil
30 | }
31 |
--------------------------------------------------------------------------------
/examples/custom-node/nodes/div.go:
--------------------------------------------------------------------------------
1 | package nodes
2 |
3 | import "github.com/gorules/zen-go"
4 |
5 | type divNode struct {
6 | }
7 |
8 | func (a divNode) Handle(request zen.NodeRequest) (zen.NodeResponse, error) {
9 | left, err := zen.GetNodeField[float64](request, "left")
10 | if err != nil {
11 | return zen.NodeResponse{}, err
12 | }
13 |
14 | right, err := zen.GetNodeField[float64](request, "right")
15 | if err != nil {
16 | return zen.NodeResponse{}, err
17 | }
18 |
19 | key, err := zen.GetNodeFieldRaw[string](request, "key")
20 | if err != nil {
21 | return zen.NodeResponse{}, err
22 | }
23 |
24 | output := make(map[string]any)
25 | output[key] = left / right
26 |
27 | return zen.NodeResponse{Output: output}, nil
28 | }
29 |
--------------------------------------------------------------------------------
/examples/custom-node/nodes/interface.go:
--------------------------------------------------------------------------------
1 | package nodes
2 |
3 | import (
4 | "errors"
5 | "github.com/gorules/zen-go"
6 | )
7 |
8 | type NodeHandler interface {
9 | Handle(request zen.NodeRequest) (zen.NodeResponse, error)
10 | }
11 |
12 | var customNodes = map[string]NodeHandler{
13 | "add": addNode{},
14 | "mul": mulNode{},
15 | "sub": subNode{},
16 | "div": divNode{},
17 | }
18 |
19 | func CustomNodeHandler(request zen.NodeRequest) (zen.NodeResponse, error) {
20 | nodeHandler, ok := customNodes[request.Node.Kind]
21 | if !ok {
22 | return zen.NodeResponse{}, errors.New("component not found")
23 | }
24 |
25 | return nodeHandler.Handle(request)
26 | }
27 |
--------------------------------------------------------------------------------
/examples/custom-node/nodes/mul.go:
--------------------------------------------------------------------------------
1 | package nodes
2 |
3 | import "github.com/gorules/zen-go"
4 |
5 | type mulNode struct {
6 | }
7 |
8 | func (a mulNode) Handle(request zen.NodeRequest) (zen.NodeResponse, error) {
9 | left, err := zen.GetNodeField[float64](request, "left")
10 | if err != nil {
11 | return zen.NodeResponse{}, err
12 | }
13 |
14 | right, err := zen.GetNodeField[float64](request, "right")
15 | if err != nil {
16 | return zen.NodeResponse{}, err
17 | }
18 |
19 | key, err := zen.GetNodeFieldRaw[string](request, "key")
20 | if err != nil {
21 | return zen.NodeResponse{}, err
22 | }
23 |
24 | output := make(map[string]any)
25 | output[key] = left * right
26 |
27 | return zen.NodeResponse{Output: output}, nil
28 | }
29 |
--------------------------------------------------------------------------------
/examples/custom-node/nodes/sub.go:
--------------------------------------------------------------------------------
1 | package nodes
2 |
3 | import "github.com/gorules/zen-go"
4 |
5 | type subNode struct {
6 | }
7 |
8 | func (a subNode) Handle(request zen.NodeRequest) (zen.NodeResponse, error) {
9 | left, err := zen.GetNodeField[float64](request, "left")
10 | if err != nil {
11 | return zen.NodeResponse{}, err
12 | }
13 |
14 | right, err := zen.GetNodeField[float64](request, "right")
15 | if err != nil {
16 | return zen.NodeResponse{}, err
17 | }
18 |
19 | key, err := zen.GetNodeFieldRaw[string](request, "key")
20 | if err != nil {
21 | return zen.NodeResponse{}, err
22 | }
23 |
24 | output := make(map[string]any)
25 | output[key] = left - right
26 |
27 | return zen.NodeResponse{Output: output}, nil
28 | }
29 |
--------------------------------------------------------------------------------
/examples/custom-node/rules/custom-node.json:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": [
3 | {
4 | "id": "115975ef-2f43-4e22-b553-0da6f4cc7f68",
5 | "type": "inputNode",
6 | "position": {
7 | "x": 180,
8 | "y": 240
9 | },
10 | "name": "Request"
11 | },
12 | {
13 | "id": "138b3b11-ff46-450f-9704-3f3c712067b2",
14 | "type": "customNode",
15 | "position": {
16 | "x": 470,
17 | "y": 240
18 | },
19 | "name": "customNode1",
20 | "content": {
21 | "kind": "add",
22 | "config": {
23 | "left": "{{ a + 20 }}",
24 | "right": 20,
25 | "key": "hello"
26 | }
27 | }
28 | },
29 | {
30 | "id": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e",
31 | "type": "outputNode",
32 | "position": {
33 | "x": 780,
34 | "y": 240
35 | },
36 | "name": "Response"
37 | }
38 | ],
39 | "edges": [
40 | {
41 | "id": "05740fa7-3755-4756-b85e-bc1af2f6773b",
42 | "sourceId": "115975ef-2f43-4e22-b553-0da6f4cc7f68",
43 | "type": "edge",
44 | "targetId": "138b3b11-ff46-450f-9704-3f3c712067b2"
45 | },
46 | {
47 | "id": "5d89c1d6-e894-4e8a-bd13-22368c2a6bc7",
48 | "sourceId": "138b3b11-ff46-450f-9704-3f3c712067b2",
49 | "type": "edge",
50 | "targetId": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e"
51 | }
52 | ]
53 | }
--------------------------------------------------------------------------------
/expression.go:
--------------------------------------------------------------------------------
1 | package zen
2 |
3 | // #include "zen_engine.h"
4 | import "C"
5 | import (
6 | "encoding/json"
7 | "errors"
8 | "fmt"
9 | "unsafe"
10 | )
11 |
12 | func EvaluateExpression[T any](expression string, context any) (T, error) {
13 | jsonData, err := extractJsonFromAny(context)
14 | if err != nil {
15 | var zero T
16 | return zero, err
17 | }
18 |
19 | expressionCString := C.CString(expression)
20 | defer C.free(unsafe.Pointer(expressionCString))
21 |
22 | contextCString := C.CString(string(jsonData))
23 | defer C.free(unsafe.Pointer(contextCString))
24 |
25 | resultPtr := C.zen_evaluate_expression(expressionCString, contextCString)
26 | if resultPtr.error > 0 {
27 | var errorDetails string
28 | if resultPtr.details != nil {
29 | defer C.free(unsafe.Pointer(resultPtr.details))
30 | errorDetails = C.GoString(resultPtr.details)
31 | } else {
32 | errorDetails = fmt.Sprintf("Error code: %d", resultPtr.error)
33 | }
34 |
35 | var zero T
36 | return zero, errors.New(errorDetails)
37 | }
38 |
39 | defer C.free(unsafe.Pointer(resultPtr.result))
40 | resultJson := C.GoString(resultPtr.result)
41 |
42 | var result T
43 | if err := json.Unmarshal([]byte(resultJson), &result); err != nil {
44 | var zero T
45 | return zero, err
46 | }
47 |
48 | return result, nil
49 | }
50 |
51 | func EvaluateUnaryExpression(expression string, context any) (bool, error) {
52 | jsonData, err := extractJsonFromAny(context)
53 | if err != nil {
54 | return false, err
55 | }
56 |
57 | expressionCString := C.CString(expression)
58 | defer C.free(unsafe.Pointer(expressionCString))
59 |
60 | contextCString := C.CString(string(jsonData))
61 | defer C.free(unsafe.Pointer(contextCString))
62 |
63 | resultPtr := C.zen_evaluate_unary_expression(expressionCString, contextCString)
64 | if resultPtr.error > 0 {
65 | var errorDetails string
66 | if resultPtr.details != nil {
67 | defer C.free(unsafe.Pointer(resultPtr.details))
68 | errorDetails = C.GoString(resultPtr.details)
69 | } else {
70 | errorDetails = fmt.Sprintf("Error code: %d", resultPtr.error)
71 | }
72 |
73 | return false, errors.New(errorDetails)
74 | }
75 |
76 | isSuccess := int(*resultPtr.result)
77 | defer C.free(unsafe.Pointer(resultPtr.result))
78 |
79 | return isSuccess == 1, nil
80 | }
81 |
82 | func RenderTemplate[T any](template string, context any) (T, error) {
83 | jsonData, err := extractJsonFromAny(context)
84 | if err != nil {
85 | return *new(T), err
86 | }
87 |
88 | templateCString := C.CString(template)
89 | defer C.free(unsafe.Pointer(templateCString))
90 |
91 | contextCString := C.CString(string(jsonData))
92 | defer C.free(unsafe.Pointer(contextCString))
93 |
94 | resultPtr := C.zen_evaluate_template(templateCString, contextCString)
95 | if resultPtr.error > 0 {
96 | var errorDetails string
97 | if resultPtr.details != nil {
98 | defer C.free(unsafe.Pointer(resultPtr.details))
99 | errorDetails = C.GoString(resultPtr.details)
100 | } else {
101 | errorDetails = fmt.Sprintf("Error code: %d", resultPtr.error)
102 | }
103 |
104 | return *new(T), errors.New(errorDetails)
105 | }
106 |
107 | defer C.free(unsafe.Pointer(resultPtr.result))
108 | resultJson := C.GoString(resultPtr.result)
109 |
110 | var result T
111 | if err := json.Unmarshal([]byte(resultJson), &result); err != nil {
112 | return *new(T), err
113 | }
114 |
115 | return result, nil
116 | }
117 |
--------------------------------------------------------------------------------
/expression_test.go:
--------------------------------------------------------------------------------
1 | package zen
2 |
3 | import (
4 | "github.com/stretchr/testify/assert"
5 | "testing"
6 | )
7 |
8 | func TestEvaluateExpression(t *testing.T) {
9 | type TestCase[T any] struct {
10 | expression string
11 | output T
12 | context any
13 | }
14 |
15 | // Example usage with int
16 | intTestCases := []TestCase[int]{
17 | {expression: "1 + 1", output: 2},
18 | {expression: "2 + 2", output: 4},
19 | {expression: "10 + a", output: 14, context: map[string]int{"a": 4}},
20 | }
21 |
22 | // Example usage with string
23 | stringTestCases := []TestCase[string]{
24 | {expression: `"hello" + " " + "world"`, output: "hello world"},
25 | {expression: `"foo" + "bar"`, output: "foobar"},
26 | }
27 |
28 | for _, intTestCase := range intTestCases {
29 | res, err := EvaluateExpression[int](intTestCase.expression, intTestCase.context)
30 | assert.NoError(t, err)
31 | assert.Equal(t, intTestCase.output, res)
32 | }
33 |
34 | for _, stringTestCase := range stringTestCases {
35 | res, err := EvaluateExpression[string](stringTestCase.expression, stringTestCase.context)
36 | assert.NoError(t, err)
37 | assert.Equal(t, stringTestCase.output, res)
38 | }
39 | }
40 |
41 | func TestEvaluateUnaryExpression(t *testing.T) {
42 | type TestCase struct {
43 | expression string
44 | output bool
45 | context any
46 | }
47 |
48 | testCases := []TestCase{
49 | {
50 | expression: "> 10",
51 | output: false,
52 | context: map[string]any{"$": 5},
53 | },
54 | {
55 | expression: "> 10",
56 | output: true,
57 | context: map[string]any{"$": 15},
58 | },
59 | {
60 | expression: "'US', 'GB'",
61 | output: true,
62 | context: map[string]any{"$": "US"},
63 | },
64 | {
65 | expression: "'US', 'GB'",
66 | output: false,
67 | context: map[string]any{"$": "AA"},
68 | },
69 | }
70 |
71 | for _, testCase := range testCases {
72 | isTrue, err := EvaluateUnaryExpression(testCase.expression, testCase.context)
73 | assert.NoError(t, err)
74 | assert.Equal(t, testCase.output, isTrue)
75 | }
76 | }
77 |
78 | func TestRenderTemplate(t *testing.T) {
79 | type TestCase[T any] struct {
80 | template string
81 | output any
82 | context any
83 | }
84 |
85 | intTestCases := []TestCase[int]{
86 | {
87 | template: "{{ a + b }}",
88 | output: 15,
89 | context: map[string]any{"a": 5, "b": 10},
90 | },
91 | }
92 |
93 | stringTestCases := []TestCase[string]{
94 | {
95 | template: "Hello: {{ a + b }}",
96 | output: "Hello: 15",
97 | context: map[string]any{"a": 5, "b": 10},
98 | },
99 | }
100 |
101 | for _, testCase := range intTestCases {
102 | isTrue, err := RenderTemplate[int](testCase.template, testCase.context)
103 | assert.NoError(t, err)
104 | assert.Equal(t, testCase.output, isTrue)
105 | }
106 |
107 | for _, testCase := range stringTestCases {
108 | isTrue, err := RenderTemplate[string](testCase.template, testCase.context)
109 | assert.NoError(t, err)
110 | assert.Equal(t, testCase.output, isTrue)
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/gorules/zen-go
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/stretchr/testify v1.8.4
7 | github.com/tidwall/gjson v1.17.1
8 | )
9 |
10 | require (
11 | github.com/davecgh/go-spew v1.1.1 // indirect
12 | github.com/pmezard/go-difflib v1.0.0 // indirect
13 | github.com/tidwall/match v1.1.1 // indirect
14 | github.com/tidwall/pretty v1.2.0 // indirect
15 | gopkg.in/yaml.v3 v3.0.1 // indirect
16 | )
17 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
6 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
7 | github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
8 | github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
9 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
10 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
11 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
12 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
13 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
15 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
16 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
17 |
--------------------------------------------------------------------------------
/memory.go:
--------------------------------------------------------------------------------
1 | //go:build memory_test
2 | // +build memory_test
3 |
4 | package zen
5 |
6 | // #cgo CPPFLAGS: -fsanitize=address
7 | // #cgo LDFLAGS: -fsanitize=address
8 | //
9 | // #include
10 | import "C"
11 | import (
12 | "runtime"
13 | )
14 |
15 | // Call LLVM Leak Sanitizer's at-exit hook that doesn't
16 | // get called automatically by Go.
17 | func doLeakSanitizerCheck() {
18 | runtime.GC()
19 | C.__lsan_do_leak_check()
20 | }
21 |
--------------------------------------------------------------------------------
/memory_test.go:
--------------------------------------------------------------------------------
1 | //go:build memory_test
2 | // +build memory_test
3 |
4 | package zen
5 |
6 | import (
7 | "os"
8 | "testing"
9 | )
10 |
11 | func TestMain(m *testing.M) {
12 | exitCode := m.Run()
13 | doLeakSanitizerCheck()
14 | os.Exit(exitCode)
15 | }
16 |
--------------------------------------------------------------------------------
/test-data/custom-node.json:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": [
3 | {
4 | "id": "115975ef-2f43-4e22-b553-0da6f4cc7f68",
5 | "type": "inputNode",
6 | "position": {
7 | "x": 180,
8 | "y": 240
9 | },
10 | "name": "request"
11 | },
12 | {
13 | "id": "138b3b11-ff46-450f-9704-3f3c712067b2",
14 | "type": "customNode",
15 | "position": {
16 | "x": 470,
17 | "y": 240
18 | },
19 | "name": "customNode1",
20 | "content": {
21 | "kind": "sum",
22 | "config": {
23 | "a": "{{ a + 10 }}",
24 | "b": "{{ b + 5 }}",
25 | "key": "sum"
26 | }
27 | }
28 | },
29 | {
30 | "id": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e",
31 | "type": "outputNode",
32 | "position": {
33 | "x": 780,
34 | "y": 240
35 | },
36 | "name": "Response"
37 | }
38 | ],
39 | "edges": [
40 | {
41 | "id": "05740fa7-3755-4756-b85e-bc1af2f6773b",
42 | "sourceId": "115975ef-2f43-4e22-b553-0da6f4cc7f68",
43 | "type": "edge",
44 | "targetId": "138b3b11-ff46-450f-9704-3f3c712067b2"
45 | },
46 | {
47 | "id": "5d89c1d6-e894-4e8a-bd13-22368c2a6bc7",
48 | "sourceId": "138b3b11-ff46-450f-9704-3f3c712067b2",
49 | "type": "edge",
50 | "targetId": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e"
51 | }
52 | ]
53 | }
--------------------------------------------------------------------------------
/test-data/expression.json:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": [
3 | {
4 | "id": "115975ef-2f43-4e22-b553-0da6f4cc7f68",
5 | "type": "inputNode",
6 | "position": {
7 | "x": 180,
8 | "y": 240
9 | },
10 | "name": "Request"
11 | },
12 | {
13 | "id": "138b3b11-ff46-450f-9704-3f3c712067b2",
14 | "type": "expressionNode",
15 | "position": {
16 | "x": 470,
17 | "y": 240
18 | },
19 | "name": "expressionNode 1",
20 | "content": {
21 | "expressions": [
22 | {
23 | "id": "xWauegxfG7",
24 | "key": "deep.nested.sum",
25 | "value": "sum(numbers)"
26 | },
27 | {
28 | "id": "qGAHmak0xj",
29 | "key": "fullName",
30 | "value": "firstName + ' ' + lastName"
31 | },
32 | {
33 | "id": "5ZnYGPFT-N",
34 | "key": "largeNumbers",
35 | "value": "filter(numbers, # >= 10)"
36 | },
37 | {
38 | "id": "pSg-vIQR5Q",
39 | "key": "smallNumbers",
40 | "value": "filter(numbers, # < 10)"
41 | }
42 | ]
43 | }
44 | },
45 | {
46 | "id": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e",
47 | "type": "outputNode",
48 | "position": {
49 | "x": 780,
50 | "y": 240
51 | },
52 | "name": "Response"
53 | }
54 | ],
55 | "edges": [
56 | {
57 | "id": "05740fa7-3755-4756-b85e-bc1af2f6773b",
58 | "sourceId": "115975ef-2f43-4e22-b553-0da6f4cc7f68",
59 | "type": "edge",
60 | "targetId": "138b3b11-ff46-450f-9704-3f3c712067b2"
61 | },
62 | {
63 | "id": "5d89c1d6-e894-4e8a-bd13-22368c2a6bc7",
64 | "sourceId": "138b3b11-ff46-450f-9704-3f3c712067b2",
65 | "type": "edge",
66 | "targetId": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e"
67 | }
68 | ]
69 | }
--------------------------------------------------------------------------------
/test-data/function.json:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": [
3 | {
4 | "id": "115975ef-2f43-4e22-b553-0da6f4cc7f68",
5 | "type": "inputNode",
6 | "position": {
7 | "x": 180,
8 | "y": 240
9 | },
10 | "name": "Request"
11 | },
12 | {
13 | "id": "138b3b11-ff46-450f-9704-3f3c712067b2",
14 | "type": "functionNode",
15 | "position": {
16 | "x": 470,
17 | "y": 240
18 | },
19 | "name": "functionNode 1",
20 | "content": "/**\n* @param {import('gorules').Input} input\n* @param {{\n* moment: import('dayjs')\n* env: Record\n* }} helpers\n*/\nconst handler = (input, { moment, env }) => {\n return {\n output: input.input * 2,\n };\n}"
21 | },
22 | {
23 | "id": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e",
24 | "type": "outputNode",
25 | "position": {
26 | "x": 780,
27 | "y": 240
28 | },
29 | "name": "Response"
30 | }
31 | ],
32 | "edges": [
33 | {
34 | "id": "05740fa7-3755-4756-b85e-bc1af2f6773b",
35 | "sourceId": "115975ef-2f43-4e22-b553-0da6f4cc7f68",
36 | "type": "edge",
37 | "targetId": "138b3b11-ff46-450f-9704-3f3c712067b2"
38 | },
39 | {
40 | "id": "5d89c1d6-e894-4e8a-bd13-22368c2a6bc7",
41 | "sourceId": "138b3b11-ff46-450f-9704-3f3c712067b2",
42 | "type": "edge",
43 | "targetId": "db8797b1-bcc1-4fbf-a5d8-e7d43a181d5e"
44 | }
45 | ]
46 | }
--------------------------------------------------------------------------------
/test-data/large.json:
--------------------------------------------------------------------------------
1 | {
2 | "edges": [
3 | {
4 | "id": "316bbf87-527a-4297-9371-04bf64364d9f",
5 | "type": "edge",
6 | "sourceId": "44770982-a911-4ae5-a746-f45435342e2b",
7 | "targetId": "9320e3ae-feb1-4041-9c4a-5631d1e1ce78"
8 | },
9 | {
10 | "id": "cb14a39e-8afc-499a-954b-37d885643d8a",
11 | "type": "edge",
12 | "sourceId": "9320e3ae-feb1-4041-9c4a-5631d1e1ce78",
13 | "targetId": "76d5310f-2c8f-4b8d-861b-cf1028d7a7c1"
14 | }
15 | ],
16 | "nodes": [
17 | {
18 | "id": "44770982-a911-4ae5-a746-f45435342e2b",
19 | "name": "Request",
20 | "type": "inputNode",
21 | "position": {
22 | "x": 150,
23 | "y": 330
24 | }
25 | },
26 | {
27 | "id": "9320e3ae-feb1-4041-9c4a-5631d1e1ce78",
28 | "name": "decisionTableNode 1",
29 | "type": "decisionTableNode",
30 | "content": {
31 | "rules": [
32 | {
33 | "_id": "5wI_skcnvo",
34 | "AA6l-cCjsx": "'GB'",
35 | "BJRjlmVMsK": "",
36 | "HRNNd8No8t": "'GBP'",
37 | "MekLi0Q_EA": "< 100",
38 | "SGs208ovNq": "endsWith($, '@gmail.com')",
39 | "Zic2WQhwse": "< 100",
40 | "oq14hGDO21": "product.currency",
41 | "tVwBYJCB4e": "5"
42 | },
43 | {
44 | "_id": "QoAQvkWVYQ",
45 | "AA6l-cCjsx": "'GB'",
46 | "BJRjlmVMsK": "",
47 | "HRNNd8No8t": "'GBP'",
48 | "MekLi0Q_EA": "< 100",
49 | "SGs208ovNq": "endsWith($, '@gmail.com')",
50 | "Zic2WQhwse": "< 100",
51 | "oq14hGDO21": "product.currency",
52 | "tVwBYJCB4e": "5"
53 | },
54 | {
55 | "_id": "d9SeCnBzmz",
56 | "AA6l-cCjsx": "'GB'",
57 | "BJRjlmVMsK": "",
58 | "HRNNd8No8t": "'GBP'",
59 | "MekLi0Q_EA": "< 100",
60 | "SGs208ovNq": "endsWith($, '@gmail.com')",
61 | "Zic2WQhwse": "< 100",
62 | "oq14hGDO21": "product.currency",
63 | "tVwBYJCB4e": "5"
64 | },
65 | {
66 | "_id": "tY0gpnE1VG",
67 | "AA6l-cCjsx": "'GB'",
68 | "BJRjlmVMsK": "",
69 | "HRNNd8No8t": "'GBP'",
70 | "MekLi0Q_EA": "< 100",
71 | "SGs208ovNq": "endsWith($, '@gmail.com')",
72 | "Zic2WQhwse": "< 100",
73 | "oq14hGDO21": "product.currency",
74 | "tVwBYJCB4e": "5"
75 | },
76 | {
77 | "_id": "RP_fp350yF",
78 | "AA6l-cCjsx": "'GB'",
79 | "BJRjlmVMsK": "",
80 | "HRNNd8No8t": "'GBP'",
81 | "MekLi0Q_EA": "< 100",
82 | "SGs208ovNq": "endsWith($, '@gmail.com')",
83 | "Zic2WQhwse": "< 100",
84 | "oq14hGDO21": "product.currency",
85 | "tVwBYJCB4e": "5"
86 | },
87 | {
88 | "_id": "3V-X9VWiL7",
89 | "AA6l-cCjsx": "'GB'",
90 | "BJRjlmVMsK": "",
91 | "HRNNd8No8t": "'GBP'",
92 | "MekLi0Q_EA": "< 100",
93 | "SGs208ovNq": "endsWith($, '@gmail.com')",
94 | "Zic2WQhwse": "< 100",
95 | "oq14hGDO21": "product.currency",
96 | "tVwBYJCB4e": "5"
97 | },
98 | {
99 | "_id": "5PE7bZJSPt",
100 | "AA6l-cCjsx": "'GB'",
101 | "BJRjlmVMsK": "",
102 | "HRNNd8No8t": "'GBP'",
103 | "MekLi0Q_EA": "< 100",
104 | "SGs208ovNq": "endsWith($, '@gmail.com')",
105 | "Zic2WQhwse": "< 100",
106 | "oq14hGDO21": "product.currency",
107 | "tVwBYJCB4e": "5"
108 | },
109 | {
110 | "_id": "ajvI5fJT0g",
111 | "AA6l-cCjsx": "'GB'",
112 | "BJRjlmVMsK": "",
113 | "HRNNd8No8t": "'GBP'",
114 | "MekLi0Q_EA": "< 100",
115 | "SGs208ovNq": "endsWith($, '@gmail.com')",
116 | "Zic2WQhwse": "< 100",
117 | "oq14hGDO21": "product.currency",
118 | "tVwBYJCB4e": "5"
119 | },
120 | {
121 | "_id": "bLqQ42Yddu",
122 | "AA6l-cCjsx": "'GB'",
123 | "BJRjlmVMsK": "",
124 | "HRNNd8No8t": "'GBP'",
125 | "MekLi0Q_EA": "< 100",
126 | "SGs208ovNq": "endsWith($, '@gmail.com')",
127 | "Zic2WQhwse": "< 100",
128 | "oq14hGDO21": "product.currency",
129 | "tVwBYJCB4e": "5"
130 | },
131 | {
132 | "_id": "uvDbNlDiTZ",
133 | "AA6l-cCjsx": "'GB'",
134 | "BJRjlmVMsK": "",
135 | "HRNNd8No8t": "'GBP'",
136 | "MekLi0Q_EA": "< 100",
137 | "SGs208ovNq": "endsWith($, '@gmail.com')",
138 | "Zic2WQhwse": "< 100",
139 | "oq14hGDO21": "product.currency",
140 | "tVwBYJCB4e": "5"
141 | },
142 | {
143 | "_id": "gt5-0tO9bI",
144 | "AA6l-cCjsx": "'GB'",
145 | "BJRjlmVMsK": "",
146 | "HRNNd8No8t": "'GBP'",
147 | "MekLi0Q_EA": "< 100",
148 | "SGs208ovNq": "endsWith($, '@gmail.com')",
149 | "Zic2WQhwse": "< 100",
150 | "oq14hGDO21": "product.currency",
151 | "tVwBYJCB4e": "5"
152 | },
153 | {
154 | "_id": "rNJpoYXu2R",
155 | "AA6l-cCjsx": "'GB'",
156 | "BJRjlmVMsK": "",
157 | "HRNNd8No8t": "'GBP'",
158 | "MekLi0Q_EA": "< 100",
159 | "SGs208ovNq": "endsWith($, '@gmail.com')",
160 | "Zic2WQhwse": "< 100",
161 | "oq14hGDO21": "product.currency",
162 | "tVwBYJCB4e": "5"
163 | },
164 | {
165 | "_id": "g35xvK0Uhp",
166 | "AA6l-cCjsx": "'GB'",
167 | "BJRjlmVMsK": "",
168 | "HRNNd8No8t": "'GBP'",
169 | "MekLi0Q_EA": "< 100",
170 | "SGs208ovNq": "endsWith($, '@gmail.com')",
171 | "Zic2WQhwse": "< 100",
172 | "oq14hGDO21": "product.currency",
173 | "tVwBYJCB4e": "5"
174 | },
175 | {
176 | "_id": "fyow0Ij3yd",
177 | "AA6l-cCjsx": "'GB'",
178 | "BJRjlmVMsK": "",
179 | "HRNNd8No8t": "'GBP'",
180 | "MekLi0Q_EA": "< 100",
181 | "SGs208ovNq": "endsWith($, '@gmail.com')",
182 | "Zic2WQhwse": "< 100",
183 | "oq14hGDO21": "product.currency",
184 | "tVwBYJCB4e": "5"
185 | },
186 | {
187 | "_id": "0R65tDCWe5",
188 | "AA6l-cCjsx": "'GB'",
189 | "BJRjlmVMsK": "",
190 | "HRNNd8No8t": "'GBP'",
191 | "MekLi0Q_EA": "< 100",
192 | "SGs208ovNq": "endsWith($, '@gmail.com')",
193 | "Zic2WQhwse": "< 100",
194 | "oq14hGDO21": "product.currency",
195 | "tVwBYJCB4e": "5"
196 | },
197 | {
198 | "_id": "-stykUqove",
199 | "AA6l-cCjsx": "'GB'",
200 | "BJRjlmVMsK": "",
201 | "HRNNd8No8t": "'GBP'",
202 | "MekLi0Q_EA": "< 100",
203 | "SGs208ovNq": "endsWith($, '@gmail.com')",
204 | "Zic2WQhwse": "< 100",
205 | "oq14hGDO21": "product.currency",
206 | "tVwBYJCB4e": "5"
207 | },
208 | {
209 | "_id": "6ZXlEG-S3N",
210 | "AA6l-cCjsx": "'GB'",
211 | "BJRjlmVMsK": "",
212 | "HRNNd8No8t": "'GBP'",
213 | "MekLi0Q_EA": "< 100",
214 | "SGs208ovNq": "endsWith($, '@gmail.com')",
215 | "Zic2WQhwse": "< 100",
216 | "oq14hGDO21": "product.currency",
217 | "tVwBYJCB4e": "5"
218 | },
219 | {
220 | "_id": "0t7AO9B3LR",
221 | "AA6l-cCjsx": "'GB'",
222 | "BJRjlmVMsK": "",
223 | "HRNNd8No8t": "'GBP'",
224 | "MekLi0Q_EA": "< 100",
225 | "SGs208ovNq": "endsWith($, '@gmail.com')",
226 | "Zic2WQhwse": "< 100",
227 | "oq14hGDO21": "product.currency",
228 | "tVwBYJCB4e": "5"
229 | },
230 | {
231 | "_id": "Dhzs9dGLYZ",
232 | "AA6l-cCjsx": "'GB'",
233 | "BJRjlmVMsK": "",
234 | "HRNNd8No8t": "'GBP'",
235 | "MekLi0Q_EA": "< 100",
236 | "SGs208ovNq": "endsWith($, '@gmail.com')",
237 | "Zic2WQhwse": "< 100",
238 | "oq14hGDO21": "product.currency",
239 | "tVwBYJCB4e": "5"
240 | },
241 | {
242 | "_id": "V4ojUU5GdQ",
243 | "AA6l-cCjsx": "'GB'",
244 | "BJRjlmVMsK": "",
245 | "HRNNd8No8t": "'GBP'",
246 | "MekLi0Q_EA": "< 100",
247 | "SGs208ovNq": "endsWith($, '@gmail.com')",
248 | "Zic2WQhwse": "< 100",
249 | "oq14hGDO21": "product.currency",
250 | "tVwBYJCB4e": "5"
251 | },
252 | {
253 | "_id": "UadZo_7DCs",
254 | "AA6l-cCjsx": "'GB'",
255 | "BJRjlmVMsK": "",
256 | "HRNNd8No8t": "'GBP'",
257 | "MekLi0Q_EA": "< 100",
258 | "SGs208ovNq": "endsWith($, '@gmail.com')",
259 | "Zic2WQhwse": "< 100",
260 | "oq14hGDO21": "product.currency",
261 | "tVwBYJCB4e": "5"
262 | },
263 | {
264 | "_id": "elkUNNGyDi",
265 | "AA6l-cCjsx": "'GB'",
266 | "BJRjlmVMsK": "",
267 | "HRNNd8No8t": "'GBP'",
268 | "MekLi0Q_EA": "< 100",
269 | "SGs208ovNq": "endsWith($, '@gmail.com')",
270 | "Zic2WQhwse": "< 100",
271 | "oq14hGDO21": "product.currency",
272 | "tVwBYJCB4e": "5"
273 | },
274 | {
275 | "_id": "Zkn-v2mtFg",
276 | "AA6l-cCjsx": "'GB'",
277 | "BJRjlmVMsK": "",
278 | "HRNNd8No8t": "'GBP'",
279 | "MekLi0Q_EA": "< 100",
280 | "SGs208ovNq": "endsWith($, '@gmail.com')",
281 | "Zic2WQhwse": "< 100",
282 | "oq14hGDO21": "product.currency",
283 | "tVwBYJCB4e": "5"
284 | },
285 | {
286 | "_id": "4x8vb1SCKd",
287 | "AA6l-cCjsx": "'GB'",
288 | "BJRjlmVMsK": "",
289 | "HRNNd8No8t": "'GBP'",
290 | "MekLi0Q_EA": "< 100",
291 | "SGs208ovNq": "endsWith($, '@gmail.com')",
292 | "Zic2WQhwse": "< 100",
293 | "oq14hGDO21": "product.currency",
294 | "tVwBYJCB4e": "5"
295 | },
296 | {
297 | "_id": "riBDyjeC0U",
298 | "AA6l-cCjsx": "'GB'",
299 | "BJRjlmVMsK": "",
300 | "HRNNd8No8t": "'GBP'",
301 | "MekLi0Q_EA": "< 100",
302 | "SGs208ovNq": "endsWith($, '@gmail.com')",
303 | "Zic2WQhwse": "< 100",
304 | "oq14hGDO21": "product.currency",
305 | "tVwBYJCB4e": "5"
306 | },
307 | {
308 | "_id": "IqeW0MmNBT",
309 | "AA6l-cCjsx": "'GB'",
310 | "BJRjlmVMsK": "",
311 | "HRNNd8No8t": "'GBP'",
312 | "MekLi0Q_EA": "< 100",
313 | "SGs208ovNq": "endsWith($, '@gmail.com')",
314 | "Zic2WQhwse": "< 100",
315 | "oq14hGDO21": "product.currency",
316 | "tVwBYJCB4e": "5"
317 | },
318 | {
319 | "_id": "VAVScfCfJS",
320 | "AA6l-cCjsx": "'GB'",
321 | "BJRjlmVMsK": "",
322 | "HRNNd8No8t": "'GBP'",
323 | "MekLi0Q_EA": "< 100",
324 | "SGs208ovNq": "endsWith($, '@gmail.com')",
325 | "Zic2WQhwse": "< 100",
326 | "oq14hGDO21": "product.currency",
327 | "tVwBYJCB4e": "5"
328 | },
329 | {
330 | "_id": "YRdxvrLOQE",
331 | "AA6l-cCjsx": "'GB'",
332 | "BJRjlmVMsK": "",
333 | "HRNNd8No8t": "'GBP'",
334 | "MekLi0Q_EA": "< 100",
335 | "SGs208ovNq": "endsWith($, '@gmail.com')",
336 | "Zic2WQhwse": "< 100",
337 | "oq14hGDO21": "product.currency",
338 | "tVwBYJCB4e": "5"
339 | },
340 | {
341 | "_id": "GwPV_T1fSG",
342 | "AA6l-cCjsx": "'GB'",
343 | "BJRjlmVMsK": "",
344 | "HRNNd8No8t": "'GBP'",
345 | "MekLi0Q_EA": "< 100",
346 | "SGs208ovNq": "endsWith($, '@gmail.com')",
347 | "Zic2WQhwse": "< 100",
348 | "oq14hGDO21": "product.currency",
349 | "tVwBYJCB4e": "5"
350 | },
351 | {
352 | "_id": "YmzBD59L_v",
353 | "AA6l-cCjsx": "'GB'",
354 | "BJRjlmVMsK": "",
355 | "HRNNd8No8t": "'GBP'",
356 | "MekLi0Q_EA": "< 100",
357 | "SGs208ovNq": "endsWith($, '@gmail.com')",
358 | "Zic2WQhwse": "< 100",
359 | "oq14hGDO21": "product.currency",
360 | "tVwBYJCB4e": "5"
361 | },
362 | {
363 | "_id": "ZGNdGFMlhY",
364 | "AA6l-cCjsx": "'GB'",
365 | "BJRjlmVMsK": "",
366 | "HRNNd8No8t": "'GBP'",
367 | "MekLi0Q_EA": "< 100",
368 | "SGs208ovNq": "endsWith($, '@gmail.com')",
369 | "Zic2WQhwse": "< 100",
370 | "oq14hGDO21": "product.currency",
371 | "tVwBYJCB4e": "5"
372 | },
373 | {
374 | "_id": "i8zpEDkFo6",
375 | "AA6l-cCjsx": "'GB'",
376 | "BJRjlmVMsK": "",
377 | "HRNNd8No8t": "'GBP'",
378 | "MekLi0Q_EA": "< 100",
379 | "SGs208ovNq": "endsWith($, '@gmail.com')",
380 | "Zic2WQhwse": "< 100",
381 | "oq14hGDO21": "product.currency",
382 | "tVwBYJCB4e": "5"
383 | },
384 | {
385 | "_id": "z61HE9JFJQ",
386 | "AA6l-cCjsx": "'GB'",
387 | "BJRjlmVMsK": "",
388 | "HRNNd8No8t": "'GBP'",
389 | "MekLi0Q_EA": "< 100",
390 | "SGs208ovNq": "endsWith($, '@gmail.com')",
391 | "Zic2WQhwse": "< 100",
392 | "oq14hGDO21": "product.currency",
393 | "tVwBYJCB4e": "5"
394 | },
395 | {
396 | "_id": "kDfPGlAkGQ",
397 | "AA6l-cCjsx": "",
398 | "BJRjlmVMsK": "",
399 | "HRNNd8No8t": "",
400 | "MekLi0Q_EA": "",
401 | "SGs208ovNq": "",
402 | "Zic2WQhwse": "",
403 | "oq14hGDO21": "\"USD\"",
404 | "tVwBYJCB4e": "10"
405 | }
406 | ],
407 | "inputs": [
408 | {
409 | "id": "AA6l-cCjsx",
410 | "name": "Customer country",
411 | "type": "expression",
412 | "field": "customer.country"
413 | },
414 | {
415 | "id": "MekLi0Q_EA",
416 | "name": "Total spend",
417 | "type": "expression",
418 | "field": "customer.totalSpend"
419 | },
420 | {
421 | "id": "SGs208ovNq",
422 | "name": "Email",
423 | "type": "expression",
424 | "field": "customer.email"
425 | },
426 | {
427 | "id": "BJRjlmVMsK",
428 | "name": "Category",
429 | "type": "expression",
430 | "field": "product.category"
431 | },
432 | {
433 | "id": "HRNNd8No8t",
434 | "name": "Currency",
435 | "type": "expression",
436 | "field": "product.currency"
437 | },
438 | {
439 | "id": "Zic2WQhwse",
440 | "name": "Price",
441 | "type": "expression",
442 | "field": "product.price"
443 | }
444 | ],
445 | "outputs": [
446 | {
447 | "id": "tVwBYJCB4e",
448 | "name": "Shipping price",
449 | "type": "expression",
450 | "field": "shipping.price"
451 | },
452 | {
453 | "id": "oq14hGDO21",
454 | "name": "Shipping currency",
455 | "type": "expression",
456 | "field": "shipping.currency"
457 | }
458 | ],
459 | "hitPolicy": "first"
460 | },
461 | "position": {
462 | "x": 390,
463 | "y": 330
464 | }
465 | },
466 | {
467 | "id": "76d5310f-2c8f-4b8d-861b-cf1028d7a7c1",
468 | "name": "Response",
469 | "type": "outputNode",
470 | "position": {
471 | "x": 630,
472 | "y": 330
473 | }
474 | }
475 | ]
476 | }
--------------------------------------------------------------------------------
/test-data/table.json:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": [
3 | {
4 | "id": "3e3f5093-c969-4c3a-97e1-560e4b769a12",
5 | "type": "inputNode",
6 | "position": {
7 | "x": 150,
8 | "y": 210
9 | },
10 | "name": "Request"
11 | },
12 | {
13 | "id": "0624d5fd-1944-4781-92bb-e32873ce91e2",
14 | "type": "decisionTableNode",
15 | "position": {
16 | "x": 410,
17 | "y": 210
18 | },
19 | "name": "Hello",
20 | "content": {
21 | "hitPolicy": "first",
22 | "inputs": [
23 | {
24 | "field": "input",
25 | "id": "xWauegxfG7",
26 | "name": "Input",
27 | "type": "expression"
28 | }
29 | ],
30 | "outputs": [
31 | {
32 | "field": "output",
33 | "id": "qGAHmak0xj",
34 | "name": "Output",
35 | "type": "expression"
36 | }
37 | ],
38 | "rules": [
39 | {
40 | "_id": "5ZnYGPFT-N",
41 | "xWauegxfG7": "> 10",
42 | "qGAHmak0xj": "10"
43 | },
44 | {
45 | "_id": "pSg-vIQR5Q",
46 | "xWauegxfG7": "",
47 | "qGAHmak0xj": "0"
48 | }
49 | ]
50 | }
51 | },
52 | {
53 | "id": "e0438c6b-dee0-405e-a941-9b4c3d9c4b83",
54 | "type": "outputNode",
55 | "position": {
56 | "x": 660,
57 | "y": 210
58 | },
59 | "name": "Response"
60 | }
61 | ],
62 | "edges": [
63 | {
64 | "id": "c30b9bfd-2da6-445f-a31a-31eeb4bfa803",
65 | "sourceId": "3e3f5093-c969-4c3a-97e1-560e4b769a12",
66 | "type": "edge",
67 | "targetId": "0624d5fd-1944-4781-92bb-e32873ce91e2"
68 | },
69 | {
70 | "id": "dbda85da-4c1d-4e0b-b4e7-9bb475bd00b9",
71 | "sourceId": "0624d5fd-1944-4781-92bb-e32873ce91e2",
72 | "type": "edge",
73 | "targetId": "e0438c6b-dee0-405e-a941-9b4c3d9c4b83"
74 | }
75 | ]
76 | }
--------------------------------------------------------------------------------
/util.go:
--------------------------------------------------------------------------------
1 | package zen
2 |
3 | import "encoding/json"
4 |
5 | func extractJsonFromAny(data any) (json.RawMessage, error) {
6 | if d, ok := data.([]byte); ok {
7 | return d, nil
8 | }
9 |
10 | return json.Marshal(data)
11 | }
12 |
--------------------------------------------------------------------------------
/zen.go:
--------------------------------------------------------------------------------
1 | package zen
2 |
3 | import "encoding/json"
4 |
5 | type EvaluationOptions struct {
6 | Trace bool `json:"trace"`
7 | MaxDepth uint8 `json:"maxDepth"`
8 | }
9 |
10 | type EvaluationResponse struct {
11 | Performance string `json:"performance"`
12 | Result json.RawMessage `json:"result"`
13 | Trace *json.RawMessage `json:"trace"`
14 | }
15 |
16 | type Engine interface {
17 | Evaluate(key string, context any) (*EvaluationResponse, error)
18 | EvaluateWithOpts(key string, context any, options EvaluationOptions) (*EvaluationResponse, error)
19 | GetDecision(key string) (Decision, error)
20 | CreateDecision(data []byte) (Decision, error)
21 | Dispose()
22 | }
23 |
24 | type Decision interface {
25 | Evaluate(context any) (*EvaluationResponse, error)
26 | EvaluateWithOpts(context any, options EvaluationOptions) (*EvaluationResponse, error)
27 | Dispose()
28 | }
29 |
--------------------------------------------------------------------------------
/zen_engine.h:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 | #include
4 | #include
5 |
6 | typedef struct ZenDecisionStruct {
7 | uint8_t _data[0];
8 | } ZenDecisionStruct;
9 |
10 | /**
11 | * CResult can be seen as Either. It cannot, and should not, be initialized
12 | * manually. Instead, use error or ok functions for initialisation.
13 | */
14 | typedef struct ZenResult_c_char {
15 | char *result;
16 | uint8_t error;
17 | char *details;
18 | } ZenResult_c_char;
19 |
20 | typedef struct ZenEngineEvaluationOptions {
21 | bool trace;
22 | uint8_t max_depth;
23 | } ZenEngineEvaluationOptions;
24 |
25 | typedef struct ZenEngineStruct {
26 | uint8_t _data[0];
27 | } ZenEngineStruct;
28 |
29 | /**
30 | * CResult can be seen as Either. It cannot, and should not, be initialized
31 | * manually. Instead, use error or ok functions for initialisation.
32 | */
33 | typedef struct ZenResult_ZenDecisionStruct {
34 | struct ZenDecisionStruct *result;
35 | uint8_t error;
36 | char *details;
37 | } ZenResult_ZenDecisionStruct;
38 |
39 | /**
40 | * CResult can be seen as Either. It cannot, and should not, be initialized
41 | * manually. Instead, use error or ok functions for initialisation.
42 | */
43 | typedef struct ZenResult_c_int {
44 | int *result;
45 | uint8_t error;
46 | char *details;
47 | } ZenResult_c_int;
48 |
49 | typedef struct ZenDecisionLoaderResult {
50 | char *content;
51 | char *error;
52 | } ZenDecisionLoaderResult;
53 |
54 | typedef struct ZenDecisionLoaderResult (*ZenDecisionLoaderNativeCallback)(const char *key);
55 |
56 | typedef struct ZenCustomNodeResult {
57 | char *content;
58 | char *error;
59 | } ZenCustomNodeResult;
60 |
61 | typedef struct ZenCustomNodeResult (*ZenCustomNodeNativeCallback)(const char *request);
62 |
63 | /**
64 | * Frees ZenDecision
65 | */
66 | void zen_decision_free(struct ZenDecisionStruct *decision);
67 |
68 | /**
69 | * Evaluates ZenDecision
70 | * Caller is responsible for freeing context and ZenResult.
71 | */
72 | struct ZenResult_c_char zen_decision_evaluate(const struct ZenDecisionStruct *decision,
73 | const char *context_ptr,
74 | struct ZenEngineEvaluationOptions options);
75 |
76 | /**
77 | * Create a new ZenEngine instance, caller is responsible for freeing the returned reference
78 | * by calling zen_engine_free.
79 | */
80 | struct ZenEngineStruct *zen_engine_new(void);
81 |
82 | /**
83 | * Frees the ZenEngine instance reference from the memory
84 | */
85 | void zen_engine_free(struct ZenEngineStruct *engine);
86 |
87 | /**
88 | * Creates a Decision using a reference of DecisionEngine and content (JSON)
89 | * Caller is responsible for freeing content and ZenResult.
90 | */
91 | struct ZenResult_ZenDecisionStruct zen_engine_create_decision(const struct ZenEngineStruct *engine,
92 | const char *content);
93 |
94 | /**
95 | * Evaluates rules engine using a DecisionEngine reference via loader
96 | * Caller is responsible for freeing: key, context and ZenResult.
97 | */
98 | struct ZenResult_c_char zen_engine_evaluate(const struct ZenEngineStruct *engine,
99 | const char *key,
100 | const char *context,
101 | struct ZenEngineEvaluationOptions options);
102 |
103 | /**
104 | * Loads a Decision through DecisionEngine
105 | * Caller is responsible for freeing: key and ZenResult.
106 | */
107 | struct ZenResult_ZenDecisionStruct zen_engine_get_decision(const struct ZenEngineStruct *engine,
108 | const char *key);
109 |
110 | struct ZenResult_c_char zen_evaluate_expression(const char *expression, const char *context);
111 |
112 | /**
113 | * Evaluate unary expression, responsible for freeing expression and context
114 | * True = 1
115 | * False = 0
116 | */
117 | struct ZenResult_c_int zen_evaluate_unary_expression(const char *expression, const char *context);
118 |
119 | /**
120 | * Evaluate unary expression, responsible for freeing expression and context
121 | * True = 1
122 | * False = 0
123 | */
124 | struct ZenResult_c_char zen_evaluate_template(const char *template_, const char *context);
125 |
126 | /**
127 | * Creates a new ZenEngine instance with loader, caller is responsible for freeing the returned reference
128 | * by calling zen_engine_free.
129 | */
130 | struct ZenEngineStruct *zen_engine_new_native(ZenDecisionLoaderNativeCallback loader_callback,
131 | ZenCustomNodeNativeCallback custom_node_callback);
132 |
133 | /**
134 | * Creates a DecisionEngine for using GoLang handler (optional). Caller is responsible for freeing DecisionEngine.
135 | */
136 | struct ZenEngineStruct *zen_engine_new_golang(const uintptr_t *maybe_loader,
137 | const uintptr_t *maybe_custom_node);
138 |
--------------------------------------------------------------------------------