equation 0.5.0 → 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +60 -0
- data/lib/equation.rb +27 -15
- data/lib/equation_grammar.rb +90 -26
- metadata +4 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bc3a0622cdb38f0db990030f8ec8b9c70fec671e1dbe5d26167787e272ed4bbc
|
4
|
+
data.tar.gz: 1c4ca3a83d2cb17b0fb954f84ed53ae6f919aef866a05cb87ea219092cf13165
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: edf2c16a123be3141a6757b95423fae213ee3b5028f48ea00fab72a75cb5bf1bd3d19ba4f7509466b9f8bfc4a5f572e0781beff8d5d152dc9b7d0d9e660dff8d
|
7
|
+
data.tar.gz: e3d07dc903426b9d718b0cd4d32cfefd08e2eeb09ab5069079fc95100302a74f0ae526a96d618827810d4834a0cbe5e98f7371b43ef20916f264f3aba423a109
|
data/README.md
CHANGED
@@ -10,6 +10,10 @@ 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
|
+
## Demos
|
14
|
+
|
15
|
+
Take a look at the examples directory for some demonstrations of Equation's capabilities. The main example is a [web application firewall](https://github.com/ancat/equation/tree/main/examples/rails_waf) for Rails; rules can be managed from any ActiveStorage compatible backend (e.g. filesystem, Amazon AWS, etc) and updated independently of application code, allowing for faster and more expressive blocking logic without having to wait for a deploy to go through.
|
16
|
+
|
13
17
|
## Example
|
14
18
|
|
15
19
|
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 +33,59 @@ if suspicious_request
|
|
29
33
|
# log some things, notify some people
|
30
34
|
end
|
31
35
|
```
|
36
|
+
|
37
|
+
## Language Features
|
38
|
+
|
39
|
+
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).
|
40
|
+
|
41
|
+
### Literals
|
42
|
+
|
43
|
+
* Strings: double quotes, e.g. `"hello world"`
|
44
|
+
* Numbers: all treated as floats, e.g. `0`, `-10`, `0.5`
|
45
|
+
* Arrays: square brackets, e.g. `[403, 404]` or `["yes", "no", "maybe"]`; can be mixed types
|
46
|
+
* Booleans: `true`, `false`
|
47
|
+
* Null: `nil`
|
48
|
+
|
49
|
+
### Variables
|
50
|
+
|
51
|
+
Variables are only made available to the engine at initialization. For example, given this setup code:
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
engine = EquationEngine.new(default: {name: "OMAR", age: 12})
|
55
|
+
```
|
56
|
+
|
57
|
+
These variables and all their properties are accessible from within rules:
|
58
|
+
|
59
|
+
```
|
60
|
+
$name == "OMAR" # true
|
61
|
+
$name.length # 4
|
62
|
+
$name.reverse # RAMO
|
63
|
+
```
|
64
|
+
|
65
|
+
### Methods
|
66
|
+
|
67
|
+
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.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
engine = EquationEngine.new(default: {age: 12}, methods: {is_even: ->(n) {n%2==0}})
|
71
|
+
```
|
72
|
+
|
73
|
+
`is_even` can now be called as follows:
|
74
|
+
|
75
|
+
```
|
76
|
+
is_even(5) # false
|
77
|
+
is_even($age) # true
|
78
|
+
```
|
79
|
+
|
80
|
+
### Comparisons
|
81
|
+
|
82
|
+
```
|
83
|
+
$name == "Dumpling" && $age >= 12
|
84
|
+
$name in ["Dumpling", "Meatball"] || $age == 12
|
85
|
+
```
|
86
|
+
|
87
|
+
## Development
|
88
|
+
|
89
|
+
* 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/).
|
90
|
+
* Run `bundle exec rake build_grammar` to generate the corresponding parser Ruby code.
|
91
|
+
* Run `bundle exec rake spec` to run tests.
|
data/lib/equation.rb
CHANGED
@@ -3,28 +3,31 @@ 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
|
-
segment_name = segment.elements[1].text_value
|
22
|
-
if child.respond_to?(segment_name)
|
23
|
-
child = child.send(segment_name)
|
24
|
-
elsif child.include?(segment_name)
|
25
|
-
child = child[segment_name]
|
22
|
+
segment_name = segment.elements[1].text_value
|
23
|
+
if child.respond_to?(segment_name.to_sym)
|
24
|
+
child = child.send(segment_name.to_sym)
|
25
|
+
elsif child.is_a? Hash and child.include?(segment_name.to_sym)
|
26
|
+
child = child[segment_name.to_sym]
|
27
|
+
elsif child.is_a? Hash and child.include?(segment_name.to_s)
|
28
|
+
child = child[segment_name.to_s]
|
26
29
|
else
|
27
|
-
|
30
|
+
return nil
|
28
31
|
end
|
29
32
|
}
|
30
33
|
|
@@ -38,7 +41,7 @@ class Context
|
|
38
41
|
|
39
42
|
private
|
40
43
|
def assert_defined!(identifier:)
|
41
|
-
raise "Undefined variable: #{identifier}" unless
|
44
|
+
raise "Undefined variable: #{identifier}" unless symbols.has_key?(identifier.to_sym)
|
42
45
|
end
|
43
46
|
|
44
47
|
def assert_method_exists!(method:)
|
@@ -47,6 +50,8 @@ class Context
|
|
47
50
|
end
|
48
51
|
|
49
52
|
class EquationEngine
|
53
|
+
attr_accessor :parser, :context
|
54
|
+
|
50
55
|
def initialize(default: {}, methods: {})
|
51
56
|
@parser = EquationParser.new
|
52
57
|
@context = Context.new(default: default, methods: methods)
|
@@ -59,10 +64,17 @@ class EquationEngine
|
|
59
64
|
parsed_rule
|
60
65
|
end
|
61
66
|
|
67
|
+
def parse_and_eval(rule:)
|
68
|
+
parse(rule: rule).value(ctx: @context)
|
69
|
+
end
|
70
|
+
|
62
71
|
def eval(rule:)
|
63
|
-
|
64
|
-
|
72
|
+
rule.value(ctx: @context)
|
73
|
+
end
|
65
74
|
|
66
|
-
|
75
|
+
def eval_with(rule:, values: {})
|
76
|
+
rule.value(ctx: @context.tap { |x|
|
77
|
+
x.transient_symbols = values
|
78
|
+
})
|
67
79
|
end
|
68
80
|
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.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- OMAR
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-01-29 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:
|
@@ -41,7 +40,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
41
40
|
- !ruby/object:Gem::Version
|
42
41
|
version: '0'
|
43
42
|
requirements: []
|
44
|
-
rubygems_version: 3.
|
43
|
+
rubygems_version: 3.1.6
|
45
44
|
signing_key:
|
46
45
|
specification_version: 4
|
47
46
|
summary: A rules engine for your Ruby apps.
|