Skip to content

Commit

Permalink
Merge pull request #155 from madeintandem/bugfix/deserialization-of-d…
Browse files Browse the repository at this point in the history
…atetime

Properly parse datetime values when attributes are set from json value
  • Loading branch information
haffla authored Sep 23, 2022
2 parents 7f728fd + 7f2287b commit 5bf0690
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 24 deletions.
4 changes: 2 additions & 2 deletions lib/jsonb_accessor/attribute_query_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ def define(store_key_mapping_method_name, jsonb_attribute)

# <jsonb_attribute>_where scope
klass.define_singleton_method "#{jsonb_attribute}_where" do |attributes|
store_key_attributes = ::JsonbAccessor::Helpers.convert_keys_to_store_keys(attributes, all.model.public_send(store_key_mapping_method_name))
store_key_attributes = JsonbAccessor::Helpers.convert_keys_to_store_keys(attributes, all.model.public_send(store_key_mapping_method_name))
jsonb_where(jsonb_attribute, store_key_attributes)
end

# <jsonb_attribute>_where_not scope
klass.define_singleton_method "#{jsonb_attribute}_where_not" do |attributes|
store_key_attributes = ::JsonbAccessor::Helpers.convert_keys_to_store_keys(attributes, all.model.public_send(store_key_mapping_method_name))
store_key_attributes = JsonbAccessor::Helpers.convert_keys_to_store_keys(attributes, all.model.public_send(store_key_mapping_method_name))
jsonb_where_not(jsonb_attribute, store_key_attributes)
end

Expand Down
14 changes: 14 additions & 0 deletions lib/jsonb_accessor/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,19 @@ def convert_keys_to_store_keys(attributes, store_key_mapping)
def convert_store_keys_to_keys(attributes, store_key_mapping)
convert_keys_to_store_keys(attributes, store_key_mapping.invert)
end

def deserialize_value(value, attribute_type)
return value if value.blank?

if attribute_type == :datetime
value = if active_record_default_timezone == :utc
Time.find_zone("UTC").parse(value).in_time_zone
else
Time.zone.parse(value)
end
end

value
end
end
end
15 changes: 9 additions & 6 deletions lib/jsonb_accessor/macro.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def jsonb_accessor(jsonb_attribute, field_types)
end

# Get store keys to default values mapping
store_keys_and_defaults = ::JsonbAccessor::Helpers.convert_keys_to_store_keys(names_and_defaults, public_send(store_key_mapping_method_name))
store_keys_and_defaults = JsonbAccessor::Helpers.convert_keys_to_store_keys(names_and_defaults, public_send(store_key_mapping_method_name))

# Define jsonb_defaults_mapping_for_<jsonb_attribute>
defaults_mapping_method_name = "jsonb_defaults_mapping_for_#{jsonb_attribute}"
Expand Down Expand Up @@ -69,7 +69,7 @@ def jsonb_accessor(jsonb_attribute, field_types)
attribute_value = public_send(name)
# Rails always saves time based on `default_timezone`. Since #as_json considers timezone, manual conversion is needed
if attribute_value.acts_like?(:time)
attribute_value = (::JsonbAccessor::Helpers.active_record_default_timezone == :utc ? attribute_value.utc : attribute_value.in_time_zone).strftime("%F %R:%S.%L")
attribute_value = (JsonbAccessor::Helpers.active_record_default_timezone == :utc ? attribute_value.utc : attribute_value.in_time_zone).strftime("%F %R:%S.%L")
end

new_values = (public_send(jsonb_attribute) || {}).merge(store_key => attribute_value)
Expand All @@ -83,11 +83,11 @@ def jsonb_accessor(jsonb_attribute, field_types)
names_to_store_keys = self.class.public_send(store_key_mapping_method_name)

# this is the raw hash we want to save in the jsonb_attribute
value_with_store_keys = ::JsonbAccessor::Helpers.convert_keys_to_store_keys(value, names_to_store_keys)
value_with_store_keys = JsonbAccessor::Helpers.convert_keys_to_store_keys(value, names_to_store_keys)
write_attribute(jsonb_attribute, value_with_store_keys)

# this maps attributes to values
value_with_named_keys = ::JsonbAccessor::Helpers.convert_store_keys_to_keys(value, names_to_store_keys)
value_with_named_keys = JsonbAccessor::Helpers.convert_store_keys_to_keys(value, names_to_store_keys)

empty_named_attributes = names_to_store_keys.transform_values { nil }
empty_named_attributes.merge(value_with_named_keys).each do |name, attribute_value|
Expand All @@ -109,12 +109,15 @@ def jsonb_accessor(jsonb_attribute, field_types)
name = names_and_store_keys.key(store_key)
next unless name

write_attribute(name, value)
write_attribute(
name,
JsonbAccessor::Helpers.deserialize_value(value, self.class.type_for_attribute(name).type)
)
clear_attribute_change(name) if persisted?
end
end

::JsonbAccessor::AttributeQueryMethods.new(self).define(store_key_mapping_method_name, jsonb_attribute)
JsonbAccessor::AttributeQueryMethods.new(self).define(store_key_mapping_method_name, jsonb_attribute)
end
end
end
Expand Down
36 changes: 20 additions & 16 deletions spec/jsonb_accessor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -579,32 +579,36 @@ def build_class(jsonb_accessor_config, &block)
end

context "datetime field" do
let(:klass) do
build_class(foo: :datetime)
end
let(:time_with_zone) do
Time.new(2022, 1, 1, 12, 5, 0, "-03:00")
end
let(:klass) { build_class(foo: :datetime) }
let(:time_with_zone) { Time.new(2022, 1, 1, 12, 5, 0, "-03:00") }
let(:now) { Time.zone.parse("2022-09-18 09:44:00") }

it "saves in UTC" do
instance.foo = time_with_zone
expect(instance.options).to eq({ "foo" => "2022-01-01 15:05:00.000" })
end

context "when default_timezone is local" do
around(:each) do |example|
active_record_base = if ActiveRecord.respond_to? :default_timezone
ActiveRecord
else
ActiveRecord::Base
end
active_record_base.default_timezone = :local
example.run
active_record_base.default_timezone = :utc
it "deserializes to time with zone", tz: "America/Los_Angeles" do
travel_to now do
# we are -7 hours from UTC
instance = klass.new({ options: { "foo" => "2022-09-18 16:44:00" } })
expect(instance.foo).to eq Time.new(2022, 9, 18, 9, 44, 0, "-07:00")
end
end

context "when default_timezone is local", ar_default_tz: :local do
it "saves in local time" do
instance.foo = time_with_zone
expect(instance.options).to eq({ "foo" => "2022-01-01 12:05:00.000" })
end

it "deserializes to time with zone", tz: "Europe/Berlin" do
travel_to now do
# we are +2 hours from UTC
instance = klass.new({ options: { "foo" => "2022-09-18 16:44:00" } })
expect(instance.foo).to eq Time.new(2022, 9, 18, 16, 44, 0, "+02:00")
end
end
end
end

Expand Down
18 changes: 18 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require "awesome_print"
require "database_cleaner"
require "yaml"
require "active_support/testing/time_helpers"

dbconfig = YAML.safe_load(ERB.new(File.read(File.join("db", "config.yml"))).result, aliases: true)
ActiveRecord::Base.establish_connection(dbconfig["test"])
Expand All @@ -34,6 +35,7 @@ class ProductCategory < ActiveRecord::Base
end

RSpec.configure do |config|
config.include ActiveSupport::Testing::TimeHelpers
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
Expand All @@ -42,6 +44,22 @@ class ProductCategory < ActiveRecord::Base
mocks.verify_partial_doubles = true
end

config.around :example, :tz do |example|
Time.use_zone(example.metadata[:tz]) { example.run }
end

config.around :example, :ar_default_tz do |example|
active_record_base = if ActiveRecord.respond_to? :default_timezone
ActiveRecord
else
ActiveRecord::Base
end
old_default = active_record_base.default_timezone
active_record_base.default_timezone = example.metadata[:ar_default_tz]
example.run
active_record_base.default_timezone = old_default
end

config.filter_run :focus
config.run_all_when_everything_filtered = true
config.disable_monkey_patching!
Expand Down

0 comments on commit 5bf0690

Please sign in to comment.