decko-commons/decko

View on GitHub
card/lib/card/view/options/voo_api.rb

Summary

Maintainability
A
2 hrs
Test Coverage
A
94%
class Card
  class View
    module Options
      # VooApi methods let developers use view options dynamically.
      module VooApi
        # There are two primary options hashes:

        # - @normalized_options are determined upon initialization and do not change
        # after that.
        # @return [Hash] options
        attr_reader :normalized_options

        class << self
          def included base
            # Developers can also set most options directly via accessors,
            # eg voo.title = "King"
            # :view, :show, and :hide have non-standard access (see #accessible_keys)

            base.accessible_keys.each do |option_key|
              define_getter option_key unless option_key == :items
              define_setter option_key
            end
          end

          def define_getter option_key
            define_method option_key do
              live_options[option_key]
            end
          end

          def define_setter option_key
            define_method "#{option_key}=" do |value|
              live_options[option_key] = special_option_value(option_key, value) || value
            end
          end
        end

        # "items", the option used to configure views of each of a list of cards, is
        # currently the only Hash option (thus this accessor override)
        # @return [Hash]
        def items
          live_options[:items] ||= {}
        end

        # options to be used in data attributes of card slots (normalized options
        # with standard keys)
        # FIXME: what we really want is options as they were when render was called.
        # normalized is wrong because it can get changed before render. live is wrong
        # because they can get changed after.  current solution is a compromise.
        # @return [Hash]
        def slot_options
          normalized_options.merge(view: requested_view)
                            .merge(normalized_visibility_options)
                            .slice(*Options.slot_keys)
        end

        # ACCESSOR_HELPERS
        # methods that follow the normalize_#{key} pattern are called by accessors
        # (arguably that should be done during normalization!)

        def normalize_special_options! opts
          opts.each do |option_key, value|
            new_value = special_option_value option_key, value
            opts[option_key] = new_value if new_value
          end
        end

        def special_option_value option_key, value
          try "normalize_#{option_key}", value
        end

        def normalize_input_type value
          value&.to_sym
        end

        def normalize_edit value
          value&.to_sym
        end

        def normalize_cache value
          value&.to_sym
        end

        def normalize_wrap value
          value = value.split(",").map(&:strip) if value.is_a? String
          Array.wrap(value).compact.flatten
        end

        protected

        # - @live_options are dynamic and can be altered by the "voo" API at any time.
        # Such alterations are NOT used in the stub of the current voo, but they
        # ARE inherited by children voos.
        #
        # @return [Hash]
        def live_options
          @live_options ||= process_live_options
        end

        private

        # option normalization includes standardizing options into a hash with
        # symbols as keys, managing standard view inheritance, and special
        # handling for main_views.
        def normalize_options
          @normalized_options = opts = options_to_hash @raw_options.clone
          normalize_special_options! opts
          @optional = opts.delete(:optional) || false
          add_implicit_options!
          inherit_options_from_parent!
          validate_options! opts
          opts
        end

        def add_implicit_options!
          @normalized_options[:view] = @raw_view
          @normalized_options[:main] = true if format.main?
          # opts[:context_names] = format.context_names
        end

        # typically options are already a hash.  this also handles an array of
        # hashes and nil.
        def options_to_hash opts
          case opts
          when ActionController::Parameters
            opts.to_unsafe_h.deep_symbolize_keys
          when Hash  then opts.deep_symbolize_keys!
          when Array then opts[0].merge(opts[1]).deep_symbolize_keys!
          when nil   then {}
          else raise Card::Error, "bad view options: #{opts}"
          end
        end

        # standard inheritance from parent view object
        def inherit_options_from_parent!
          return unless parent

          Options.heir_keys.each do |option_key|
            inherit_from_parent! option_key
          end
        end

        def inherit_from_parent! option_key
          return unless (parent_value = parent.live_options[option_key])

          @normalized_options[option_key] ||= parent_value
        end

        def process_live_options
          @live_options = normalized_options.clone
          process_main_nest_options
          process_before_view
          process_visibility
          return :hide if hide_requested_view? # bail to avoid unnecessary processing

          process_view_wrappers
          @live_options
        end

        # This method triggers the "before" blocks which can alter the @live_options
        # hash both directly and indirectly (via the voo API)
        def process_before_view
          format.before_view requested_view
        end

        # adds the wrappers assigned to ok_view in view definition
        def process_view_wrappers
          view_wrappers = format.view_setting(:wrap, ok_view)
          return unless view_wrappers.present?

          @live_options[:wrap] = Array.wrap(@live_options[:wrap])
          if view_wrappers.is_a? ::Hash
            view_wrappers.each_pair do |name, opts|
              @live_options[:wrap] << [name, opts]
            end
          else
            @live_options[:wrap] += Array.wrap(view_wrappers)
          end
        end

        # merge the options of the main nest into the @live_options
        # They are not processed in normalize_options so that
        # they're NOT locked in the stub.
        def process_main_nest_options
          @live_options.merge! format.main_nest_options if @live_options[:main_view]
        end

        def validate_options! opts
          return unless (foreign_opts = foreign_options_in opts)

          # TODO: this should raise a UserError if the options come directly from params
          # (eg, mycard?slot[badoption]=true should not be treated as a server error)
          raise Card::Error, "illegal view options: #{foreign_opts}"
        end

        # find non-standard option in Hash
        # @param opts [Hash] options hash
        # @return [Hash] options Hash
        def foreign_options_in opts
          foreign_opts = opts.reject { |k, _v| Options.all_keys.include? k }
          foreign_opts.empty? ? nil : foreign_opts
        end
      end
    end
  end
end