frozen_record 0.17.0 → 0.18.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: f2012cdafeeed41657b76dbe5b5489a47f5e38d863335d0e2db87609d2030974
4
- data.tar.gz: 636ea1f081a595230c27a2a4abb40978b977d1e3b541d23e039aa790d5bdb62d
3
+ metadata.gz: a47352d34eb964f32bf8d253cffe682abc221f80db5782ed7dce6aee76967661
4
+ data.tar.gz: ea62e1c1bcc0cf927a2614f2fb4cfc1acbb6780cdfec273a44dbbe1bd14a55e2
5
5
  SHA512:
6
- metadata.gz: ae134c3c39dca424590880b06190656c2d03aa0516fa387a5742ecf2dc1943e33f4c6eee30cc7fa600e63bd3c14186c69dc348ed20783256e563657bf9058874
7
- data.tar.gz: bad575675f9a764c2e84f67b9c550beee1506ef763907d32a87d5eaf967eedeb3521ec28d6d9e6dd4ebc790ce0400bf579fb1764af3d13e46f14e5a6343a1a44
6
+ metadata.gz: 33f76d7869d6a1e8a9cae954b074fa93cb89ffa3fcee9bc170d3e23b5ee2fcb39164a7e9de0b0f1570588bd7919d88b288bb35bc9c965cb32b77e3e9c6b5928a
7
+ data.tar.gz: e38977cfbdb6136aed4042060e130bc260f6e33c8db9c876f05f832d65037ba504fe7119a5b338c62e9a17391b2d1472ab2e2de5df5ae2c90b72772bd0599cd8
@@ -1,6 +1,5 @@
1
1
  sudo: false
2
2
  rvm:
3
- - '2.4'
4
3
  - '2.5'
5
4
  - '2.6'
6
5
  - '2.7'
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
+ gem 'benchmark-ips'
4
+ gem 'byebug'
3
5
  gemspec
data/README.md CHANGED
@@ -60,7 +60,7 @@ A custom backend must implement the methods `filename` and `load` as follows:
60
60
 
