feelin 7.0.2 → 7.0.3
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 +4 -4
- data/CHANGELOG.md +8 -4
- data/README.md +44 -8
- data/lib/feelin/js/dist/feelin.js +17 -5
- data/lib/feelin/js/entry.js +24 -5
- data/lib/feelin/version.rb +1 -1
- data/lib/feelin.rb +47 -7
- data/spec/feelin/feelin_spec.rb +55 -13
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d42d010d26029134be39b2bcdbd097683a00b0866820f1d816f69607b5bd053
|
|
4
|
+
data.tar.gz: cbee6c7dc98e5c2e078ebdecdbe3abac6dee702966841ffa3e5a3df399e69853
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 247e1e7cce0bf1a66df682268273282000c3ef98ba755af7ac213f5b9c978cb30aa9f68bef46e4ab49ff0c2573cf25838ed11e0da51aca861d0b30ccd268f7cf
|
|
7
|
+
data.tar.gz: 5ff326f4111002f0f65fef3e84f31bf307d1fea982372c72805936ada9484e8b579a57fe853a3a88c1cf2f35b4b116271996327498853b8ef608969a3483050b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [7.0.3] - 2026-10-05
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
* **Breaking:** `FEELIN.parse` is `FEELIN.parse_expression`, after feelin's own `parseExpression`.
|
|
13
|
+
* **Breaking:** errors are the gem's own. A failure inside V8 is raised as `FEELIN::Error` — `FEELIN::SyntaxError` for an expression that does not parse, `FEELIN::TimeoutError` and `FEELIN::MemoryError` for a context's limits — instead of `MiniRacer::RuntimeError`, `MiniRacer::ScriptTerminatedError` and `MiniRacer::V8OutOfMemoryError`. The message names the expression, also available as `error.expression`; `error.reason` is the cause without it. An exception raised by a custom function still passes unwrapped.
|
|
14
|
+
* **Breaking:** a custom function receives a date, a time and a duration as their ISO 8601 strings — the form a result has — instead of a hash of the internal fields of the JavaScript object.
|
|
15
|
+
|
|
8
16
|
## [7.0.2] - 2026-10-05
|
|
9
17
|
|
|
10
18
|
### Added
|
|
@@ -18,10 +26,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
18
26
|
* **Breaking:** a result is JSON-compatible. A date, a time and a duration are now the ISO 8601 strings FEEL's `string()` gives them (`"2020-01-02"`, `"10:00:00+03:00"`, `"2020-01-02T03:04:05Z"`, `"P1DT2H"`) instead of a hash of the internal fields of the JavaScript object. A function is a hash of its `parameterNames`.
|
|
19
27
|
* The context is passed to V8 as an argument rather than written into the evaluated script, so V8 no longer compiles and keeps a script per distinct context. Measured speed is the same as before.
|
|
20
28
|
|
|
21
|
-
### Not updated
|
|
22
|
-
|
|
23
|
-
* `feelin` 8.2.0 was tried and held back: evaluating an expression that names a variable takes about twice as long there (8.2.0 looks every variable name up among the built-ins on each `evaluate`), and it produces enough garbage to run a context with `max_memory: 64_000_000` out of memory after ~10 000 evaluations.
|
|
24
|
-
|
|
25
29
|
## [7.0.1] - 2026-06-22
|
|
26
30
|
|
|
27
31
|
### Changed
|
data/README.md
CHANGED
|
@@ -43,11 +43,11 @@ FEELIN.unary_test('[1..end]', 1, { 'end' => 10 }) # true
|
|
|
43
43
|
|
|
44
44
|
### Syntax tree
|
|
45
45
|
|
|
46
|
-
`
|
|
46
|
+
`parse_expression` and `parse_unary_tests` answer the syntax tree of an expression without evaluating it — for
|
|
47
47
|
translating FEEL into something else (SQL, for one) or inspecting what an expression refers to.
|
|
48
48
|
|
|
49
49
|
```ruby
|
|
50
|
-
FEELIN.
|
|
50
|
+
FEELIN.parse_expression('price > 10')
|
|
51
51
|
# { "type" => "Expression", "from" => 0, "to" => 10, "text" => "price > 10", "children" => [
|
|
52
52
|
# { "type" => "Comparison", "from" => 0, "to" => 10, "text" => "price > 10", "children" => [
|
|
53
53
|
# { "type" => "VariableName", "from" => 0, "to" => 5, "text" => "price", "children" => [
|
|
@@ -68,10 +68,10 @@ The optional second argument is a context, of which only the names matter: it is
|
|
|
68
68
|
spaces in it be read as one name.
|
|
69
69
|
|
|
70
70
|
```ruby
|
|
71
|
-
FEELIN.
|
|
71
|
+
FEELIN.parse_expression("Mike's daughter.name + 1", { "Mike's daughter.name" => nil })
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
An expression that does not parse raises `
|
|
74
|
+
An expression that does not parse raises `FEELIN::SyntaxError`, with the same message `evaluate` gives.
|
|
75
75
|
|
|
76
76
|
### Custom functions
|
|
77
77
|
|
|
@@ -80,6 +80,18 @@ FEELIN.add_function('rates', proc { [10, 20] })
|
|
|
80
80
|
FEELIN.evaluate('every rate in rates() satisfies rate < 10') # false
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
+
Arguments reach the function in the form a result has: a date, a time and a duration as their ISO 8601
|
|
84
|
+
strings, wherever they are among the arguments. What the function returns is plain data to FEEL — an ISO
|
|
85
|
+
string it returns is a string there, and `date(...)` makes a date of it.
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
FEELIN.add_function('last day of month', proc do |date|
|
|
89
|
+
day = Date.iso8601(date)
|
|
90
|
+
Date.new(day.year, day.month, -1).iso8601
|
|
91
|
+
end)
|
|
92
|
+
FEELIN.evaluate('date(last day of month(date("2024-02-10"))) + duration("P1D")') # "2024-03-01"
|
|
93
|
+
```
|
|
94
|
+
|
|
83
95
|
### A context of one's own
|
|
84
96
|
|
|
85
97
|
The methods above work on one shared V8 context, which has no limits. `FEELIN::Context` is a separate
|
|
@@ -91,14 +103,38 @@ context = FEELIN::Context.new(timeout: 1_000, max_memory: 64_000_000) # ms, byte
|
|
|
91
103
|
|
|
92
104
|
context.add_function('rate', proc { 0.2 })
|
|
93
105
|
context.evaluate('price * rate()', { 'price' => 100 }) # 20
|
|
94
|
-
context.
|
|
106
|
+
context.parse_expression('price * rate()')
|
|
95
107
|
|
|
96
108
|
context.dispose
|
|
97
109
|
```
|
|
98
110
|
|
|
99
|
-
Past its timeout an evaluation raises `
|
|
100
|
-
|
|
101
|
-
|
|
111
|
+
Past its timeout an evaluation raises `FEELIN::TimeoutError`, past its memory `FEELIN::MemoryError`.
|
|
112
|
+
Contexts are created from one snapshot of the bundle, so a new one does not load feelin again.
|
|
113
|
+
|
|
114
|
+
### Errors
|
|
115
|
+
|
|
116
|
+
Whatever goes wrong inside V8 is raised as a `FEELIN::Error`, never as an error of MiniRacer. Its message
|
|
117
|
+
names the expression, which is also there as `error.expression`; `error.reason` is what went wrong without
|
|
118
|
+
it.
|
|
119
|
+
|
|
120
|
+
| error | when |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `FEELIN::SyntaxError` | the expression does not parse |
|
|
123
|
+
| `FEELIN::TimeoutError` | the evaluation ran past the context's `timeout` |
|
|
124
|
+
| `FEELIN::MemoryError` | the evaluation ran past the context's `max_memory` |
|
|
125
|
+
| `FEELIN::Error` | anything else, and the parent of the three above |
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
begin
|
|
129
|
+
FEELIN.evaluate('1 +')
|
|
130
|
+
rescue FEELIN::SyntaxError => e
|
|
131
|
+
e.message # "1 +" is not a FEEL expression: Incomplete <ArithmeticExpression>
|
|
132
|
+
e.expression # 1 +
|
|
133
|
+
e.reason # Incomplete <ArithmeticExpression>
|
|
134
|
+
end
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
An exception raised by a custom function is not wrapped: it reaches the caller as it was raised.
|
|
102
138
|
|
|
103
139
|
## Development
|
|
104
140
|
|
|
@@ -13687,14 +13687,26 @@ var feel = (() => {
|
|
|
13687
13687
|
const context = (contextJson == null ? null : JSON.parse(contextJson)) || {};
|
|
13688
13688
|
return hasFunctions ? Object.assign(context, functions) : context;
|
|
13689
13689
|
}
|
|
13690
|
+
function guarded(fn2) {
|
|
13691
|
+
return (...args) => {
|
|
13692
|
+
try {
|
|
13693
|
+
return fn2(...args);
|
|
13694
|
+
} catch (error) {
|
|
13695
|
+
if (error instanceof SyntaxError2) {
|
|
13696
|
+
error.name = "FeelSyntaxError";
|
|
13697
|
+
}
|
|
13698
|
+
throw error;
|
|
13699
|
+
}
|
|
13700
|
+
};
|
|
13701
|
+
}
|
|
13690
13702
|
var json = {
|
|
13691
|
-
evaluate: (expression, contextJson) => toJson(evaluate(expression, readContext(contextJson)).value),
|
|
13692
|
-
unaryTest: (expression, contextJson) => toJson(unaryTest(expression, readContext(contextJson)).value),
|
|
13693
|
-
parseExpression: (expression, contextJson) => JSON.stringify(toAst(parseExpression(expression, readContext(contextJson)), expression)),
|
|
13694
|
-
parseUnaryTests: (expression, contextJson) => JSON.stringify(toAst(parseUnaryTests(expression, readContext(contextJson)), expression)),
|
|
13703
|
+
evaluate: guarded((expression, contextJson) => toJson(evaluate(expression, readContext(contextJson)).value)),
|
|
13704
|
+
unaryTest: guarded((expression, contextJson) => toJson(unaryTest(expression, readContext(contextJson)).value)),
|
|
13705
|
+
parseExpression: guarded((expression, contextJson) => JSON.stringify(toAst(parseExpression(expression, readContext(contextJson)), expression))),
|
|
13706
|
+
parseUnaryTests: guarded((expression, contextJson) => JSON.stringify(toAst(parseUnaryTests(expression, readContext(contextJson)), expression))),
|
|
13695
13707
|
addFunction: (name2) => {
|
|
13696
13708
|
functions[name2] = function(...args) {
|
|
13697
|
-
return globalThis[name2](...args);
|
|
13709
|
+
return globalThis[name2](...JSON.parse(toJson(args)));
|
|
13698
13710
|
};
|
|
13699
13711
|
hasFunctions = true;
|
|
13700
13712
|
}
|
data/lib/feelin/js/entry.js
CHANGED
|
@@ -72,6 +72,9 @@ const toJson = (value) => JSON.stringify(value === undefined ? null : value, rep
|
|
|
72
72
|
// Custom functions, by name. Each forwards to the global the Ruby side attached under that name; the
|
|
73
73
|
// `(...args)` forwarder is deliberate: feelin reads a function's source to learn its parameters, so a bare
|
|
74
74
|
// reference to the (native) attached global would be seen as taking no arguments.
|
|
75
|
+
//
|
|
76
|
+
// The arguments go to Ruby in the form a result does — through the same JSON, so a date, time or duration
|
|
77
|
+
// among them, however deep, is its ISO 8601 string there too and not the fields of a luxon object.
|
|
75
78
|
const functions = {};
|
|
76
79
|
let hasFunctions = false;
|
|
77
80
|
|
|
@@ -81,13 +84,29 @@ function readContext(contextJson) {
|
|
|
81
84
|
return hasFunctions ? Object.assign(context, functions) : context;
|
|
82
85
|
}
|
|
83
86
|
|
|
87
|
+
// A syntax error leaves under a name of its own: across the V8 boundary an error is only its name and its
|
|
88
|
+
// message, and feelin's SyntaxError keeps the name `Error`.
|
|
89
|
+
function guarded(fn) {
|
|
90
|
+
return (...args) => {
|
|
91
|
+
try {
|
|
92
|
+
return fn(...args);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (error instanceof SyntaxError) {
|
|
95
|
+
error.name = 'FeelSyntaxError';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
84
103
|
export const json = {
|
|
85
|
-
evaluate: (expression, contextJson) => toJson(evaluate(expression, readContext(contextJson)).value),
|
|
86
|
-
unaryTest: (expression, contextJson) => toJson(unaryTest(expression, readContext(contextJson)).value),
|
|
87
|
-
parseExpression: (expression, contextJson) => JSON.stringify(toAst(parseExpression(expression, readContext(contextJson)), expression)),
|
|
88
|
-
parseUnaryTests: (expression, contextJson) => JSON.stringify(toAst(parseUnaryTests(expression, readContext(contextJson)), expression)),
|
|
104
|
+
evaluate: guarded((expression, contextJson) => toJson(evaluate(expression, readContext(contextJson)).value)),
|
|
105
|
+
unaryTest: guarded((expression, contextJson) => toJson(unaryTest(expression, readContext(contextJson)).value)),
|
|
106
|
+
parseExpression: guarded((expression, contextJson) => JSON.stringify(toAst(parseExpression(expression, readContext(contextJson)), expression))),
|
|
107
|
+
parseUnaryTests: guarded((expression, contextJson) => JSON.stringify(toAst(parseUnaryTests(expression, readContext(contextJson)), expression))),
|
|
89
108
|
addFunction: (name) => {
|
|
90
|
-
functions[name] = function(...args) { return globalThis[name](...args); };
|
|
109
|
+
functions[name] = function(...args) { return globalThis[name](...JSON.parse(toJson(args))); };
|
|
91
110
|
hasFunctions = true;
|
|
92
111
|
}
|
|
93
112
|
};
|
data/lib/feelin/version.rb
CHANGED
data/lib/feelin.rb
CHANGED
|
@@ -9,12 +9,35 @@ module FEELIN
|
|
|
9
9
|
# assembled at load time.
|
|
10
10
|
BUNDLE_PATH = File.expand_path("feelin/js/dist/feelin.js", __dir__)
|
|
11
11
|
|
|
12
|
+
# What goes wrong inside V8 is raised as one of these, never as a MiniRacer
|
|
13
|
+
# error: the message names the expression, which is also kept in `expression`,
|
|
14
|
+
# and `reason` is what went wrong without it. An exception raised by a custom
|
|
15
|
+
# function is not one of them and passes as it is.
|
|
16
|
+
class Error < StandardError
|
|
17
|
+
attr_reader :expression, :reason
|
|
18
|
+
|
|
19
|
+
def initialize(message, expression = nil, reason = message)
|
|
20
|
+
super(message)
|
|
21
|
+
@expression = expression
|
|
22
|
+
@reason = reason
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# the expression does not parse
|
|
27
|
+
class SyntaxError < Error; end
|
|
28
|
+
|
|
29
|
+
# the evaluation ran past the context's `timeout`
|
|
30
|
+
class TimeoutError < Error; end
|
|
31
|
+
|
|
32
|
+
# the evaluation ran past the context's `max_memory`
|
|
33
|
+
class MemoryError < Error; end
|
|
34
|
+
|
|
12
35
|
# A V8 context with feelin loaded. The module-level methods below work on one
|
|
13
36
|
# shared context without limits; a context of one's own is for an expression
|
|
14
37
|
# that is not trusted to end or to stay small — `timeout` (ms) and `max_memory`
|
|
15
|
-
# (bytes) are
|
|
16
|
-
#
|
|
17
|
-
#
|
|
38
|
+
# (bytes) are its limits, and going over one raises TimeoutError or
|
|
39
|
+
# MemoryError — and for custom functions that the shared context should not
|
|
40
|
+
# see.
|
|
18
41
|
#
|
|
19
42
|
# Every context starts from one snapshot of the bundle, so creating one does
|
|
20
43
|
# not load and compile feelin again.
|
|
@@ -33,6 +56,7 @@ module FEELIN
|
|
|
33
56
|
end
|
|
34
57
|
|
|
35
58
|
def initialize(timeout: nil, max_memory: nil)
|
|
59
|
+
@timeout = timeout
|
|
36
60
|
limits = { timeout: timeout, max_memory: max_memory }.compact
|
|
37
61
|
@context = MiniRacer::Context.new(snapshot: self.class.snapshot, **limits)
|
|
38
62
|
end
|
|
@@ -50,8 +74,8 @@ module FEELIN
|
|
|
50
74
|
# counted in UTF-16 code units), `text` (that span) and `children`. Tokens are
|
|
51
75
|
# nodes too. The context only supplies the names of the variables, which is
|
|
52
76
|
# what lets a name with spaces in it be read as one. An expression that does
|
|
53
|
-
# not parse raises, as it does in `evaluate`.
|
|
54
|
-
def
|
|
77
|
+
# not parse raises SyntaxError, as it does in `evaluate`.
|
|
78
|
+
def parse_expression(expression, context = nil)
|
|
55
79
|
call("parseExpression", expression, context)
|
|
56
80
|
end
|
|
57
81
|
|
|
@@ -70,8 +94,24 @@ module FEELIN
|
|
|
70
94
|
|
|
71
95
|
private
|
|
72
96
|
|
|
97
|
+
SYNTAX_ERROR = /\AFeelSyntaxError: /
|
|
98
|
+
|
|
73
99
|
def call(function, expression, context)
|
|
74
100
|
JSON.parse(@context.call("feel.json.#{function}", expression, context.nil? ? nil : JSON.generate(context)))
|
|
101
|
+
rescue MiniRacer::ScriptTerminatedError
|
|
102
|
+
raise failure(TimeoutError, expression, "took longer than #{@timeout} ms", "")
|
|
103
|
+
rescue MiniRacer::V8OutOfMemoryError
|
|
104
|
+
raise failure(MemoryError, expression, "ran out of memory", "")
|
|
105
|
+
rescue MiniRacer::Error => e
|
|
106
|
+
reason = e.message.lines.first.to_s.strip
|
|
107
|
+
|
|
108
|
+
raise failure(SyntaxError, expression, reason.sub(SYNTAX_ERROR, ''), " is not a FEEL expression:") if reason.match?(SYNTAX_ERROR)
|
|
109
|
+
|
|
110
|
+
raise failure(Error, expression, reason.sub(/\AError: /, ''), ":")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def failure(error, expression, reason, link)
|
|
114
|
+
error.new("#{expression.inspect}#{link} #{reason}", expression, reason)
|
|
75
115
|
end
|
|
76
116
|
end
|
|
77
117
|
|
|
@@ -84,8 +124,8 @@ module FEELIN
|
|
|
84
124
|
shared.unary_test(expression, value, context)
|
|
85
125
|
end
|
|
86
126
|
|
|
87
|
-
def
|
|
88
|
-
shared.
|
|
127
|
+
def parse_expression(expression, context = nil)
|
|
128
|
+
shared.parse_expression(expression, context)
|
|
89
129
|
end
|
|
90
130
|
|
|
91
131
|
def parse_unary_tests(expression, context = nil)
|
data/spec/feelin/feelin_spec.rb
CHANGED
|
@@ -36,6 +36,26 @@ RSpec.describe FEELIN do
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
describe "custom function arguments" do
|
|
40
|
+
it "passes a date, a time and a duration as their ISO 8601 strings" do
|
|
41
|
+
seen = nil
|
|
42
|
+
FEELIN.add_function('seen', proc { |*args| seen = args; nil })
|
|
43
|
+
|
|
44
|
+
FEELIN.evaluate('seen(date("2020-01-02"), time("10:00:00+03:00"), date and time("2020-01-02T03:04:05Z"), duration("P1DT2H"))')
|
|
45
|
+
|
|
46
|
+
expect(seen).to eq [ '2020-01-02', '10:00:00+03:00', '2020-01-02T03:04:05Z', 'P1DT2H' ]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it "does so inside a list and a context, and leaves the rest as it is" do
|
|
50
|
+
seen = nil
|
|
51
|
+
FEELIN.add_function('seen', proc { |*args| seen = args; nil })
|
|
52
|
+
|
|
53
|
+
FEELIN.evaluate('seen([ @"2020-01-02" ], { on: @"2020-01-03", n: 1.5 }, "text", null, true)')
|
|
54
|
+
|
|
55
|
+
expect(seen).to eq [ [ '2020-01-02' ], { 'on' => '2020-01-03', 'n' => 1.5 }, 'text', nil, true ]
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
39
59
|
describe "temporal results" do
|
|
40
60
|
it "answers a date, a time and a duration in their ISO 8601 form" do
|
|
41
61
|
expect(FEELIN.evaluate('date("2020-01-02")')).to eq '2020-01-02'
|
|
@@ -70,7 +90,7 @@ RSpec.describe FEELIN do
|
|
|
70
90
|
context = FEELIN::Context.new(timeout: 50)
|
|
71
91
|
|
|
72
92
|
expect { context.evaluate('count(for a in 1..3000, b in 1..3000 return a * b)') }
|
|
73
|
-
.to raise_error(
|
|
93
|
+
.to raise_error(FEELIN::TimeoutError, /took longer than 50 ms/)
|
|
74
94
|
ensure
|
|
75
95
|
context&.dispose
|
|
76
96
|
end
|
|
@@ -89,13 +109,35 @@ RSpec.describe FEELIN do
|
|
|
89
109
|
end
|
|
90
110
|
end
|
|
91
111
|
|
|
92
|
-
describe "
|
|
93
|
-
it "raises
|
|
94
|
-
expect { FEELIN.evaluate('1 +') }.to raise_error(
|
|
112
|
+
describe "errors" do
|
|
113
|
+
it "raises a syntax error that names the expression" do
|
|
114
|
+
expect { FEELIN.evaluate('1 +') }.to raise_error(FEELIN::SyntaxError, '"1 +" is not a FEEL expression: Incomplete <ArithmeticExpression>') do |error|
|
|
115
|
+
expect(error.expression).to eq '1 +'
|
|
116
|
+
expect(error.reason).to eq 'Incomplete <ArithmeticExpression>'
|
|
117
|
+
end
|
|
118
|
+
expect { FEELIN.unary_test('[1..', 1) }.to raise_error(FEELIN::SyntaxError)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it "raises nothing of MiniRacer's" do
|
|
122
|
+
expect { FEELIN.evaluate('1 +') }.to raise_error(FEELIN::Error) { |error| expect(error).not_to be_a(MiniRacer::Error) }
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "raises a memory error past the context's limit" do
|
|
126
|
+
context = FEELIN::Context.new(max_memory: 4_000_000)
|
|
127
|
+
|
|
128
|
+
expect { context.evaluate('for a in 1..300000 return { n: a, s: string(a) }') }.to raise_error(FEELIN::MemoryError, /ran out of memory/)
|
|
129
|
+
ensure
|
|
130
|
+
context&.dispose
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it "lets an exception of a custom function through as it is" do
|
|
134
|
+
FEELIN.add_function('fails', proc { raise ArgumentError, 'no' })
|
|
135
|
+
|
|
136
|
+
expect { FEELIN.evaluate('fails()') }.to raise_error(ArgumentError, 'no')
|
|
95
137
|
end
|
|
96
138
|
end
|
|
97
139
|
|
|
98
|
-
describe "#
|
|
140
|
+
describe "#parse_expression" do
|
|
99
141
|
# the types of a tree, nested as the tree is, tokens left out
|
|
100
142
|
def shape(node)
|
|
101
143
|
children = node['children'].reject { |child| child['children'].empty? && child['type'] !~ /\A[A-Z]/ }
|
|
@@ -103,7 +145,7 @@ RSpec.describe FEELIN do
|
|
|
103
145
|
end
|
|
104
146
|
|
|
105
147
|
it "answers the syntax tree without evaluating the expression" do
|
|
106
|
-
tree = FEELIN.
|
|
148
|
+
tree = FEELIN.parse_expression('a.b + 1 > 2')
|
|
107
149
|
|
|
108
150
|
expect(shape(tree)).to eq(
|
|
109
151
|
'Expression' => [ {
|
|
@@ -121,33 +163,33 @@ RSpec.describe FEELIN do
|
|
|
121
163
|
end
|
|
122
164
|
|
|
123
165
|
it "gives every node its span and its text" do
|
|
124
|
-
comparison = FEELIN.
|
|
166
|
+
comparison = FEELIN.parse_expression('a.b + 1 > 2')['children'].first
|
|
125
167
|
|
|
126
168
|
expect(comparison.slice('type', 'from', 'to', 'text')).to eq('type' => 'Comparison', 'from' => 0, 'to' => 11, 'text' => 'a.b + 1 > 2')
|
|
127
169
|
expect(comparison['children'].map { |child| child['text'] }).to eq [ 'a.b + 1', '>', '2' ]
|
|
128
170
|
end
|
|
129
171
|
|
|
130
172
|
it "reads a name with spaces as one name when the context has it" do
|
|
131
|
-
tree = FEELIN.
|
|
173
|
+
tree = FEELIN.parse_expression("Mike's daughter.name + 1", { "Mike's daughter.name" => 'Lisa' })
|
|
132
174
|
name = tree['children'].first['children'].first
|
|
133
175
|
|
|
134
176
|
expect(name.slice('type', 'text')).to eq('type' => 'VariableName', 'text' => "Mike's daughter.name")
|
|
135
177
|
end
|
|
136
178
|
|
|
137
|
-
it "raises on an expression that does not parse
|
|
138
|
-
expect { FEELIN.
|
|
139
|
-
expect { FEELIN.
|
|
179
|
+
it "raises on an expression that does not parse" do
|
|
180
|
+
expect { FEELIN.parse_expression('1 +') }.to raise_error(FEELIN::SyntaxError, /Incomplete <ArithmeticExpression>/)
|
|
181
|
+
expect { FEELIN.parse_expression('1 + ) 2') }.to raise_error(FEELIN::SyntaxError, /Unrecognized token/)
|
|
140
182
|
end
|
|
141
183
|
|
|
142
184
|
it "reports a syntax error as evaluate reports it" do
|
|
143
185
|
[ '1 +', '{ a: 1', 'if x then', '1 + ) 2', '1 2', '[1, 2' ].each do |expression|
|
|
144
186
|
expected = begin
|
|
145
187
|
FEELIN.evaluate(expression)
|
|
146
|
-
rescue
|
|
188
|
+
rescue FEELIN::SyntaxError => e
|
|
147
189
|
e.message
|
|
148
190
|
end
|
|
149
191
|
|
|
150
|
-
expect { FEELIN.
|
|
192
|
+
expect { FEELIN.parse_expression(expression) }.to raise_error(FEELIN::SyntaxError, expected), expression
|
|
151
193
|
end
|
|
152
194
|
end
|
|
153
195
|
end
|