frozen_record 0.17.0 → 0.18.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: 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: