Class: Browsable::PolicyResolver

Inherits:
Object
  • Object
show all
Defined in:
lib/browsable/policy_resolver.rb

Overview

Maps a ‘(controller_class, action_name)` pair to the effective Browsable Policy. This is what runtime mode uses, per response, to decide which browsers an endpoint’s assets must support.

Resolution rules (matching Rails’ own filter-callback semantics):

1. Walk the controller's ancestor chain from the most-specific class up
   to ApplicationController, ignoring anonymous classes and modules.
2. For each class, look up its allow_browser callsites (PolicyScanner
   data) and pick the *last* call whose `only:`/`except:` filter matches.
3. The first ancestor with a matching call wins — its last matching call
   becomes the effective policy.
4. If no call matches, return the configured default Policy (the policy
   from ApplicationController, falling through to the project default).

The scanned policy data is built lazily on first use. Tests can call ‘.reset!` between examples to swap roots without process restart.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(controller_class, action_name) ⇒ PolicyResolver

Returns a new instance of PolicyResolver.



79
80
81
82
# File 'lib/browsable/policy_resolver.rb', line 79

def initialize(controller_class, action_name)
  @controller_class = controller_class
  @action_name = action_name&.to_s
end

Instance Attribute Details

#action_nameObject (readonly)

Returns the value of attribute action_name.



77
78
79
# File 'lib/browsable/policy_resolver.rb', line 77

def action_name
  @action_name
end

#controller_classObject (readonly)

Returns the value of attribute controller_class.



77
78
79
# File 'lib/browsable/policy_resolver.rb', line 77

def controller_class
  @controller_class
end

Class Method Details

.configure(root: nil, policies: nil, default: nil) ⇒ Object

Inject pre-scanned data — used by drivers (which know the Rails root) and by tests (which want to bypass disk).



30
31
32
33
34
35
# File 'lib/browsable/policy_resolver.rb', line 30

def configure(root: nil, policies: nil, default: nil)
  @root = root
  @policies = policies
  @default = default
  @lookup = nil
end

.default_policyObject



53
54
55
# File 'lib/browsable/policy_resolver.rb', line 53

def default_policy
  @default ||= build_default_policy
end

.for(controller_class, action_name) ⇒ Object

Convenience: resolve a single controller#action using the shared state.



24
25
26
# File 'lib/browsable/policy_resolver.rb', line 24

def for(controller_class, action_name)
  new(controller_class, action_name).resolve
end

.lookupObject

{ “PostsController” => [PolicyScanner::Policy, …] }, built once.



58
59
60
# File 'lib/browsable/policy_resolver.rb', line 58

def lookup
  @lookup ||= policies.group_by(&:scope)
end

.policiesObject



49
50
51
# File 'lib/browsable/policy_resolver.rb', line 49

def policies
  @policies ||= PolicyScanner.call(root)
end

.reset!Object

Forget any cached state. Called between test files.



38
39
40
41
42
43
# File 'lib/browsable/policy_resolver.rb', line 38

def reset!
  @root = nil
  @policies = nil
  @default = nil
  @lookup = nil
end

.rootObject



45
46
47
# File 'lib/browsable/policy_resolver.rb', line 45

def root
  @root ||= (defined?(Rails) && Rails.application ? Rails.root.to_s : Dir.pwd)
end

Instance Method Details

#resolveObject



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/browsable/policy_resolver.rb', line 84

def resolve
  return self.class.default_policy if controller_class.nil? || action_name.nil? || action_name.empty?

  ancestor_class_names.each do |name|
    calls = self.class.lookup[name]
    next unless calls && !calls.empty?

    match = calls.reverse.find { |call| applies?(call) }
    next unless match

    return policy_from(match, scope: name, source: same_class?(name) ? :controller : :ancestor)
  end

  self.class.default_policy
end