equation 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +57 -0
- data/lib/equation.rb +19 -9
- data/lib/equation_grammar.rb +90 -26
- metadata +3 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e5a5da51d16c1c12ef567b1842c964e65dfb7729836770685270767674f8a532
|
4
|
+
data.tar.gz: 4b6f537c8f047cced5414b133cf8dd8749b92faa0930548cecf5b192ba96e122
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5dc8d42ce509c4ee4df2c2fe59e17bd9050cd4fc717372c04ef38826dd66b9a663a35823161873fd54df1c91279c3eb38bc318c5325be411cfa6eeb186333c74
|
7
|
+
data.tar.gz: a8d9a8878e0445074867459dec86d63c7b640a72f3e15411ce1314544f15c6c7f81dfb35cbcaccdb2cbe84c3a6b03299cad1d57d2d213d77a52d07dc11461f16
|
data/README.md
CHANGED
@@ -10,6 +10,7 @@ Use cases include:
|
|
10
10
|
|
11
11
|
Modeled loosely after [Symfony Expression Language](https://symfony.com/doc/current/components/expression_language.html).
|
12
12
|
|
13
|
+
|
13
14
|
## Example
|
14
15
|
|
15
16
|
In this example, we'll use a rule to determine whether a request should be dropped or not. While the rule here is hardcoded into the program, it could just as easily be pulled from a database, some redis cache, etc instead. Rules can also be cached, saving you an extra parsing step.
|
@@ -29,3 +30,59 @@ if suspicious_request
|
|
29
30
|
# log some things, notify some people
|
30
31
|
end
|
31
32
|
```
|
33
|
+
|
34
|
+
## Language Features
|
35
|
+
|
36
|
+
Because Equation is modeled after [Symfony Expression Language](https://symfony.com/doc/current/components/expression_language/syntax.html), it supports a lot of the same features. For a more exhaustive list, check out the [tests](https://github.com/ancat/equation/blob/main/spec/equation_spec.rb).
|
37
|
+
|
38
|
+
### Literals
|
39
|
+
|
40
|
+
* Strings: double quotes, e.g. `"hello world"`
|
41
|
+
* Numbers: all treated as floats, e.g. `0`, `-10`, `0.5`
|
42
|
+
* Arrays: square brackets, e.g. `[403, 404]` or `["yes", "no", "maybe"]`; can be mixed types
|
43
|
+
* Booleans: `true`, `false`
|
44
|
+
* Null: `nil`
|
45
|
+
|
46
|
+
### Variables
|
47
|
+
|
48
|
+
Variables are only made available to the engine at initialization. For example, given this setup code:
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
engine = EquationEngine.new(default: {name: "OMAR", age: 12})
|
52
|
+
```
|
53
|
+
|
54
|
+
These variables and all their properties are accessible from within rules:
|
55
|
+
|
56
|
+
```
|
57
|
+
$name == "OMAR" # true
|
58
|
+
$name.length # 4
|
59
|
+
$name.reverse # RAMO
|
60
|
+
```
|
61
|
+
|
62
|
+
### Methods
|
63
|
+
|
64
|
+
Like variables, methods are only made available to the engine at initialization. They can take any number and type of arguments, including variables or return values from other methods.
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
engine = EquationEngine.new(default: {age: 12}, methods: {is_even: ->(n) {n%2==0}})
|
68
|
+
```
|
69
|
+
|
70
|
+
`is_even` can now be called as follows:
|
71
|
+
|
72
|
+
```
|
73
|
+
is_even(5) # false
|
74
|
+
is_even($age) # true
|
75
|
+
```
|
76
|
+
|
77
|
+
### Comparisons
|
78
|
+
|
79
|
+
```
|
80
|
+
$name == "Dumpling" && $age >= 12
|
81
|
+
$name in ["Dumpling", "Meatball"] || $age == 12
|
82
|
+
```
|
83
|
+
|
84
|
+
## Development
|
85
|
+
|
86
|
+
* To work on the expression language itself, take a look at `lib/equation_grammar.treetop`. Equation is built using [treetop](https://cjheath.github.io/treetop/).
|
87
|
+
* Run `bundle exec rake build_grammar` to generate the corresponding parser Ruby code.
|
88
|
+
* Run `bundle exec rake spec` to run tests.
|
data/lib/equation.rb
CHANGED
@@ -3,20 +3,21 @@ require_relative 'equation_node_classes'
|
|
3
3
|
require_relative 'equation_grammar'
|
4
4
|
|
5
5
|
class Context
|
6
|
+
attr_accessor :transient_symbols
|
7
|
+
|
6
8
|
def initialize(default: {}, methods: {})
|
7
9
|
@symbol_table = default
|
10
|
+
@transient_symbols = {}
|
8
11
|
@methods = methods
|
9
12
|
end
|
10
13
|
|
11
|
-
def
|
12
|
-
@symbol_table
|
14
|
+
def symbols
|
15
|
+
@symbol_table.merge(@transient_symbols)
|
13
16
|
end
|
14
17
|
|
15
18
|
def get(identifier:, path: {})
|
16
19
|
assert_defined!(identifier: identifier)
|
17
|
-
|
18
|
-
root = @symbol_table[identifier.to_sym]
|
19
|
-
child = root
|
20
|
+
child = symbols[identifier.to_sym]
|
20
21
|
path.each{|segment|
|
21
22
|
segment_name = segment.elements[1].text_value.to_sym
|
22
23
|
if child.respond_to?(segment_name)
|
@@ -38,7 +39,7 @@ class Context
|
|
38
39
|
|
39
40
|
private
|
40
41
|
def assert_defined!(identifier:)
|
41
|
-
raise "Undefined variable: #{identifier}" unless
|
42
|
+
raise "Undefined variable: #{identifier}" unless symbols.has_key?(identifier.to_sym)
|
42
43
|
end
|
43
44
|
|
44
45
|
def assert_method_exists!(method:)
|
@@ -47,6 +48,8 @@ class Context
|
|
47
48
|
end
|
48
49
|
|
49
50
|
class EquationEngine
|
51
|
+
attr_accessor :parser, :context
|
52
|
+
|
50
53
|
def initialize(default: {}, methods: {})
|
51
54
|
@parser = EquationParser.new
|
52
55
|
@context = Context.new(default: default, methods: methods)
|
@@ -59,10 +62,17 @@ class EquationEngine
|
|
59
62
|
parsed_rule
|
60
63
|
end
|
61
64
|
|
65
|
+
def parse_and_eval(rule:)
|
66
|
+
parse(rule: rule).value(ctx: @context)
|
67
|
+
end
|
68
|
+
|
62
69
|
def eval(rule:)
|
63
|
-
|
64
|
-
|
70
|
+
rule.value(ctx: @context)
|
71
|
+
end
|
65
72
|
|
66
|
-
|
73
|
+
def eval_with(rule:, values: {})
|
74
|
+
rule.value(ctx: @context.tap { |x|
|
75
|
+
x.transient_symbols = values
|
76
|
+
})
|
67
77
|
end
|
68
78
|
end
|
data/lib/equation_grammar.rb
CHANGED
@@ -709,6 +709,8 @@ module Equation
|
|
709
709
|
base *= k.operand.value(ctx: ctx)
|
710
710
|
when '/'
|
711
711
|
base /= k.operand.value(ctx: ctx)
|
712
|
+
when '%'
|
713
|
+
base %= k.operand.value(ctx: ctx)
|
712
714
|
end
|
713
715
|
end
|
714
716
|
|
@@ -760,17 +762,29 @@ module Equation
|
|
760
762
|
r7 = SyntaxNode.new(input, (index-1)...index) if r7 == true
|
761
763
|
r5 = r7
|
762
764
|
else
|
763
|
-
|
764
|
-
|
765
|
+
if (match_len = has_terminal?('%', false, index))
|
766
|
+
r8 = true
|
767
|
+
@index += match_len
|
768
|
+
else
|
769
|
+
terminal_parse_failure('\'%\'')
|
770
|
+
r8 = nil
|
771
|
+
end
|
772
|
+
if r8
|
773
|
+
r8 = SyntaxNode.new(input, (index-1)...index) if r8 == true
|
774
|
+
r5 = r8
|
775
|
+
else
|
776
|
+
@index = i5
|
777
|
+
r5 = nil
|
778
|
+
end
|
765
779
|
end
|
766
780
|
end
|
767
781
|
s3 << r5
|
768
782
|
if r5
|
769
|
-
|
770
|
-
s3 <<
|
771
|
-
if
|
772
|
-
|
773
|
-
s3 <<
|
783
|
+
r9 = _nt_space
|
784
|
+
s3 << r9
|
785
|
+
if r9
|
786
|
+
r10 = _nt_standalone
|
787
|
+
s3 << r10
|
774
788
|
end
|
775
789
|
end
|
776
790
|
end
|
@@ -804,6 +818,27 @@ module Equation
|
|
804
818
|
r0
|
805
819
|
end
|
806
820
|
|
821
|
+
module Standalone0
|
822
|
+
def negate
|
823
|
+
elements[0]
|
824
|
+
end
|
825
|
+
|
826
|
+
def unit
|
827
|
+
elements[1]
|
828
|
+
end
|
829
|
+
end
|
830
|
+
|
831
|
+
module Standalone1
|
832
|
+
def value(ctx:)
|
833
|
+
base = unit.value(ctx: ctx)
|
834
|
+
negate.text_value.length.times {
|
835
|
+
base = !base
|
836
|
+
}
|
837
|
+
|
838
|
+
base
|
839
|
+
end
|
840
|
+
end
|
841
|
+
|
807
842
|
def _nt_standalone
|
808
843
|
start_index = index
|
809
844
|
if node_cache[:standalone].has_key?(index)
|
@@ -815,32 +850,61 @@ module Equation
|
|
815
850
|
return cached
|
816
851
|
end
|
817
852
|
|
818
|
-
i0 = index
|
819
|
-
|
820
|
-
|
821
|
-
|
822
|
-
|
823
|
-
|
824
|
-
|
853
|
+
i0, s0 = index, []
|
854
|
+
s1, i1 = [], index
|
855
|
+
loop do
|
856
|
+
if (match_len = has_terminal?('!', false, index))
|
857
|
+
r2 = true
|
858
|
+
@index += match_len
|
859
|
+
else
|
860
|
+
terminal_parse_failure('\'!\'')
|
861
|
+
r2 = nil
|
862
|
+
end
|
825
863
|
if r2
|
826
|
-
|
827
|
-
r0 = r2
|
864
|
+
s1 << r2
|
828
865
|
else
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
|
866
|
+
break
|
867
|
+
end
|
868
|
+
end
|
869
|
+
r1 = instantiate_node(SyntaxNode,input, i1...index, s1)
|
870
|
+
s0 << r1
|
871
|
+
if r1
|
872
|
+
i3 = index
|
873
|
+
r4 = _nt_symbol
|
874
|
+
if r4
|
875
|
+
r4 = SyntaxNode.new(input, (index-1)...index) if r4 == true
|
876
|
+
r3 = r4
|
877
|
+
else
|
878
|
+
r5 = _nt_method_call
|
879
|
+
if r5
|
880
|
+
r5 = SyntaxNode.new(input, (index-1)...index) if r5 == true
|
881
|
+
r3 = r5
|
833
882
|
else
|
834
|
-
|
835
|
-
if
|
836
|
-
|
837
|
-
|
883
|
+
r6 = _nt_literals
|
884
|
+
if r6
|
885
|
+
r6 = SyntaxNode.new(input, (index-1)...index) if r6 == true
|
886
|
+
r3 = r6
|
838
887
|
else
|
839
|
-
|
840
|
-
|
888
|
+
r7 = _nt_subexpression
|
889
|
+
if r7
|
890
|
+
r7 = SyntaxNode.new(input, (index-1)...index) if r7 == true
|
891
|
+
r3 = r7
|
892
|
+
else
|
893
|
+
@index = i3
|
894
|
+
r3 = nil
|
895
|
+
end
|
841
896
|
end
|
842
897
|
end
|
843
898
|
end
|
899
|
+
s0 << r3
|
900
|
+
end
|
901
|
+
if s0.last
|
902
|
+
r0 = instantiate_node(SyntaxNode,input, i0...index, s0)
|
903
|
+
r0.extend(Standalone0)
|
904
|
+
r0.extend(Standalone1)
|
905
|
+
else
|
906
|
+
@index = i0
|
907
|
+
r0 = nil
|
844
908
|
end
|
845
909
|
|
846
910
|
node_cache[:standalone][start_index] = r0
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: equation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- OMAR
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-08-
|
11
|
+
date: 2022-08-19 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Equation exposes a minimal environment to allow safe execution of Ruby
|
14
14
|
code represented via a custom expression language.
|
@@ -24,8 +24,7 @@ files:
|
|
24
24
|
homepage: https://github.com/ancat/equation
|
25
25
|
licenses:
|
26
26
|
- MIT
|
27
|
-
metadata:
|
28
|
-
rubygems_mfa_required: 'true'
|
27
|
+
metadata: {}
|
29
28
|
post_install_message:
|
30
29
|
rdoc_options: []
|
31
30
|
require_paths:
|