attr_json 2.2.0 → 2.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
  SHA256:
3
- metadata.gz: dadc04fb081fee0b326eaff03f569f15d745948119959e2a8eb5d91b690dec38
4
- data.tar.gz: fb6a28e048e03822432e63a7205d0242d218ddace88dd0566624517c3a5454b5
3
+ metadata.gz: eb4f8078039ec81cc25e7a95318ff5e42cb5bf85a955b40326f60f9022af16a2
4
+ data.tar.gz: 635f08ce5fa34ce45dfdd5256ae0e81fdcc2d455f4525bad5f71f5b0c4391c6b
5
5
  SHA512:
6
- metadata.gz: 6048863f54826dc166200400a93cdfdb91055ddba960d6d2b8587077eafda5e3058e49d030f60c3eb05f74ba2ea99512c087c403f8eb0a18e4dd45c4f7929bee
7
- data.tar.gz: afafb7cf57637cce5f99c3afacd38bc802aca8c5e60b9c0cd1adebc08a482b064ce5a7398c44507b612f94691fd8362bba9cb00d13f8d14603524e235e93b617
6
+ metadata.gz: 22c35f674c7b5f3a4e91f348d20439df3e8fe175ca5d3c8938720a5e71739df2474cdddd5fdab5f0527f8751ed0fb6dee11ba7f75889c143daca92bd84afc86b
7
+ data.tar.gz: 636628f0083dd61c869de5b9eb65a0c11988560e26d5dcd8e822759788bdac4bdb3c6f0359e2257867ac91a61721fda32df54a36f51a001450ef883289be433a
@@ -18,7 +18,10 @@ jobs:
18
18
  tests:
19
19
  services:
20
20
  db:
21
- image: postgres:9.4
21
+ # used to be 9.4, should work on 9.4, only moved to 10.0
22
+ # because of rails 7.1 bug (rails intends to support 9 too!)
23
+ # https://github.com/jrochkind/attr_json/issues/211
24
+ image: postgres:10.0
22
25
  env:
23
26
  POSTGRES_USER: postgres
24
27
  POSTGRES_PASSWORD: postgres
@@ -49,7 +52,7 @@ jobs:
49
52
 
50
53
  - gemfile: rails_7_0
51
54
  ruby: 3.2
52
-
55
+
53
56
  - gemfile: rails_7_1
54
57
  ruby: '3.0'
55
58
 
