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