frozen_record 0.13.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
- SHA1:
3
- metadata.gz: a2caf0ce8ea29240d70604b68d7e1edeaa8b331c
4
- data.tar.gz: 4ccec1e2b69e9a882d48cbbad9df73c9732b5fae
2
+ SHA256:
3
+ metadata.gz: a47352d34eb964f32bf8d253cffe682abc221f80db5782ed7dce6aee76967661
4
+ data.tar.gz: ea62e1c1bcc0cf927a2614f2fb4cfc1acbb6780cdfec273a44dbbe1bd14a55e2
5
5
  SHA512:
6
- metadata.gz: 62e573bedb84600e916de3784e764eeb9400555c8f11118dc27787ef7f642f15e927a6e304e96e118eb8c1f3317c57613c1bd395fc6330d2b59ad36d014c653c
7
- data.tar.gz: c225c4584ee248a86cd88e5f888618745d6275fdd27582a47fbedf405095d521c16a7d8484c6866b1c4ce2e618d921a772b0d45335253f4b50d3bc4c1a2d385a
6
+ metadata.gz: 33f76d7869d6a1e8a9cae954b074fa93cb89ffa3fcee9bc170d3e23b5ee2fcb39164a7e9de0b0f1570588bd7919d88b288bb35bc9c965cb32b77e3e9c6b5928a
7
+ data.tar.gz: e38977cfbdb6136aed4042060e130bc260f6e33c8db9c876f05f832d65037ba504fe7119a5b338c62e9a17391b2d1472ab2e2de5df5ae2c90b72772bd0599cd8
@@ -1,5 +1,5 @@
1
1
  sudo: false
2
2
  rvm:
3
- - '2.4.5'
4
- - '2.5.3'
5
- - '2.6.1'
3
+ - '2.5'
4
+ - '2.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
@@ -50,7 +50,7 @@ but this option can be changed per model:
50
50
 
51
51
  ```ruby
52
52
  class Country < FrozenRecord::Base
53
- self.backend = FrozenRecord::Backends::Yaml
53
+ self.backend = FrozenRecord::Backends::Json
54
54
  end
55
55
  ```
56
56
 
@@ -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
@@ -95,10 +95,12 @@ Country.
95
95
 
96
96
  ### Scopes
97
97
 
98
- While the `scope :symbol, lambda` syntax is not supported, the class methods way is:
98
+ Basic `scope :symbol, lambda` syntax is now supported in addition to class method syntax.
99
99
 
100
100
  ```ruby
101
101
  class Country
102
+ scope :european, -> { where(continent: 'Europe' ) }
103
+
102
104
  def self.republics
103
105
  where(king: nil)
104
106
  end
@@ -108,7 +110,7 @@ class Country
108
110
  end
109
111
  end
110
112
 
111
- Country.republics.part_of_nato.order(id: :desc)
113
+ Country.european.republics.part_of_nato.order(id: :desc)
112
114
  ```
113
115
 
114
116
  ### Supported query methods
@@ -181,7 +183,7 @@ class CountryTest < ActiveSupport::TestCase
181
183
  teardown do
182
184
  FrozenRecord::TestHelper.unload_fixtures
183
185
  end
184
-
186
+
185
187
  test "countries have a valid name" do
186
188
  # ...