data/CHANGELOG.md CHANGED
@@ -4,7 +4,7 @@ Notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased](https://github.com/jrochkind/attr_json/compare/v2.2.0...HEAD)
7
+ ## [Unreleased](https://github.com/jrochkind/attr_json/compare/v2.3.0...HEAD)
8
8
 
9
9
  ### Fixed
10
10
 
@@ -22,6 +22,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
22
22
 
23
23
  *
24
24
 
25
+ ### Changed
26
+
27
+ *
28
+
29
+ *
30
+
31
+ *
32
+
33
+ ## [2.3.0]((https://github.com/jrochkind/attr_json/compare/v2.2.0...v2.3.0))
34
+
35
+
36
+ ### Changed
37
+
38
+ * Avoid private ActiveRecord API when lazily registering container attributes. (Compat with Rails post 7.1) https://github.com/jrochkind/attr_json/pull/214
39
+
40
+ * Freeze more strings to reduce String allocations https://github.com/jrochkind/attr_json/pull/216
41
+
25
42
  ## [2.2.0](https://github.com/jrochkind/attr_json/compare/v2.1.0...v2.2.0)
26
43
 
27
44
  ### Added
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'attr_json/attribute_definition'
2
4
 
3
5
  module AttrJson
@@ -25,7 +27,10 @@ module AttrJson
25
27
  def initialize(hash = {})
26
28
  @name_to_definition = hash.dup
27
29
  @store_key_to_definition = {}
28
- definitions.each { |d| store_key_index!(d) }
30
+
31
+ @name_to_definition.values.each { |d| store_key_index!(d) }
32
+
33
+ @container_attributes_registered = Hash.new { Set.new }
29
34
  end
30
35
 
31
36
  def fetch(key, *args, &block)
@@ -46,12 +51,13 @@ module AttrJson
46
51
 
47
52
  # Can return nil if none found.
48
53
  def store_key_lookup(container_attribute, store_key)
49
- @store_key_to_definition[container_attribute.to_s] &&
50
- @store_key_to_definition[container_attribute.to_s][store_key.to_s]
54
+ @store_key_to_definition[AttrJson.efficient_to_s(container_attribute)] &&
55
+ @store_key_to_definition[AttrJson.efficient_to_s(container_attribute)][AttrJson.efficient_to_s(store_key)]
51
56
  end
52
57
 
53
58
  def definitions
54
- @name_to_definition.values
59
+ # Since we are immutable, we can cache this to avoid allocation in a hot spot
60
+ @stored_definitions ||= @name_to_definition.values
55
61
  end
56
62
 
57
63
  # Returns all registered attributes as an array of symbols
@@ -60,7 +66,7 @@ module AttrJson
60
66
  end
61
67
 
62
68
  def container_attributes
63
- @store_key_to_definition.keys.collect(&:to_s)
69
+ @store_key_to_definition.keys.collect { |s| AttrJson.efficient_to_s(s) }
64
70
  end
65
71
 
66
72
  # This is how you register additional definitions, as a non-mutating
@@ -73,6 +79,32 @@ module AttrJson
73
79
  end
74
80
  end
75
81
 
82
+
83
+ # We need to lazily set the container type only the FIRST time
84
+ #
85
+ # While also avoiding this triggering ActiveRecord to actually go to DB,
86
+ # we don't want DB connection forced on boot, that's a problem for many apps,
87
+ # including that may not have a DB connection in initial development.
88
+ # (#type_for_attribute forces DB connection)
89
+ #
90
+ # AND we need to call container attriubte on SUB-CLASS AGAIN, iff sub-class
91
+ # has any of it's own new registrations, to make sure we get the right type in
92
+ # sub-class!
93
+ #
94
+ # So we just keep track of whether we've registered ourselves, so we can
95
+ # first time we need to.
96
+ #
97
+ # While current implementation is simple, this has ended up a bit fragile,
98
+ # a different API that doesn't require us to do this implicitly lazily
99
+ # might be preferred! But this is what we got for now.
100
+ def register_container_attribute(attribute_name:, model:)
101
+ @container_attributes_registered[attribute_name.to_sym] << model
102
+ end
103
+
104
+ def container_attribute_registered?(attribute_name:, model:)
105
+ @container_attributes_registered[attribute_name.to_sym].include?(model)
106
+ end
107
+
76
108
  protected
77
109
 
78
110
  def add!(definition)
@@ -81,17 +113,19 @@ module AttrJson
81
113
  end
82
114
  @name_to_definition[definition.name.to_sym] = definition
83
115
  store_key_index!(definition)
116
+
117
+ @stored_definitions = nil
84
118
  end
85
119
 
86
120
  def store_key_index!(definition)
87
- container_hash = (@store_key_to_definition[definition.container_attribute.to_s] ||= {})
121
+ container_hash = (@store_key_to_definition[AttrJson.efficient_to_s(definition.container_attribute)] ||= {})
88
122
 
89
- if container_hash.has_key?(definition.store_key.to_s)
90
- existing = container_hash[definition.store_key.to_s]
123
+ if container_hash.has_key?(AttrJson.efficient_to_s(definition.store_key))
124
+ existing = container_hash[AttrJson.efficient_to_s(definition.store_key)]
91
125
  raise ArgumentError, "Can't add, store key `#{definition.store_key}` conflicts with existing attribute: #{existing.original_args}"
92
126
  end
93
127
 
94
- container_hash[definition.store_key.to_s] = definition
128
+ container_hash[AttrJson.efficient_to_s(definition.store_key)] = definition
95
129
  end
96
130
  end
97
131
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'attr_json/type/array'
2
4
 
3
5
  module AttrJson
@@ -62,7 +64,7 @@
62
64
  end
63
65
 
64
66
  def store_key
65
- (@store_key || name).to_s
67
+ AttrJson.efficient_to_s(@store_key || name)
66
68
  end
67
69
 
68
70
  def has_default?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/concern'
2
4
  require 'active_model/type'
3
5
 
@@ -136,8 +138,8 @@ module AttrJson
136
138
  def new_from_serializable(attributes = {})
137
139
  attributes = attributes.collect do |key, value|
138
140
  # store keys in arguments get translated to attribute names on initialize.
139
- if attribute_def = self.attr_json_registry.store_key_lookup("", key.to_s)
140
- key = attribute_def.name.to_s
141
+ if attribute_def = self.attr_json_registry.store_key_lookup("".freeze, AttrJson.efficient_to_s(key))
142
+ key = AttrJson.efficient_to_s(attribute_def.name)
141
143
  end
142
144
 
143
145
  attr_type = self.attr_json_registry.has_attribute?(key) && self.attr_json_registry.type_for_attribute(key)
@@ -184,7 +186,7 @@ module AttrJson
184
186
 
185
187
  # like the ActiveModel::Attributes method, hash with name keys, and ActiveModel::Type values
186
188
  def attribute_types
187
- attribute_names.collect { |name| [name.to_s, attr_json_registry.type_for_attribute(name)]}.to_h
189
+ attribute_names.collect { |name| [AttrJson.efficient_to_s(name), attr_json_registry.type_for_attribute(name)]}.to_h
188
190
  end
189
191
 
190
192
 
@@ -225,11 +227,11 @@ module AttrJson
225
227
 
226
228
  _attr_jsons_module.module_eval do
227
229
  define_method("#{name}=") do |value|
228
- _attr_json_write(name.to_s, value)
230
+ _attr_json_write(AttrJson.efficient_to_s(name), value)
229
231
  end
230
232
 
231
233
  define_method("#{name}") do
232
- attributes[name.to_s]
234
+ attributes[AttrJson.efficient_to_s(name)]
233
235
  end
234
236
  end
235
237
  end
@@ -427,7 +429,7 @@ module AttrJson
427
429
 
428
430
  def fill_in_defaults!
429
431
  self.class.attr_json_registry.definitions.each do |definition|
430
- if definition.has_default? && !attributes.has_key?(definition.name.to_s)
432
+ if definition.has_default? && !attributes.has_key?(AttrJson.efficient_to_s(definition.name))
431
433
  self.send("#{definition.name.to_s}=", definition.provide_default!)
432
434
  end
433
435
  end
@@ -435,10 +437,10 @@ module AttrJson
435
437
 
436
438
  def _attr_json_write(key, value)
437
439
  if attribute_def = self.class.attr_json_registry[key.to_sym]
438
- attributes[key.to_s] = attribute_def.cast(value)
440
+ attributes[AttrJson.efficient_to_s(key)] = attribute_def.cast(value)
439
441
  else
440
442
  # TODO, strict mode, ignore, raise, allow.
441
- attributes[key.to_s] = value
443
+ attributes[AttrJson.efficient_to_s(key)] = value
442
444
  end
443
445
  end
444
446
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AttrJson
2
4
  module NestedAttributes
3
5
  # Implementation of `build_` methods, called by the `build_` methods
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AttrJson
2
4
  module NestedAttributes
3
5
  # Rails has a weird "multiparameter attribute" thing, that is used for simple_form's
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'attr_json/nested_attributes/multiparameter_attribute_writer'
2
4
 
3
5
  module AttrJson
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'attr_json/nested_attributes/builder'
2
4
  require 'attr_json/nested_attributes/writer'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AttrJson
2
4
  module Record
3
5
  # Implementation class called by #jsonb_contains scope method. Ordinarily
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'attr_json/record/query_builder'
2
4
 
3
5
  module AttrJson
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'attr_json/attribute_definition'
2
4
  require 'attr_json/attribute_definition/registry'
3
5
  require 'attr_json/type/container_attribute'
@@ -160,22 +162,14 @@ module AttrJson
160
162
  options.assert_valid_keys(AttributeDefinition::VALID_OPTIONS + [:validate, :accepts_nested_attributes])
161
163
  container_attribute = options[:container_attribute]
162
164
 
163
- # TODO arg check container_attribute make sure it exists. Hard cause
164
- # schema isn't loaded yet when class def is loaded. Maybe not.
165
165
 
166
- # Want to lazily add an attribute cover to the json container attribute,
167
- # only if it hasn't already been done. WARNING we are using internal
168
- # Rails API here, but only way to do this lazily, which I thought was
169
- # worth it. On the other hand, I think .attribute is idempotent, maybe we don't need it...
170
- #
171
- # We set default to empty hash, because that 'tricks' AR into knowing any
172
- # application of defaults is a change that needs to be saved.
173
- unless attributes_to_define_after_schema_loads[container_attribute.to_s] &&
174
- attributes_to_define_after_schema_loads[container_attribute.to_s].first.is_a?(AttrJson::Type::ContainerAttribute) &&
175
- attributes_to_define_after_schema_loads[container_attribute.to_s].first.model == self
176
- # If this is already defined, but was for superclass, we need to define it again for
177
- # this class.
166
+ # Make sure to "lazily" register attribute for *container* class if this is the first time
167
+ # this container attribute hsa been encountered for this specific class. The registry
168
+ # helps us keep track. Kinda messy, in future we may want a more explicit API
169
+ # that does not require us to implicitly track first-time per-container.
170
+ unless self.attr_json_registry.container_attribute_registered?(model: self, attribute_name: container_attribute.to_sym)
178
171
  attribute container_attribute.to_sym, AttrJson::Type::ContainerAttribute.new(self, container_attribute), default: -> { {} }
172
+ self.attr_json_registry.register_container_attribute(model: self, attribute_name: container_attribute.to_sym)
179
173
  end
180
174
 
181
175
  self.attr_json_registry = attr_json_registry.with(
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AttrJson
2
4
 
3
5
  # A little wrapper to provide an object that provides #dump and #load method for use
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AttrJson
2
4
  module Type
3
5
  # You can wrap any ActiveModel::Type in one of these, and it's magically
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AttrJson
2
4
  module Type
3
5
  # A type that gets applied to the AR container/store jsonb attribute,
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AttrJson
2
4
  module Type
3
5
  # An ActiveModel::Type representing a particular AttrJson::Model
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module AttrJson
2
4
  module Type
3
5
  # AttrJson::Type::PolymorphicModel can be used to create attr_json attributes
@@ -1,3 +1,3 @@
1
1
  module AttrJson
2
- VERSION = "2.2.0"
2
+ VERSION = "2.3.0"
3
3
  end
data/lib/attr_json.rb CHANGED
@@ -10,5 +10,25 @@ require 'attr_json/record/query_scopes'
10
10
  require 'attr_json/type/polymorphic_model'
11
11
 
12
12
  module AttrJson
13
-
13
+ # We need to convert Symbols to strings a lot at present -- ActiveRecord does too, so
14
+ # not too suprrising.
15
+ #
16
+ # In Rails 3.0 and above, we can use Symbol#name to get a frozen string back
17
+ # and avoid extra allocations. https://bugs.ruby-lang.org/issues/16150
18
+ #
19
+ # Ruby 2.7 doens't yet have it though. As long as we are supporting ruby 2.7,
20
+ # we'll just check at runtime to keep this lean
21
+ if RUBY_VERSION.split('.').first.to_i >= 3
22
+ def self.efficient_to_s(obj)
23
+ if obj.kind_of?(Symbol)
24
+ obj.name
25
+ else
26
+ obj.to_s
27
+ end
28
+ end
29
+ else
30
+ def self.efficient_to_s(obj)
31
+ obj.to_s
32
+ end
33
+ end
14
34
  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: 2.2.0
4
+ version: 2.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: 2023-10-05 00:00:00.000000000 Z
11
+ date: 2023-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -207,7 +207,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
207
207
  - !ruby/object:Gem::Version
208
208
  version: '0'
209
209
  requirements: []
210
- rubygems_version: 3.4.10
210
+ rubygems_version: 3.4.21
211
211
  signing_key:
212
212
  specification_version: 4
213
213
  summary: ActiveRecord attributes stored serialized in a json column, super smooth.