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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4e4a597e4419873da69776c887c5ef35c812f785493cd9597d3d3ced891578ab
4
- data.tar.gz: d46b86085ceeee261b39a281aab1c62a2551e872ad1f4696e8a7325b7cfa9a7a
3
+ metadata.gz: 9ad9e1749e0b964178ce78eb8825cc64feddc347d6ac2952b5992cc97b5f4212
4
+ data.tar.gz: 107c164a1ba9442bf7ab5fa59c03400502c2e31b8ce2e7429e2cf2992e38fcbc
5
5
  SHA512:
6
- metadata.gz: 8e68a5dfb1d888de4f6dc72bb42ffdc1c08af87229b7bd0dcf107d1da9b48ef5b948adea5652b132cb497a1a8c15b513aa7ba8beea7c863b06f19de14ae02865
7
- data.tar.gz: f787b5072efd3997fb41942c1d9b6014ee872d6faec12a4c5a77ef489a7039ed980987bbf4102ed7d9c5cd2b296e49c1bd6a25f578559969d200d875d6cdf877
6
+ metadata.gz: 2297514000fb2ee38d534084285d16c3768a9795869e0af2955e134c6f08114f77128689d8aa8fd95fa775c7cba8732a9f34917a0b886d3286b07aa6a36f3f4e
7
+ data.tar.gz: ce33c77289242c3f1d2341491c644226dad7b14fe709544c2f706d5e37c02c9649a59902f8eab9898e532eea89a0d84905b278bc299b64c217d9a53861d487c5
@@ -8,7 +8,7 @@ jobs:
8
8
  strategy:
9
9
  fail-fast: false
10
10
  matrix:
11
- ruby: [ '2.5', '2.6', '2.7', '3.0' , '3.1']
11
+ ruby: [ '2.5', '2.6', '2.7', '3.0' , '3.1', '3.2']
12
12
  minimal: [ false, true ]
13
13
  name: Ruby ${{ matrix.ruby }} tests, minimal=${{ matrix.minimal }}
14
14
  steps:
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 globaly
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 repetedly, you can define indices for faster access.
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.
@@ -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
- @records = nil
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FrozenRecord
4
- VERSION = '0.26.1'
4
+ VERSION = '0.27.0'
5
5
  end
@@ -9,6 +9,7 @@
9
9
  nato: true
10
10
  king: Elisabeth II
11
11
  continent: North America
12
+ currency_code: CAD
12
13
 
13
14
  - id: 2
14
15
  name: France
@@ -29,3 +30,4 @@
29
30
  updated_at: 2014-02-12T19:02:03-02:00
30
31
  continent: Europe
31
32
  available: false
33
+ currency_code: EUR
@@ -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
@@ -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.26.1
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: 2022-08-11 00:00:00.000000000 Z
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.2.20
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