lib/jsonb_accessor/macro.rb



module JsonbAccessor
  module Macro
    module ClassMethods
      def jsonb_accessor(jsonb_attribute, *value_fields, **typed_fields)
        fields_map = JsonbAccessor::FieldsMap.new(value_fields, typed_fields)
        class_namespace = ClassBuilder.generate_class_namespace(name)
        attribute_namespace = ClassBuilder.generate_attribute_namespace(jsonb_attribute, class_namespace)
        nested_classes = ClassBuilder.generate_nested_classes(attribute_namespace, fields_map.nested_fields)
        jsonb_attribute_initialization_method_name = "initialize_jsonb_attrs_for_#{jsonb_attribute}"
        jsonb_attribute_scope_name = "#{jsonb_attribute}_contains"

        singleton_class.send(:define_method, "#{jsonb_attribute}_classes") do
          nested_classes
        end

        delegate "#{jsonb_attribute}_classes", to: :class

        _initialize_jsonb_attrs(jsonb_attribute, fields_map, jsonb_attribute_initialization_method_name)
        _create_jsonb_attribute_scope_name(jsonb_attribute, jsonb_attribute_scope_name)
        _create_jsonb_scopes(jsonb_attribute, fields_map, jsonb_attribute_scope_name)
        _create_jsonb_accessor_methods(jsonb_attribute, jsonb_attribute_initialization_method_name, fields_map)

        _register_jsonb_classes_for_cleanup
      end

      private

      def _register_jsonb_classes_for_cleanup
        if defined?(ActionDispatch) && ENV["RACK_ENV"] == "development"
          class_name = CLASS_PREFIX + name
          ActionDispatch::Reloader.to_cleanup do
            if JsonbAccessor.constants.any? { |c| c.to_s == class_name }
              JsonbAccessor.send(:remove_const, class_name)
            end
          end
        end
      end

      def _initialize_jsonb_attrs(jsonb_attribute, fields_map, jsonb_attribute_initialization_method_name)
        define_method(jsonb_attribute_initialization_method_name) do
          if has_attribute?(jsonb_attribute)
            jsonb_attribute_hash = send(jsonb_attribute) || {}
            fields_map.names.each do |field|
              send("#{field}=", jsonb_attribute_hash[field.to_s])
            end
          end
        end
        after_initialize(jsonb_attribute_initialization_method_name)
      end

      def _create_jsonb_attribute_scope_name(jsonb_attribute, jsonb_attribute_scope_name)
        scope jsonb_attribute_scope_name, (lambda do |attributes|
                                             query_options = new(attributes).send(jsonb_attribute)
                                             fields = attributes.keys.map(&:to_s)
                                             query_options.delete_if { |key, value| fields.exclude?(key) }
                                             query_json = TypeHelper.type_cast_as_jsonb(query_options)
                                             where("#{table_name}.#{jsonb_attribute} @> ?", query_json)
                                           end)
      end

      def _create_jsonb_scopes(jsonb_attribute, fields_map, jsonb_attribute_scope_name)
        __create_jsonb_standard_scopes(fields_map, jsonb_attribute_scope_name)
        __create_jsonb_typed_scopes(jsonb_attribute, fields_map)
      end

      def __create_jsonb_standard_scopes(fields_map, jsonb_attribute_scope_name)
        fields_map.names.each do |field|
          scope "with_#{field}", -> (value) { send(jsonb_attribute_scope_name, field => value) }
        end
      end

      def __create_jsonb_typed_scopes(jsonb_attribute, fields_map)
        fields_map.typed_fields.each do |field, type|
          case type
          when :boolean
            ___create_jsonb_boolean_scopes(field)
          when :integer, :float, :decimal, :big_integer
            ___create_jsonb_numeric_scopes(field, jsonb_attribute, type)
          when :date_time, :date
            ___create_jsonb_date_time_scopes(field, jsonb_attribute, type)
          when /array/
            ___create_jsonb_array_scopes(field)
          end
        end
      end

      def ___create_jsonb_boolean_scopes(field)
        scope "is_#{field}", -> { send("with_#{field}", true) }
        scope "not_#{field}", -> { send("with_#{field}", false) }
      end

      def ___create_jsonb_numeric_scopes(field, jsonb_attribute, type)
        safe_type = type.to_s.gsub("big_", "")
        scope "__numeric_#{field}_comparator", -> (value, operator) { where("((#{table_name}.#{jsonb_attribute}) ->> ?)::#{safe_type} #{operator} ?", field, value) }
        scope "#{field}_lt", -> (value) { send("__numeric_#{field}_comparator", value, "<") }
        scope "#{field}_lte", -> (value) { send("__numeric_#{field}_comparator", value, "<=") }
        scope "#{field}_gte", -> (value) { send("__numeric_#{field}_comparator", value, ">=") }
        scope "#{field}_gt", -> (value) { send("__numeric_#{field}_comparator", value, ">") }
      end

      def ___create_jsonb_date_time_scopes(field, jsonb_attribute, type)
        scope "__date_time_#{field}_comparator", -> (value, operator) { where("((#{table_name}.#{jsonb_attribute}) ->> ?)::timestamp #{operator} ?::timestamp", field, value.to_json) }
        scope "#{field}_before", -> (value) { send("__date_time_#{field}_comparator", value, "<") }
        scope "#{field}_after", -> (value) { send("__date_time_#{field}_comparator", value, ">") }
      end

      def ___create_jsonb_array_scopes(field)
        scope "#{field}_contains", -> (value) { send("with_#{field}", [value]) }
      end

      def _create_jsonb_accessor_methods(jsonb_attribute, jsonb_attribute_initialization_method_name, fields_map)
        jsonb_accessor_methods = Module.new do
          define_method("#{jsonb_attribute}=") do |value|
            write_attribute(jsonb_attribute, value)
            send(jsonb_attribute_initialization_method_name)
          end

          define_method(:reload) do |*args, &block|
            super(*args, &block)
            send(jsonb_attribute_initialization_method_name)
            self
          end
        end

        __create_jsonb_typed_field_setters(jsonb_attribute, jsonb_accessor_methods, fields_map)
        __create_jsonb_nested_field_accessors(jsonb_attribute, jsonb_accessor_methods, fields_map)
        include jsonb_accessor_methods
      end

      def __create_jsonb_typed_field_setters(jsonb_attribute, jsonb_accessor_methods, fields_map)
        fields_map.typed_fields.each do |field, type|
          attribute(field.to_s, TypeHelper.fetch(type))

          jsonb_accessor_methods.instance_eval do
            define_method("#{field}=") do |value, *args, &block|
              super(value, *args, &block)
              new_jsonb_value = (send(jsonb_attribute) || {}).merge(field => attributes[field.to_s])
              write_attribute(jsonb_attribute, new_jsonb_value)
            end
          end
        end
      end

      def __create_jsonb_nested_field_accessors(jsonb_attribute, jsonb_accessor_methods, fields_map)
        fields_map.nested_fields.each do |field, nested_attributes|
          attribute(field.to_s, TypeHelper.fetch(:value))
          jsonb_accessor_methods.instance_eval do
            define_method("#{field}=") do |value|
              instance_class = send("#{jsonb_attribute}_classes")[field]
              instance = cast_nested_field_value(value, instance_class, __method__)

              new_jsonb_value = (send(jsonb_attribute) || {}).merge(field.to_s => instance.attributes)
              write_attribute(jsonb_attribute, new_jsonb_value)
              super(instance)
            end
          end
        end
      end
    end
  end
end