187
189
  ```
@@ -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'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'yaml'
2
4
  require 'set'
3
5
  require 'active_support/all'
@@ -6,13 +8,21 @@ require 'active_model'
6
8
  require 'frozen_record/version'
7
9
  require 'frozen_record/scope'
8
10
  require 'frozen_record/base'
11
+ require 'frozen_record/compact'
12
+ require 'frozen_record/deduplication'
9
13
 
10
14
  module FrozenRecord
11
15
  RecordNotFound = Class.new(StandardError)
12
16
 
13
17
  class << self
18
+ attr_accessor :deprecated_yaml_erb_backend
19
+
14
20
  def eager_load!
15
21
  Base.descendants.each(&:eager_load!)
16
22
  end
17
23
  end
24
+
25
+ self.deprecated_yaml_erb_backend = true
18
26
  end
27
+
28
+ require 'frozen_record/railtie' if defined?(Rails)
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FrozenRecord
4
+ module Backends
5
+ autoload :Json, 'frozen_record/backends/json'
6
+ autoload :Yaml, 'frozen_record/backends/yaml'
7
+ end
8
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FrozenRecord
4
+ module Backends
5
+ module Json
6
+ extend self
7
+
8
+ def filename(model_name)
9
+ "#{model_name.underscore.pluralize}.json"
10
+ end
11
+
12
+ def load(file_path)
13
+ json_data = File.read(file_path)
14
+ JSON.parse(json_data) || []
15
+ end
16
+ end
17
+ end
18
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FrozenRecord
2
4
  module Backends
3
5
  module Yaml
@@ -17,10 +19,29 @@ module FrozenRecord
17
19
  # @param format [String] the file path
18
20
  # @return [Array] an Array of Hash objects with keys being attributes.
19
21
  def load(file_path)
20
- yml_erb_data = File.read(file_path)
21
- yml_data = ERB.new(yml_erb_data).result
22
+ if !File.exist?(file_path) && File.exist?("#{file_path}.erb")
23
+ file_path = "#{file_path}.erb"
24
+ end
25
+
26
+ if FrozenRecord.deprecated_yaml_erb_backend
27
+ yml_erb_data = File.read(file_path)
28
+ yml_data = ERB.new(yml_erb_data).result
29
+
30
+ unless file_path.end_with?('.erb')
31
+ if yml_data != yml_erb_data
32
+ basename = File.basename(file_path)
33
+ raise "[FrozenRecord] Deprecated: `#{basename}` contains ERB tags and should be renamed `#{basename}.erb`.\nSet FrozenRecord.deprecated_yaml_erb_backend = false to enable the future behavior"
34
+ end
35
+ end
22
36
 
23
- YAML.load(yml_data) || []
37
+ YAML.load(yml_data) || []
38
+ else
39
+ if file_path.end_with?('.erb')
40
+ YAML.load(ERB.new(File.read(file_path)).result) || []
41
+ else
42
+ YAML.load_file(file_path) || []
43
+ end
44
+ end
24
45
  end
25
46
  end
26
47
  end
@@ -1,6 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'set'
2
4
  require 'active_support/descendants_tracker'
3
- require 'frozen_record/backends/yaml'
5
+ require 'frozen_record/backends'
6
+ require 'objspace'
4
7
 
5
8
  module FrozenRecord
6
9
  class Base
@@ -15,19 +18,15 @@ module FrozenRecord
15
18
  end
16
19
 
17
20
  FIND_BY_PATTERN = /\Afind_by_(\w+)(!?)/
18
- FALSY_VALUES = [false, nil, 0, ''].to_set
21
+ FALSY_VALUES = [false, nil, 0, -''].to_set
19
22
 
20
- class_attribute :base_path
23
+ class_attribute :base_path, :primary_key, :backend, :auto_reloading, :default_attributes, instance_accessor: false
21
24
 
22
- class_attribute :primary_key
23
- self.primary_key = :id
25
+ self.primary_key = 'id'
24
26
 
25
- class_attribute :backend
26
27
  self.backend = FrozenRecord::Backends::Yaml
27
28
 
28
- class_attribute :auto_reloading
29
-
30
- attribute_method_suffix '?'
29
+ attribute_method_suffix -'?'
31
30
 
32
31
  class ThreadSafeStorage
33
32
 
@@ -48,8 +47,34 @@ module FrozenRecord
48
47
  end
49
48
 
50
49
  class << self
50
+ alias_method :set_default_attributes, :default_attributes=
51
+ private :set_default_attributes
52
+ def default_attributes=(default_attributes)
53
+ set_default_attributes(Deduplication.deep_deduplicate!(default_attributes.stringify_keys))
54
+ end
55
+
56
+ alias_method :set_primary_key, :primary_key=
57
+ private :set_primary_key
58
+ def primary_key=(primary_key)
59
+ set_primary_key(-primary_key.to_s)
60
+ end
61
+
62
+ alias_method :set_base_path, :base_path=
63
+ private :set_base_path
64
+ def base_path=(base_path)
65
+ @file_path = nil
66
+ set_base_path(base_path)
67
+ end
68
+
51
69
  attr_accessor :abstract_class
52
70
 
71
+ def attributes
72
+ @attributes ||= begin
73
+ load_records
74
+ @attributes
75
+ end
76
+ end
77
+
53
78
  def abstract_class?
54
79
  defined?(@abstract_class) && @abstract_class
55
80
  end
@@ -68,11 +93,33 @@ module FrozenRecord
68
93
 
69
94
  def file_path