61
61
  ```ruby
62
62
  module MyCustomBackend
63
- extend self
63
+ extend self
64
64
 
65
65
  def filename(model_name)
66
66
  # Returns the file name as a String
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'setup'
5
+
6
+ regular = Country.first
7
+ compact = Compact::Country.first
8
+
9
+
10
+ puts "=== record.attribute ==="
11
+ Benchmark.ips do |x|
12
+ x.report('regular') { regular.name }
13
+ x.report('compact') { compact.name }
14
+ x.compare!
15
+ end
16
+
17
+ puts "=== record[:attribute] ==="
18
+ Benchmark.ips do |x|
19
+ x.report('regular') { regular[:name] }
20
+ x.report('compact') { compact[:name] }
21
+ x.compare!
22
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative 'setup'
5
+
6
+ puts "regular: #{Country.memsize} bytes"
7
+ puts "compact: #{Compact::Country.memsize} bytes"
8
+
9
+ diff = (Compact::Country.memsize - Country.memsize).to_f / Country.memsize
10
+ puts "diff: #{(diff * 100).round(2)}%"
@@ -0,0 +1,6 @@
1
+ require 'bundler/setup'
2
+ require 'benchmark/ips'
3
+
4
+ require 'frozen_record'
5
+ require_relative '../spec/support/country'
6
+ FrozenRecord::Base.base_path = File.expand_path('../spec/fixtures', __dir__)
@@ -17,6 +17,8 @@ Gem::Specification.new do |spec|
17
17
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
18
18
  spec.require_paths = ['lib']
19
19
 
20
+ spec.required_ruby_version = '>= 2.5'
21
+
20
22
  spec.add_runtime_dependency 'activemodel'
21
23
  spec.add_development_dependency 'bundler'
22
24
  spec.add_development_dependency 'rake'
@@ -8,6 +8,7 @@ require 'active_model'
8
8
  require 'frozen_record/version'
9
9
  require 'frozen_record/scope'
10
10
  require 'frozen_record/base'
11
+ require 'frozen_record/compact'
11
12
  require 'frozen_record/deduplication'
12
13
 
13
14
  module FrozenRecord
@@ -3,6 +3,7 @@
3
3
  require 'set'
4
4
  require 'active_support/descendants_tracker'
5
5
  require 'frozen_record/backends'
6
+ require 'objspace'
6
7
 
7
8
  module FrozenRecord
8
9
  class Base
@@ -102,6 +103,21 @@ module FrozenRecord
102
103
  end
103
104
  end
104
105
 
106
+ def memsize(object = self, seen = Set.new.compare_by_identity)
107
+ return 0 unless seen.add?(object)
108
+
109
+ size = ObjectSpace.memsize_of(object)
110
+ object.instance_variables.each { |v| size += memsize(object.instance_variable_get(v), seen) }
111
+
112
+ case object
113
+ when Hash
114
+ object.each { |k, v| size += memsize(k, seen) + memsize(v, seen) }
115
+ when Array
116
+ object.each { |i| size += memsize(i, seen) }
117
+ end
118
+ size
119
+ end
120
+
105
121
  def respond_to_missing?(name, *)
106
122
  if name.to_s =~ FIND_BY_PATTERN
107
123
  load_records # ensure attribute methods are defined
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FrozenRecord
4
+ module Compact
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def load_records(force: false)
9
+ if force || (auto_reloading && file_changed?)
10
+ @records = nil
11
+ undefine_attribute_methods
12
+ end
13
+
14
+ @records ||= begin
15
+ records = backend.load(file_path)
16
+ records.each { |r| assign_defaults!(r) }
17
+ records = Deduplication.deep_deduplicate!(records)
18
+ @attributes = list_attributes(records).freeze
19
+ build_attributes_cache
20
+ define_attribute_methods(@attributes.to_a)
21
+ records.map { |r| load(r) }.freeze
22
+ end
23
+ end
24
+
25
+ if ActiveModel.gem_version >= Gem::Version.new('6.1.0.alpha')
26
+ def define_method_attribute(attr, owner:)
27
+ owner << "attr_reader #{attr.inspect}"
28
+ end
29
+ else
30
+ def define_method_attribute(attr)
31
+ generated_attribute_methods.attr_reader(attr)
32
+ end
33
+ end
34
+
35
+ attr_reader :_attributes_cache
36
+
37
+ private
38
+
39
+ def build_attributes_cache
40
+ @_attributes_cache = @attributes.each_with_object({}) do |attr, cache|
41
+ var = :"@#{attr}"
42
+ cache[attr.to_s] = var
43
+ cache[attr.to_sym] = var
44
+ end
45
+ end
46
+ end
47
+
48
+ def initialize(attrs = {})
49
+ self.attributes = attrs
50
+ end
51
+
52
+ def attributes
53
+ self.class.attributes.each_with_object({}) do |attr, hash|
54
+ hash[attr] = self[attr]
55
+ end
56
+ end
57
+
58
+ def [](attr)
59
+ if var = self.class._attributes_cache[attr]
60
+ instance_variable_get(var)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def attributes=(attributes)
67
+ self.class.attributes.each do |attr|
68
+ instance_variable_set(self.class._attributes_cache[attr], Deduplication.deep_deduplicate!(attributes[attr]))
69
+ end
70
+ end
71
+
72
+ def attribute?(attribute_name)
73
+ val = self[attribute_name]
74
+ Base::FALSY_VALUES.exclude?(val) && val.present?
75
+ end
76
+ end
77
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FrozenRecord
4
- VERSION = '0.17.0'
4
+ VERSION = '0.18.0'
5
5
  end
@@ -1,32 +1,21 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe FrozenRecord::Base do
4
-
5
- describe '.base_path' do
6
-
7
- it 'raise a RuntimeError on first query attempt if not set' do
8
- allow(Country).to receive_message_chain(:base_path).and_return(nil)
9
- expect {
10
- Country.file_path
11
- }.to raise_error(ArgumentError)
12
- end
13
-
14
- end
3
+ RSpec.shared_examples 'main' do
15
4
 
16
5
  describe '.primary_key' do
17
6
 
18
7
  around do |example|
19
- previous_primary_key = Country.primary_key
8
+ previous_primary_key = country_model.primary_key
20
9
  begin
21
10
  example.run
22
11
  ensure
23
- Country.primary_key = previous_primary_key
12
+ country_model.primary_key = previous_primary_key
24
13
  end
25
14
  end
26
15
 
27
16
  it 'is coerced to string' do
28
- Country.primary_key = :foobar
29
- expect(Country.primary_key).to be == 'foobar'
17
+ country_model.primary_key = :foobar
18
+ expect(country_model.primary_key).to be == 'foobar'
30
19
  end
31
20
 
32
21
  end
@@ -36,24 +25,24 @@ describe FrozenRecord::Base do
36
25
  context 'when enabled' do
37
26
 
38
27
  around do |example|
39
- previous_auto_reloading = Country.auto_reloading
40
- Country.auto_reloading = true
28
+ previous_auto_reloading = country_model.auto_reloading
29
+ country_model.auto_reloading = true
41
30
  begin
42
31
  example.run
43
32
  ensure
44
- Country.auto_reloading = previous_auto_reloading
33
+ country_model.auto_reloading = previous_auto_reloading
45
34
  end
46
35
  end
47
36
 
48
37
  it 'reloads the records if the file mtime changed' do
49
- mtime = File.mtime(Country.file_path)
38
+ mtime = File.mtime(country_model.file_path)
50
39
  expect {
51
- File.utime(mtime + 1, mtime + 1, Country.file_path)
52
- }.to change { Country.first.object_id }
40
+ File.utime(mtime + 1, mtime + 1, country_model.file_path)
41
+ }.to change { country_model.first.object_id }
53
42
  end
54
43
 
55
44
  it 'does not reload if the file has not changed' do
56
- expect(Country.first.object_id).to be == Country.first.object_id
45
+ expect(country_model.first.object_id).to be == country_model.first.object_id
57
46
  end
58
47
 
59
48
  end
@@ -61,10 +50,10 @@ describe FrozenRecord::Base do
61
50
  context 'when disabled' do
62
51
 
63
52
  it 'does not reloads the records if the file mtime changed' do
64
- mtime = File.mtime(Country.file_path)
53
+ mtime = File.mtime(country_model.file_path)
65
54
  expect {
66
- File.utime(mtime + 1, mtime + 1, Country.file_path)
67
- }.to_not change { Country.first.object_id }
55
+ File.utime(mtime + 1, mtime + 1, country_model.file_path)
56
+ }.to_not change { country_model.first.object_id }
68
57
  end
69
58
 
70
59
  end
@@ -74,40 +63,52 @@ describe FrozenRecord::Base do
74
63
  describe '.default_attributes' do
75
64
 
76
65
  it 'define the attribute' do
77
- expect(Country.new).to respond_to :contemporary
66
+ expect(country_model.new).to respond_to :contemporary
78
67
  end
79
68
 
80
69
  it 'sets the value as default' do
81
- expect(Country.find_by(name: 'Austria').contemporary).to be == true
70
+ expect(country_model.find_by(name: 'Austria').contemporary).to be == true
82
71
  end
83
72
 
84
73
  it 'gives precedence to the data file' do
85
- expect(Country.find_by(name: 'Austria').available).to be == false
74
+ expect(country_model.find_by(name: 'Austria').available).to be == false
86
75
  end
87
76
 
88
77
  it 'is also set in the initializer' do
89
- expect(Country.new.contemporary).to be == true
78
+ expect(country_model.new.contemporary).to be == true
90
79
  end
80
+
91
81
  end
92
82
 
93
83
  describe '.scope' do
84
+
94
85
  it 'defines a scope method' do
86
+ country_model.scope :north_american, -> { country_model.where(continent: 'North America') }
87
+ expect(country_model).to respond_to(:north_american)
88
+ expect(country_model.north_american.first.name).to be == 'Canada'
89
+ end
95
90
 
96
- Country.scope :north_american, -> { Country.where(continent: 'North America') }
97
- expect(Country).to respond_to(:north_american)
98
- expect(Country.north_american.first.name).to be == 'Canada'
91
+ end
92
+
93
+ describe '.memsize' do
94
+
95
+ it 'retuns the records memory footprint' do
96
+ # Memory footprint is very dependent on the Ruby implementation and version
97
+ expect(country_model.memsize).to be > 0
98
+ expect(car_model.memsize).to be > 0
99
99
  end
100
+
100
101
  end
101
102
 
102
103
  describe '#load_records' do
103
104
 
104
105
  it 'processes erb by default' do
105
- country = Country.first
106
+ country = country_model.first
106
107
  expect(country.capital).to be == 'Ottawa'
107
108
  end
108
109
 
109
110
  it 'loads records with a custom backend' do
110
- animal = Animal.first
111
+ animal = animal_model.first
111
112
  expect(animal.name).to be == 'cat'
112
113
  end
113
114
 
@@ -116,22 +117,22 @@ describe FrozenRecord::Base do
116
117
  describe '#==' do
117
118
 
118
119
  it 'returns true if both instances are from the same class and have the same id' do
119
- country = Country.first
120
+ country = country_model.first
120
121
  second_country = country.dup
121
122
 
122
123
  expect(country).to be == second_country
123
124
  end
124
125
 
125
126
  it 'returns false if both instances are not from the same class' do
126
- country = Country.first
127
- car = Car.new(id: country.id)
127
+ country = country_model.first
128
+ car = car_model.new(id: country.id)
128
129
 
129
130
  expect(country).to_not be == car
130
131
  end
131
132
 
132
133
  it 'returns false if both instances do not have the same id' do
133
- country = Country.first
134
- second_country = Country.last
134
+ country = country_model.first
135
+ second_country = country_model.last
135
136
 
136
137
  expect(country).to_not be == second_country
137
138
  end
@@ -141,7 +142,7 @@ describe FrozenRecord::Base do
141
142
  describe '#attributes' do
142
143
 
143
144
  it 'returns a Hash of the record attributes' do
144
- attributes = Country.first.attributes
145
+ attributes = country_model.first.attributes
145
146
  expect(attributes).to be == {
146
147
  'id' => 1,
147
148
  'name' => 'Canada',
@@ -162,9 +163,9 @@ describe FrozenRecord::Base do
162
163
 
163
164
  describe '`attribute`?' do
164
165
 
165
- let(:blank) { Country.new(id: 0, name: '', nato: false, king: nil) }
166
+ let(:blank) { country_model.new(id: 0, name: '', nato: false, king: nil) }
166
167
 
167
- let(:present) { Country.new(id: 42, name: 'Groland', nato: true, king: Object.new) }
168
+ let(:present) { country_model.new(id: 42, name: 'Groland', nato: true, king: Object.new) }
168
169
 
169
170
  it 'considers `0` as missing' do
170
171
  expect(blank.id?).to be false
@@ -203,7 +204,7 @@ describe FrozenRecord::Base do
203
204
  describe '#present?' do
204
205
 
205
206
  it 'returns true' do
206
- expect(Country.first).to be_present
207
+ expect(country_model.first).to be_present
207
208
  end
208
209
 
209
210
  end
@@ -211,12 +212,39 @@ describe FrozenRecord::Base do
211
212
  describe '#count' do
212
213
 
213
214
  it 'can count objects with no records' do
214
- expect(Car.count).to be 0
215
+ expect(car_model.count).to be 0
215
216
  end
216
217
 
217
218
  it 'can count objects with records' do
218
- expect(Country.count).to be 3
219
+ expect(country_model.count).to be 3
219
220
  end
220
221
 
221
222
  end
222
223
  end
224
+
225
+ describe FrozenRecord::Base do
226
+ let(:country_model) { Country }
227
+ let(:car_model) { Car }
228
+ let(:animal_model) { Animal }
229
+
230
+ it_behaves_like 'main'
231
+
232
+ describe '.base_path' do
233
+
234
+ it 'raise a RuntimeError on first query attempt if not set' do
235
+ allow(country_model).to receive_message_chain(:base_path).and_return(nil)
236
+ expect {
237
+ country_model.file_path
238
+ }.to raise_error(ArgumentError)
239
+ end
240
+
241
+ end
242
+ end
243
+
244
+ describe FrozenRecord::Compact do
245
+ let(:country_model) { Compact::Country }
246
+ let(:car_model) { Compact::Car }
247
+ let(:animal_model) { Compact::Animal }
248
+
249
+ it_behaves_like 'main'
250
+ end
@@ -1,3 +1,12 @@
1
1
  class Animal < FrozenRecord::Base
2
2
  self.backend = FrozenRecord::Backends::Json
3
3
  end
4
+
5
+ module Compact
6
+ class Animal < ::Animal
7
+ include FrozenRecord::Compact
8
+ def self.file_path
9
+ superclass.file_path
10
+ end
11
+ end
12
+ end
@@ -1,2 +1,12 @@
1
1
  class Car < FrozenRecord::Base
2
2
  end
3
+
4
+ module Compact
5
+ class Car < ::Car
6
+ include FrozenRecord::Compact
7
+
8
+ def self.file_path
9
+ superclass.file_path
10
+ end
11
+ end
12
+ end
@@ -13,3 +13,12 @@ class Country < FrozenRecord::Base
13
13
  name.reverse
14
14
  end
15
15
  end
16
+
17
+ module Compact
18
+ class Country < ::Country
19
+ include FrozenRecord::Compact
20
+ def self.file_path
21
+ superclass.file_path
22
+ end
23
+ end
24
+ 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.17.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-01-16 00:00:00.000000000 Z
11
+ date: 2020-07-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -94,7 +94,7 @@ dependencies:
94
94
  - - ">="
95
95
  - !ruby/object:Gem::Version
96
96
  version: '0'
97
- description:
97
+ description:
98
98
  email:
99
99
  - jean.boussier@gmail.com
100
100
  executables: []
@@ -108,12 +108,16 @@ files:
108
108
  - LICENSE.txt
109
109
  - README.md
110
110
  - Rakefile
111
+ - benchmark/attribute-read
112
+ - benchmark/memory-usage
113
+ - benchmark/setup.rb
111
114
  - frozen_record.gemspec
112
115
  - lib/frozen_record.rb
113
116
  - lib/frozen_record/backends.rb
114
117
  - lib/frozen_record/backends/json.rb
115
118
  - lib/frozen_record/backends/yaml.rb
116
119
  - lib/frozen_record/base.rb
120
+ - lib/frozen_record/compact.rb
117
121
  - lib/frozen_record/deduplication.rb
118
122
  - lib/frozen_record/railtie.rb
119
123
  - lib/frozen_record/scope.rb
@@ -136,7 +140,7 @@ homepage: https://github.com/byroot/frozen_record
136
140
  licenses:
137
141
  - MIT
138
142
  metadata: {}
139
- post_install_message:
143
+ post_install_message:
140
144
  rdoc_options: []
141
145
  require_paths:
142
146
  - lib
@@ -144,15 +148,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
144
148
  requirements:
145
149
  - - ">="
146
150
  - !ruby/object:Gem::Version
147
- version: '0'
151
+ version: '2.5'
148
152
  required_rubygems_version: !ruby/object:Gem::Requirement
149
153
  requirements:
150
154
  - - ">="
151
155
  - !ruby/object:Gem::Version
152
156
  version: '0'
153
157
  requirements: []
154
- rubygems_version: 3.0.4
155
- signing_key:
158
+ rubygems_version: 3.1.2
159
+ signing_key:
156
160
  specification_version: 4
157
161
  summary: ActiveRecord like interface to read only access and query static YAML files
158
162
  test_files: