attr_json 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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
  - - ">="