70
95
  raise ArgumentError, "You must define `#{name}.base_path`" unless base_path
71
- File.join(base_path, backend.filename(name))
96
+ @file_path ||= begin
97
+ file_path = File.join(base_path, backend.filename(name))
98
+ if !File.exist?(file_path) && File.exist?("#{file_path}.erb")
99
+ "#{file_path}.erb"
100
+ else
101
+ file_path
102
+ end
103
+ end
104
+ end
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
72
119
  end
73
120
 
74
121
  def respond_to_missing?(name, *)
75
- if name =~ FIND_BY_PATTERN
122
+ if name.to_s =~ FIND_BY_PATTERN
76
123
  load_records # ensure attribute methods are defined
77
124
  return true if $1.split('_and_').all? { |attr| instance_method_already_implemented?(attr) }
78
125
  end
@@ -92,12 +139,26 @@ module FrozenRecord
92
139
 
93
140
  @records ||= begin
94
141
  records = backend.load(file_path)
95
- define_attribute_methods(list_attributes(records))
96
- records.map do |attributes|
97
- attributes.symbolize_keys!
98
- new(attributes)
99
- end.freeze
142
+ records.each { |r| assign_defaults!(r) }
143
+ records = Deduplication.deep_deduplicate!(records)
144
+ @attributes = list_attributes(records).freeze
145
+ define_attribute_methods(@attributes.to_a)
146
+ records.map { |r| load(r) }.freeze
147
+ end
148
+ end
149
+
150
+ def scope(name, body)
151
+ unless body.respond_to?(:call)
152
+ raise ArgumentError, "The scope body needs to be callable."
100
153
  end
154
+ singleton_class.send(:define_method, name) { |*args| body.call(*args) }
155
+ end
156
+
157
+ alias_method :load, :new
158
+ private :load
159
+
160
+ def new(attrs = {})
161
+ load(assign_defaults!(attrs.stringify_keys))
101
162
  end
102
163
 
103
164
  private
@@ -112,45 +173,54 @@ module FrozenRecord
112
173
  @store ||= ThreadSafeStorage.new(name)
113
174
  end
114
175
 
176
+ def assign_defaults!(record)
177
+ if default_attributes
178
+ default_attributes.each do |key, value|
179
+ unless record.key?(key)
180
+ record[key] = value
181
+ end
182
+ end
183
+ end
184
+
185
+ record
186
+ end
187
+
115
188
  def method_missing(name, *args)
116
- if name =~ FIND_BY_PATTERN
189
+ if name.to_s =~ FIND_BY_PATTERN
117
190
  return dynamic_match($1, args, $2.present?)
118
191
  end
119
192
  super
120
193
  end
121
194
 
122
195
  def dynamic_match(expression, values, bang)
123
- results = where(expression.split('_and_').zip(values))
196
+ results = where(expression.split('_and_'.freeze).zip(values))
124
197
  bang ? results.first! : results.first
125
198
  end
126
199
 
127
200
  def list_attributes(records)
128
201
  attributes = Set.new
129
202
  records.each do |record|
130
- record.keys.each do |key|
131
- attributes.add(key.to_sym)
132
- end
203
+ attributes.merge(record.keys)
133
204
  end
134
- attributes.to_a
205
+ attributes
135
206
  end
136
207
 
137
208
  end
138
209
 
139
210
  def initialize(attrs = {})
140
- @attributes = attrs
211
+ @attributes = attrs.freeze
141
212
  end
142
213
 
143
214
  def attributes
144
- # We have to return a hash with string keys for backward compatibity reasons
145
- @attributes.stringify_keys
215
+ @attributes.dup
146
216
  end
147
217
 
148
218
  def id
149
- self[primary_key]
219
+ self[self.class.primary_key]
150
220
  end
151
221
 
152
222
  def [](attr)
153
- @attributes[attr.to_sym]
223
+ @attributes[attr.to_s]
154
224
  end
155
225
  alias_method :attribute, :[]
156
226
 
@@ -169,9 +239,11 @@ module FrozenRecord
169
239
  private
170
240
 
171
241
  def attribute?(attribute_name)
172
- value = self[attribute_name]
173
- FALSY_VALUES.exclude?(value) && value.present?
242
+ FALSY_VALUES.exclude?(self[attribute_name]) && self[attribute_name].present?
174
243
  end
175
244
 
245
+ def attribute_method?(attribute_name)
246
+ respond_to_without_attributes?(:attributes) && self.class.attributes.include?(attribute_name)
247
+ end
176
248
  end
