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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +80 -0
  3. data/README.md +302 -0
  4. data/app/assets/javascripts/overule/builder.js +268 -0
  5. data/app/controllers/overule/activities_controller.rb +10 -0
  6. data/app/controllers/overule/application_controller.rb +35 -0
  7. data/app/controllers/overule/rule_versions_controller.rb +24 -0
  8. data/app/controllers/overule/rules_controller.rb +60 -0
  9. data/app/models/concerns/overule/rule_activity_behavior.rb +24 -0
  10. data/app/models/concerns/overule/rule_behavior.rb +106 -0
  11. data/app/models/concerns/overule/rule_version_behavior.rb +15 -0
  12. data/app/models/overule/current.rb +7 -0
  13. data/app/models/overule/rule.rb +48 -0
  14. data/app/models/overule/rule_activity.rb +40 -0
  15. data/app/models/overule/rule_version.rb +42 -0
  16. data/app/views/layouts/overule/application.html.erb +39 -0
  17. data/app/views/overule/activities/_activity.html.erb +56 -0
  18. data/app/views/overule/activities/index.html.erb +30 -0
  19. data/app/views/overule/rule_versions/index.html.erb +44 -0
  20. data/app/views/overule/rule_versions/show.html.erb +55 -0
  21. data/app/views/overule/rules/_form.html.erb +95 -0
  22. data/app/views/overule/rules/_group.html.erb +106 -0
  23. data/app/views/overule/rules/_static_node.html.erb +79 -0
  24. data/app/views/overule/rules/edit.html.erb +2 -0
  25. data/app/views/overule/rules/index.html.erb +45 -0
  26. data/app/views/overule/rules/new.html.erb +2 -0
  27. data/app/views/overule/rules/show.html.erb +54 -0
  28. data/config/routes.rb +8 -0
  29. data/lib/generators/overule/install/USAGE +25 -0
  30. data/lib/generators/overule/install/install_generator.rb +64 -0
  31. data/lib/generators/overule/install/templates/add_rule_version_to_overule_rule_activities.rb.tt +21 -0
  32. data/lib/generators/overule/install/templates/create_overule_rule_activities.rb.tt +15 -0
  33. data/lib/generators/overule/install/templates/create_overule_rule_versions.rb.tt +28 -0
  34. data/lib/generators/overule/install/templates/create_overule_rules.rb.tt +13 -0
  35. data/lib/generators/overule/install/templates/overule.rb.tt +35 -0
  36. data/lib/overule/action.rb +22 -0
  37. data/lib/overule/condition.rb +40 -0
  38. data/lib/overule/configuration.rb +75 -0
  39. data/lib/overule/context.rb +22 -0
  40. data/lib/overule/engine.rb +7 -0
  41. data/lib/overule/inference.rb +38 -0
  42. data/lib/overule/operator.rb +25 -0
  43. data/lib/overule/version.rb +3 -0
  44. data/lib/overule.rb +13 -0
  45. 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,7 @@
1
+ require "rails/engine"
2
+
3
+ module Overule
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Overule
6
+ end
7
+ 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
@@ -0,0 +1,3 @@
1
+ module Overule
2
+ VERSION = "0.1.0".freeze
3
+ 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: []