buzz_logic 1.0.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/.rubocop.yml +8 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +109 -0
- data/Rakefile +12 -0
- data/lib/buzz_logic/engine.rb +181 -0
- data/lib/buzz_logic/version.rb +3 -0
- data/lib/buzz_logic.rb +24 -0
- metadata +58 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: cc4c70ca95e3e3e8eed36b92f72f2668f7b5c05a1b0599b930e8caa84983bbd4
|
4
|
+
data.tar.gz: e073cd8be81197b5c3f418e0b6a2abaada5c8b3f665268c86e150a65aaed23a9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1ec7200ccb43f8fa26c63b30eb2ca5350d27c7308a7ab0bf9d0b15e829398d4bd1bbc8bfbd22d62f4c26b8ac6ef50f433df08e3c02ea35422f3e4d331b4be954
|
7
|
+
data.tar.gz: d753ba2cafb0636f904a6930d178de911794c9a14b0ba8c9e94fb2fd8b49c0a6235e27e0a161053f74c52028da5b21501d3060714928152105e144d93988cc4b
|
data/.rubocop.yml
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 FutureFund Technology, LLC.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
# BuzzLogic Rules Engine
|
2
|
+
|
3
|
+
Welcome to BuzzLogic, the rules engine that brings the power of dynamic, secure logic to your hive! Built by FutureFund for our online platform for K-12 school groups and PTAs, BuzzLogic makes it easy to evaluate rules without exposing your application to security risks.
|
4
|
+
|
5
|
+
It's the bee's knees for handling logic.
|
6
|
+
|
7
|
+
## Features
|
8
|
+
|
9
|
+
- Dynamic Rules: Define rules as simple strings (e.g., `user.age >= 21 and fundraiser.status == 'active'`).
|
10
|
+
- Secure by Design: Uses a custom parser and interpreter, avoiding `eval()` and other unsafe methods. Only allows predefined operations, keeping your hive safe.
|
11
|
+
- Context-Aware: Evaluate rules against a context of one or more objects from your application.
|
12
|
+
- Rich Operator Support: Includes standard comparison (`==`, `!=`, `<`, `>`, `<=`, `>=`) and logical (`and`, `or`) operators.
|
13
|
+
- Nested Attribute Access: Safely access nested object attributes (e.g., `user.school.mascot`).
|
14
|
+
- Extensible: Designed to be easy to extend with custom functions or operators.
|
15
|
+
- Thoroughly Tested: Comes with a comprehensive Minitest test suite.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
```
|
22
|
+
gem "buzz_logic"
|
23
|
+
```
|
24
|
+
|
25
|
+
And then execute:
|
26
|
+
|
27
|
+
```
|
28
|
+
$ bundle install
|
29
|
+
```
|
30
|
+
|
31
|
+
Or install it yourself as:
|
32
|
+
|
33
|
+
```
|
34
|
+
$ gem install buzz_logic
|
35
|
+
```
|
36
|
+
|
37
|
+
## Usage
|
38
|
+
|
39
|
+
The primary interface for the engine is the BuzzLogic::RulesEngine.evaluate method. It takes two arguments:
|
40
|
+
|
41
|
+
- `rule_string` (String): The rule you want to evaluate.
|
42
|
+
- `context` (Hash): A hash where keys are the names used in the rule (the "buzz words") and values are the corresponding objects.
|
43
|
+
|
44
|
+
### Basic Example
|
45
|
+
|
46
|
+
```ruby
|
47
|
+
require 'buzz_logic'
|
48
|
+
|
49
|
+
# Define some objects to evaluate against
|
50
|
+
student = OpenStruct.new(grade: 5, has_permission_slip: true)
|
51
|
+
fundraiser = OpenStruct.new(status: 'active', goal_amount: 500)
|
52
|
+
|
53
|
+
# The context maps the names used in the rule to the objects
|
54
|
+
context = {
|
55
|
+
'student' => student,
|
56
|
+
'fundraiser' => fundraiser
|
57
|
+
}
|
58
|
+
|
59
|
+
# Define a rule
|
60
|
+
rule = "student.grade >= 4 and fundraiser.status == 'active'"
|
61
|
+
|
62
|
+
# Evaluate the rule
|
63
|
+
result = BuzzLogic::RulesEngine.evaluate(rule, context)
|
64
|
+
|
65
|
+
puts "Is the student eligible? #{result}" # => true
|
66
|
+
```
|
67
|
+
|
68
|
+
## Supported Syntax
|
69
|
+
|
70
|
+
### Operands
|
71
|
+
|
72
|
+
- Literals:
|
73
|
+
- `String`: e.g., 'active', "Go Bees!"
|
74
|
+
- `Integer`: e.g., 5, 1000
|
75
|
+
- `Float`: e.g., 99.9
|
76
|
+
- `Boolean`: true, false
|
77
|
+
- `Nil`: nil
|
78
|
+
- Variables: Access object attributes using dot notation.
|
79
|
+
- `student.grade`
|
80
|
+
- `fundraiser.school.principal_name`
|
81
|
+
|
82
|
+
### Operators
|
83
|
+
|
84
|
+
- Comparison: `==`, `!=`, `<`, `<=`, `>`, `>=`
|
85
|
+
- Logical: `and`, `or`
|
86
|
+
|
87
|
+
Parentheses `()` can be used to group expressions and control precedence.
|
88
|
+
|
89
|
+
### Security
|
90
|
+
|
91
|
+
Security is the queen bee of BuzzLogic's design. Unlike approaches that use eval, BuzzLogic parses the rule into an Abstract Syntax Tree (AST) and then interprets it.
|
92
|
+
|
93
|
+
This means:
|
94
|
+
|
95
|
+
- **No Arbitrary Code Execution:** A rule like `system('rm -rf /')` will result in a parsing error, not a swarm of problems.
|
96
|
+
- **No Unsafe Method Calls:** A rule like `student.destroy` is impossible. The interpreter only allows attribute access on the provided context objects, not method calls.
|
97
|
+
|
98
|
+
## Development
|
99
|
+
After checking out the repo, run `bundle install` to install dependencies. Then, run `rake test` to run the tests.
|
100
|
+
|
101
|
+
To install this gem onto your local machine, run bundle exec `rake install`.
|
102
|
+
|
103
|
+
## Contributing
|
104
|
+
|
105
|
+
Bug reports and pull requests are welcome on GitHub. Let's build a sweeter future together!
|
106
|
+
|
107
|
+
## License
|
108
|
+
|
109
|
+
The gem is available as open source under the terms of the MIT License by FutureFund.
|
data/Rakefile
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
require "strscan"
|
2
|
+
|
3
|
+
module BuzzLogic
|
4
|
+
# The internal engine that parses and evaluates rules.
|
5
|
+
# This class is not meant to be used directly. Use BuzzLogic::RulesEngine.evaluate instead.
|
6
|
+
class Engine
|
7
|
+
# Tokenizer patterns
|
8
|
+
TOKEN_PATTERNS = {
|
9
|
+
float: /-?\d+\.\d+/,
|
10
|
+
integer: /-?\d+/,
|
11
|
+
string: /'(?:[^']|\\.)*'|"(?:[^"]|\\.)*"/,
|
12
|
+
boolean: /true|false/,
|
13
|
+
nil: /nil/,
|
14
|
+
identifier: /[a-zA-Z_][a-zA-Z0-9_]*/,
|
15
|
+
operator: /==|!=|<=|>=|<|>/,
|
16
|
+
logical: /and|or/,
|
17
|
+
lparen: /\(/,
|
18
|
+
rparen: /\)/,
|
19
|
+
dot: /\./,
|
20
|
+
space: /\s+/
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
# Operator precedence for the parser
|
24
|
+
PRECEDENCE = {
|
25
|
+
"or" => 1,
|
26
|
+
"and" => 2,
|
27
|
+
"==" => 3, "!=" => 3,
|
28
|
+
"<" => 4, "<=" => 4, ">" => 4, ">=" => 4
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
def initialize(context)
|
32
|
+
@context = context
|
33
|
+
@tokens = []
|
34
|
+
end
|
35
|
+
|
36
|
+
# Primary method to evaluate a rule string.
|
37
|
+
def evaluate(rule_string)
|
38
|
+
@tokens = tokenize(rule_string)
|
39
|
+
ast = parse
|
40
|
+
raise ParsingError, "Invalid or empty rule." if ast.nil?
|
41
|
+
eval_ast(ast)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Step 1: Tokenization
|
47
|
+
def tokenize(rule_string)
|
48
|
+
scanner = StringScanner.new(rule_string)
|
49
|
+
tokens = []
|
50
|
+
until scanner.eos?
|
51
|
+
match = nil
|
52
|
+
TOKEN_PATTERNS.each do |type, pattern|
|
53
|
+
if (match = scanner.scan(pattern))
|
54
|
+
tokens << { type: type, value: match } unless type == :space
|
55
|
+
break
|
56
|
+
end
|
57
|
+
end
|
58
|
+
raise ParsingError, "Unexpected character at: #{scanner.rest}" unless match || scanner.eos?
|
59
|
+
end
|
60
|
+
tokens
|
61
|
+
end
|
62
|
+
|
63
|
+
# Step 2: Parsing
|
64
|
+
def parse(precedence = 0)
|
65
|
+
left_node = parse_prefix
|
66
|
+
return nil unless left_node
|
67
|
+
|
68
|
+
while !@tokens.empty? && precedence < (PRECEDENCE[@tokens.first[:value]] || 0)
|
69
|
+
op_token = @tokens.shift
|
70
|
+
right_node = parse(PRECEDENCE[op_token[:value]])
|
71
|
+
left_node = { type: :binary_op, op: op_token[:value], left: left_node, right: right_node }
|
72
|
+
end
|
73
|
+
|
74
|
+
left_node
|
75
|
+
end
|
76
|
+
|
77
|
+
def parse_prefix
|
78
|
+
token = @tokens.shift
|
79
|
+
case token[:type]
|
80
|
+
when :integer, :float, :string, :boolean, :nil
|
81
|
+
parse_literal(token)
|
82
|
+
when :identifier
|
83
|
+
parse_variable(token)
|
84
|
+
when :lparen
|
85
|
+
parse_grouped_expression
|
86
|
+
else
|
87
|
+
raise ParsingError, "Unexpected token: #{token[:value]}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def parse_literal(token)
|
92
|
+
value = token[:value]
|
93
|
+
case token[:type]
|
94
|
+
when :integer then { type: :literal, value: value.to_i }
|
95
|
+
when :float then { type: :literal, value: value.to_f }
|
96
|
+
when :string then { type: :literal, value: value[1..-2].gsub(/\\./, { "\\'" => "'", '\\"' => '"' }) }
|
97
|
+
when :boolean then { type: :literal, value: value == "true" }
|
98
|
+
when :nil then { type: :literal, value: nil }
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def parse_variable(token)
|
103
|
+
node = { type: :variable, name: token[:value] }
|
104
|
+
while !@tokens.empty? && @tokens.first[:type] == :dot
|
105
|
+
@tokens.shift # consume dot
|
106
|
+
attr_token = @tokens.shift
|
107
|
+
raise ParsingError, "Expected identifier after '.'" unless attr_token && attr_token[:type] == :identifier
|
108
|
+
node = { type: :attribute_access, object: node, attribute: attr_token[:value] }
|
109
|
+
end
|
110
|
+
node
|
111
|
+
end
|
112
|
+
|
113
|
+
def parse_grouped_expression
|
114
|
+
node = parse(0)
|
115
|
+
raise ParsingError, "Expected ')' to close expression" if @tokens.shift&.dig(:type) != :rparen
|
116
|
+
node
|
117
|
+
end
|
118
|
+
|
119
|
+
# Step 3: AST Evaluation
|
120
|
+
def eval_ast(node)
|
121
|
+
return node[:value] if node[:type] == :literal
|
122
|
+
|
123
|
+
case node[:type]
|
124
|
+
when :variable
|
125
|
+
resolve_variable(node[:name])
|
126
|
+
when :attribute_access
|
127
|
+
object = eval_ast(node[:object])
|
128
|
+
resolve_attribute(object, node[:attribute])
|
129
|
+
when :binary_op
|
130
|
+
left_val = eval_ast(node[:left])
|
131
|
+
return true if node[:op] == "or" && left_val
|
132
|
+
return false if node[:op] == "and" && !left_val
|
133
|
+
right_val = eval_ast(node[:right])
|
134
|
+
perform_operation(node[:op], left_val, right_val)
|
135
|
+
else
|
136
|
+
raise EvaluationError, "Unknown AST node type: #{node[:type]}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def resolve_variable(name)
|
141
|
+
raise EvaluationError, "Undefined variable: '#{name}'" unless @context.key?(name)
|
142
|
+
@context[name]
|
143
|
+
end
|
144
|
+
|
145
|
+
def resolve_attribute(object, attribute)
|
146
|
+
raise EvaluationError, "Cannot access attribute on nil" if object.nil?
|
147
|
+
|
148
|
+
if object.respond_to?(attribute)
|
149
|
+
method = object.method(attribute)
|
150
|
+
if method.arity == 0
|
151
|
+
return method.call
|
152
|
+
else
|
153
|
+
raise EvaluationError, "Access to method '#{attribute}' with arguments is not allowed."
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
raise EvaluationError, "Cannot access '#{attribute}' on object of type #{object.class}"
|
158
|
+
rescue NoMethodError
|
159
|
+
raise EvaluationError, "Object does not have attribute '#{attribute}'"
|
160
|
+
rescue => e
|
161
|
+
raise EvaluationError, "An error occurred while accessing attribute '#{attribute}': #{e.message}"
|
162
|
+
end
|
163
|
+
|
164
|
+
def perform_operation(op, left, right)
|
165
|
+
case op
|
166
|
+
when "==" then left == right
|
167
|
+
when "!=" then left != right
|
168
|
+
when "<" then left < right
|
169
|
+
when "<=" then left <= right
|
170
|
+
when ">" then left > right
|
171
|
+
when ">=" then left >= right
|
172
|
+
when "and" then !!(left && right)
|
173
|
+
when "or" then !!(left || right)
|
174
|
+
else
|
175
|
+
raise EvaluationError, "Unknown operator: #{op}"
|
176
|
+
end
|
177
|
+
rescue ArgumentError => e
|
178
|
+
raise EvaluationError, "Type mismatch for operator '#{op}': #{e.message}"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
data/lib/buzz_logic.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative "buzz_logic/version"
|
2
|
+
require_relative "buzz_logic/engine"
|
3
|
+
|
4
|
+
# Main module for the BuzzLogic Rules Engine from FutureFund.
|
5
|
+
# The primary entry point is `BuzzLogic::RulesEngine.evaluate`.
|
6
|
+
module BuzzLogic
|
7
|
+
class Error < StandardError; end
|
8
|
+
class ParsingError < Error; end
|
9
|
+
class EvaluationError < Error; end
|
10
|
+
|
11
|
+
# The main interface for the rules engine.
|
12
|
+
module RulesEngine
|
13
|
+
# Evaluates a given rule string against a context of objects.
|
14
|
+
#
|
15
|
+
# @param rule_string [String] The rule to evaluate.
|
16
|
+
# @param context [Hash<String, Object>] A hash mapping names to objects.
|
17
|
+
# @return [Boolean] The result of the evaluation.
|
18
|
+
# @raise [BuzzLogic::ParsingError] if the rule has invalid syntax.
|
19
|
+
# @raise [BuzzLogic::EvaluationError] if an error occurs during evaluation.
|
20
|
+
def self.evaluate(rule_string, context)
|
21
|
+
Engine.new(context).evaluate(rule_string)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: buzz_logic
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Darian Shimy
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-06-17 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: BuzzLogic, created by FutureFund, allows dynamic rule evaluation against
|
14
|
+
application objects for platforms like our K-12 fundraising site. It avoids the
|
15
|
+
risks of arbitrary code execution by using a custom, secure parser.
|
16
|
+
email:
|
17
|
+
- dshimy@futurefund.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- ".rubocop.yml"
|
23
|
+
- CHANGELOG.md
|
24
|
+
- LICENSE.txt
|
25
|
+
- README.md
|
26
|
+
- Rakefile
|
27
|
+
- lib/buzz_logic.rb
|
28
|
+
- lib/buzz_logic/engine.rb
|
29
|
+
- lib/buzz_logic/version.rb
|
30
|
+
homepage: https://github.com/futurefund/buzz_logic
|
31
|
+
licenses:
|
32
|
+
- MIT
|
33
|
+
metadata:
|
34
|
+
allowed_push_host: https://rubygems.org
|
35
|
+
homepage_uri: https://github.com/futurefund/buzz_logic
|
36
|
+
source_code_uri: https://github.com/futurefund/buzz_logic
|
37
|
+
changelog_uri: https://github.com/FutureFund/buzz_logic/CHANGELOG.md
|
38
|
+
github_repo: ssh://github.com/FutureFund/buzz_logic
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options: []
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.1.0
|
48
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '0'
|
53
|
+
requirements: []
|
54
|
+
rubygems_version: 3.5.18
|
55
|
+
signing_key:
|
56
|
+
specification_version: 4
|
57
|
+
summary: A simple, powerful, and secure rules engine for busy bees.
|
58
|
+
test_files: []
|