177
249
  end
@@ -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
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/object/duplicable'
4
+
5
+ module FrozenRecord
6
+ module Deduplication
7
+ extend self
8
+
9
+ # We deduplicate data in place because it is assumed it directly
10
+ # comes from the parser, and won't be held by anyone.
11
+ #
12
+ # Frozen Hashes and Arrays are ignored because they are likely
13
+ # the result of the use of YAML anchor. Meaning we already deduplicated
14
+ # them.
15
+ if RUBY_VERSION >= '2.7'
16
+ def deep_deduplicate!(data)
17
+ case data
18
+ when Hash
19
+ return data if data.frozen?
20
+ data.transform_keys! { |k| deep_deduplicate!(k) }
21
+ data.transform_values! { |v| deep_deduplicate!(v) }
22
+ data.freeze
23
+ when Array
24
+ return data if data.frozen?
25
+ data.map! { |d| deep_deduplicate!(d) }.freeze
26
+ when String
27
+ -data
28
+ else
29
+ data.duplicable? ? data.freeze : data
30
+ end
31
+ end
32
+ else
33
+ def deep_deduplicate!(data)
34
+ case data
35
+ when Hash
36
+ return data if data.frozen?
37
+ data.transform_keys! { |k| deep_deduplicate!(k) }
38
+ data.transform_values! { |v| deep_deduplicate!(v) }
39
+ data.freeze
40
+ when Array
41
+ return data if data.frozen?
42
+ data.map! { |d| deep_deduplicate!(d) }.freeze
43
+ when String
44
+ # String#-@ doesn't deduplicate the string if it's tainted.
45
+ # So in such case we need to untaint it first.
46
+ if data.tainted?
47
+ -(+data).untaint
48
+ else
49
+ -data
50
+ end
51
+ else
52
+ data.duplicable? ? data.freeze : data
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FrozenRecord
4
+ class Railtie < Rails::Railtie
5
+ initializer "frozen_record.setup" do |app|
6
+ app.config.eager_load_namespaces << FrozenRecord
7
+ end
8
+ end
9
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FrozenRecord
2
4
  class Scope
