equation 0.5.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 +7 -0
- data/README.md +31 -0
- data/lib/equation.rb +68 -0
- data/lib/equation_grammar.rb +1977 -0
- data/lib/equation_node_classes.rb +19 -0
- metadata +48 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f4f1469e22a3d098dfcb0c2ca974a2d081c0c7be90afddb20fea36f65bdc0804
|
4
|
+
data.tar.gz: 5925da1a74a22bfe8e1cc1b7f4dd43a61cf02b05a61ca2a15821f184ff45e5a7
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5ef71408034ed55d48efadd5b99c6e3c41ae49680b673f658b5fa0689731834a0260e4810c3a189f670b97c6451845b3c082ef9ad82ff65c813de5b13a6dee27
|
7
|
+
data.tar.gz: 595552e82f8f4412b0a379025883669b0f05e1199948de2ed1221c14092a17cd2b296f396ad2e3e0091e1095307b392904866457222d21895c0c7b66978dda87
|
data/README.md
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Equation [WIP]
|
2
|
+
|
3
|
+
A rules engine for your Ruby app! Use a constrained and relatively safe language to express logic for your Ruby app, without having to write Ruby. This allows you to use text (e.g. in a configuration file or database) to store logic in a way that can be updated independently from your code faster than it takes for a deploy without opening extra security vulnerabilities.
|
4
|
+
|
5
|
+
Use cases include:
|
6
|
+
|
7
|
+
* writing rules to describe HTTP traffic that then gets dropped, like a WAF
|
8
|
+
* writing policies to express authorization logic
|
9
|
+
* etc
|
10
|
+
|
11
|
+
Modeled loosely after [Symfony Expression Language](https://symfony.com/doc/current/components/expression_language.html).
|
12
|
+
|
13
|
+
## Example
|
14
|
+
|
15
|
+
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.
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
require 'equation'
|
19
|
+
|
20
|
+
# set up the execution environment and give it access to the rails request object
|
21
|
+
engine = EquationEngine.new(default: {
|
22
|
+
age: 12,
|
23
|
+
username: "OMAR",
|
24
|
+
request: request
|
25
|
+
})
|
26
|
+
|
27
|
+
suspicious_request = engine.eval(rule: '$request.path == "/api/login" && $request.remote_ip == "1.2.3.4" && $username == "OMAR"')
|
28
|
+
if suspicious_request
|
29
|
+
# log some things, notify some people
|
30
|
+
end
|
31
|
+
```
|
data/lib/equation.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'treetop'
|
2
|
+
require_relative 'equation_node_classes'
|
3
|
+
require_relative 'equation_grammar'
|
4
|
+
|
5
|
+
class Context
|
6
|
+
def initialize(default: {}, methods: {})
|
7
|
+
@symbol_table = default
|
8
|
+
@methods = methods
|
9
|
+
end
|
10
|
+
|
11
|
+
def set(identifier:, value:)
|
12
|
+
@symbol_table[identifier.to_sym] = value
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(identifier:, path: {})
|
16
|
+
assert_defined!(identifier: identifier)
|
17
|
+
@symbol_table[identifier.to_sym]
|
18
|
+
root = @symbol_table[identifier.to_sym]
|
19
|
+
child = root
|
20
|
+
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]
|
26
|
+
else
|
27
|
+
raise "no"
|
28
|
+
end
|
29
|
+
}
|
30
|
+
|
31
|
+
child
|
32
|
+
end
|
33
|
+
|
34
|
+
def call(method:, args:)
|
35
|
+
assert_method_exists!(method: method)
|
36
|
+
@methods[method.to_sym].call(*args)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def assert_defined!(identifier:)
|
41
|
+
raise "Undefined variable: #{identifier}" unless @symbol_table.has_key?(identifier.to_sym)
|
42
|
+
end
|
43
|
+
|
44
|
+
def assert_method_exists!(method:)
|
45
|
+
raise "Undefined method: #{method}" unless @methods.has_key?(method.to_sym)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class EquationEngine
|
50
|
+
def initialize(default: {}, methods: {})
|
51
|
+
@parser = EquationParser.new
|
52
|
+
@context = Context.new(default: default, methods: methods)
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse(rule:)
|
56
|
+
parsed_rule = @parser.parse(rule)
|
57
|
+
raise "Parse Error: #{@parser.failure_reason}" unless parsed_rule
|
58
|
+
|
59
|
+
parsed_rule
|
60
|
+
end
|
61
|
+
|
62
|
+
def eval(rule:)
|
63
|
+
parsed_rule = @parser.parse(rule)
|
64
|
+
raise "Parse Error: #{rule}" unless parsed_rule
|
65
|
+
|
66
|
+
parsed_rule.value(ctx: @context)
|
67
|
+
end
|
68
|
+
end
|