janet_sandbox 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +316 -0
- data/examples/dsls/form_helpers.janet +90 -0
- data/examples/dsls/math_helpers.janet +71 -0
- data/examples/dsls/validation_helpers.janet +86 -0
- data/examples/initializers/janet_sandbox.rb +74 -0
- data/lib/janet_sandbox/builtin_dsls.rb +38 -0
- data/lib/janet_sandbox/configuration.rb +158 -0
- data/lib/janet_sandbox/engine.rb +55 -0
- data/lib/janet_sandbox/errors.rb +42 -0
- data/lib/janet_sandbox/rails/railtie.rb +37 -0
- data/lib/janet_sandbox/rails/script_concern.rb +78 -0
- data/lib/janet_sandbox/result.rb +38 -0
- data/lib/janet_sandbox/sandbox.rb +202 -0
- data/lib/janet_sandbox/version.rb +5 -0
- data/lib/janet_sandbox.rb +114 -0
- data/wasm/janet.wasm +0 -0
- metadata +126 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 37d030736271dabc94ac931ad2cd3af8c5cfc1ccf8570b47cba5ab1c5c9c3907
|
|
4
|
+
data.tar.gz: fbd1407297ff9a11c2c561e262c368d404363c6097e936e76940d6b5dff6ec4f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4fcf025e8c2f3ae4b40db1e459d15ffa6e07188a758eb0c98c9254ad043aef99aa406b4e7539b521afd2cd221550fd5cfbba85d14eceee2226a2de67be7424fc
|
|
7
|
+
data.tar.gz: c2b3a8624af767fbeff5bf457008337b999b6a1c0a542d22f3a85ceac8bdc3a6cec762de22360ca4cc66026652a3a59241289e7d6fa9050c55d73cf0f3220ac9
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Your Name
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# JanetSandbox
|
|
2
|
+
|
|
3
|
+
Safely embed [Janet](https://janet-lang.org) scripting in your Ruby application via WebAssembly. Execute user-authored scripts with configurable sandboxing, resource limits, and pluggable DSL modules.
|
|
4
|
+
|
|
5
|
+
Note that janet.c in `wasm/build/janet` is pulled from the [Janet Repo](https://github.com/janet-lang/janet). The JSON module (`wasm/json.c`) is pulled from [spork](https://github.com/janet-lang/spork) to provide `json/encode` and `json/decode` for Ruby/Janet interop.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Secure sandboxing** via WebAssembly isolation
|
|
10
|
+
- **Resource limits** - fuel-based execution limits prevent infinite loops
|
|
11
|
+
- **Memory caps** - bounded WASM linear memory
|
|
12
|
+
- **Pluggable DSLs** - register domain-specific Janet functions for your use case
|
|
13
|
+
- **Rails integration** - optional Railtie and ActiveRecord concern
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add to your Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem 'janet_sandbox'
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The gem ships with a prebuilt `janet.wasm` binary, so it works out of the box.
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
require 'janet_sandbox'
|
|
29
|
+
|
|
30
|
+
# Simple evaluation
|
|
31
|
+
result = JanetSandbox.evaluate(
|
|
32
|
+
source: '(+ 1 2 3)',
|
|
33
|
+
context: {}
|
|
34
|
+
)
|
|
35
|
+
result.raw #=> 6
|
|
36
|
+
|
|
37
|
+
# With context
|
|
38
|
+
result = JanetSandbox.evaluate(
|
|
39
|
+
source: '(* (get ctx :quantity) (get ctx :price))',
|
|
40
|
+
context: { quantity: 5, price: 10 }
|
|
41
|
+
)
|
|
42
|
+
result.raw #=> 50
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Configurable DSLs
|
|
46
|
+
|
|
47
|
+
Unlike traditional embedded scripting, JanetSandbox doesn't impose a fixed DSL. Instead, you register your own domain-specific functions:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
JanetSandbox.configure do |config|
|
|
51
|
+
# Register a DSL module
|
|
52
|
+
config.register_dsl(:form_helpers, <<~'JANET')
|
|
53
|
+
(defn get-field [name]
|
|
54
|
+
(get-in ctx [:values (keyword name)]))
|
|
55
|
+
|
|
56
|
+
(defn show [field]
|
|
57
|
+
{:action :show :field (keyword field)})
|
|
58
|
+
|
|
59
|
+
(defn hide [field]
|
|
60
|
+
{:action :hide :field (keyword field)})
|
|
61
|
+
JANET
|
|
62
|
+
|
|
63
|
+
# Enable it by default
|
|
64
|
+
config.enable_dsl(:form_helpers)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Now use it
|
|
68
|
+
result = JanetSandbox.evaluate(
|
|
69
|
+
source: '(if (= (get-field "role") "admin") (show "budget") (hide "budget"))',
|
|
70
|
+
context: { values: { role: "admin" } }
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Per-Evaluation DSL Override
|
|
75
|
+
|
|
76
|
+
```ruby
|
|
77
|
+
# Use specific DSLs for this evaluation
|
|
78
|
+
result = JanetSandbox.evaluate(
|
|
79
|
+
source: code,
|
|
80
|
+
context: ctx,
|
|
81
|
+
dsls: [:form_helpers, :validation_helpers]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Or disable all DSLs
|
|
85
|
+
result = JanetSandbox.evaluate(
|
|
86
|
+
source: '(+ 1 2)',
|
|
87
|
+
context: {},
|
|
88
|
+
dsls: []
|
|
89
|
+
)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Example DSLs
|
|
93
|
+
|
|
94
|
+
The gem includes example DSLs in `examples/dsls/`:
|
|
95
|
+
|
|
96
|
+
- **form_helpers.janet** - Form visibility, validation, computed fields
|
|
97
|
+
- **validation_helpers.janet** - Email, phone, length validators
|
|
98
|
+
- **math_helpers.janet** - Safe arithmetic, tax/discount calculations
|
|
99
|
+
|
|
100
|
+
Copy these to your project and customize as needed.
|
|
101
|
+
|
|
102
|
+
## Configuration
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
JanetSandbox.configure do |config|
|
|
106
|
+
# Execution limits (apply only to user code, not setup/DSL loading)
|
|
107
|
+
config.fuel_limit = 10_000_000 # Instruction budget for user code
|
|
108
|
+
config.epoch_interval = 100 # Wall-clock check interval (ms)
|
|
109
|
+
config.epoch_deadline = 10 # Max intervals (~1 second timeout)
|
|
110
|
+
config.max_result_size = 64 * 1024 # 64KB max JSON output
|
|
111
|
+
|
|
112
|
+
# Path to WASM binary
|
|
113
|
+
config.wasm_path = '/path/to/janet.wasm'
|
|
114
|
+
|
|
115
|
+
# Block additional symbols
|
|
116
|
+
config.blocked_symbols << 'some/dangerous-fn'
|
|
117
|
+
|
|
118
|
+
# Register DSLs
|
|
119
|
+
config.register_dsl(:my_dsl, janet_source, description: "My custom DSL")
|
|
120
|
+
config.enable_dsl(:my_dsl)
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Execution Limits
|
|
125
|
+
|
|
126
|
+
JanetSandbox uses two complementary limits:
|
|
127
|
+
|
|
128
|
+
- **Fuel** (instruction counting) - Catches CPU-intensive computation
|
|
129
|
+
- **Epochs** (wall-clock time) - Catches blocking operations like sleep
|
|
130
|
+
|
|
131
|
+
Set `epoch_interval` to `nil` to disable wall-clock timeouts.
|
|
132
|
+
|
|
133
|
+
## Result Object
|
|
134
|
+
|
|
135
|
+
Evaluations return a `JanetSandbox::Result`:
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
result = JanetSandbox.evaluate(
|
|
139
|
+
source: '{:status "ok" :total 150 :items [{:name "A"} {:name "B"}]}',
|
|
140
|
+
context: {}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
result.raw #=> {status: "ok", total: 150, items: [{name: "A"}, {name: "B"}]}
|
|
144
|
+
result[:status] #=> "ok"
|
|
145
|
+
result[:total] #=> 150
|
|
146
|
+
result.to_h #=> Same as raw for hashes, or {value: raw} for primitives
|
|
147
|
+
result.to_json #=> JSON string
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The Result object is intentionally minimal - your DSL defines the structure, not the gem.
|
|
151
|
+
|
|
152
|
+
## Rails Integration
|
|
153
|
+
|
|
154
|
+
### Setup
|
|
155
|
+
|
|
156
|
+
The gem auto-loads Rails integration when Rails is detected.
|
|
157
|
+
|
|
158
|
+
```ruby
|
|
159
|
+
# config/initializers/janet_sandbox.rb
|
|
160
|
+
JanetSandbox.configure do |config|
|
|
161
|
+
config.register_dsl(:form_helpers, File.read(Rails.root.join('lib/janet_dsls/form_helpers.janet')))
|
|
162
|
+
config.enable_dsl(:form_helpers)
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Model Concern
|
|
167
|
+
|
|
168
|
+
```ruby
|
|
169
|
+
class ScriptRule < ApplicationRecord
|
|
170
|
+
include JanetSandbox::Rails::ScriptConcern
|
|
171
|
+
|
|
172
|
+
# Specify DSLs for this model
|
|
173
|
+
janet_dsls :form_helpers, :validation_helpers
|
|
174
|
+
|
|
175
|
+
belongs_to :form
|
|
176
|
+
end
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Expected schema:
|
|
180
|
+
|
|
181
|
+
- `janet_source` (text) - Required
|
|
182
|
+
- `active` (boolean) - Optional, for scoping
|
|
183
|
+
- `priority` (integer) - Optional, for ordering
|
|
184
|
+
|
|
185
|
+
### Usage
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
# Single rule
|
|
189
|
+
rule = ScriptRule.find(1)
|
|
190
|
+
result = rule.evaluate({ values: params, user: current_user.attributes })
|
|
191
|
+
|
|
192
|
+
# All rules for a form, merged
|
|
193
|
+
result = ScriptRule.evaluate_all(
|
|
194
|
+
form.script_rules,
|
|
195
|
+
context: { values: params, user: current_user.attributes }
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
render json: result.to_h
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Security
|
|
202
|
+
|
|
203
|
+
Scripts run in a WASM sandbox with:
|
|
204
|
+
|
|
205
|
+
- **No filesystem access** - `file/*` functions removed
|
|
206
|
+
- **No network access** - `net/*` functions removed
|
|
207
|
+
- **No process spawning** - `os/execute`, `os/spawn` removed
|
|
208
|
+
- **No eval/compile** - Prevents meta-evaluation escapes
|
|
209
|
+
- **Fuel-limited execution** - Instruction budget terminates runaway computation
|
|
210
|
+
- **Epoch-limited execution** - Wall-clock timeout catches blocking operations
|
|
211
|
+
- **Memory-capped** - WASM linear memory has a hard ceiling
|
|
212
|
+
- **Result size limit** - Prevents memory bombs via output
|
|
213
|
+
|
|
214
|
+
## Error Handling
|
|
215
|
+
|
|
216
|
+
```ruby
|
|
217
|
+
begin
|
|
218
|
+
result = JanetSandbox.evaluate(source: user_code, context: ctx)
|
|
219
|
+
rescue JanetSandbox::TimeoutError
|
|
220
|
+
# Script exceeded fuel limit
|
|
221
|
+
rescue JanetSandbox::MemoryError
|
|
222
|
+
# Script exceeded memory limit
|
|
223
|
+
rescue JanetSandbox::EvaluationError => e
|
|
224
|
+
# Script had a syntax or runtime error
|
|
225
|
+
puts e.janet_error
|
|
226
|
+
rescue JanetSandbox::ResultError
|
|
227
|
+
# Result was too large or couldn't be parsed
|
|
228
|
+
rescue JanetSandbox::DSLNotFoundError => e
|
|
229
|
+
# Referenced DSL is not registered
|
|
230
|
+
end
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Custom WASM Binary
|
|
234
|
+
|
|
235
|
+
To use a different janet.wasm (e.g., a custom build or newer version):
|
|
236
|
+
|
|
237
|
+
```ruby
|
|
238
|
+
JanetSandbox.configure do |config|
|
|
239
|
+
config.wasm_path = '/custom/path/janet.wasm'
|
|
240
|
+
end
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Development
|
|
244
|
+
|
|
245
|
+
### Rebuilding janet.wasm
|
|
246
|
+
|
|
247
|
+
The gem ships with a prebuilt janet.wasm, but you can rebuild it to update Janet or modify the build.
|
|
248
|
+
|
|
249
|
+
**Dependencies:**
|
|
250
|
+
|
|
251
|
+
- [wasi-sdk](https://github.com/WebAssembly/wasi-sdk) - WASI-enabled Clang/LLVM toolchain
|
|
252
|
+
- `curl` - for downloading Janet source files
|
|
253
|
+
|
|
254
|
+
**Setup:**
|
|
255
|
+
|
|
256
|
+
1. Download wasi-sdk from the [releases page](https://github.com/WebAssembly/wasi-sdk/releases)
|
|
257
|
+
2. Extract it and set the environment variable:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
export WASI_SDK_PATH=~/wasi-sdk-22.0
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Build:**
|
|
264
|
+
|
|
265
|
+
```bash
|
|
266
|
+
cd wasm
|
|
267
|
+
./build.sh
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Or via rake:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
rake janet_sandbox:build_wasm
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
The build script downloads Janet source files automatically if not present.
|
|
277
|
+
|
|
278
|
+
## Thread Safety
|
|
279
|
+
|
|
280
|
+
- `Engine` is thread-safe and should be shared (one per app)
|
|
281
|
+
- `Sandbox` is per-evaluation with isolated WASM memory
|
|
282
|
+
- Configuration should be set at boot time
|
|
283
|
+
|
|
284
|
+
## API Reference
|
|
285
|
+
|
|
286
|
+
### `JanetSandbox.evaluate(source:, context:, dsls: nil, **opts)`
|
|
287
|
+
|
|
288
|
+
Evaluate Janet code. Returns `Result`.
|
|
289
|
+
|
|
290
|
+
### `JanetSandbox.configure { |config| }`
|
|
291
|
+
|
|
292
|
+
Configure the gem.
|
|
293
|
+
|
|
294
|
+
### `JanetSandbox.register_dsl(name, source, description: nil)`
|
|
295
|
+
|
|
296
|
+
Register a DSL module.
|
|
297
|
+
|
|
298
|
+
### `JanetSandbox::Configuration`
|
|
299
|
+
|
|
300
|
+
- `#register_dsl(name, source)` - Register a DSL
|
|
301
|
+
- `#unregister_dsl(name)` - Remove a DSL
|
|
302
|
+
- `#enable_dsl(name)` - Enable by default
|
|
303
|
+
- `#disable_dsl(name)` - Disable from defaults
|
|
304
|
+
- `#dsl_registered?(name)` - Check if registered
|
|
305
|
+
- `#dsl_names` - List all registered DSLs
|
|
306
|
+
|
|
307
|
+
### `JanetSandbox::Result`
|
|
308
|
+
|
|
309
|
+
- `#raw` - Full parsed result
|
|
310
|
+
- `#[key]` - Bracket access to hash keys (symbol or string)
|
|
311
|
+
- `#to_h` - Hash representation
|
|
312
|
+
- `#to_json` - JSON string
|
|
313
|
+
|
|
314
|
+
## License
|
|
315
|
+
|
|
316
|
+
MIT License. See LICENSE.txt.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# Form DSL Helpers
|
|
2
|
+
#
|
|
3
|
+
# This DSL provides functions for building dynamic form rules.
|
|
4
|
+
# Register it in your application:
|
|
5
|
+
#
|
|
6
|
+
# JanetSandbox.configure do |config|
|
|
7
|
+
# config.register_dsl(:form_helpers, File.read("path/to/form_helpers.janet"))
|
|
8
|
+
# config.enable_dsl(:form_helpers)
|
|
9
|
+
# end
|
|
10
|
+
#
|
|
11
|
+
# Expected context structure:
|
|
12
|
+
# {
|
|
13
|
+
# values: { field_name: value, ... },
|
|
14
|
+
# user: { role: "admin", id: 1, ... },
|
|
15
|
+
# meta: { form_id: 123, ... }
|
|
16
|
+
# }
|
|
17
|
+
|
|
18
|
+
(defn get-field [name]
|
|
19
|
+
"Get the current value of a form field"
|
|
20
|
+
(get-in ctx [:values (keyword name)]))
|
|
21
|
+
|
|
22
|
+
(defn get-user [key]
|
|
23
|
+
"Get a property from the current user context"
|
|
24
|
+
(get-in ctx [:user (keyword key)]))
|
|
25
|
+
|
|
26
|
+
(defn get-meta [key]
|
|
27
|
+
"Get form metadata"
|
|
28
|
+
(get-in ctx [:meta (keyword key)]))
|
|
29
|
+
|
|
30
|
+
(defn show [field]
|
|
31
|
+
"Mark a field as visible"
|
|
32
|
+
{:action :show :field (keyword field)})
|
|
33
|
+
|
|
34
|
+
(defn hide [field]
|
|
35
|
+
"Mark a field as hidden"
|
|
36
|
+
{:action :hide :field (keyword field)})
|
|
37
|
+
|
|
38
|
+
(defn require-field [field message]
|
|
39
|
+
"Add a required validation to a field"
|
|
40
|
+
{:action :require :field (keyword field) :message message})
|
|
41
|
+
|
|
42
|
+
(defn set-value [field value]
|
|
43
|
+
"Set a computed value for a field"
|
|
44
|
+
{:action :set-value :field (keyword field) :value value})
|
|
45
|
+
|
|
46
|
+
(defn add-error [field message]
|
|
47
|
+
"Add a validation error to a field"
|
|
48
|
+
{:action :error :field (keyword field) :message message})
|
|
49
|
+
|
|
50
|
+
(defn add-warning [field message]
|
|
51
|
+
"Add a validation warning to a field"
|
|
52
|
+
{:action :warning :field (keyword field) :message message})
|
|
53
|
+
|
|
54
|
+
(defn validate [field pred message]
|
|
55
|
+
"Conditionally add an error if pred is falsy"
|
|
56
|
+
(when (not pred)
|
|
57
|
+
(add-error field message)))
|
|
58
|
+
|
|
59
|
+
(defn field-present? [name]
|
|
60
|
+
"Check if a field has a non-nil, non-empty value"
|
|
61
|
+
(let [v (get-field name)]
|
|
62
|
+
(and (not (nil? v)) (not= v ""))))
|
|
63
|
+
|
|
64
|
+
(defn field-matches? [name pattern]
|
|
65
|
+
"Check if a field value matches a PEG pattern"
|
|
66
|
+
(not (nil? (peg/find pattern (or (get-field name) "")))))
|
|
67
|
+
|
|
68
|
+
(defn when-field [name value body-fn]
|
|
69
|
+
"Execute body-fn when field equals value"
|
|
70
|
+
(when (= (get-field name) value) (body-fn)))
|
|
71
|
+
|
|
72
|
+
(defn one-of? [value options]
|
|
73
|
+
"Check if value is one of the given options"
|
|
74
|
+
(not (nil? (find |(= $ value) options))))
|
|
75
|
+
|
|
76
|
+
(defn collect-actions [& forms]
|
|
77
|
+
"Collect all non-nil action results into categorized maps"
|
|
78
|
+
(def actions (filter truthy? (flatten forms)))
|
|
79
|
+
(def result {:visibility {} :validation {} :computed {}})
|
|
80
|
+
(each a actions
|
|
81
|
+
(case (a :action)
|
|
82
|
+
:show (put-in result [:visibility (a :field)] true)
|
|
83
|
+
:hide (put-in result [:visibility (a :field)] false)
|
|
84
|
+
:error (put-in result [:validation (a :field)] (a :message))
|
|
85
|
+
:warning (put-in result [:validation (a :field)]
|
|
86
|
+
(string "Warning: " (a :message)))
|
|
87
|
+
:require (when (not (field-present? (string (a :field))))
|
|
88
|
+
(put-in result [:validation (a :field)] (a :message)))
|
|
89
|
+
:set-value (put-in result [:computed (a :field)] (a :value))))
|
|
90
|
+
result)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Math DSL Helpers
|
|
2
|
+
#
|
|
3
|
+
# This DSL provides utility functions for calculations and aggregations.
|
|
4
|
+
# Useful for computed fields, pricing calculations, etc.
|
|
5
|
+
#
|
|
6
|
+
# Register it in your application:
|
|
7
|
+
#
|
|
8
|
+
# JanetSandbox.configure do |config|
|
|
9
|
+
# config.register_dsl(:math_helpers, File.read("path/to/math_helpers.janet"))
|
|
10
|
+
# config.enable_dsl(:math_helpers)
|
|
11
|
+
# end
|
|
12
|
+
|
|
13
|
+
(defn safe-number [value &opt default]
|
|
14
|
+
"Convert value to number, returning default (0) if nil or not a number"
|
|
15
|
+
(default default 0)
|
|
16
|
+
(if (number? value)
|
|
17
|
+
value
|
|
18
|
+
(if (and (string? value) (not= value ""))
|
|
19
|
+
(or (scan-number value) default)
|
|
20
|
+
default)))
|
|
21
|
+
|
|
22
|
+
(defn sum [& values]
|
|
23
|
+
"Sum all values, treating nil as 0"
|
|
24
|
+
(reduce + 0 (map |(safe-number $) values)))
|
|
25
|
+
|
|
26
|
+
(defn product [& values]
|
|
27
|
+
"Multiply all values, treating nil as 1"
|
|
28
|
+
(reduce * 1 (map |(safe-number $ 1) values)))
|
|
29
|
+
|
|
30
|
+
(defn average [& values]
|
|
31
|
+
"Calculate average of values, ignoring nil"
|
|
32
|
+
(def nums (filter number? values))
|
|
33
|
+
(if (empty? nums)
|
|
34
|
+
0
|
|
35
|
+
(/ (sum ;nums) (length nums))))
|
|
36
|
+
|
|
37
|
+
(defn percentage [value total]
|
|
38
|
+
"Calculate percentage (value / total * 100)"
|
|
39
|
+
(def t (safe-number total 1))
|
|
40
|
+
(if (= t 0)
|
|
41
|
+
0
|
|
42
|
+
(* (/ (safe-number value) t) 100)))
|
|
43
|
+
|
|
44
|
+
(defn round-to [value decimals]
|
|
45
|
+
"Round value to specified decimal places"
|
|
46
|
+
(def multiplier (math/pow 10 decimals))
|
|
47
|
+
(/ (math/round (* (safe-number value) multiplier)) multiplier))
|
|
48
|
+
|
|
49
|
+
(defn clamp [value min-val max-val]
|
|
50
|
+
"Clamp value to be within min and max"
|
|
51
|
+
(max min-val (min max-val (safe-number value))))
|
|
52
|
+
|
|
53
|
+
(defn lerp [a b t]
|
|
54
|
+
"Linear interpolation between a and b by factor t (0-1)"
|
|
55
|
+
(+ a (* (- b a) (clamp t 0 1))))
|
|
56
|
+
|
|
57
|
+
(defn tax [amount rate]
|
|
58
|
+
"Calculate tax amount"
|
|
59
|
+
(* (safe-number amount) (/ (safe-number rate) 100)))
|
|
60
|
+
|
|
61
|
+
(defn with-tax [amount rate]
|
|
62
|
+
"Add tax to amount"
|
|
63
|
+
(+ (safe-number amount) (tax amount rate)))
|
|
64
|
+
|
|
65
|
+
(defn discount [amount percent]
|
|
66
|
+
"Calculate discount amount"
|
|
67
|
+
(* (safe-number amount) (/ (safe-number percent) 100)))
|
|
68
|
+
|
|
69
|
+
(defn with-discount [amount percent]
|
|
70
|
+
"Apply percentage discount to amount"
|
|
71
|
+
(- (safe-number amount) (discount amount percent)))
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Validation DSL Helpers
|
|
2
|
+
#
|
|
3
|
+
# This DSL provides common validation functions.
|
|
4
|
+
# Can be used standalone or combined with form_helpers.
|
|
5
|
+
#
|
|
6
|
+
# Register it in your application:
|
|
7
|
+
#
|
|
8
|
+
# JanetSandbox.configure do |config|
|
|
9
|
+
# config.register_dsl(:validation_helpers, File.read("path/to/validation_helpers.janet"))
|
|
10
|
+
# config.enable_dsl(:validation_helpers)
|
|
11
|
+
# end
|
|
12
|
+
|
|
13
|
+
# Email validation pattern
|
|
14
|
+
(def email-pattern
|
|
15
|
+
~{:main (* :start (some :local) "@" (some :domain) "." (between 2 10 :alpha) :end)
|
|
16
|
+
:start (if-not :s 0)
|
|
17
|
+
:end (if-not :s -1)
|
|
18
|
+
:local (+ :w (set "._+-"))
|
|
19
|
+
:domain (+ :w (set ".-"))})
|
|
20
|
+
|
|
21
|
+
(defn valid-email? [value]
|
|
22
|
+
"Check if value is a valid email address"
|
|
23
|
+
(if (nil? value)
|
|
24
|
+
false
|
|
25
|
+
(not (nil? (peg/match email-pattern value)))))
|
|
26
|
+
|
|
27
|
+
# Phone validation (basic - digits, spaces, dashes, parens)
|
|
28
|
+
(def phone-pattern
|
|
29
|
+
~{:main (* :start (between 10 20 :phone-char) :end)
|
|
30
|
+
:start (if-not :s 0)
|
|
31
|
+
:end (if-not :s -1)
|
|
32
|
+
:phone-char (+ :d (set " ()-+"))})
|
|
33
|
+
|
|
34
|
+
(defn valid-phone? [value]
|
|
35
|
+
"Check if value looks like a phone number"
|
|
36
|
+
(if (nil? value)
|
|
37
|
+
false
|
|
38
|
+
(not (nil? (peg/match phone-pattern value)))))
|
|
39
|
+
|
|
40
|
+
(defn min-length? [value min]
|
|
41
|
+
"Check if string has at least min characters"
|
|
42
|
+
(if (nil? value)
|
|
43
|
+
false
|
|
44
|
+
(>= (length value) min)))
|
|
45
|
+
|
|
46
|
+
(defn max-length? [value max]
|
|
47
|
+
"Check if string has at most max characters"
|
|
48
|
+
(if (nil? value)
|
|
49
|
+
true
|
|
50
|
+
(<= (length value) max)))
|
|
51
|
+
|
|
52
|
+
(defn in-range? [value min max]
|
|
53
|
+
"Check if numeric value is within range (inclusive)"
|
|
54
|
+
(and (not (nil? value))
|
|
55
|
+
(>= value min)
|
|
56
|
+
(<= value max)))
|
|
57
|
+
|
|
58
|
+
(defn positive? [value]
|
|
59
|
+
"Check if value is a positive number"
|
|
60
|
+
(and (number? value) (> value 0)))
|
|
61
|
+
|
|
62
|
+
(defn non-negative? [value]
|
|
63
|
+
"Check if value is zero or positive"
|
|
64
|
+
(and (number? value) (>= value 0)))
|
|
65
|
+
|
|
66
|
+
(defn matches-pattern? [value pattern]
|
|
67
|
+
"Check if value matches a PEG pattern"
|
|
68
|
+
(if (nil? value)
|
|
69
|
+
false
|
|
70
|
+
(not (nil? (peg/match pattern value)))))
|
|
71
|
+
|
|
72
|
+
(defn not-blank? [value]
|
|
73
|
+
"Check if value is not nil and not an empty string"
|
|
74
|
+
(and (not (nil? value))
|
|
75
|
+
(not= value "")
|
|
76
|
+
(if (string? value)
|
|
77
|
+
(not= (string/trim value) "")
|
|
78
|
+
true)))
|
|
79
|
+
|
|
80
|
+
(defn all-valid? [& predicates]
|
|
81
|
+
"Return true only if all predicates are truthy"
|
|
82
|
+
(all truthy? predicates))
|
|
83
|
+
|
|
84
|
+
(defn any-valid? [& predicates]
|
|
85
|
+
"Return true if any predicate is truthy"
|
|
86
|
+
(some truthy? predicates))
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# config/initializers/janet_sandbox.rb
|
|
2
|
+
#
|
|
3
|
+
# Example Rails initializer for JanetSandbox
|
|
4
|
+
# Copy and customize for your application.
|
|
5
|
+
|
|
6
|
+
JanetSandbox.configure do |config|
|
|
7
|
+
# Execution limits
|
|
8
|
+
config.fuel_limit = 100_000 # ~100ms of computation
|
|
9
|
+
config.memory_limit = 4 * 1024 * 1024 # 4MB
|
|
10
|
+
config.max_result_size = 64 * 1024 # 64KB
|
|
11
|
+
|
|
12
|
+
# Optional: Override WASM path if you vendor it
|
|
13
|
+
# config.wasm_path = Rails.root.join("vendor", "wasm", "janet.wasm").to_s
|
|
14
|
+
|
|
15
|
+
# Register your DSLs
|
|
16
|
+
# You can load from files or define inline
|
|
17
|
+
|
|
18
|
+
# Option 1: Load from file
|
|
19
|
+
# config.register_dsl(
|
|
20
|
+
# :form_helpers,
|
|
21
|
+
# File.read(Rails.root.join("lib", "janet_dsls", "form_helpers.janet")),
|
|
22
|
+
# description: "Form field helpers for visibility and validation"
|
|
23
|
+
# )
|
|
24
|
+
|
|
25
|
+
# Option 2: Define inline
|
|
26
|
+
config.register_dsl(:form_helpers, <<~'JANET', description: "Form field helpers")
|
|
27
|
+
(defn get-field [name]
|
|
28
|
+
"Get the current value of a form field"
|
|
29
|
+
(get-in ctx [:values (keyword name)]))
|
|
30
|
+
|
|
31
|
+
(defn get-user [key]
|
|
32
|
+
"Get a property from the current user context"
|
|
33
|
+
(get-in ctx [:user (keyword key)]))
|
|
34
|
+
|
|
35
|
+
(defn show [field]
|
|
36
|
+
"Mark a field as visible"
|
|
37
|
+
{:action :show :field (keyword field)})
|
|
38
|
+
|
|
39
|
+
(defn hide [field]
|
|
40
|
+
"Mark a field as hidden"
|
|
41
|
+
{:action :hide :field (keyword field)})
|
|
42
|
+
|
|
43
|
+
(defn add-error [field message]
|
|
44
|
+
"Add a validation error to a field"
|
|
45
|
+
{:action :error :field (keyword field) :message message})
|
|
46
|
+
|
|
47
|
+
(defn set-value [field value]
|
|
48
|
+
"Set a computed value for a field"
|
|
49
|
+
{:action :set-value :field (keyword field) :value value})
|
|
50
|
+
|
|
51
|
+
(defn field-present? [name]
|
|
52
|
+
"Check if a field has a non-nil, non-empty value"
|
|
53
|
+
(let [v (get-field name)]
|
|
54
|
+
(and (not (nil? v)) (not= v ""))))
|
|
55
|
+
|
|
56
|
+
(defn collect-actions [& forms]
|
|
57
|
+
"Collect all non-nil action results into categorized maps"
|
|
58
|
+
(def actions (filter truthy? (flatten forms)))
|
|
59
|
+
(def result {:visibility {} :validation {} :computed {}})
|
|
60
|
+
(each a actions
|
|
61
|
+
(case (a :action)
|
|
62
|
+
:show (put-in result [:visibility (a :field)] true)
|
|
63
|
+
:hide (put-in result [:visibility (a :field)] false)
|
|
64
|
+
:error (put-in result [:validation (a :field)] (a :message))
|
|
65
|
+
:set-value (put-in result [:computed (a :field)] (a :value))))
|
|
66
|
+
result)
|
|
67
|
+
JANET
|
|
68
|
+
|
|
69
|
+
# Enable DSLs by default for all evaluations
|
|
70
|
+
config.enable_dsl(:form_helpers)
|
|
71
|
+
|
|
72
|
+
# Block additional symbols if needed
|
|
73
|
+
# config.blocked_symbols << "some/dangerous-fn"
|
|
74
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JanetSandbox
|
|
4
|
+
# Built-in DSLs provided by the janet_sandbox gem.
|
|
5
|
+
#
|
|
6
|
+
# These DSLs can be registered and enabled in your configuration:
|
|
7
|
+
#
|
|
8
|
+
# JanetSandbox.configure do |config|
|
|
9
|
+
# config.register_dsl(:code_inspector, JanetSandbox::BuiltinDsls::CODE_INSPECTOR)
|
|
10
|
+
# config.enable_dsl(:code_inspector)
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
module BuiltinDsls
|
|
14
|
+
DSL_DIR = File.expand_path("dsls", __dir__)
|
|
15
|
+
|
|
16
|
+
# Code Inspector DSL - Parse and inspect Janet code structure without execution.
|
|
17
|
+
#
|
|
18
|
+
# Provides functions:
|
|
19
|
+
# - parse-to-tree: Parse code string into a structured tree
|
|
20
|
+
# - parse-all-to-tree: Parse multiple expressions into array of trees
|
|
21
|
+
# - to-tree: Convert a parsed Janet value into a tree representation
|
|
22
|
+
# - extract-symbols: Get all symbol names from an expression
|
|
23
|
+
# - extract-calls: Get all function/macro call names from an expression
|
|
24
|
+
# - validate-syntax: Check if code is syntactically valid
|
|
25
|
+
#
|
|
26
|
+
# @example
|
|
27
|
+
# JanetSandbox.register_dsl(:code_inspector, JanetSandbox::BuiltinDsls::CODE_INSPECTOR)
|
|
28
|
+
# result = JanetSandbox.evaluate(
|
|
29
|
+
# source: '(parse-to-tree "(if (> x 3) (foo) (bar))")',
|
|
30
|
+
# context: {},
|
|
31
|
+
# dsls: [:code_inspector]
|
|
32
|
+
# )
|
|
33
|
+
# result.raw
|
|
34
|
+
# # => { type: "call", op: "if", args: [...] }
|
|
35
|
+
#
|
|
36
|
+
CODE_INSPECTOR = File.read(File.join(DSL_DIR, "code_inspector.janet")).freeze
|
|
37
|
+
end
|
|
38
|
+
end
|