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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4f1469e22a3d098dfcb0c2ca974a2d081c0c7be90afddb20fea36f65bdc0804
4
- data.tar.gz: 5925da1a74a22bfe8e1cc1b7f4dd43a61cf02b05a61ca2a15821f184ff45e5a7
3
+ metadata.gz: bc3a0622cdb38f0db990030f8ec8b9c70fec671e1dbe5d26167787e272ed4bbc
4
+ data.tar.gz: 1c4ca3a83d2cb17b0fb954f84ed53ae6f919aef866a05cb87ea219092cf13165
5
5
  SHA512:
6
- metadata.gz: 5ef71408034ed55d48efadd5b99c6e3c41ae49680b673f658b5fa0689731834a0260e4810c3a189f670b97c6451845b3c082ef9ad82ff65c813de5b13a6dee27
7
- data.tar.gz: 595552e82f8f4412b0a379025883669b0f05e1199948de2ed1221c14092a17cd2b296f396ad2e3e0091e1095307b392904866457222d21895c0c7b66978dda87
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 set(identifier:, value:)
12
- @symbol_table[identifier.to_sym] = value
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
- @symbol_table[identifier.to_sym]
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.to_sym
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
- raise "no"
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 @symbol_table.has_key?(identifier.to_sym)
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
- parsed_rule = @parser.parse(rule)
64
- raise "Parse Error: #{rule}" unless parsed_rule
72
+ rule.value(ctx: @context)
73
+ end
65
74
 
66
- parsed_rule.value(ctx: @context)
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
@@ -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
- @index = i5
764
- r5 = nil
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
- r8 = _nt_space
770
- s3 << r8
771
- if r8
772
- r9 = _nt_standalone
773
- s3 << r9
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
- r1 = _nt_symbol
820
- if r1
821
- r1 = SyntaxNode.new(input, (index-1)...index) if r1 == true
822
- r0 = r1
823
- else
824
- r2 = _nt_method_call
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
- r2 = SyntaxNode.new(input, (index-1)...index) if r2 == true
827
- r0 = r2
864
+ s1 << r2
828
865
  else
829
- r3 = _nt_literals
830
- if r3
831
- r3 = SyntaxNode.new(input, (index-1)...index) if r3 == true
832
- r0 = r3
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
- r4 = _nt_subexpression
835
- if r4
836
- r4 = SyntaxNode.new(input, (index-1)...index) if r4 == true
837
- r0 = r4
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
- @index = i0
840
- r0 = nil
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.5.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: 2022-08-08 00:00:00.000000000 Z
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.3.19
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.