3
5
  BLACKLISTED_ARRAY_METHODS = [
@@ -62,7 +64,7 @@ module FrozenRecord
62
64
  def pluck(*attributes)
63
65
  case attributes.length
64
66
  when 1
65
- to_a.map(&attributes.first)
67
+ to_a.map(&attributes.first.to_sym)
66
68
  when 0
67
69
  raise NotImplementedError, '`.pluck` without arguments is not supported yet'
68
70
  else
@@ -122,8 +124,28 @@ module FrozenRecord
122
124
  array_delegable?(method_name) || @klass.respond_to?(method_name) || super
123
125
  end
124
126
 
127
+ def hash
128
+ comparable_attributes.hash
129
+ end
130
+
131
+ def ==(other)
132
+ self.class === other &&
133
+ comparable_attributes == other.comparable_attributes
134
+ end
135
+
125
136
  protected
126
137
 
138
+ def comparable_attributes
139
+ @comparable_attributes ||= {
140
+ klass: @klass,
141
+ where_values: @where_values.uniq.sort,
142
+ where_not_values: @where_not_values.uniq.sort,
143
+ order_values: @order_values.uniq,
144
+ limit: @limit,
145
+ offset: @offset,
146
+ }
147
+ end
148
+
127
149
  def scoping
128
150
  previous, @klass.current_scope = @klass.current_scope, self
129
151
  yield
@@ -136,6 +158,7 @@ module FrozenRecord
136
158
  end
137
159
 
138
160
  def clear_cache!
161
+ @comparable_attributes = nil
139
162
  @results = nil
140
163
  @matches = nil
141
164
  self
@@ -230,6 +253,7 @@ module FrozenRecord
230
253
  end
231
254
 
232
255
  private
256
+
233
257
  def compare_value(actual, requested)
234
258
  return actual == requested unless requested.is_a?(Array) || requested.is_a?(Range)
235
259
  requested.include?(actual)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FrozenRecord
2
4
  module TestHelper
3
5
  NoFixturesLoaded = Class.new(StandardError)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FrozenRecord
2
- VERSION = '0.13.0'
4
+ VERSION = '0.18.0'
3
5
  end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'deduplication' do
4
+
5
+ it 'deduplicate string values' do
6
+ pending("Strings can't be deduplicated before Ruby 2.5") if RUBY_VERSION < '2.5'
7
+
8
+ records = [
9
+ { 'name' => 'George'.dup },
10
+ ]
11
+
12
+ expect(records[0]['name']).to_not equal 'George'.freeze
13
+ FrozenRecord::Deduplication.deep_deduplicate!(records)
14
+ expect(records[0]['name']).to equal 'George'.freeze
15
+ end
16
+
17
+ it 'handles duplicated references' do
18
+ # This simulates the YAML anchor behavior
19
+ tags = { 'foo' => 'bar' }
20
+ records = [
21
+ { 'name' => 'George', 'tags' => tags },
22
+ { 'name' => 'Peter', 'tags' => tags },
23
+ ]
24
+
25
+ expect(records[0]['tags']).to_not be_frozen
26
+ FrozenRecord::Deduplication.deep_deduplicate!(records)
27
+ expect(records[0]['tags']).to be_frozen
28
+ expect(records[0]['tags']).to equal records[1]['tags']
29
+ end
30
+
31
+ end
@@ -8,6 +8,7 @@
8
8
  updated_at: 2014-02-24T19:08:06-05:00
9
9
  nato: true
10
10
  king: Elisabeth II
11
+ continent: North America
11
12
 
12
13
  - id: 2
13
14
  name: France
@@ -16,6 +17,8 @@
16
17
  population: 65.7
17
18
  founded_on: 486-01-01
18
19
  updated_at: 2014-02-12T19:02:03-02:00
20
+ nato: true
21
+ continent: Europe
19
22
 
20
23
  - id: 3
21
24
  name: Austria
@@ -24,3 +27,5 @@
24
27
  population: 8.462
25
28
  founded_on: 1156-01-01
26
29
  updated_at: 2014-02-12T19:02:03-02:00
30
+ continent: Europe
31
+ available: false
@@ -1,14 +1,21 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe FrozenRecord::Base do
3
+ RSpec.shared_examples 'main' do
4
4
 
5
- describe '.base_path' do
5
+ describe '.primary_key' do
6
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)
7
+ around do |example|
8
+ previous_primary_key = country_model.primary_key
9
+ begin
10
+ example.run
11
+ ensure
12
+ country_model.primary_key = previous_primary_key
13
+ end
14
+ end
15
+
16
+ it 'is coerced to string' do
17
+ country_model.primary_key = :foobar
18
+ expect(country_model.primary_key).to be == 'foobar'
12
19
  end
13
20
 
14
21
  end
@@ -18,24 +25,24 @@ describe FrozenRecord::Base do
18
25
  context 'when enabled' do
19
26
 
20
27
  around do |example|
21
- previous_auto_reloading = Country.auto_reloading
22
- Country.auto_reloading = true
28
+ previous_auto_reloading = country_model.auto_reloading
29
+ country_model.auto_reloading = true
23
30
  begin
24
31
  example.run
25
32
  ensure
26
- Country.auto_reloading = previous_auto_reloading
33
+ country_model.auto_reloading = previous_auto_reloading
27
34
  end
28
35
  end
29
36
 
30
37
  it 'reloads the records if the file mtime changed' do
31
- mtime = File.mtime(Country.file_path)
38
+ mtime = File.mtime(country_model.file_path)
32
39
  expect {
33
- File.utime(mtime + 1, mtime + 1, Country.file_path)
34
- }.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 }
35
42
  end
36
43
 
37
44
  it 'does not reload if the file has not changed' do
38
- 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
39
46
  end
40
47
 
41
48
  end
@@ -43,25 +50,65 @@ describe FrozenRecord::Base do
43
50
  context 'when disabled' do
44
51
 
45
52
  it 'does not reloads the records if the file mtime changed' do
46
- mtime = File.mtime(Country.file_path)
53
+ mtime = File.mtime(country_model.file_path)
47
54
  expect {
48
- File.utime(mtime + 1, mtime + 1, Country.file_path)
49
- }.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 }
50
57
  end
51
58
 
52
59
  end
53
60
 
54
61
  end
55
62
 
