module Eco module API class Session class Batch # Helper class to build a hierarchical model of policies # @example Usage: # class PolicyModel < Eco::API::Session::Batch::BasePolicy # MODEL = {attr1: ["prop1a", "prop1b", {"prop1c": ["prop1c1"]}]} # self.model = MODEL # policy_attrs *model_attrs # end # # policies = PolicyModel.new("batch_policy") # policies.attr1c do |attr1c| # attr1c.prop1c1.max = 30 # end # # @attr_reader attr [Symbol] the Symbol `name` of the current policy # @attr_reader max [Integer] `max` **allowed** number of occurrences of the property # @attr_reader min [Integer] `min` **required** number of occurrences of the property class BasePolicy extend Eco::API::Common::ClassHierarchy class << self # If the class for `key` exists, it returns it. Otherwise it generates it. # @note for this to work, `key` should be one of the submodels of the current class' `model` # @return [Eco::API::Session::Batch::BasePolicy] or subclass thereof def policy_class(key) key = key.to_sym.freeze class_name = to_constant(key) new_class(class_name, inherits: Eco::API::Session::Batch::BasePolicy) do |klass| klass.model = model[key] klass.policy_attrs *klass.model_attrs end end # Attributes of this level of the model that should be included # @param attrs [Array, Array] each of the subpolicies of the model that should be available def policy_attrs(*attrs) attrs = attrs.map(&:to_sym) attrs.each do |attr| method = attr.to_s.freeze var = "@#{method}".freeze define_method(method) do |&block| unless policy = self[attr] klass = self.class.policy_class(attr) policy = self[attr] = klass.new(attr, _parent: self) end if block block.call(policy) self else policy end end end end end include Enumerable attr_reader :attr attr_accessor :max, :min def initialize(attr = nil, _parent: self) @_parent = _parent @attr = attr.to_sym @policies = {} end def attr(as_namespace: false) return @attr if !as_namespace || root? "#{@_parent.attr(as_namespace: true)}:#{@attr}" end # @note if there's no `min` defined, it always returns `true` # @param value [Integer] value to check if it's in the minimum required # @return [Boolen] `true` if `value` is grater or equal to `min` def min?(value) !min || !value|| (min <= value) end # @note if there's no `max` defined, it always returns `true` # @param value [Integer] value to check if it's in the maximum allowed # @return [Boolen] `true` if `value` is lesser or equal to `min` def max?(value) !max || !value || (max >= value) end # return [Integer] number of declared subpolicies def length count end # @return [Boolean] `true` if there are no active subpolicies, `false` otherwise def empty? count == 0 end # @return [Boolean] `true` if there are active subpolicies, `false` otherwise def subpolicies? !empty? end def each(&block) return to_enum(:each) unless block items.each(&block) end # @return [Array] the active subpolicies def items @policies.values end # @param attr [Symbol, String] name of the policy # @return [Boolean] if `attr` is an active subpolicy def active?(attr) @policies.key?(attr.to_sym) end # @param attr [Symbol, String] name of the policy # @return [Array] the used subpolicies def [](attr) @policies[attr.to_sym] end # @param attr [Symbol, String] name of the policy # @param value [Expected object of Eco::API::Session::Batch::BasePolicy] a subpolicy to assign to a name `attr` def []=(attr, value) raise "Expected object of Eco::API::Session::Batch::BasePolicy. Given #{value.class}" unless value.is_a?(Eco::API::Session::Batch::BasePolicy) @policies[attr.to_sym] = value end # @param model [Hash] plain hash (or hashable object) with the stats to check policy compliance against # @param recurse [Boolean] to determine if we only check the current policy or also all active subpolicies # @return [Boolean] `true` if `model` is compliant with the current policy def compliant?(model, recurse: true) unless hash = model_to_hash(model) raise "Expected 'model' to be a Hash (or hashable) object. Given: #{model}" end value = model_attr(hash) good = !model_attr?(hash) || (min?(value) && max?(value)) #pp "batch_policy: '#{attr}' - #{value}: 'min' #{min?(value)}; 'max' #{max?(value)}" good &&= all? {|active| active.compliant?(model, recurse: recurse)} if recurse good end def validate!(model) unless compliant?(model) msg = self.uncompliance(model) raise "Uncompliance Exception\n#{msg}" end end # @param model [Hash] plain hash (or hashable object) with the stats to check policy compliance against # @return [Array] **non-compliant** policies for the `model` def uncompliant(model) each_with_object([]) do |active, arr| arr.concat(active.uncompliant(model)) end.tap do |arr| arr.unshift(self) unless compliant?(model, recurse: false) end end # @param model [Hash] plain hash (or hashable object) with the stats to check policy compliance against # @param recurse [Boolean] to determine if we only check the current policy or also all active subpolicies # @return [String] message with what failed to meet compliance def uncompliance(model, recurse: true) unless hash = model_to_hash(model) raise "Expected 'model' to be a Hash (or hashable) object. Given: #{model}" end msg = "" unless compliant?(hash, recurse: false) value = model_attr(hash) msg += "'#{attr(as_namespace: true)}' fails to meet: " msg += " [ min(#{min}) >= #{value}] " unless min?(value) msg += " [ max(#{max}) <= #{value}] " unless max?(value) msg += "\n" end if recurse map do |active| active.uncompliance(hash, recurse: true) end.compact.tap do |msgs| msg += "\n" + msgs.join("\n") unless msgs.empty? end end msg end protected # Internal helper to know if we are at the top/root of the hierarchical model # @return [Boolean] `true` if this object is the top `root`, `false` otherwise def root? @_parent == self end def to_h @policies end def model_attr?(hash) hash.key?(self.attr) || hash.key?(self.attr.to_s) end def model_attr(hash) return hash[self.attr] if hash.key?(self.attr) hash[self.attr.to_s] if model_attr?(hash) end private def model_to_hash(model) return model if model.is_a?(Hash) model.to_h if model.respond_to?(:to_h) end end end end end end