| 
          # I would really like to introspect complex authorization rules to understand why users can/cannot perform an action. | 
        
        
           | 
          # That helps with debugging and offers better error messages. | 
        
        
           | 
          
 | 
        
        
           | 
          #  Sample: | 
        
        
           | 
          # | 
        
        
           | 
          #  ```ruby | 
        
        
           | 
          #  auth = Authorize.new(CanUpdate, owner, message_1) | 
        
        
           | 
          #  pp auth.permitted? # => false | 
        
        
           | 
          #  pp auth.explain | 
        
        
           | 
          #  # => "(❌ IsAdmin) or (✅ IsPublisher and ❌ IsOwner)" | 
        
        
           | 
          #  ``` | 
        
        
           | 
          
 | 
        
        
           | 
          # Let's define a User and a Message struct for demonstration purposes. | 
        
        
           | 
          User = Struct.new(:id, :admin, :publisher, keyword_init: true) do | 
        
        
           | 
            def admin? | 
        
        
           | 
              admin | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            def publisher? | 
        
        
           | 
              publisher | 
        
        
           | 
            end | 
        
        
           | 
          end | 
        
        
           | 
          
 | 
        
        
           | 
          Message = Struct.new(:id, :user_id, keyword_init: true) | 
        
        
           | 
          
 | 
        
        
           | 
          at_exit do | 
        
        
           | 
            # We now define the rules. | 
        
        
           | 
            # Low level rules that take a user (and optionally a resource) | 
        
        
           | 
            IsAdmin = Rule.define("IsAdmin") { |user| user.admin? } | 
        
        
           | 
            IsPublisher = Rule.define("IsPublisher") { |user| user.publisher? } | 
        
        
           | 
            IsOwner = Rule.define("IsOwner") { |user, message| message.user_id == user.id } | 
        
        
           | 
          
 | 
        
        
           | 
            # High level rules are combinations of low level rules | 
        
        
           | 
            CanRead = Rule.define("CanRead") { true } | 
        
        
           | 
            CanCreate = Rule.any(IsAdmin, IsPublisher) | 
        
        
           | 
            CanUpdate = Rule.any(IsAdmin, Rule.all(IsPublisher, IsOwner)) | 
        
        
           | 
            CanDelete = Rule.any(IsAdmin, Rule.all(IsPublisher, IsOwner)) | 
        
        
           | 
          
 | 
        
        
           | 
            admin = User.new(id: 1, admin: true, publisher: false) | 
        
        
           | 
            owner = User.new(id: 2, admin: false, publisher: true) | 
        
        
           | 
            not_publisher = User.new(id: 3, admin: false, publisher: false) | 
        
        
           | 
          
 | 
        
        
           | 
            message_1 = Message.new(id: 100, user_id: 1) | 
        
        
           | 
            message_2 = Message.new(id: 101, user_id: 2) | 
        
        
           | 
            message_3 = Message.new(id: 102, user_id: 3) | 
        
        
           | 
          
 | 
        
        
           | 
            auth = Authorize.new(CanUpdate, admin, message_1) | 
        
        
           | 
            puts "Can admin update message_1?" | 
        
        
           | 
            pp auth.permitted? # => true | 
        
        
           | 
            pp auth.explain | 
        
        
           | 
            # => "(✅ IsAdmin) or (❌ IsPublisher and ✅ IsOwner)" | 
        
        
           | 
            pp auth.to_h | 
        
        
           | 
            # => {:type=>:any, :label=>"Any", :ok=>true, :children=>[ | 
        
        
           | 
            #      {:type=>:leaf, :label=>"IsAdmin", :ok=>true}, | 
        
        
           | 
            #      {:type=>:all, :label=>"All", :ok=>false, children=>[ | 
        
        
           | 
            #        {:type=>:leaf, :label=>"IsPublisher", :ok=>false}, | 
        
        
           | 
            #        {:type=>:leaf, :label=>"IsOwner", :ok=>true} | 
        
        
           | 
            #      ]} | 
        
        
           | 
            #    ]} | 
        
        
           | 
          
 | 
        
        
           | 
            auth = Authorize.new(CanUpdate, owner, message_1) | 
        
        
           | 
            puts "Can owner update message_1?" | 
        
        
           | 
            pp auth.permitted? # => false | 
        
        
           | 
            pp auth.explain | 
        
        
           | 
            # => "(❌ IsAdmin) or (✅ IsPublisher and ❌ IsOwner)" | 
        
        
           | 
          
 | 
        
        
           | 
            auth = Authorize.new(CanUpdate, owner, message_2) | 
        
        
           | 
            puts "Can owner update message_2?" | 
        
        
           | 
            pp auth.permitted? # => true | 
        
        
           | 
            pp auth.explain | 
        
        
           | 
            # => "(❌ IsAdmin) or (✅ IsPublisher and ✅ IsOwner)" | 
        
        
           | 
          
 | 
        
        
           | 
            auth = Authorize.new(CanUpdate, not_publisher, message_3) | 
        
        
           | 
            puts "Can not_publisher update message_3?" | 
        
        
           | 
            pp auth.permitted? # => false | 
        
        
           | 
            pp auth.explain | 
        
        
           | 
            # => "(❌ IsAdmin) or (❌ IsPublisher and ✅ IsOwner)" | 
        
        
           | 
          end | 
        
        
           | 
          
 | 
        
        
           | 
          # Implementation | 
        
        
           | 
          
 | 
        
        
           | 
          # An AstNode represents the result of evaluating a Rule. | 
        
        
           | 
          class AstNode | 
        
        
           | 
            attr_reader :type, :label, :ok, :children | 
        
        
           | 
          
 | 
        
        
           | 
            def initialize(type:, label:, ok:, children: []) | 
        
        
           | 
              @type     = type       # :leaf | :all | :any | 
        
        
           | 
              @label    = label      # e.g., "IsAdmin", "All", "Any" | 
        
        
           | 
              @ok       = !!ok       # boolean result at this node | 
        
        
           | 
              @children = children   # [AstNode] | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            # Pretty rendering: "(✅ IsAdmin) or (❌ IsOwner and ✅ IsPublisher)" | 
        
        
           | 
            def to_s | 
        
        
           | 
              case type | 
        
        
           | 
              when :leaf | 
        
        
           | 
                "#{ok ? '✅' : '❌'} #{label}" | 
        
        
           | 
              when :any | 
        
        
           | 
                inner = children.map(&:group_string) | 
        
        
           | 
                "(#{inner.join(') or (')})" | 
        
        
           | 
              when :all | 
        
        
           | 
                # keep a stable order; tweak if you prefer alpha: | 
        
        
           | 
                inner = children.map(&:to_s) | 
        
        
           | 
                "(#{inner.join(' and ')})" | 
        
        
           | 
              end | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            # Minimal group string (used by :any to avoid double parens) | 
        
        
           | 
            def group_string | 
        
        
           | 
              return to_s if type == :leaf | 
        
        
           | 
          
 | 
        
        
           | 
              to_s.sub(/\A\(/, '').sub(/\)\z/, '') | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            # Hash for UI (e.g., JSON to your frontend) | 
        
        
           | 
            def to_h | 
        
        
           | 
              h = { | 
        
        
           | 
                type: type, | 
        
        
           | 
                label: label, | 
        
        
           | 
                ok: ok | 
        
        
           | 
              } | 
        
        
           | 
          
 | 
        
        
           | 
              h[:children] = children.map(&:to_h) unless children.empty? | 
        
        
           | 
          
 | 
        
        
           | 
              h | 
        
        
           | 
            end | 
        
        
           | 
          end | 
        
        
           | 
          
 | 
        
        
           | 
          # A Rule can be a leaf (a callable) or a composite (any/all of other rules). | 
        
        
           | 
          class Rule | 
        
        
           | 
            attr_reader :type, :children | 
        
        
           | 
          
 | 
        
        
           | 
            def initialize(name: nil, type: :leaf, callable: nil, children: []) | 
        
        
           | 
              @explicit_name = name | 
        
        
           | 
              @type = type | 
        
        
           | 
              @callable = callable | 
        
        
           | 
              @children = children | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            def self.define(name = nil, &block) | 
        
        
           | 
              raise ArgumentError, "block required" unless block | 
        
        
           | 
          
 | 
        
        
           | 
              new(name: name, type: :leaf, callable: block) | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            def self.any(*rules) | 
        
        
           | 
              flat = rules.flatten | 
        
        
           | 
              raise ArgumentError, "at least one rule" if flat.empty? | 
        
        
           | 
          
 | 
        
        
           | 
              new(name: "Any", type: :any, children: flat) | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            def self.all(*rules) | 
        
        
           | 
              flat = rules.flatten | 
        
        
           | 
              raise ArgumentError, "at least one rule" if flat.empty? | 
        
        
           | 
          
 | 
        
        
           | 
              new(name: "All", type: :all, children: flat) | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            def evaluate(*ctx) | 
        
        
           | 
              case type | 
        
        
           | 
              when :leaf then [email protected](*ctx) | 
        
        
           | 
              when :any  then children.any?  { |r| r.evaluate(*ctx) } | 
        
        
           | 
              when :all  then children.all?  { |r| r.evaluate(*ctx) } | 
        
        
           | 
              else raise "unknown rule type #{type}" | 
        
        
           | 
              end | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            def label | 
        
        
           | 
              @explicit_name || infer_constant_name || (type == :leaf ? "(anonymous rule)" : type.to_s.capitalize) | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            # --- AST builder --- | 
        
        
           | 
            def build_ast(*ctx) | 
        
        
           | 
              case type | 
        
        
           | 
              when :leaf | 
        
        
           | 
                AstNode.new(type: :leaf, label: label, ok: evaluate(*ctx)) | 
        
        
           | 
              when :any | 
        
        
           | 
                child_nodes = children.map { |r| r.build_ast(*ctx) } | 
        
        
           | 
                AstNode.new(type: :any, label: label, ok: child_nodes.any?(&:ok), children: child_nodes) | 
        
        
           | 
              when :all | 
        
        
           | 
                child_nodes = children.map { |r| r.build_ast(*ctx) } | 
        
        
           | 
                AstNode.new(type: :all, label: label, ok: child_nodes.all?(&:ok), children: child_nodes) | 
        
        
           | 
              end | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            private | 
        
        
           | 
          
 | 
        
        
           | 
            def infer_constant_name | 
        
        
           | 
              ObjectSpace.each_object(Module) do |mod| | 
        
        
           | 
                mod.constants(false).each do |const| | 
        
        
           | 
                  begin | 
        
        
           | 
                    return const.to_s if mod.const_get(const).equal?(self) | 
        
        
           | 
                  rescue NameError | 
        
        
           | 
                    next | 
        
        
           | 
                  end | 
        
        
           | 
                end | 
        
        
           | 
              end | 
        
        
           | 
              nil | 
        
        
           | 
            end | 
        
        
           | 
          end | 
        
        
           | 
          
 | 
        
        
           | 
          # Take a Rule and context (user, resource, etc) and evaluate it. | 
        
        
           | 
          class Authorize | 
        
        
           | 
            def initialize(rule, *context) | 
        
        
           | 
              @rule    = rule | 
        
        
           | 
              @context = context | 
        
        
           | 
              @ast     = nil | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            # Build (or return cached) AST | 
        
        
           | 
            def ast | 
        
        
           | 
              @ast ||= @rule.build_ast(*@context) | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            # High-level API | 
        
        
           | 
            def permitted? | 
        
        
           | 
              ast.ok | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            def explain | 
        
        
           | 
              ast.to_s | 
        
        
           | 
            end | 
        
        
           | 
          
 | 
        
        
           | 
            # For UIs that want raw data (e.g., JSON) | 
        
        
           | 
            def to_h | 
        
        
           | 
              ast.to_h | 
        
        
           | 
            end | 
        
        
           | 
          end | 
        
  
I would really like to introspect complex authorization rules to understand why users can/cannot perform an action. That helps with debugging and offers better error messages.
I played with this idea 10 years ago... so it's been on my mind for a while. :)
It'd be easy to add this to existing policy frameworks such as pundit or activepolicy.
The API remains the same (ex:
message_policy.update?) but we'd get better error messages, such as:Cannot update: "(❌ IsAdmin) or (✅ IsPublisher and ❌ IsOwner)"