active_typed_store 1.1.0 → 1.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: b7e95f3805fc3ef46bb5fa9ce567e2470396150130720d78d7617a1bd1b63d44
4
- data.tar.gz: 9d178c5936bab0bd8ec4c400102b7b784ea5a4666697e34a6efeebfd3703088a
3
+ metadata.gz: c00294f06d7b42ef340d56715c9e86525be7ce1c1aea054fed1faf11b7d6d7bf
4
+ data.tar.gz: 271d28763908598806ff221a292f9a88d4b773909da217cdec13199d8d7a0a20
5
5
  SHA512:
6
- metadata.gz: 8c140d6d25206075d0db2493f54dbb468902b45a98a42748c5e56fe7e513b418d1c2ca527fdada93309718dd3f4d92050f66ddd067d5e879c62b0cadd22ba1bc
7
- data.tar.gz: ba3b3ce6ed5a4c7c17464765dbe9cfad4aa910fc48e03b3a89ddbaef5fba4a84b2ad57ac2aaad618a45bcf1b7ff59d93d25b0380158001443e6747b45e71bf69
6
+ metadata.gz: 2d26d57a57f40f3b02007a3033990a1102183043e184e8a25406ef7afbf7b3e884f3a3c7b529be25b05c253042ddfb4503b9ef1279a12ac99914577167c96a9a
7
+ data.tar.gz: 11a1002a926fd01f00bc6b922aff49cde6847f66f38260275c0f45127f75484a67280be86716b0873de70b29cf7894b5b2bd3d44334e95d7a8a9e341a575cbe9
data/Gemfile.lock CHANGED
@@ -22,7 +22,7 @@ GIT
22
22
  PATH
23
23
  remote: .
24
24
  specs:
25
- active_typed_store (1.0.0)
25
+ active_typed_store (1.2.0)
26
26
  activemodel
27
27
  activerecord
28
28
  activesupport
@@ -141,6 +141,8 @@ GEM
141
141
 
142
142
  PLATFORMS
143
143
  arm64-darwin-21
144
+ arm64-darwin-23
145
+ arm64-darwin-24
144
146
  x86_64-darwin-21
145
147
  x86_64-linux
146
148
 
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # ActiveTypedStore
2
2
 
3
- `active_typed_store` is a lightweight (__65 lines of code__) and highly performant gem (see [benchmarks](#benchmarks))
3
+ `active_typed_store` is a lightweight (__105 lines of code__) and highly performant gem (see [benchmarks](#benchmarks))
4
4
  designed to help you store and manage typed data in JSON format within database.
5
5
  This gem provides a simple, yet powerful way to ensure that your JSON data cast
6
6
  to specific types, enabling more structured and reliable use of JSON fields in your Rails models.
@@ -47,7 +47,7 @@ m.asap? # => true
47
47
  - `name` the name of the accessor to the store
48
48
  - `type` a symbol such as `:string` or `:integer`, or a type object to be used for the accessor
49
49
  - `options` (optional), a hash of cast type options such as:
50
- - `precision`, `limit`, `scale`
50
+ - `precision`, `limit`, `scale`
51
51
  - `default` the default value to use when no value is provided. Otherwise, the default will be nil
52
52
  - `array` specifies that the type should be an array
53
53
 
@@ -80,17 +80,41 @@ class Model < ActiveRecord::Base
80
80
  end
81
81
  ```
82
82
 
83
+ ### Hash safety
84
+ This gem assumes you're using a database that supports structured data types, such as `json` in `PostgreSQL` or `MySQL`, and leverages Rails' [store_accessor](https://edgeapi.rubyonrails.org/classes/ActiveRecord/Store.html) under the hood. However, there’s one caveat: JSON columns use a string-keyed hash and don’t support access via symbols. To avoid unexpected errors when accessing the hash, we raise an error if a symbol is used. You can disable this behavior by setting `config.hash_safety = false`.
85
+
86
+ ```ruby
87
+ class Model < ActiveRecord::Base
88
+ typed_store(:params) do
89
+ attr :price, :decimal
90
+ end
91
+ end
92
+
93
+ record = Model.new(price: 1)
94
+ record["price"] # 1
95
+ record[:price] # raises "Symbol keys are not allowed `:price` (ActiveTypedStore::SymbolKeysDisallowed)"
96
+
97
+ # initializers/active_type_store.rb
98
+ ActiveTypeStore.configure do |config|
99
+ config.hash_safety = false # default :disallow_symbol_keys
100
+ end
101
+
102
+ record["price"] # 1
103
+ record[:price] # nil - isn't the expected behavior for most applications
104
+ ```
105
+
83
106
  ### Benchmarks
84
107
  compare `active_typed_store` with other gems
85
108
  ```ruby
109
+ # ruby 3.3.5 arm64-darwin24
86
110
  # gem getter i/s setter i/s Lines of code
87
- # rails (without types): 27930.8 660 170
88
- # active_typed_store: 24318.5 - 1.15x slower 656 65
89
- # store_attribute: 23748.3 - 1.18x slower 639 276
90
- # store_model: 23324.4 - 1.20x slower 595 857
91
- # attr_json: 15541.4 - 1.80x slower 577 - 1.14x slower 1195
92
- # jsonb_accessor: 15000.1 - 1.86x slower 626 324
93
- ```
111
+ # active_typed_store: 28502.2 656 105
112
+ # rails (without types): 27350.5 660 170
113
+ # store_attribute: 24592.2 - 1.16x slower 639 276
114
+ # store_model: 22833.6 - 1.25x slower 595 857
115
+ # attr_json: 14000.4 - 2.03x slower 577 - 1.14x slower 1195
116
+ # jsonb_accessor: 13995.4 - 2.04x slower 626 324
117
+ ```
94
118
 
95
119
  ## License
96
120
 
@@ -5,7 +5,7 @@ module ActiveTypedStore
5
5
  attr_reader :fields, :store_module, :store_attribute
6
6
 
7
7
  def initialize(store_attribute)
8
- @store_attribute = store_attribute
8
+ @store_attribute = store_attribute.is_a?(Symbol) ? store_attribute.name : store_attribute
9
9
  @fields = []
10
10
  @store_module = Module.new
11
11
  end
@@ -50,9 +50,14 @@ module ActiveTypedStore
50
50
  cache_val =
51
51
  if val.nil? && !default.nil?
52
52
  is_changed = attribute_changed?(store_attribute)
53
- stored_value = self[store_attribute][field] = default.dup
54
- clear_attribute_change(store_attribute) unless is_changed
55
- stored_value
53
+ self[store_attribute][field] = default.dup
54
+
55
+ if is_changed
56
+ self[store_attribute][field]
57
+ else
58
+ # for discard changes
59
+ @attributes.write_from_database(store_attribute, self[store_attribute]).value[field]
60
+ end
56
61
  elsif type.respond_to?(:cast)
57
62
  casted = type.cast(val)
58
63
  casted.eql?(val) ? val : casted
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveTypedStore
4
+ class Configuration
5
+ attr_accessor :hash_safety
6
+
7
+ def initialize
8
+ self.hash_safety = :disallow_symbol_keys
9
+ end
10
+ end
11
+
12
+ module Configurable
13
+ attr_writer :config
14
+
15
+ def config
16
+ @config ||= ActiveTypedStore::Configuration.new
17
+ end
18
+
19
+ def configure
20
+ yield(config)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveTypedStore
4
+ class SymbolKeysDisallowed < StandardError; end
5
+
6
+ DEFAULT_PROC =
7
+ proc do |_hash, key|
8
+ raise(SymbolKeysDisallowed, "Symbol keys are not allowed `#{key.inspect}`") if key.is_a?(Symbol)
9
+ end
10
+
11
+ module DisallowSymbolKeys
12
+ def self.call!(hash)
13
+ return unless hash.is_a?(Hash)
14
+
15
+ hash.default_proc ||= DEFAULT_PROC
16
+ hash.each_value { call!(_1) }
17
+ end
18
+ end
19
+
20
+ # Same interface as ActiveRecord::Store::HashAccessor
21
+ # Optimized by using object.read_attribute(attribute) instead of object.send(attribute)
22
+ class StoreHashAccessor
23
+ def self.read(object, attribute, key)
24
+ prepare(object, attribute)
25
+ object.read_attribute(attribute)[key]
26
+ end
27
+
28
+ def self.write(object, attribute, key, value)
29
+ prepare(object, attribute)
30
+ object.read_attribute(attribute)[key] = value if value != object.read_attribute(attribute)[key]
31
+ end
32
+
33
+ def self.prepare(object, attribute)
34
+ object.public_send :"#{attribute}=", {} unless object.read_attribute(attribute)
35
+ end
36
+ end
37
+ end
@@ -6,7 +6,20 @@ module ActiveTypedStore
6
6
  attrs = Attrs.new(store_attribute)
7
7
  attrs.instance_eval(&)
8
8
 
9
- store store_attribute, accessors: attrs.fields, coder: JSON
9
+ store_accessor store_attribute, attrs.fields
10
+
11
+ if ActiveTypedStore.config.hash_safety == :disallow_symbol_keys
12
+ define_method store_attribute do
13
+ super().tap { ActiveTypedStore::DisallowSymbolKeys.call!(_1) }
14
+ end
15
+
16
+ define_method :store_accessor_for do |_store_attribute|
17
+ ActiveTypedStore::StoreHashAccessor
18
+ end
19
+
20
+ private :store_accessor_for
21
+ end
22
+
10
23
  include attrs.store_module
11
24
  end
12
25
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveTypedStore
4
- VERSION = "1.1.0"
4
+ VERSION = "1.3.0"
5
5
  end
@@ -1,10 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "active_typed_store/version"
4
+ require_relative "active_typed_store/configuration"
4
5
  require "active_support"
5
6
 
6
7
  ActiveSupport.on_load(:active_record) do
7
8
  require_relative "active_typed_store/store"
8
9
  require_relative "active_typed_store/attrs"
10
+ require_relative "active_typed_store/disallow_symbol_keys"
11
+
9
12
  ActiveSupport.on_load(:active_record) { extend ActiveTypedStore::Store }
10
13
  end
14
+
15
+ module ActiveTypedStore
16
+ extend ActiveTypedStore::Configurable
17
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_typed_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ermolaev Andrey
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-11 00:00:00.000000000 Z
11
+ date: 2025-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -70,6 +70,8 @@ files:
70
70
  - bin/setup
71
71
  - lib/active_typed_store.rb
72
72
  - lib/active_typed_store/attrs.rb
73
+ - lib/active_typed_store/configuration.rb
74
+ - lib/active_typed_store/disallow_symbol_keys.rb
73
75
  - lib/active_typed_store/store.rb
74
76
  - lib/active_typed_store/version.rb
75
77
  homepage: https://github.com/corp-gp/active_typed_store
@@ -92,8 +94,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
92
94
  - !ruby/object:Gem::Version
93
95
  version: '0'
94
96
  requirements: []
95
- rubygems_version: 3.3.17
97
+ rubygems_version: 3.3.7
96
98
  signing_key:
97
99
  specification_version: 4
98
- summary: Typed store for active records json/hstore columns
100
+ summary: Typed store for active records json columns (postgresql, mysql, sqlite)
99
101
  test_files: []