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 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