overule 0.1.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/CHANGELOG.md +80 -0
- data/README.md +302 -0
- data/app/assets/javascripts/overule/builder.js +268 -0
- data/app/controllers/overule/activities_controller.rb +10 -0
- data/app/controllers/overule/application_controller.rb +35 -0
- data/app/controllers/overule/rule_versions_controller.rb +24 -0
- data/app/controllers/overule/rules_controller.rb +60 -0
- data/app/models/concerns/overule/rule_activity_behavior.rb +24 -0
- data/app/models/concerns/overule/rule_behavior.rb +106 -0
- data/app/models/concerns/overule/rule_version_behavior.rb +15 -0
- data/app/models/overule/current.rb +7 -0
- data/app/models/overule/rule.rb +48 -0
- data/app/models/overule/rule_activity.rb +40 -0
- data/app/models/overule/rule_version.rb +42 -0
- data/app/views/layouts/overule/application.html.erb +39 -0
- data/app/views/overule/activities/_activity.html.erb +56 -0
- data/app/views/overule/activities/index.html.erb +30 -0
- data/app/views/overule/rule_versions/index.html.erb +44 -0
- data/app/views/overule/rule_versions/show.html.erb +55 -0
- data/app/views/overule/rules/_form.html.erb +95 -0
- data/app/views/overule/rules/_group.html.erb +106 -0
- data/app/views/overule/rules/_static_node.html.erb +79 -0
- data/app/views/overule/rules/edit.html.erb +2 -0
- data/app/views/overule/rules/index.html.erb +45 -0
- data/app/views/overule/rules/new.html.erb +2 -0
- data/app/views/overule/rules/show.html.erb +54 -0
- data/config/routes.rb +8 -0
- data/lib/generators/overule/install/USAGE +25 -0
- data/lib/generators/overule/install/install_generator.rb +64 -0
- data/lib/generators/overule/install/templates/add_rule_version_to_overule_rule_activities.rb.tt +21 -0
- data/lib/generators/overule/install/templates/create_overule_rule_activities.rb.tt +15 -0
- data/lib/generators/overule/install/templates/create_overule_rule_versions.rb.tt +28 -0
- data/lib/generators/overule/install/templates/create_overule_rules.rb.tt +13 -0
- data/lib/generators/overule/install/templates/overule.rb.tt +35 -0
- data/lib/overule/action.rb +22 -0
- data/lib/overule/condition.rb +40 -0
- data/lib/overule/configuration.rb +75 -0
- data/lib/overule/context.rb +22 -0
- data/lib/overule/engine.rb +7 -0
- data/lib/overule/inference.rb +38 -0
- data/lib/overule/operator.rb +25 -0
- data/lib/overule/version.rb +3 -0
- data/lib/overule.rb +13 -0
- metadata +103 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Overule
|
|
2
|
+
# Evaluates conditions defined in rules against a given context
|
|
3
|
+
# Conditions are used to determine whether actions should be executed
|
|
4
|
+
# based on the state of variables in the context
|
|
5
|
+
class Condition
|
|
6
|
+
NUMERIC_DATATYPES = %w[number integer float decimal].freeze
|
|
7
|
+
ORDERING_OPS = %w[gt lt gte lte range].freeze
|
|
8
|
+
|
|
9
|
+
def self.evaluate(conditions, context)
|
|
10
|
+
return [] if conditions.empty?
|
|
11
|
+
|
|
12
|
+
conditions.map do |condition|
|
|
13
|
+
actual = context.get(condition["var"])
|
|
14
|
+
expected = condition["value"]
|
|
15
|
+
actual, expected = coerce(actual, expected, condition["datatype"], condition["op"])
|
|
16
|
+
|
|
17
|
+
Operator.operate(condition["op"], actual, expected)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Coerce numeric strings before ordering comparisons so that
|
|
22
|
+
# "1220000" > "2000000" doesn't compare lexically.
|
|
23
|
+
def self.coerce(actual, expected, datatype, op)
|
|
24
|
+
return [actual, expected] unless ORDERING_OPS.include?(op)
|
|
25
|
+
return [actual, expected] unless NUMERIC_DATATYPES.include?(datatype)
|
|
26
|
+
|
|
27
|
+
[to_number(actual), to_number(expected)]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.to_number(value)
|
|
31
|
+
return value if value.is_a?(Numeric)
|
|
32
|
+
return value.map { |v| to_number(v) } if value.is_a?(Array)
|
|
33
|
+
return nil if value.nil?
|
|
34
|
+
|
|
35
|
+
Float(value.to_s)
|
|
36
|
+
rescue ArgumentError, TypeError
|
|
37
|
+
value
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require "active_support/security_utils"
|
|
2
|
+
|
|
3
|
+
module Overule
|
|
4
|
+
# Host-app configuration for the Overule gem. Populate via the initializer
|
|
5
|
+
# the install generator drops at `config/initializers/overule.rb`, or call
|
|
6
|
+
# `Overule.configure { |c| ... }` directly from non-Rails code.
|
|
7
|
+
class Configuration
|
|
8
|
+
SUPPORTED_ORMS = %i[active_record mongoid].freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :orm
|
|
11
|
+
attr_accessor :actor_proc,
|
|
12
|
+
:http_basic_auth,
|
|
13
|
+
:http_basic_auth_username,
|
|
14
|
+
:http_basic_auth_password
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@orm = :active_record
|
|
18
|
+
@actor_proc = nil
|
|
19
|
+
@http_basic_auth = false
|
|
20
|
+
@http_basic_auth_username = nil
|
|
21
|
+
@http_basic_auth_password = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def http_basic_auth_configured?
|
|
25
|
+
!!http_basic_auth
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns true iff the given credentials match the configured username and
|
|
29
|
+
# password. Uses constant-time comparison on both fields (always evaluating
|
|
30
|
+
# both with bitwise `&`) so the response time can't reveal which side
|
|
31
|
+
# mismatched. Raises if auth is toggled on but credentials weren't set.
|
|
32
|
+
def http_basic_auth_matches?(username, password)
|
|
33
|
+
return true unless http_basic_auth_configured?
|
|
34
|
+
|
|
35
|
+
if http_basic_auth_username.nil? || http_basic_auth_password.nil?
|
|
36
|
+
raise ArgumentError,
|
|
37
|
+
"Overule.config.http_basic_auth is true but http_basic_auth_username " \
|
|
38
|
+
"and/or http_basic_auth_password are not set"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
ActiveSupport::SecurityUtils.secure_compare(username.to_s, http_basic_auth_username.to_s) &
|
|
42
|
+
ActiveSupport::SecurityUtils.secure_compare(password.to_s, http_basic_auth_password.to_s)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def orm=(value)
|
|
46
|
+
sym = value.to_sym
|
|
47
|
+
unless SUPPORTED_ORMS.include?(sym)
|
|
48
|
+
raise ArgumentError, "Unsupported ORM #{value.inspect}. Supported: #{SUPPORTED_ORMS.inspect}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
@orm = sym
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def actor_for(controller)
|
|
55
|
+
return nil unless actor_proc.respond_to?(:call)
|
|
56
|
+
|
|
57
|
+
actor_proc.call(controller)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class << self
|
|
62
|
+
def config
|
|
63
|
+
@config ||= Configuration.new
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def configure
|
|
67
|
+
yield(config)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Test/reset hook — clears any host-app configuration. Internal use.
|
|
71
|
+
def reset_configuration!
|
|
72
|
+
@config = Configuration.new
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "active_support/core_ext/hash/indifferent_access"
|
|
2
|
+
|
|
3
|
+
module Overule
|
|
4
|
+
# Maintains the state of facts and variables used in rule evaluation
|
|
5
|
+
# Provides methods to get and set values in the context
|
|
6
|
+
# Uses HashWithIndifferentAccess for flexible key access
|
|
7
|
+
class Context
|
|
8
|
+
attr_reader :facts
|
|
9
|
+
|
|
10
|
+
def initialize(facts = {})
|
|
11
|
+
@facts = HashWithIndifferentAccess.new(facts)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def get(key)
|
|
15
|
+
@facts[key]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def set(key, value)
|
|
19
|
+
@facts[key] = value
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require "active_support/hash_with_indifferent_access"
|
|
2
|
+
|
|
3
|
+
module Overule
|
|
4
|
+
# Main class for executing business rules
|
|
5
|
+
# Takes a rule set and facts, evaluates conditions,
|
|
6
|
+
# and executes corresponding actions when conditions are met
|
|
7
|
+
class Inference
|
|
8
|
+
def initialize(rule, facts)
|
|
9
|
+
@rules = HashWithIndifferentAccess.new(rule)
|
|
10
|
+
@ctx = Context.new(facts)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def infer
|
|
14
|
+
return unless @rules["when"] && evaluate(@rules["when"])
|
|
15
|
+
|
|
16
|
+
action = Action.new(@rules["then"], @ctx)
|
|
17
|
+
action.fire
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def evaluate(rule)
|
|
23
|
+
cond = Array(rule["cond"])
|
|
24
|
+
set = Array(rule["set"])
|
|
25
|
+
|
|
26
|
+
return true if cond.empty? && set.empty?
|
|
27
|
+
|
|
28
|
+
results = Condition.evaluate(cond, @ctx) + set.map { |sub| evaluate(sub) }
|
|
29
|
+
|
|
30
|
+
case rule["op"]&.downcase
|
|
31
|
+
when "and" then results.all?
|
|
32
|
+
when "or" then results.any?
|
|
33
|
+
else
|
|
34
|
+
raise Overule::Error, "Unknown logical operator: #{rule["op"].inspect}"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Overule
|
|
2
|
+
# Provides comparison operations for rule evaluation
|
|
3
|
+
# Supports various operators like equality, inequality,
|
|
4
|
+
# greater/less than, inclusion, and range checking
|
|
5
|
+
class Operator
|
|
6
|
+
OPERATORS = {
|
|
7
|
+
"eq" => ->(a, b) { a == b },
|
|
8
|
+
"neq" => ->(a, b) { a != b },
|
|
9
|
+
"gt" => ->(a, b) { a > b },
|
|
10
|
+
"lt" => ->(a, b) { a < b },
|
|
11
|
+
"gte" => ->(a, b) { a >= b },
|
|
12
|
+
"lte" => ->(a, b) { a <= b },
|
|
13
|
+
"in" => ->(a, b) { b.include?(a) },
|
|
14
|
+
"nin" => ->(a, b) { !b.include?(a) },
|
|
15
|
+
"contains" => ->(a, b) { a.include?(b) },
|
|
16
|
+
"range" => ->(a, b) { a >= b.first && a <= b.last }
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def self.operate(operator, actual, expected)
|
|
20
|
+
raise NoMethodError, "Unsupported operator: #{operator}" unless OPERATORS.key?(operator)
|
|
21
|
+
|
|
22
|
+
OPERATORS[operator].call(actual, expected)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/overule.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require_relative "overule/version"
|
|
2
|
+
require_relative "overule/configuration"
|
|
3
|
+
require_relative "overule/context"
|
|
4
|
+
require_relative "overule/operator"
|
|
5
|
+
require_relative "overule/condition"
|
|
6
|
+
require_relative "overule/action"
|
|
7
|
+
require_relative "overule/inference"
|
|
8
|
+
|
|
9
|
+
module Overule
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
require_relative "overule/engine" if defined?(Rails::Engine)
|
metadata
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: overule
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- bugloper
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '8.0'
|
|
26
|
+
description: Overule is a rule engine designed to help developers define, manage,
|
|
27
|
+
and evaluate logical rules and conditions in their Ruby applications.
|
|
28
|
+
email:
|
|
29
|
+
- bugloper@gmail.com
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- CHANGELOG.md
|
|
35
|
+
- README.md
|
|
36
|
+
- app/assets/javascripts/overule/builder.js
|
|
37
|
+
- app/controllers/overule/activities_controller.rb
|
|
38
|
+
- app/controllers/overule/application_controller.rb
|
|
39
|
+
- app/controllers/overule/rule_versions_controller.rb
|
|
40
|
+
- app/controllers/overule/rules_controller.rb
|
|
41
|
+
- app/models/concerns/overule/rule_activity_behavior.rb
|
|
42
|
+
- app/models/concerns/overule/rule_behavior.rb
|
|
43
|
+
- app/models/concerns/overule/rule_version_behavior.rb
|
|
44
|
+
- app/models/overule/current.rb
|
|
45
|
+
- app/models/overule/rule.rb
|
|
46
|
+
- app/models/overule/rule_activity.rb
|
|
47
|
+
- app/models/overule/rule_version.rb
|
|
48
|
+
- app/views/layouts/overule/application.html.erb
|
|
49
|
+
- app/views/overule/activities/_activity.html.erb
|
|
50
|
+
- app/views/overule/activities/index.html.erb
|
|
51
|
+
- app/views/overule/rule_versions/index.html.erb
|
|
52
|
+
- app/views/overule/rule_versions/show.html.erb
|
|
53
|
+
- app/views/overule/rules/_form.html.erb
|
|
54
|
+
- app/views/overule/rules/_group.html.erb
|
|
55
|
+
- app/views/overule/rules/_static_node.html.erb
|
|
56
|
+
- app/views/overule/rules/edit.html.erb
|
|
57
|
+
- app/views/overule/rules/index.html.erb
|
|
58
|
+
- app/views/overule/rules/new.html.erb
|
|
59
|
+
- app/views/overule/rules/show.html.erb
|
|
60
|
+
- config/routes.rb
|
|
61
|
+
- lib/generators/overule/install/USAGE
|
|
62
|
+
- lib/generators/overule/install/install_generator.rb
|
|
63
|
+
- lib/generators/overule/install/templates/add_rule_version_to_overule_rule_activities.rb.tt
|
|
64
|
+
- lib/generators/overule/install/templates/create_overule_rule_activities.rb.tt
|
|
65
|
+
- lib/generators/overule/install/templates/create_overule_rule_versions.rb.tt
|
|
66
|
+
- lib/generators/overule/install/templates/create_overule_rules.rb.tt
|
|
67
|
+
- lib/generators/overule/install/templates/overule.rb.tt
|
|
68
|
+
- lib/overule.rb
|
|
69
|
+
- lib/overule/action.rb
|
|
70
|
+
- lib/overule/condition.rb
|
|
71
|
+
- lib/overule/configuration.rb
|
|
72
|
+
- lib/overule/context.rb
|
|
73
|
+
- lib/overule/engine.rb
|
|
74
|
+
- lib/overule/inference.rb
|
|
75
|
+
- lib/overule/operator.rb
|
|
76
|
+
- lib/overule/version.rb
|
|
77
|
+
homepage: https://github.com/SELISEdigitalplatforms/overule.git
|
|
78
|
+
licenses:
|
|
79
|
+
- MIT
|
|
80
|
+
metadata:
|
|
81
|
+
allowed_push_host: https://rubygems.org
|
|
82
|
+
homepage_uri: https://github.com/SELISEdigitalplatforms/overule.git
|
|
83
|
+
source_code_uri: https://github.com/SELISEdigitalplatforms/overule.git
|
|
84
|
+
changelog_uri: https://github.com/SELISEdigitalplatforms/overule/blob/main/CHANGELOG.md
|
|
85
|
+
rubygems_mfa_required: 'true'
|
|
86
|
+
rdoc_options: []
|
|
87
|
+
require_paths:
|
|
88
|
+
- lib
|
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: 3.0.0
|
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '0'
|
|
99
|
+
requirements: []
|
|
100
|
+
rubygems_version: 3.7.2
|
|
101
|
+
specification_version: 4
|
|
102
|
+
summary: A powerful and customizable rule engine for Ruby applications.
|
|
103
|
+
test_files: []
|