frozen_record 0.26.1 → 0.27.0

Sign up to get free protection for your applications and to get access to all the features.
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