attr_json 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1033d8a923d5935a8b18e10612883d6756279089
4
- data.tar.gz: 8c8b5faf3c24cc27d1d6e382b44c3a63b180826a
3
+ metadata.gz: 9a4f8e06e4c2ae8894433a2271d782e0aa023d3c
4
+ data.tar.gz: 4b4e44c1af5d89affd2f2aadde14610eda05aadd
5
5
  SHA512:
6
- metadata.gz: 57dec15a2438c7d5fa8b2460afc3c3fcedef01aad80a78bf9bb1511b9be68bb6b7166acb7dc9b1aa25d5614c8cd7eb688fc55433ed9bce1fdb8eab8d1061e22d
7
- data.tar.gz: 76f0397e93f14b3ca5ff38c01bf4da3b6d4ea11080414d60e7da2126ecffc78926e087245fe82cc8d30a5d8bc6ffa41d2092b096ed8bcb2ad91149960610a85d
6
+ metadata.gz: 1de9b6472284debc8aa8e60e26d7a8ba2463543b8515248470584ddf06b493d90c9fc4e4e221f244e00421a07703564e3ff3a3d7d6e35bead76adedaeeaabd3a
7
+ data.tar.gz: cb0567d3031ececf33c64895130cd78bd2295e382547fc3cb4f0ad17072602e6fa71ad24645564abc1ff3ed354a07655140cea7a57cc477e3887470c9e546be2
data/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![Gem Version](https://badge.fury.io/rb/attr_json.svg)](https://badge.fury.io/rb/attr_json)
4
4
 
5
5
 
6
- ActiveRecord attributes stored serialized in a json column, super smooth. For Rails 5.0, 5.1, or 5.2.
6
+ ActiveRecord attributes stored serialized in a json column, super smooth. For Rails 5.0, 5.1, or 5.2. Ruby 2.4+.
7
7
 
8
8
  Typed and cast like Active Record. Supporting [nested models](#nested), [dirty tracking](#dirty), some [querying](#querying) (with postgres [jsonb](https://www.postgresql.org/docs/9.5/static/datatype-json.html) contains), and [working smoothy with form builders](#forms).
9
9
 
@@ -85,7 +85,7 @@ class OtherModel < ActiveRecord::Base
85
85
  include AttrJson::Record
86
86
 
87
87
  # as a default for the model
88
- self.default_json_container_attribute = :some_other_column_name
88
+ attr_json_config(default_container_attribute: :some_other_column_name)
89
89
 
90
90
  # now this is going to serialize to column 'some_other_column_name'
91
91
  attr_json :my_int, :integer
@@ -410,6 +410,16 @@ Except for the jsonb_contains stuff using postgres jsonb contains operator, I do
410
410
  * Could we make these attributes work in ordinary AR where, same
411
411
  as they do in jsonb_contains? Maybe.
412
412
 
413
+ ## Development
414
+
415
+ While `attr_json` depends only on `active_record`, we run integration tests in the context of a full Rails app, in order to test working with simple_form and cocoon, among other things. (Via [combustion](https://github.com/pat/combustion), with app skeleton at [./spec/internal](./spec/internal)).
416
+
417
+ At present this does mean that all our automated tests are run in a full Rails environment, which is not great (any suggestions or PR's to fix this while still running integration tests under CI with full Rails app).
418
+
419
+ Tests are in rspec, run tests simply with `./bin/rspec`.
420
+
421
+ There is a `./bin/console` that will give you a console in the context of attr_json and all it's dependencies, including the combustion rails app, and the models defined there.
422
+
413
423
  ## Acknowledements and Prior Art
414
424
 
415
425
  * The excellent work [Sean Griffin](https://twitter.com/sgrif) did on ActiveModel::Type
@@ -40,6 +40,8 @@ attributes use as much of the existing ActiveRecord architecture as we can.}
40
40
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
41
41
  spec.require_paths = ["lib"]
42
42
 
43
+ spec.required_ruby_version = '>= 2.4.0'
44
+
43
45
  spec.add_runtime_dependency "activerecord", ">= 5.0.0", "< 5.3"
44
46
 
45
47
  spec.add_development_dependency "bundler", "~> 1.14"
@@ -133,7 +133,7 @@ model.attr_json_changes.merged.changes_to_save
133
133
  # and attr_jsons, as applicable for changes.
134
134
  ```
135
135
 
136
- This will ordinarily include your json container attributes (eg `attr_jsons`)
136
+ This will ordinarily include your json container attributes (eg `json_attributes`)
137
137
  too, as they will show up in ordinary AR dirty tracking since they are just AR
138
138
  columns.
139
139
 
@@ -17,7 +17,7 @@ You can look at our [stub app used for integration tests](../spec/internal) as a
17
17
  Use with form builder just as you would anything else.
18
18
 
19
19
  f.text_field :some_string
20
- f.datetimme_field :some_datetime
20
+ f.datetime_field :some_datetime
21
21
 
22
22
  It _will_ work with the weird rails multi-param setting used for date fields.
23
23
 
@@ -3,6 +3,7 @@ require "attr_json/version"
3
3
  require "active_record"
4
4
  require "active_record/connection_adapters/postgresql_adapter"
5
5
 
6
+ require 'attr_json/config'
6
7
  require 'attr_json/record'
7
8
  require 'attr_json/model'
8
9
  require 'attr_json/nested_attributes'
@@ -23,7 +23,7 @@ module AttrJson
23
23
  # All references in code to "definition" are to a AttrJson::AttributeDefinition instance.
24
24
  class Registry
25
25
  def initialize(hash = {})
26
- @name_to_definition = hash
26
+ @name_to_definition = hash.dup
27
27
  @store_key_to_definition = {}
28
28
  definitions.each { |d| store_key_index!(d) }
29
29
  end
@@ -0,0 +1,45 @@
1
+ module AttrJson
2
+ # Intentionally non-mutable, to avoid problems with subclass inheritance
3
+ # and rails class_attribute. Instead, you set to new Config object
4
+ # changed with {#merge}.
5
+ class Config
6
+ RECORD_ALLOWED_KEYS = %i{default_container_attribute}
7
+ MODEL_ALLOWED_KEYS = %i{unknown_key}
8
+ DEFAULTS = {
9
+ default_container_attribute: "json_attributes",
10
+ unknown_key: :raise
11
+ }
12
+
13
+ (MODEL_ALLOWED_KEYS + RECORD_ALLOWED_KEYS).each do |key|
14
+ define_method(key) do
15
+ attributes[key]
16
+ end
17
+ end
18
+
19
+ attr_reader :mode
20
+
21
+ def initialize(options = {})
22
+ @mode = options.delete(:mode)
23
+ unless mode == :record || mode == :model
24
+ raise ArgumentError, "required :mode argument must be :record or :model"
25
+ end
26
+ valid_keys = mode == :record ? RECORD_ALLOWED_KEYS : MODEL_ALLOWED_KEYS
27
+ options.assert_valid_keys(valid_keys)
28
+
29
+ options.reverse_merge!(DEFAULTS.slice(*valid_keys))
30
+
31
+ @attributes = options
32
+ end
33
+
34
+ # Returns a new Config object, with changes merged in.
35
+ def merge(changes = {})
36
+ self.class.new(attributes.merge(changes).merge(mode: mode))
37
+ end
38
+
39
+ protected
40
+
41
+ def attributes
42
+ @attributes
43
+ end
44
+ end
45
+ end
@@ -21,12 +21,18 @@ module AttrJson
21
21
  # @note Includes ActiveModel::Model whether you like it or not. TODO, should it?
22
22
  #
23
23
  # You can control what happens if you set an unknown key (one that you didn't
24
- # register with `attr_json`) with the class attribute `attr_json_unknown_key`.
24
+ # register with `attr_json`) with the config attribute `attr_json_config(unknown_key:)`.
25
25
  # * :raise (default) raise ActiveModel::UnknownAttributeError
26
26
  # * :strip Ignore the unknown key and do not include it, without raising.
27
27
  # * :allow Allow the unknown key and it's value to be in the serialized hash,
28
28
  # and written to the database. May be useful for legacy data or columns
29
29
  # that other software touches, to let unknown keys just flow through.
30
+ #
31
+ # class Something
32
+ # include AttrJson::Model
33
+ # attr_json_config(unknown_key: :ignore)
34
+ # #...
35
+ # end
30
36
  module Model
31
37
  extend ActiveSupport::Concern
32
38
 
@@ -41,13 +47,30 @@ module AttrJson
41
47
 
42
48
  class_attribute :attr_json_registry, instance_accessor: false
43
49
  self.attr_json_registry = ::AttrJson::AttributeDefinition::Registry.new
44
-
45
- # :raise, :strip, :allow. :raise is default. Is there some way to enforce this.
46
- class_attribute :attr_json_unknown_key
47
- self.attr_json_unknown_key ||= :raise
48
50
  end
49
51
 
50
52
  class_methods do
53
+ def attr_json_config(new_values = {})
54
+ if new_values.present?
55
+ # get one without new values, then merge new values into it, and
56
+ # set it locally for this class.
57
+ @attr_json_config = attr_json_config.merge(new_values)
58
+ else
59
+ if instance_variable_defined?("@attr_json_config")
60
+ # we have a custom one for this class, return it.
61
+ @attr_json_config
62
+ elsif superclass.respond_to?(:attr_json_config)
63
+ # return superclass without setting it locally, so changes in superclass
64
+ # will continue effecting us.
65
+ superclass.attr_json_config
66
+ else
67
+ # no superclass, no nothing, set it to blank one.
68
+ @attr_json_config = Config.new(mode: :model)
69
+ end
70
+ end
71
+ end
72
+
73
+
51
74
  # Like `.new`, but translate store keys in hash
52
75
  def new_from_serializable(attributes = {})
53
76
  attributes = attributes.transform_keys do |key|
@@ -247,7 +270,7 @@ module AttrJson
247
270
 
248
271
 
249
272
  def _attr_json_write_unknown_attribute(key, value)
250
- case attr_json_unknown_key
273
+ case self.class.attr_json_config.unknown_key
251
274
  when :strip
252
275
  # drop it, no-op
253
276
  when :allow
@@ -17,8 +17,6 @@ module AttrJson
17
17
  module Record
18
18
  extend ActiveSupport::Concern
19
19
 
20
- DEFAULT_CONTAINER_ATTRIBUTE = :json_attributes
21
-
22
20
  included do
23
21
  unless self < ActiveRecord::Base
24
22
  raise TypeError, "AttrJson::Record can only be used with an ActiveRecord::Base model. #{self} does not appear to be one. Are you looking for ::AttrJson::Model?"
@@ -26,12 +24,45 @@ module AttrJson
26
24
 
27
25
  class_attribute :attr_json_registry, instance_accessor: false
28
26
  self.attr_json_registry = AttrJson::AttributeDefinition::Registry.new
29
-
30
- class_attribute :default_json_container_attribute, instance_acessor: false
31
- self.default_json_container_attribute ||= DEFAULT_CONTAINER_ATTRIBUTE
32
27
  end
33
28
 
34
29
  class_methods do
30
+ # Access or set class-wide json_attribute_config. Inherited by sub-classes,
31
+ # but setting on sub-classes is unique to subclass. Similar to how
32
+ # rails class_attribute's are used.
33
+ #
34
+ # @example access config
35
+ # SomeClass.attr_json_config
36
+ #
37
+ # @example set config variables
38
+ # class SomeClass < ActiveRecordBase
39
+ # include JsonAttribute::Record
40
+ #
41
+ # attr_json_config(default_container_attribute: "some_column")
42
+ # end
43
+ # TODO make Model match please.
44
+ def attr_json_config(new_values = {})
45
+ if new_values.present?
46
+ # get one without new values, then merge new values into it, and
47
+ # set it locally for this class.
48
+ @attr_json_config = attr_json_config.merge(new_values)
49
+ else
50
+ if instance_variable_defined?("@attr_json_config")
51
+ # we have a custom one for this class, return it.
52
+ @attr_json_config
53
+ elsif superclass.respond_to?(:attr_json_config)
54
+ # return superclass without setting it locally, so changes in superclass
55
+ # will continue effecting us.
56
+ superclass.attr_json_config
57
+ else
58
+ # no superclass, no nothing, set it to blank one.
59
+ @attr_json_config = Config.new(mode: :record)
60
+ end
61
+ end
62
+ end
63
+
64
+
65
+
35
66
  # Type can be a symbol that will be looked up in `ActiveModel::Type.lookup`,
36
67
  # or an ActiveModel:::Type::Value).
37
68
  #
@@ -47,9 +78,9 @@ module AttrJson
47
78
  # @option options [String,Symbol] :store_key (nil) Serialize to JSON using
48
79
  # given store_key, rather than name as would be usual.
49
80
  #
50
- # @option options [Symbol,String] :container_attribute (self.default_json_container_attribute) The real
81
+ # @option options [Symbol,String] :container_attribute (attr_json_config.default_container_attribute, normally `json_attributes`) The real
51
82
  # json(b) ActiveRecord attribute/column to serialize as a key in. Defaults to
52
- # `self.default_json_container_attribute`, which defaults to `:attr_jsons`
83
+ # `attr_json_config.default_container_attribute`, which defaults to `:json_attributes`
53
84
  #
54
85
  # @option options [Boolean] :validate (true) Create an ActiveRecord::Validations::AssociatedValidator so
55
86
  # validation errors on the attributes post up to self.
@@ -62,7 +93,7 @@ module AttrJson
62
93
  options = {
63
94
  rails_attribute: false,
64
95
  validate: true,
65
- container_attribute: self.default_json_container_attribute
96
+ container_attribute: self.attr_json_config.default_container_attribute
66
97
  }.merge!(options)
67
98
  options.assert_valid_keys(AttributeDefinition::VALID_OPTIONS + [:validate, :rails_attribute])
68
99
  container_attribute = options[:container_attribute]
@@ -74,9 +105,15 @@ module AttrJson
74
105
  # only if it hasn't already been done. WARNING we are using internal
75
106
  # Rails API here, but only way to do this lazily, which I thought was
76
107
  # worth it. On the other hand, I think .attribute is idempotent, maybe we don't need it...
108
+ #
109
+ # We set default to empty hash, because that 'tricks' AR into knowing any
110
+ # application of defaults is a change that needs to be saved.
77
111
  unless attributes_to_define_after_schema_loads[container_attribute.to_s] &&
78
- attributes_to_define_after_schema_loads[container_attribute.to_s].first.is_a?(AttrJson::Type::ContainerAttribute)
79
- attribute container_attribute.to_sym, AttrJson::Type::ContainerAttribute.new(self, container_attribute)
112
+ attributes_to_define_after_schema_loads[container_attribute.to_s].first.is_a?(AttrJson::Type::ContainerAttribute) &&
113
+ attributes_to_define_after_schema_loads[container_attribute.to_s].first.model == self
114
+ # If this is already defined, but was for superclass, we need to define it again for
115
+ # this class.
116
+ attribute container_attribute.to_sym, AttrJson::Type::ContainerAttribute.new(self, container_attribute), default: -> { {} }
80
117
  end
81
118
 
82
119
  self.attr_json_registry = attr_json_registry.with(
@@ -137,20 +137,20 @@ module AttrJson
137
137
  end
138
138
 
139
139
  def saved_changes
140
- saved_changes = model.saved_changes
141
- return {} if saved_changes == {}
140
+ original_saved_changes = model.saved_changes
141
+ return {} if original_saved_changes.blank?
142
142
 
143
143
  json_attr_changes = registry.definitions.collect do |definition|
144
- if container_change = saved_changes[definition.container_attribute]
145
- old_v = container_change[0][definition.store_key]
146
- new_v = container_change[1][definition.store_key]
144
+ if container_change = original_saved_changes[definition.container_attribute]
145
+ old_v = container_change.dig(0, definition.store_key)
146
+ new_v = container_change.dig(1, definition.store_key)
147
147
  if old_v != new_v
148
148
  [ definition.name.to_s, formatted_before_after(old_v, new_v, definition) ]
149
149
  end
150
150
  end
151
151
  end.compact.to_h
152
152
 
153
- prepared_changes(json_attr_changes, saved_changes)
153
+ prepared_changes(json_attr_changes, original_saved_changes)
154
154
  end
155
155
 
156
156
  def saved_changes?
@@ -198,21 +198,21 @@ module AttrJson
198
198
  end
199
199
 
200
200
  def changes_to_save
201
- changes_to_save = model.changes_to_save
201
+ original_changes_to_save = model.changes_to_save
202
202
 
203
- return {} if changes_to_save == {}
203
+ return {} if original_changes_to_save.blank?
204
204
 
205
205
  json_attr_changes = registry.definitions.collect do |definition|
206
- if container_change = changes_to_save[definition.container_attribute]
207
- old_v = container_change[0][definition.store_key]
208
- new_v = container_change[1][definition.store_key]
206
+ if container_change = original_changes_to_save[definition.container_attribute]
207
+ old_v = container_change.dig(0, definition.store_key)
208
+ new_v = container_change.dig(1, definition.store_key)
209
209
  if old_v != new_v
210
210
  [ definition.name.to_s, formatted_before_after(old_v, new_v, definition) ]
211
211
  end
212
212
  end
213
213
  end.compact.to_h
214
214
 
215
- prepared_changes(json_attr_changes, changes_to_save)
215
+ prepared_changes(json_attr_changes, original_changes_to_save)
216
216
  end
217
217
 
218
218
  def has_changes_to_save?
@@ -38,19 +38,30 @@ module AttrJson
38
38
  [key, attr_def ? attr_def.serialize(value) : value]
39
39
  end.to_h)
40
40
  end
41
- def deserialize(v)
42
- h = super || {}
41
+
42
+ # optional with_defaults arg is our own, not part of ActiveModel::Type API,
43
+ # used by {#changed_in_place?} so we can consider default application to
44
+ # be a change.
45
+ def deserialize(v, with_defaults: true)
46
+ h = super(v) || {}
43
47
  model.attr_json_registry.definitions.each do |attr_def|
44
48
  next unless container_attribute.to_s == attr_def.container_attribute.to_s
45
49
 
46
50
  if h.has_key?(attr_def.store_key)
47
51
  h[attr_def.store_key] = attr_def.deserialize(h[attr_def.store_key])
48
- elsif attr_def.has_default?
52
+ elsif with_defaults && attr_def.has_default?
49
53
  h[attr_def.store_key] = attr_def.provide_default!
50
54
  end
51
55
  end
52
56
  h
53
57
  end
58
+
59
+ # Just like superclass, but we tell deserialize to NOT apply defaults,
60
+ # so we can consider default-application to be a change.
61
+ def changed_in_place?(raw_old_value, new_value)
62
+ deserialize(raw_old_value, with_defaults: false) != new_value
63
+ end
64
+
54
65
  end
55
66
  end
56
67
  end
@@ -1,3 +1,3 @@
1
1
  module AttrJson
2
- VERSION = "0.2.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: attr_json
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Rochkind
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-05-04 00:00:00.000000000 Z
11
+ date: 2018-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -133,6 +133,7 @@ files:
133
133
  - lib/attr_json.rb
134
134
  - lib/attr_json/attribute_definition.rb
135
135
  - lib/attr_json/attribute_definition/registry.rb
136
+ - lib/attr_json/config.rb
136
137
  - lib/attr_json/model.rb
137
138
  - lib/attr_json/model/cocoon_compat.rb
138
139
  - lib/attr_json/nested_attributes.rb
@@ -163,7 +164,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
163
164
  requirements:
164
165
  - - ">="
165
166
  - !ruby/object:Gem::Version
166
- version: '0'
167
+ version: 2.4.0
167
168
  required_rubygems_version: !ruby/object:Gem::Requirement
168
169
  requirements:
169
170
  - - ">="