├── .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 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](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 | Open-Source Rules Engine 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 | Decision Table 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 | Decision Table Unary Test 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 | Decision Table Expression 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 | Decision Table Output 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 | Switch / Branching 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 | Decision Table 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 | --------------------------------------------------------------------------------