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 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
@@ -0,0 +1,8 @@
1
+ # Omakase Ruby styling for Rails
2
+ inherit_gem: { rubocop-rails-omakase: rubocop.yml }
3
+
4
+ # Overwrite or add rules to create your own house style
5
+ #
6
+ # # Use `[a, [b, c]]` not `[ a, [ b, c ] ]`
7
+ # Layout/SpaceInsideArrayLiteralBrackets:
8
+ # Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Buzz Logic - Change Log
2
+
3
+ ## [1.0.0] - 2025-06-16
4
+
5
+ - First version of BuzzLogic, extracted from FutureFund.
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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ task default: :test
@@ -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
@@ -0,0 +1,3 @@
1
+ module BuzzLogic
2
+ VERSION = "1.0.0"
3
+ 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: []