frozen_record 0.26.2 → 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: e162699e1ec4c32223be26afd36c1222bedeea90b8d0f26e05407641c9f01ccd
4
- data.tar.gz: f4f6dcb51344bf99558e278a746e4ae5527355ed8771c7be26186af2cab630a1
3
+ metadata.gz: 9ad9e1749e0b964178ce78eb8825cc64feddc347d6ac2952b5992cc97b5f4212
4
+ data.tar.gz: 107c164a1ba9442bf7ab5fa59c03400502c2e31b8ce2e7429e2cf2992e38fcbc
5
5
  SHA512:
6
- metadata.gz: 594beed9888fc9379882815828f1d85a0941b143b03d177662fd02a8460867135003eeeb1391ad473f393c78f299c9e0c3ed45c9dcee88dee7bc7940ce3662c4
7
- data.tar.gz: bcc049e4643b24bca1c98f8947a924c97f829a2a66798fbe798cbeba744d9934c0b81cbb2bb5c29f686d8d6af723f99299312d56e7e484524021c57613d02f4d
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,7 +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.
3
6
  - Fix `Model.find_by` fastpath raising an error when called before records are loaded.
4
7
 
8
+ # v026.2
9
+
10
+ - Properly load records when entiring the single attribute lookup fastpath.
11
+
5
12
  # v0.26.1
6
13
 
7
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
 
@@ -154,6 +156,10 @@ module FrozenRecord
154
156
  self.index_definitions = index_definitions.merge(index.attribute => index).freeze
155
157
  end
156
158
 
159
+ def attribute(attribute, klass)
160
+ self.attribute_deserializers = attribute_deserializers.merge(attribute.to_s => klass).freeze
161
+ end
162
+
157
163
  def memsize(object = self, seen = Set.new.compare_by_identity)
158
164
  return 0 unless seen.add?(object)
159
165
 
@@ -195,8 +201,8 @@ module FrozenRecord
195
201
 
196
202
  @records ||= begin
197
203
  records = backend.load(file_path)
198
- if default_attributes
199
- 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
200
206
  end
201
207
  @attributes = list_attributes(records).freeze
202
208
  define_attribute_methods(@attributes.to_a)
@@ -214,7 +220,7 @@ module FrozenRecord
214
220
  private :load
215
221
 
216
222
  def new(attrs = {})
217
- load(assign_defaults!(attrs.transform_keys(&:to_s)))
223
+ load(assign_defaults!(deserialize_attributes!(attrs.transform_keys(&:to_s))))
218
224
  end
219
225
 
220
226
  private
@@ -241,6 +247,18 @@ module FrozenRecord
241
247
  record
242
248
  end
243
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
+
244
262
  def method_missing(name, *args)
245
263
  if name.to_s =~ FIND_BY_PATTERN
246
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.2'
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
 
@@ -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.2
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-12 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.3.7
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