frozen_record 0.26.2 → 0.27.1
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 +2 -2
- data/CHANGELOG.md +11 -0
- data/README.md +54 -11
- data/lib/frozen_record/base.rb +21 -3
- data/lib/frozen_record/compact.rb +2 -2
- data/lib/frozen_record/test_helper.rb +18 -4
- data/lib/frozen_record/version.rb +1 -1
- data/spec/fixtures/countries.yml.erb +2 -0
- data/spec/fixtures/test_helper/only_in_tests.yml.erb +3 -0
- data/spec/frozen_record_spec.rb +36 -0
- data/spec/support/country.rb +18 -1
- data/spec/test_helper_spec.rb +12 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38d59eb398601918420c427ca2608c32d88e272948f39f502c831e92be2d6017
|
4
|
+
data.tar.gz: 1762510ed02e71fdfc67fcfe6f41ba4db83f766231481490e0172a4ea0c91f5b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a58d5c272f4beb88c10a2b8186f6664843da73ee67a920a706258b1d3fd0c719fe7dcfd27d1f97485304f743c2cef86b8d5ed8924d59c69d162edf389fb0ab34
|
7
|
+
data.tar.gz: 1d587f2b2c7043e1137537ae0eb44fd84e1643876d8ca30d1704dc41c45b0739fe6612c71db97d14f32de3950e52fee289fcec34969e57505b7aebb5403264e3
|
data/.github/workflows/main.yml
CHANGED
@@ -8,11 +8,11 @@ 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:
|
15
|
-
- uses: actions/checkout@
|
15
|
+
- uses: actions/checkout@v4
|
16
16
|
- name: Set up Ruby
|
17
17
|
uses: ruby/setup-ruby@v1
|
18
18
|
with:
|
data/CHANGELOG.md
CHANGED
@@ -1,7 +1,18 @@
|
|
1
1
|
# Unreleased
|
2
2
|
|
3
|
+
# v0.27.1
|
4
|
+
|
5
|
+
- TestHelper.unload_fixture: handle models without data.
|
6
|
+
|
7
|
+
# v0.27.0
|
8
|
+
|
9
|
+
- Allow to define some richer attibute types, somewhat akin to Active Record `serialize` attributes. See the README for more information.
|
3
10
|
- Fix `Model.find_by` fastpath raising an error when called before records are loaded.
|
4
11
|
|
12
|
+
# v026.2
|
13
|
+
|
14
|
+
- Properly load records when entiring the single attribute lookup fastpath.
|
15
|
+
|
5
16
|
# v0.26.1
|
6
17
|
|
7
18
|
- Optimized single attribute lookup.
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# FrozenRecord
|
2
2
|
|
3
|
-
[](
|
4
|
-
[](
|
3
|
+
[](https://travis-ci.org/byroot/frozen_record)
|
4
|
+
[](https://badge.fury.io/rb/frozen_record)
|
5
5
|
|
6
6
|
Active Record-like interface for **read only** access to static data files of reasonable size.
|
7
7
|
|
@@ -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'
|
@@ -42,9 +42,27 @@ class Country < FrozenRecord::Base
|
|
42
42
|
end
|
43
43
|
```
|
44
44
|
|
45
|
-
|
46
|
-
|
47
|
-
|
45
|
+
FrozenRecord has two built-in backends, for JSON and YAML.
|
46
|
+
Backends are classes that know how to load records from a static file.
|
47
|
+
|
48
|
+
The default backend is YAML and it expects a file that looks like this:
|
49
|
+
|
50
|
+
```yaml
|
51
|
+
- id: 'se'
|
52
|
+
name: 'Sweden'
|
53
|
+
region: 'Europe'
|
54
|
+
language: 'Swedish'
|
55
|
+
population: 10420000
|
56
|
+
- id: 'de'
|
57
|
+
name: 'Germany'
|
58
|
+
region: 'Europe'
|
59
|
+
language: 'German'
|
60
|
+
population: 83200000
|
61
|
+
|
62
|
+
# …
|
63
|
+
```
|
64
|
+
|
65
|
+
You can also specify a custom backend:
|
48
66
|
|
49
67
|
```ruby
|
50
68
|
class Country < FrozenRecord::Base
|
@@ -74,18 +92,18 @@ end
|
|
74
92
|
|
75
93
|
FrozenRecord aim to replicate only modern Active Record querying interface, and only the non "string typed" ones.
|
76
94
|
|
77
|
-
e.g
|
78
95
|
```ruby
|
79
96
|
# Supported query interfaces
|
80
97
|
Country.
|
81
98
|
where(region: 'Europe').
|
82
99
|
where.not(language: 'English').
|
100
|
+
where(population: 10_000_000..).
|
83
101
|
order(id: :desc).
|
84
102
|
limit(10).
|
85
103
|
offset(2).
|
86
104
|
pluck(:name)
|
87
105
|
|
88
|
-
# Non
|
106
|
+
# Non-supported query interfaces
|
89
107
|
Country.
|
90
108
|
where('region = "Europe" AND language != "English"').
|
91
109
|
order('id DESC')
|
@@ -141,7 +159,7 @@ Country.european.republics.part_of_nato.order(id: :desc)
|
|
141
159
|
## Indexing
|
142
160
|
|
143
161
|
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
|
162
|
+
a collection repeatedly, you can define indices for faster access.
|
145
163
|
|
146
164
|
```ruby
|
147
165
|
class Country < FrozenRecord::Base
|
@@ -154,6 +172,31 @@ Composite index keys are not supported.
|
|
154
172
|
|
155
173
|
The primary key isn't indexed by default.
|
156
174
|
|
175
|
+
## Rich Types
|
176
|
+
|
177
|
+
The `attribute` method can be used to provide a custom class to convert an attribute to a richer type.
|
178
|
+
The class must implement a `load` class method that takes the raw attribute value and returns the deserialized value (similar to
|
179
|
+
[ActiveRecord serialization](https://api.rubyonrails.org/v7.0.4/classes/ActiveRecord/AttributeMethods/Serialization/ClassMethods.html#method-i-serialize)).
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
class ContinentString < String
|
183
|
+
class << self
|
184
|
+
alias_method :load, :new
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
Size = Struct.new(:length, :width, :depth) do
|
189
|
+
def self.load(value) # value is lxwxd eg: "23x12x5"
|
190
|
+
new(*value.split('x'))
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
class Country < FrozenRecord::Base
|
195
|
+
attribute :continent, ContinentString
|
196
|
+
attribute :size, Size
|
197
|
+
end
|
198
|
+
```
|
199
|
+
|
157
200
|
## Limitations
|
158
201
|
|
159
202
|
Frozen Record is not meant to operate on large unindexed datasets.
|
@@ -192,7 +235,7 @@ FrozenRecord::TestHelper.unload_fixtures
|
|
192
235
|
Here's a Rails-specific example:
|
193
236
|
|
194
237
|
```ruby
|
195
|
-
require
|
238
|
+
require 'test_helper'
|
196
239
|
require 'frozen_record/test_helper'
|
197
240
|
|
198
241
|
class CountryTest < ActiveSupport::TestCase
|
@@ -205,7 +248,7 @@ class CountryTest < ActiveSupport::TestCase
|
|
205
248
|
FrozenRecord::TestHelper.unload_fixtures
|
206
249
|
end
|
207
250
|
|
208
|
-
test
|
251
|
+
test 'countries have a valid name' do
|
209
252
|
# ...
|
210
253
|
```
|
211
254
|
|
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
|
|
@@ -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
|
@@ -12,7 +12,7 @@ module FrozenRecord
|
|
12
12
|
|
13
13
|
return if @cache.key?(model_class)
|
14
14
|
|
15
|
-
@cache[model_class]
|
15
|
+
@cache[model_class] = base_path_if_file_present(model_class)
|
16
16
|
|
17
17
|
model_class.base_path = alternate_base_path
|
18
18
|
model_class.load_records(force: true)
|
@@ -26,9 +26,10 @@ module FrozenRecord
|
|
26
26
|
return unless @cache.key?(model_class)
|
27
27
|
|
28
28
|
old_base_path = @cache[model_class]
|
29
|
-
|
30
|
-
|
31
|
-
|
29
|
+
if old_base_path
|
30
|
+
model_class.base_path = old_base_path
|
31
|
+
model_class.load_records(force: true)
|
32
|
+
end
|
32
33
|
@cache.delete(model_class)
|
33
34
|
end
|
34
35
|
|
@@ -40,6 +41,19 @@ module FrozenRecord
|
|
40
41
|
|
41
42
|
private
|
42
43
|
|
44
|
+
# Checks for the existence of the file for the frozen_record in the default directory.
|
45
|
+
# Returns the base_path if the file is present, otherwise nil.
|
46
|
+
# Some tests define specific test classes that do ONLY exist in the alternate directory.
|
47
|
+
# As `unload_fixture(s)` tries to force load the default file, it would raise an error for
|
48
|
+
# the "test only" fixtures. The nil value in the cache handles that case gracefully.
|
49
|
+
def base_path_if_file_present(model_class)
|
50
|
+
if File.exist?(model_class.file_path)
|
51
|
+
model_class.base_path
|
52
|
+
else
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
43
57
|
def ensure_model_class_is_frozenrecord(model_class)
|
44
58
|
unless model_class < FrozenRecord::Base
|
45
59
|
raise ArgumentError, "Model class (#{model_class}) does not inherit from #{FrozenRecord::Base}"
|
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/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
|
data/spec/test_helper_spec.rb
CHANGED
@@ -49,6 +49,18 @@ describe 'test fixture loading' do
|
|
49
49
|
expect(Continent.count).to be == 1
|
50
50
|
expect(Country.count).to be == 3
|
51
51
|
end
|
52
|
+
|
53
|
+
context "when the test fixture does not exist in normal base path" do
|
54
|
+
class OnlyInTest < FrozenRecord::Base; end
|
55
|
+
before do
|
56
|
+
test_fixtures_base_path = File.join(File.dirname(__FILE__), 'fixtures', 'test_helper')
|
57
|
+
FrozenRecord::TestHelper.load_fixture(OnlyInTest, test_fixtures_base_path)
|
58
|
+
end
|
59
|
+
it 'unload fixture gracefully recovers from an ' do
|
60
|
+
|
61
|
+
expect { FrozenRecord::TestHelper.unload_fixture(OnlyInTest) }.not_to raise_error
|
62
|
+
end
|
63
|
+
end
|
52
64
|
end
|
53
65
|
|
54
66
|
describe '.unload_fixtures' do
|
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.1
|
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: 2024-02-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -95,6 +95,7 @@ files:
|
|
95
95
|
- spec/fixtures/prices.yml.erb
|
96
96
|
- spec/fixtures/test_helper/continents.yml.erb
|
97
97
|
- spec/fixtures/test_helper/countries.yml.erb
|
98
|
+
- spec/fixtures/test_helper/only_in_tests.yml.erb
|
98
99
|
- spec/frozen_record_spec.rb
|
99
100
|
- spec/scope_spec.rb
|
100
101
|
- spec/spec_helper.rb
|
@@ -124,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
124
125
|
- !ruby/object:Gem::Version
|
125
126
|
version: '0'
|
126
127
|
requirements: []
|
127
|
-
rubygems_version: 3.3
|
128
|
+
rubygems_version: 3.5.3
|
128
129
|
signing_key:
|
129
130
|
specification_version: 4
|
130
131
|
summary: ActiveRecord like interface to read only access and query static YAML files
|
@@ -136,6 +137,7 @@ test_files:
|
|
136
137
|
- spec/fixtures/prices.yml.erb
|
137
138
|
- spec/fixtures/test_helper/continents.yml.erb
|
138
139
|
- spec/fixtures/test_helper/countries.yml.erb
|
140
|
+
- spec/fixtures/test_helper/only_in_tests.yml.erb
|
139
141
|
- spec/frozen_record_spec.rb
|
140
142
|
- spec/scope_spec.rb
|
141
143
|
- spec/spec_helper.rb
|