equation 0.5.0 → 0.6.0

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 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: