shioyama/mobility

View on GitHub
lib/mobility/backends/sequel/container.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true
require "mobility/backends/sequel"
require "mobility/backends/sequel/jsonb"
require "mobility/backends/container"

module Mobility
  module Backends
=begin

Implements the {Mobility::Backends::Container} backend for Sequel models.

=end
    class Sequel::Container
      include Sequel
      include Container

      # @!group Backend Accessors
      #
      # @note Translation may be a string, integer, boolean, hash or array
      #   since value is stored on a JSON hash.
      # @param [Symbol] locale Locale to read
      # @param [Hash] options
      # @return [String,Integer,Boolean] Value of translation
      def read(locale, _ = nil)
        model_translations(locale)[attribute]
      end

      # @note Translation may be a string, integer, boolean, hash or array
      #   since value is stored on a JSON hash.
      # @param [Symbol] locale Locale to write
      # @param [String,Integer,Boolean] value Value to write
      # @param [Hash] options
      # @return [String,Integer,Boolean] Updated value
      def write(locale, value, _ = nil)
        set_attribute_translation(locale, value)
        model_translations(locale)[attribute]
      end
      # @!endgroup
      #
      # @!group Backend Configuration
      # @option options [Symbol] column_name (:translations) Name of column on which to store translations
      def self.configure(options)
        options[:column_name] ||= :translations
        options[:column_name] = options[:column_name].to_sym
        column_name, db_schema = options[:column_name], model_class.db_schema
        options[:column_type] = db_schema[column_name] && (db_schema[column_name][:db_type]).to_sym
        unless %i[json jsonb].include?(options[:column_type])
          raise InvalidColumnType, "#{options[:column_name]} must be a column of type json or jsonb"
        end
      end
      # @!endgroup
      #
      # @!macro backend_iterator
      def each_locale
        model[column_name].each do |l, _|
          yield l.to_sym unless read(l).nil?
        end
      end

      setup do |attributes, options, backend_class|
        column_name = options[:column_name]
        mod = Module.new do
          define_method :before_validation do
            self[column_name].each do |k, v|
              v.delete_if { |_locale, translation| Util.blank?(translation) }
              self[column_name].delete(k) if v.empty?
            end
            super()
          end
        end
        include mod
        backend_class.define_hash_initializer(mod, [column_name])

        plugin :defaults_setter
        attributes.each { |attribute| default_values[attribute.to_sym] = {} }
      end

      private

      def model_translations(locale)
        model[column_name][locale.to_s] ||= {}
      end

      def set_attribute_translation(locale, value)
        translations = model[column_name] || {}
        translations[locale.to_s] ||= {}
        # Explicitly mark translations column as changed if value changed,
        # otherwise Sequel will not detect it.
        # TODO: Find a cleaner/easier way to do this.
        if translations[locale.to_s][attribute] != value
          model.instance_variable_set(:@changed_columns, model.changed_columns | [column_name])
        end
        translations[locale.to_s][attribute] = value
      end

      class InvalidColumnType < StandardError; end

      # @param [Symbol] name Attribute name
      # @param [Symbol] locale Locale
      # @return [Mobility::Backends::Sequel::Container::JSONOp,Mobility::Backends::Sequel::Container::JSONBOp]
      def self.build_op(attr, locale)
        klass = const_get("#{options[:column_type].upcase}Op")
        klass.new(klass.new(column_name.to_sym).get(locale.to_s)).get_text(attr)
      end

      class JSONOp < ::Sequel::Postgres::JSONOp; end

      class JSONBOp < Jsonb::JSONBOp
        def to_question
          left = @value.args[0].value
          JSONBOp === left ? ::Sequel.&(super, left.to_question) : super
        end
      end
    end

    register_backend(:sequel_container, Sequel::Container)
  end
end