63
+ describe '.default_attributes' do
64
+
65
+ it 'define the attribute' do
66
+ expect(country_model.new).to respond_to :contemporary
67
+ end
68
+
69
+ it 'sets the value as default' do
70
+ expect(country_model.find_by(name: 'Austria').contemporary).to be == true
71
+ end
72
+
73
+ it 'gives precedence to the data file' do
74
+ expect(country_model.find_by(name: 'Austria').available).to be == false
75
+ end
76
+
77
+ it 'is also set in the initializer' do
78
+ expect(country_model.new.contemporary).to be == true
79
+ end
80
+
81
+ end
82
+
83
+ describe '.scope' do
84
+
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
90
+
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
+ end
100
+
101
+ end
102
+
56
103
  describe '#load_records' do
57
104
 
58
105
  it 'processes erb by default' do
59
- country = Country.first
106
+ country = country_model.first
60
107
  expect(country.capital).to be == 'Ottawa'
61
108
  end
62
109
 
63
110
  it 'loads records with a custom backend' do
64
- animal = Animal.first
111
+ animal = animal_model.first
65
112
  expect(animal.name).to be == 'cat'
66
113
  end
67
114
 
@@ -70,22 +117,22 @@ describe FrozenRecord::Base do
70
117
  describe '#==' do
71
118
 
72
119
  it 'returns true if both instances are from the same class and have the same id' do
73
- country = Country.first
120
+ country = country_model.first
74
121
  second_country = country.dup
75
122
 
76
123
  expect(country).to be == second_country
77
124
  end
78
125
 
79
126
  it 'returns false if both instances are not from the same class' do
80
- country = Country.first
81
- car = Car.new(id: country.id)
127
+ country = country_model.first
128
+ car = car_model.new(id: country.id)
82
129
 
83
130
  expect(country).to_not be == car
84
131
  end
85
132
 
86
133
  it 'returns false if both instances do not have the same id' do
87
- country = Country.first
88
- second_country = Country.last
134
+ country = country_model.first
135
+ second_country = country_model.last
89
136
 
90
137
  expect(country).to_not be == second_country
91
138
  end
@@ -95,7 +142,7 @@ describe FrozenRecord::Base do
95
142
  describe '#attributes' do
96
143
 
97
144
  it 'returns a Hash of the record attributes' do
98
- attributes = Country.first.attributes
145
+ attributes = country_model.first.attributes
99
146
  expect(attributes).to be == {
100
147
  'id' => 1,
101
148
  'name' => 'Canada',
@@ -106,6 +153,9 @@ describe FrozenRecord::Base do
106
153
  'updated_at' => Time.parse('2014-02-24T19:08:06-05:00'),
107
154
  'king' => 'Elisabeth II',
108
155
  'nato' => true,
156
+ 'continent' => 'North America',
157
+ 'available' => true,
158
+ 'contemporary' => true,
109
159
  }
110
160
  end
111
161
 
@@ -113,9 +163,9 @@ describe FrozenRecord::Base do
113
163
 
114
164
  describe '`attribute`?' do
115
165
 
116
- let(:blank) { Country.new(id: 0, name: '', nato: false, king: nil) }
166
+ let(:blank) { country_model.new(id: 0, name: '', nato: false, king: nil) }
117
167
 
118
- 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) }
119
169
 
120
170
  it 'considers `0` as missing' do
121
171
  expect(blank.id?).to be false
@@ -151,15 +201,50 @@ describe FrozenRecord::Base do
151
201
 
152
202
  end
153
203
 
204
+ describe '#present?' do
205
+
206
+ it 'returns true' do
207
+ expect(country_model.first).to be_present
208
+ end
209
+
210
+ end
211
+
154
212
  describe '#count' do
155
213
 
156
214
  it 'can count objects with no records' do
157
- expect(Car.count).to be 0
215
+ expect(car_model.count).to be 0
158
216
  end
159
217
 
160
218
  it 'can count objects with records' do
161
- expect(Country.count).to be 3
219
+ expect(country_model.count).to be 3
162
220
  end
163
221
 
164
222
  end
165
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
@@ -417,6 +417,23 @@ describe 'querying' do
417
417
 
418
418
  end
419
419
 
420
+ describe '#==' do
421
+ it 'returns true when two scopes share the same hashed attributes' do
422
+ scope_a = Country.republics.nato
423
+ scope_b = Country.republics.nato
424
+ expect(scope_a.object_id).not_to be == scope_b.object_id
425
+ expect(scope_a).to be == scope_b
426
+ end
427
+
428
+ it 'returns true when the same scope has be rechained' do
429
+ scope_a = Country.nato.republics.nato.republics
430
+ scope_b = Country.republics.nato
431
+ expect(scope_a.instance_variable_get(:@where_values)).to be == [[:nato, true ], [:king, nil ], [:nato, true], [:king, nil]]
432
+ expect(scope_b.instance_variable_get(:@where_values)).to be == [[:king, nil ], [:nato, true]]
433
+ expect(scope_a).to be == scope_b
434
+ end
435
+ end
436
+
420
437
  describe 'class methods delegation' do
