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 +4 -4
- data/Gemfile.lock +3 -1
- data/README.md +33 -9
- data/lib/active_typed_store/attrs.rb +9 -4
- data/lib/active_typed_store/configuration.rb +23 -0
- data/lib/active_typed_store/disallow_symbol_keys.rb +37 -0
- data/lib/active_typed_store/store.rb +14 -1
- data/lib/active_typed_store/version.rb +1 -1
- data/lib/active_typed_store.rb +7 -0
- metadata +6 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c00294f06d7b42ef340d56715c9e86525be7ce1c1aea054fed1faf11b7d6d7bf
|
4
|
+
data.tar.gz: 271d28763908598806ff221a292f9a88d4b773909da217cdec13199d8d7a0a20
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
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 (
|
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
|
-
#
|
88
|
-
#
|
89
|
-
# store_attribute:
|
90
|
-
# store_model:
|
91
|
-
# attr_json:
|
92
|
-
# jsonb_accessor:
|
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
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
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
|
data/lib/active_typed_store.rb
CHANGED
@@ -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.
|
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:
|
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.
|
97
|
+
rubygems_version: 3.3.7
|
96
98
|
signing_key:
|
97
99
|
specification_version: 4
|
98
|
-
summary: Typed store for active records json
|
100
|
+
summary: Typed store for active records json columns (postgresql, mysql, sqlite)
|
99
101
|
test_files: []
|