class ViewModel::AccessControl::Composed

# edit_valid_unless!(“user is on fire”) { … }
# edit_valid_if!(“logged in as specified user”) { … }
# if at least one ‘if` check and no `unless` checks succeed. For example:
# for each access check (visible, editable, edit_valid). An action is permitted
# Provides access control as a combination of `x_if!` and `x_unless!` checks

def check_delegates(env, ifs, unlesses)

def check_delegates(env, ifs, unlesses)
  veto, veto_error = detect_veto(env, unlesses)
  allow, allow_error = detect_allow(env, ifs)
  ComposedResult.new(allow, veto, allow_error, veto_error)
end

def detect_allow(env, checkers)

def detect_allow(env, checkers)
  error = nil
  checkers.each do |checker|
    result = checker.check(env)
    next unless result
    if result.is_a?(StandardError)
      error ||= result
    else
      # short-circuit exit with success
      return true, nil
    end
  end
  error ||= NoRequiredConditionsError.new(
    env.view.blame_reference,
    checkers.map(&:name))
  return false, error
end

def detect_veto(env, checkers)

def detect_veto(env, checkers)
  checkers.each do |checker|
    result = checker.check(env)
    next unless result
    error =
      if result.is_a?(StandardError)
        result
      else
        checker.error_type.new('Action not permitted because: ' +
                               checker.reason,
                               env.view.blame_reference)
      end
    # short-circuit exit with failure
    return true, error
  end
  return false, nil
end

def each_check(check_name, include_ancestor = nil)

def each_check(check_name, include_ancestor = nil)
  return enum_for(:each_check, check_name, include_ancestor) unless block_given?
  self.public_send(check_name).each { |x| yield x }
  visited = Set.new
  @included_checkers.each do |ancestor|
    next unless visited.add?(ancestor)
    next if include_ancestor && !include_ancestor.call(ancestor)
    ancestor.each_check(check_name, include_ancestor) { |x| yield x }
  end
end

def edit_valid_if!(reason, &block)

def edit_valid_if!(reason, &block)
  @edit_valid_ifs      << new_permission_check(reason, &block)
end

def edit_valid_unless!(reason, &block)

def edit_valid_unless!(reason, &block)
  @edit_valid_unlesses << new_permission_check(reason, &block)
end

def editable_check(traversal_env)

final
def editable_check(traversal_env)
  check_delegates(traversal_env, self.class.each_check(:editable_ifs), self.class.each_check(:editable_unlesses))
end

def editable_if!(reason, &block)

def editable_if!(reason, &block)
  @editable_ifs        << new_permission_check(reason, &block)
end

def editable_unless!(reason, &block)

def editable_unless!(reason, &block)
  @editable_unlesses   << new_permission_check(reason, &block)
end

def include_from(ancestor)

# Configuration API
def include_from(ancestor)
  unless ancestor < ViewModel::AccessControl::Composed
    raise ArgumentError.new("Invalid ancestor: #{ancestor}")
  end
  @included_checkers << ancestor
end

def inherited(subclass)

def inherited(subclass)
  super
  subclass.initialize_as_composed_access_control
end

def initialize_as_composed_access_control

def initialize_as_composed_access_control
  @included_checkers = []
  @edit_valid_ifs      = []
  @edit_valid_unlesses = []
  @editable_ifs        = []
  @editable_unlesses   = []
  @visible_ifs         = []
  @visible_unlesses    = []
end

def inspect

def inspect
  checks = inspect_checks
  checks << "includes checkers: #{@included_checkers.inspect}" if @included_checkers.present?
  super + '(' + checks.join(', ') + ')'
end

def inspect_checks

def inspect_checks
  checks = []
  checks << "visible_if: #{@visible_ifs.map(&:reason)}"                if @visible_ifs.present?
  checks << "visible_unless: #{@visible_unlesses.map(&:reason)}"       if @visible_unlesses.present?
  checks << "editable_if: #{@editable_ifs.map(&:reason)}"              if @editable_ifs.present?
  checks << "editable_unless: #{@editable_unlesses.map(&:reason)}"     if @editable_unlesses.present?
  checks << "edit_valid_if: #{@edit_valid_ifs.map(&:reason)}"          if @edit_valid_ifs.present?
  checks << "edit_valid_unless: #{@edit_valid_unlesses.map(&:reason)}" if @edit_valid_unlesses.present?
  checks
end

def new_permission_check(reason, error_type: ViewModel::AccessControlError, &block)

def new_permission_check(reason, error_type: ViewModel::AccessControlError, &block)
  PermissionsCheck.new(self.name&.demodulize, reason, error_type, block)
end

def valid_edit_check(traversal_env)

final
def valid_edit_check(traversal_env)
  check_delegates(traversal_env, self.class.each_check(:edit_valid_ifs), self.class.each_check(:edit_valid_unlesses))
end

def visible_check(traversal_env)

final
def visible_check(traversal_env)
  check_delegates(traversal_env, self.class.each_check(:visible_ifs), self.class.each_check(:visible_unlesses))
end

def visible_if!(reason, &block)

def visible_if!(reason, &block)
  @visible_ifs         << new_permission_check(reason, &block)
end

def visible_unless!(reason, &block)

def visible_unless!(reason, &block)
  @visible_unlesses    << new_permission_check(reason, &block)
end