421
438
 
422
439
  it 'can be called from a scope' do
@@ -1,16 +1,12 @@
1
- module JsonBackend
2
- extend self
3
-
4
- def filename(model_name)
5
- "#{model_name.underscore.pluralize}.json"
6
- end
7
-
8
- def load(file_path)
9
- json_data = File.read(file_path)
10
- JSON.parse(json_data) || []
11
- end
1
+ class Animal < FrozenRecord::Base
2
+ self.backend = FrozenRecord::Backends::Json
12
3
  end
13
4
 
14
- class Animal < FrozenRecord::Base
15
- self.backend = JsonBackend
5
+ module Compact
6
+ class Animal < ::Animal
7
+ include FrozenRecord::Compact
8
+ def self.file_path
9
+ superclass.file_path
10
+ end
11
+ end
16
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
@@ -1,10 +1,24 @@
1
1
  class Country < FrozenRecord::Base
2
+ self.default_attributes = { contemporary: true, available: true }
2
3
 
3
4
  def self.republics
4
5
  where(king: nil)
5
6
  end
6
7
 
8
+ def self.nato
9
+ where(nato: true)
10
+ end
11
+
7
12
  def reverse_name
8
13
  name.reverse
9
14
  end
10
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.13.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: 2019-03-26 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,17 +108,26 @@ 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
116
+ - lib/frozen_record/backends.rb
117
+ - lib/frozen_record/backends/json.rb
113
118
  - lib/frozen_record/backends/yaml.rb
114
119
  - lib/frozen_record/base.rb
120
+ - lib/frozen_record/compact.rb
121
+ - lib/frozen_record/deduplication.rb
122
+ - lib/frozen_record/railtie.rb
115
123
  - lib/frozen_record/scope.rb
116
124
  - lib/frozen_record/test_helper.rb
117
125
  - lib/frozen_record/version.rb
126
+ - spec/deduplication_spec.rb
118
127
  - spec/fixtures/animals.json
119
128
  - spec/fixtures/cars.yml
120
- - spec/fixtures/countries.yml
121
- - spec/fixtures/test_helper/countries.yml
129
+ - spec/fixtures/countries.yml.erb
130
+ - spec/fixtures/test_helper/countries.yml.erb
122
131
  - spec/frozen_record_spec.rb
123
132
  - spec/scope_spec.rb
124
133
  - spec/spec_helper.rb
@@ -131,7 +140,7 @@ homepage: https://github.com/byroot/frozen_record
131
140
  licenses:
132
141
  - MIT
133
142
  metadata: {}
134
- post_install_message:
143
+ post_install_message:
135
144
  rdoc_options: []
136
145
  require_paths:
137
146
  - lib
@@ -139,23 +148,23 @@ required_ruby_version: !ruby/object:Gem::Requirement
139
148
  requirements:
140
149
  - - ">="
141
150
  - !ruby/object:Gem::Version
142
- version: '0'
151
+ version: '2.5'
143
152
  required_rubygems_version: !ruby/object:Gem::Requirement
144
153
  requirements:
145
154
  - - ">="
146
155
  - !ruby/object:Gem::Version
147
156
  version: '0'
148
157
  requirements: []
149
- rubyforge_project:
150
- rubygems_version: 2.5.1
151
- signing_key:
158
+ rubygems_version: 3.1.2
159
+ signing_key:
152
160
  specification_version: 4
153
161
  summary: ActiveRecord like interface to read only access and query static YAML files
154
162
  test_files:
163
+ - spec/deduplication_spec.rb
155
164
  - spec/fixtures/animals.json
156
165
  - spec/fixtures/cars.yml
157
- - spec/fixtures/countries.yml
158
- - spec/fixtures/test_helper/countries.yml
166
+ - spec/fixtures/countries.yml.erb
167
+ - spec/fixtures/test_helper/countries.yml.erb
159
168
  - spec/frozen_record_spec.rb
160
169
  - spec/scope_spec.rb
161
170
  - spec/spec_helper.rb