frozen_record 0.26.1 → 0.27.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/.github/workflows/main.yml +1 -1
- data/CHANGELOG.md +9 -0
- data/README.md +27 -2
- data/lib/frozen_record/base.rb +29 -5
- data/lib/frozen_record/compact.rb +2 -2
- data/lib/frozen_record/version.rb +1 -1
- data/spec/fixtures/countries.yml.erb +2 -0
- data/spec/frozen_record_spec.rb +36 -0
- data/spec/scope_spec.rb +5 -1
- data/spec/support/country.rb +18 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9ad9e1749e0b964178ce78eb8825cc64feddc347d6ac2952b5992cc97b5f4212
|
4
|
+
data.tar.gz: 107c164a1ba9442bf7ab5fa59c03400502c2e31b8ce2e7429e2cf2992e38fcbc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2297514000fb2ee38d534084285d16c3768a9795869e0af2955e134c6f08114f77128689d8aa8fd95fa775c7cba8732a9f34917a0b886d3286b07aa6a36f3f4e
|
7
|
+
data.tar.gz: ce33c77289242c3f1d2341491c644226dad7b14fe709544c2f706d5e37c02c9649a59902f8eab9898e532eea89a0d84905b278bc299b64c217d9a53861d487c5
|
data/.github/workflows/main.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
+
# v0.27.0
|
4
|
+
|
5
|
+
- Allow to define some richer attibute types, somewhat akin to Active Record `serialize` attributes. See the README for more information.
|
6
|
+
- Fix `Model.find_by` fastpath raising an error when called before records are loaded.
|
7
|
+
|
8
|
+
# v026.2
|
9
|
+
|
10
|
+
- Properly load records when entiring the single attribute lookup fastpath.
|
11
|
+
|
3
12
|
# v0.26.1
|
4
13
|
|
5
14
|
- Optimized single attribute lookup.
|
data/README.md
CHANGED
@@ -29,7 +29,7 @@ end
|
|
29
29
|
```
|
30
30
|
|
31
31
|
But you also have to specify in which directory your data files are located.
|
32
|
-
You can either do it
|
32
|
+
You can either do it globally
|
33
33
|
|
34
34
|
```ruby
|
35
35
|
FrozenRecord::Base.base_path = '/path/to/some/directory'
|
@@ -141,7 +141,7 @@ Country.european.republics.part_of_nato.order(id: :desc)
|
|
141
141
|
## Indexing
|
142
142
|
|
143
143
|
Querying is implemented as a simple linear search (`O(n)`). However if you are using Frozen Record with larger datasets, or are querying
|
144
|
-
a collection
|
144
|
+
a collection repeatedly, you can define indices for faster access.
|
145
145
|
|
146
146
|
```ruby
|
147
147
|
class Country < FrozenRecord::Base
|
@@ -154,6 +154,31 @@ Composite index keys are not supported.
|
|
154
154
|
|
155
155
|
The primary key isn't indexed by default.
|
156
156
|
|
157
|
+
## Rich Types
|
158
|
+
|
159
|
+
The `attribute` method can be used to provide a custom class to convert an attribute to a richer type.
|
160
|
+
The class must implement a `load` class method that takes the raw attribute value and returns the deserialized value (similar to
|
161
|
+
[ActiveRecord serialization](https://api.rubyonrails.org/v7.0.4/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html#method-i-serialize)).
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
class ContinentString < String
|
165
|
+
class << self
|
166
|
+
alias_method :load, :new
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
Size = Struct.new(:length, :width, :depth) do
|
171
|
+
def self.load(value) # value is lxwxd eg: "23x12x5"
|
172
|
+
new(*value.split('x'))
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
class Country < FrozenRecord::Base
|
177
|
+
attribute :continent, ContinentString
|
178
|
+
attribute :size, Size
|
179
|
+
end
|
180
|
+
```
|
181
|
+
|
157
182
|
## Limitations
|
158
183
|
|
159
184
|
Frozen Record is not meant to operate on large unindexed datasets.
|
data/lib/frozen_record/base.rb
CHANGED
@@ -30,8 +30,10 @@ module FrozenRecord
|
|
30
30
|
|
31
31
|
class_attribute :base_path, :primary_key, :backend, :auto_reloading, :default_attributes, instance_accessor: false
|
32
32
|
class_attribute :index_definitions, instance_accessor: false
|
33
|
+
class_attribute :attribute_deserializers, instance_accessor: false
|
33
34
|
class_attribute :max_records_scan, instance_accessor: false
|
34
35
|
self.index_definitions = {}.freeze
|
36
|
+
self.attribute_deserializers = {}.freeze
|
35
37
|
|
36
38
|
self.primary_key = 'id'
|
37
39
|
|
@@ -137,6 +139,7 @@ module FrozenRecord
|
|
137
139
|
criterias.each do |attribute, value|
|
138
140
|
attribute = attribute.to_s
|
139
141
|
if index = index_definitions[attribute]
|
142
|
+
load_records
|
140
143
|
return index.lookup(value).first
|
141
144
|
end
|
142
145
|
end
|
@@ -153,6 +156,10 @@ module FrozenRecord
|
|
153
156
|
self.index_definitions = index_definitions.merge(index.attribute => index).freeze
|
154
157
|
end
|
155
158
|
|
159
|
+
def attribute(attribute, klass)
|
160
|
+
self.attribute_deserializers = attribute_deserializers.merge(attribute.to_s => klass).freeze
|
161
|
+
end
|
162
|
+
|
156
163
|
def memsize(object = self, seen = Set.new.compare_by_identity)
|
157
164
|
return 0 unless seen.add?(object)
|
158
165
|
|
@@ -181,16 +188,21 @@ module FrozenRecord
|
|
181
188
|
load_records
|
182
189
|
end
|
183
190
|
|
191
|
+
def unload!
|
192
|
+
@records = nil
|
193
|
+
index_definitions.values.each(&:reset)
|
194
|
+
undefine_attribute_methods
|
195
|
+
end
|
196
|
+
|
184
197
|
def load_records(force: false)
|
185
198
|
if force || (auto_reloading && file_changed?)
|
186
|
-
|
187
|
-
undefine_attribute_methods
|
199
|
+
unload!
|
188
200
|
end
|
189
201
|
|
190
202
|
@records ||= begin
|
191
203
|
records = backend.load(file_path)
|
192
|
-
if default_attributes
|
193
|
-
records = records.map { |r| assign_defaults!(r.dup).freeze }.freeze
|
204
|
+
if attribute_deserializers.any? || default_attributes
|
205
|
+
records = records.map { |r| assign_defaults!(deserialize_attributes!(r.dup)).freeze }.freeze
|
194
206
|
end
|
195
207
|
@attributes = list_attributes(records).freeze
|
196
208
|
define_attribute_methods(@attributes.to_a)
|
@@ -208,7 +220,7 @@ module FrozenRecord
|
|
208
220
|
private :load
|
209
221
|
|
210
222
|
def new(attrs = {})
|
211
|
-
load(assign_defaults!(attrs.transform_keys(&:to_s)))
|
223
|
+
load(assign_defaults!(deserialize_attributes!(attrs.transform_keys(&:to_s))))
|
212
224
|
end
|
213
225
|
|
214
226
|
private
|
@@ -235,6 +247,18 @@ module FrozenRecord
|
|
235
247
|
record
|
236
248
|
end
|
237
249
|
|
250
|
+
def deserialize_attributes!(record)
|
251
|
+
if attribute_deserializers.any?
|
252
|
+
attribute_deserializers.each do |key, deserializer|
|
253
|
+
if record.key?(key)
|
254
|
+
record[key] = deserializer.load(record[key])
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
record
|
260
|
+
end
|
261
|
+
|
238
262
|
def method_missing(name, *args)
|
239
263
|
if name.to_s =~ FIND_BY_PATTERN
|
240
264
|
return dynamic_match($1, args, $2.present?)
|
@@ -13,8 +13,8 @@ module FrozenRecord
|
|
13
13
|
|
14
14
|
@records ||= begin
|
15
15
|
records = backend.load(file_path)
|
16
|
-
if default_attributes
|
17
|
-
records = records.map { |r| assign_defaults!(r.dup).freeze }.freeze
|
16
|
+
if attribute_deserializers.any? || default_attributes
|
17
|
+
records = records.map { |r| assign_defaults!(deserialize_attributes!(r.dup)).freeze }.freeze
|
18
18
|
end
|
19
19
|
@attributes = list_attributes(records).freeze
|
20
20
|
build_attributes_cache
|
data/spec/frozen_record_spec.rb
CHANGED
@@ -80,6 +80,41 @@ RSpec.shared_examples 'main' do
|
|
80
80
|
|
81
81
|
end
|
82
82
|
|
83
|
+
describe '.attribute' do
|
84
|
+
|
85
|
+
it 'deserializes the attribute' do
|
86
|
+
expect(country_model.find_by(name: 'Canada').continent).to be_a(TectonicString)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'sets the default value as default' do
|
90
|
+
expect(country_model.find_by(name: 'France').currency_code).to be == CurrencyCode.load("EUR")
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'returns the first matching record' do
|
94
|
+
country = Country.find_by(currency_code: CurrencyCode.load('EUR'))
|
95
|
+
expect(country.name).to be == 'France'
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'returns nil if record not found' do
|
99
|
+
country = Country.find_by(currency_code: CurrencyCode.load('THB'))
|
100
|
+
expect(country).to be_nil
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'raises if no record found!' do
|
104
|
+
expect {
|
105
|
+
Country.find_by!(currency_code: CurrencyCode.load('THB'))
|
106
|
+
}.to raise_error(FrozenRecord::RecordNotFound)
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'deserializes in the initializer' do
|
110
|
+
expect(country_model.new(currency_code: "CHF").currency_code).to be == CurrencyCode.load('CHF')
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'also sets the default in the initializer' do
|
114
|
+
expect(country_model.new.currency_code).to be == CurrencyCode.load('EUR')
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
83
118
|
describe '.scope' do
|
84
119
|
|
85
120
|
it 'defines a scope method' do
|
@@ -189,6 +224,7 @@ RSpec.shared_examples 'main' do
|
|
189
224
|
'continent' => 'North America',
|
190
225
|
'available' => true,
|
191
226
|
'contemporary' => true,
|
227
|
+
'currency_code' => CurrencyCode.load('CAD'),
|
192
228
|
}
|
193
229
|
end
|
194
230
|
|
data/spec/scope_spec.rb
CHANGED
@@ -101,7 +101,6 @@ describe 'querying' do
|
|
101
101
|
country = Country.find_by_id(42)
|
102
102
|
expect(country).to be_nil
|
103
103
|
end
|
104
|
-
|
105
104
|
end
|
106
105
|
|
107
106
|
describe '.find_by' do
|
@@ -116,6 +115,11 @@ describe 'querying' do
|
|
116
115
|
expect(country).to be_nil
|
117
116
|
end
|
118
117
|
|
118
|
+
it 'load records' do
|
119
|
+
Country.unload!
|
120
|
+
country = Country.find_by(name: 'France')
|
121
|
+
expect(country.name).to be == 'France'
|
122
|
+
end
|
119
123
|
end
|
120
124
|
|
121
125
|
describe '.find_by!' do
|
data/spec/support/country.rb
CHANGED
@@ -1,9 +1,26 @@
|
|
1
|
+
class CurrencyCode
|
2
|
+
class << self
|
3
|
+
def load(value)
|
4
|
+
value.to_sym
|
5
|
+
end
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class TectonicString < String
|
10
|
+
class << self
|
11
|
+
alias_method :load, :new
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
1
15
|
class Country < FrozenRecord::Base
|
2
|
-
self.default_attributes = { contemporary: true, available: true }
|
16
|
+
self.default_attributes = { contemporary: true, available: true, currency_code: CurrencyCode.load('EUR') }
|
3
17
|
|
4
18
|
add_index :name, unique: true
|
5
19
|
add_index :continent
|
6
20
|
|
21
|
+
attribute :currency_code, CurrencyCode
|
22
|
+
attribute :continent, TectonicString
|
23
|
+
|
7
24
|
def self.republics
|
8
25
|
where(king: nil)
|
9
26
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: frozen_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.27.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-03-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -124,7 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
124
124
|
- !ruby/object:Gem::Version
|
125
125
|
version: '0'
|
126
126
|
requirements: []
|
127
|
-
rubygems_version: 3.
|
127
|
+
rubygems_version: 3.4.6
|
128
128
|
signing_key:
|
129
129
|
specification_version: 4
|
130
130
|
summary: ActiveRecord like interface to read only access and query static YAML files
|