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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4f1469e22a3d098dfcb0c2ca974a2d081c0c7be90afddb20fea36f65bdc0804
4
- data.tar.gz: 5925da1a74a22bfe8e1cc1b7f4dd43a61cf02b05a61ca2a15821f184ff45e5a7
3
+ metadata.gz: e5a5da51d16c1c12ef567b1842c964e65dfb7729836770685270767674f8a532
4
+ data.tar.gz: 4b6f537c8f047cced5414b133cf8dd8749b92faa0930548cecf5b192ba96e122
5
5
  SHA512:
6
- metadata.gz: 5ef71408034ed55d48efadd5b99c6e3c41ae49680b673f658b5fa0689731834a0260e4810c3a189f670b97c6451845b3c082ef9ad82ff65c813de5b13a6dee27
7
- data.tar.gz: 595552e82f8f4412b0a379025883669b0f05e1199948de2ed1221c14092a17cd2b296f396ad2e3e0091e1095307b392904866457222d21895c0c7b66978dda87
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 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
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 @symbol_table.has_key?(identifier.to_sym)
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
- parsed_rule = @parser.parse(rule)
64
- raise "Parse Error: #{rule}" unless parsed_rule
70
+ rule.value(ctx: @context)
71
+ end
65
72
 
66
- parsed_rule.value(ctx: @context)
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
@@ -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.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-08 00:00:00.000000000 Z
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: