locomotivecms/mounter

View on GitHub
lib/locomotive/mounter/fields.rb

Summary

Maintainability
B
4 hrs
Test Coverage
module Locomotive
  module Mounter

    module Fields

      extend ActiveSupport::Concern

      included do
        include ActiveSupport::Callbacks

        define_callbacks :initialize

        class << self; attr_accessor :_fields end

        attr_accessor :_locales
      end

      # Default constructor
      #
      # @param [ Hash ] attributes The new attributes
      #
      def initialize(attributes = {})
        run_callbacks :initialize do
          _attributes = attributes.symbolize_keys

          # set default values
          self.class._fields.each do |name, options|
            next if !options.key?(:default) || _attributes.key?(name)

            _attributes[name] = options[:default]
          end

          # set default translation
          self.add_locale(Locomotive::Mounter.locale)

          self.write_attributes(_attributes)
        end
      end

      # Set or replace the attributes of the current instance by the ones
      # passed as parameter.
      # It raises an exception if one of the keys is not included in the list of fields.
      #
      # @param [ Hash ] attributes The new attributes
      #
      def write_attributes(attributes)
        return if attributes.blank?

        attributes.each do |name, value|
          unless self.class._fields.key?(name.to_sym) || self.respond_to?(:"#{name}=")
            next if name.to_s == 'id'
            raise FieldDoesNotExistException.new "[#{self.class.inspect}] setting an unknown attribute '#{name}' with the value '#{value.inspect}'"
          end

          if self.localized_field?(name) && value.is_a?(Hash)
            self.send(:"#{name}_translations=", value)
          else
            self.send(:"#{name}=", value)
          end
        end
      end

      alias :attributes= :write_attributes

      # Return the fields with their values
      #
      # @return [ Hash ] The attributes
      #
      def attributes
        {}.tap do |_attributes|
          self.class._fields.each do |name, options|
            _attributes[name] = self.send(name.to_sym)
          end
        end
      end

      def [](name)
        self.attributes[name.to_sym]
      end

      # Return the fields with their values and their translations
      #
      # @return [ Hash ] The attributes
      #
      def attributes_with_translations
        {}.tap do |_attributes|
          self.class._fields.each do |name, options|
            next if options[:association]

            if options[:localized]
              value = self.send(:"#{name}_translations")

              value = value.values.first if value.size == 1

              value = nil if value.respond_to?(:empty?) && value.empty?

              _attributes[name] = value
            else
              _attributes[name] = self.send(name.to_sym)
            end
          end
        end
      end

      # Check if the field specified by the argument is localized
      #
      # @param [ String ] name Name of the field
      #
      # @return [ Boolean ] True if the field is localized
      #
      def localized_field?(name)
        method_name = :"#{name}_localized?"
        self.respond_to?(method_name) && self.send(method_name)
      end

      # List all the translations done on that model
      #
      # @return [ List ] List of locales
      #
      def translated_in
        self._locales.map(&:to_sym)
      end

      # Tell if the object has been translated in the locale
      # passed in parameter.
      #
      # @param [ String/Symbol ] locale
      #
      # @return [ Boolean ] True if one of the fields has been translated.
      #
      def translated_in?(locale)
        self.translated_in.include?(locale.to_sym)
      end

      # Return a Hash of all the non blank attributes of the object.
      # It also performs a couple of modifications: stringify keys and
      # convert Symbol to String.
      #
      # @param [ Boolean ] translation Flag (by default true) to get the translations too.
      #
      # @return [ Hash ] The non blank attributes
      #
      def to_hash(translations = true)
        hash = translations ? self.attributes_with_translations : self.attributes

        hash.delete_if { |k, v| (!v.is_a?(FalseClass) && v.blank?) }

        hash.each { |k, v| hash[k] = v.to_s if v.is_a?(Symbol) }

        hash.deep_stringify_keys
      end

      # Provide a better output of the default to_yaml method
      #
      # @return [ String ] The YAML version of the object
      #
      def to_yaml
        # get the attributes with their translations and get rid of all the symbols
        object = self.to_hash

        object.each do |key, value|
          if value.is_a?(Array)
            object[key] = if value.first.is_a?(String)
              StyledYAML.inline(value) # inline array
            else
              value.map(&:to_hash)
            end
          end
        end

        StyledYAML.dump object
      end

      protected

      def getter(name, options = {})
        value = self.instance_variable_get(:"@#{name}")
        if options[:localized]
          value = (value || {})[Locomotive::Mounter.locale]
        end
        value
      end

      def setter(name, value, options = {})
        if options[:localized]
          # keep track of the current locale
          self.add_locale(Locomotive::Mounter.locale)

          translations = self.instance_variable_get(:"@#{name}") || {}
          translations[Locomotive::Mounter.locale] = value
          value = translations
        end

        if options[:type] == :array
          klass = options[:class_name].constantize
          value = value.map { |object| object.is_a?(Hash) ? klass.new(object) : object }
        end

        self.instance_variable_set(:"@#{name}", value)
      end

      def add_locale(locale)
        self._locales ||= []
        self._locales << locale.to_sym unless self._locales.include?(locale.to_sym)
      end

      module ClassMethods

        # Add a field to the current instance. It creates getter/setter methods related to that field.
        # A field can have translations if the option named localized is set to true.
        #
        # @param [ String ] name The name of the field
        # @param [ Hash ] options The options related to the field.
        #
        def field(name, options = {})
          options = { localized: false }.merge(options)

          @_fields ||= {} # initialize the list of fields if nil

          self._fields[name.to_sym] = options

          class_eval <<-EOV
            def #{name}
              self.getter '#{name}', self.class._fields[:#{name}]
            end

            def #{name}=(value)
              self.setter '#{name}', value, self.class._fields[:#{name}] #, #{options[:localized]}
            end

            def #{name}_localized?
              #{options[:localized]}
            end
          EOV

          if options[:localized]
            class_eval <<-EOV
              def #{name}_translations
                @#{name} || {}
              end

              def #{name}_translations=(translations)
                translations.each { |locale, value| self.add_locale(locale) }
                @#{name} = translations.symbolize_keys
              end
            EOV
          end
        end

      end

    end
  end
end