frozen_record 0.16.0 → 0.19.2
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/.gitignore +1 -0
- data/.travis.yml +3 -3
- data/Gemfile +2 -0
- data/README.md +18 -2
- data/benchmark/attribute-read +22 -0
- data/benchmark/memory-usage +10 -0
- data/benchmark/setup.rb +6 -0
- data/frozen_record.gemspec +2 -0
- data/lib/frozen_record.rb +9 -0
- data/lib/frozen_record/backends.rb +8 -0
- data/lib/frozen_record/backends/json.rb +18 -0
- data/lib/frozen_record/backends/yaml.rb +24 -3
- data/lib/frozen_record/base.rb +95 -28
- data/lib/frozen_record/compact.rb +77 -0
- data/lib/frozen_record/deduplication.rb +57 -0
- data/lib/frozen_record/index.rb +77 -0
- data/lib/frozen_record/railtie.rb +2 -0
- data/lib/frozen_record/scope.rb +17 -4
- data/lib/frozen_record/test_helper.rb +2 -0
- data/lib/frozen_record/version.rb +3 -1
- data/spec/deduplication_spec.rb +31 -0
- data/spec/fixtures/{countries.yml → countries.yml.erb} +1 -1
- data/spec/fixtures/test_helper/{countries.yml → countries.yml.erb} +0 -0
- data/spec/frozen_record_spec.rb +98 -41
- data/spec/scope_spec.rb +14 -2
- data/spec/support/animal.rb +9 -13
- data/spec/support/car.rb +10 -0
- data/spec/support/country.rb +13 -0
- metadata +22 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 501a982beb96747b9e2dfd6250e9685e73ffbb2932cb5fddb84b7b44f778c3dd
|
|
4
|
+
data.tar.gz: 9396c1416991e6a932d8a16fa430471c531281be58e8547af8a1900ebf68d4cc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: be0b8317169536a900a01769866f5b7b181974ed24dfd812abcfbd5b332324da1d8d9870c80c89af50bb3fa1565342a5e1229b198b8b6fc141d93c237566a152
|
|
7
|
+
data.tar.gz: 3a4c490e06150033c098905817842e6c2cc9b2719262f28f53e063de34815236910e18da5cf3e9848f4aed1e1db2e738d2b33dcce90f429fa1cdba2840cf4556
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
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::
|
|
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
|
|
63
|
+
extend self
|
|
64
64
|
|
|
65
65
|
def filename(model_name)
|
|
66
66
|
# Returns the file name as a String
|
|
@@ -140,6 +140,22 @@ Country.european.republics.part_of_nato.order(id: :desc)
|
|
|
140
140
|
- average
|
|
141
141
|
|
|
142
142
|
|
|
143
|
+
## Indexing
|
|
144
|
+
|
|
145
|
+
Querying is implemented as a simple linear search (`O(n)`). However if you are using Frozen Record with larger datasets, or are querying
|
|
146
|
+
a collection repetedly, you can define indices for faster access.
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
class Country < FrozenRecord::Base
|
|
150
|
+
add_index :name, unique: true
|
|
151
|
+
add_index :continent
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Composite index keys are not supported.
|
|
156
|
+
|
|
157
|
+
The primary key isn't indexed by default.
|
|
158
|
+
|
|
143
159
|
## Configuration
|
|
144
160
|
|
|
145
161
|
### Reloading
|
|
@@ -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)}%"
|
data/benchmark/setup.rb
ADDED
data/frozen_record.gemspec
CHANGED
|
@@ -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'
|
data/lib/frozen_record.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'yaml'
|
|
2
4
|
require 'set'
|
|
3
5
|
require 'active_support/all'
|
|
@@ -5,16 +7,23 @@ require 'active_model'
|
|
|
5
7
|
|
|
6
8
|
require 'frozen_record/version'
|
|
7
9
|
require 'frozen_record/scope'
|
|
10
|
+
require 'frozen_record/index'
|
|
8
11
|
require 'frozen_record/base'
|
|
12
|
+
require 'frozen_record/compact'
|
|
13
|
+
require 'frozen_record/deduplication'
|
|
9
14
|
|
|
10
15
|
module FrozenRecord
|
|
11
16
|
RecordNotFound = Class.new(StandardError)
|
|
12
17
|
|
|
13
18
|
class << self
|
|
19
|
+
attr_accessor :deprecated_yaml_erb_backend
|
|
20
|
+
|
|
14
21
|
def eager_load!
|
|
15
22
|
Base.descendants.each(&:eager_load!)
|
|
16
23
|
end
|
|
17
24
|
end
|
|
25
|
+
|
|
26
|
+
self.deprecated_yaml_erb_backend = true
|
|
18
27
|
end
|
|
19
28
|
|
|
20
29
|
require 'frozen_record/railtie' if defined?(Rails)
|
|
@@ -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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
data/lib/frozen_record/base.rb
CHANGED
|
@@ -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
|
|
5
|
+
require 'frozen_record/backends'
|
|
6
|
+
require 'objspace'
|
|
4
7
|
|
|
5
8
|
module FrozenRecord
|
|
6
9
|
class Base
|
|
@@ -17,25 +20,14 @@ module FrozenRecord
|
|
|
17
20
|
FIND_BY_PATTERN = /\Afind_by_(\w+)(!?)/
|
|
18
21
|
FALSY_VALUES = [false, nil, 0, -''].to_set
|
|
19
22
|
|
|
20
|
-
class_attribute :base_path
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class << self
|
|
25
|
-
alias_method :original_primary_key=, :primary_key=
|
|
26
|
-
|
|
27
|
-
def primary_key=(primary_key)
|
|
28
|
-
self.original_primary_key = -primary_key.to_s
|
|
29
|
-
end
|
|
30
|
-
end
|
|
23
|
+
class_attribute :base_path, :primary_key, :backend, :auto_reloading, :default_attributes, instance_accessor: false
|
|
24
|
+
class_attribute :index_definitions, instance_accessor: false
|
|
25
|
+
self.index_definitions = {}.freeze
|
|
31
26
|
|
|
32
27
|
self.primary_key = 'id'
|
|
33
28
|
|
|
34
|
-
class_attribute :backend
|
|
35
29
|
self.backend = FrozenRecord::Backends::Yaml
|
|
36
30
|
|
|
37
|
-
class_attribute :auto_reloading
|
|
38
|
-
|
|
39
31
|
attribute_method_suffix -'?'
|
|
40
32
|
|
|
41
33
|
class ThreadSafeStorage
|
|
@@ -57,8 +49,34 @@ module FrozenRecord
|
|
|
57
49
|
end
|
|
58
50
|
|
|
59
51
|
class << self
|
|
52
|
+
alias_method :set_default_attributes, :default_attributes=
|
|
53
|
+
private :set_default_attributes
|
|
54
|
+
def default_attributes=(default_attributes)
|
|
55
|
+
set_default_attributes(Deduplication.deep_deduplicate!(default_attributes.stringify_keys))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
alias_method :set_primary_key, :primary_key=
|
|
59
|
+
private :set_primary_key
|
|
60
|
+
def primary_key=(primary_key)
|
|
61
|
+
set_primary_key(-primary_key.to_s)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
alias_method :set_base_path, :base_path=
|
|
65
|
+
private :set_base_path
|
|
66
|
+
def base_path=(base_path)
|
|
67
|
+
@file_path = nil
|
|
68
|
+
set_base_path(base_path)
|
|
69
|
+
end
|
|
70
|
+
|
|
60
71
|
attr_accessor :abstract_class
|
|
61
72
|
|
|
73
|
+
def attributes
|
|
74
|
+
@attributes ||= begin
|
|
75
|
+
load_records
|
|
76
|
+
@attributes
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
62
80
|
def abstract_class?
|
|
63
81
|
defined?(@abstract_class) && @abstract_class
|
|
64
82
|
end
|
|
@@ -77,7 +95,34 @@ module FrozenRecord
|
|
|
77
95
|
|
|
78
96
|
def file_path
|
|
79
97
|
raise ArgumentError, "You must define `#{name}.base_path`" unless base_path
|
|
80
|
-
|
|
98
|
+
@file_path ||= begin
|
|
99
|
+
file_path = File.join(base_path, backend.filename(name))
|
|
100
|
+
if !File.exist?(file_path) && File.exist?("#{file_path}.erb")
|
|
101
|
+
"#{file_path}.erb"
|
|
102
|
+
else
|
|
103
|
+
file_path
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def add_index(attribute, unique: false)
|
|
109
|
+
index = unique ? UniqueIndex.new(self, attribute) : Index.new(self, attribute)
|
|
110
|
+
self.index_definitions = index_definitions.merge(index.attribute => index).freeze
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def memsize(object = self, seen = Set.new.compare_by_identity)
|
|
114
|
+
return 0 unless seen.add?(object)
|
|
115
|
+
|
|
116
|
+
size = ObjectSpace.memsize_of(object)
|
|
117
|
+
object.instance_variables.each { |v| size += memsize(object.instance_variable_get(v), seen) }
|
|
118
|
+
|
|
119
|
+
case object
|
|
120
|
+
when Hash
|
|
121
|
+
object.each { |k, v| size += memsize(k, seen) + memsize(v, seen) }
|
|
122
|
+
when Array
|
|
123
|
+
object.each { |i| size += memsize(i, seen) }
|
|
124
|
+
end
|
|
125
|
+
size
|
|
81
126
|
end
|
|
82
127
|
|
|
83
128
|
def respond_to_missing?(name, *)
|
|
@@ -101,16 +146,25 @@ module FrozenRecord
|
|
|
101
146
|
|
|
102
147
|
@records ||= begin
|
|
103
148
|
records = backend.load(file_path)
|
|
104
|
-
|
|
105
|
-
records.
|
|
149
|
+
records.each { |r| assign_defaults!(r) }
|
|
150
|
+
records = Deduplication.deep_deduplicate!(records)
|
|
151
|
+
@attributes = list_attributes(records).freeze
|
|
152
|
+
define_attribute_methods(@attributes.to_a)
|
|
153
|
+
records = records.map { |r| load(r) }.freeze
|
|
154
|
+
index_definitions.values.each { |index| index.build(records) }
|
|
155
|
+
records
|
|
106
156
|
end
|
|
107
157
|
end
|
|
108
158
|
|
|
109
159
|
def scope(name, body)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
160
|
+
singleton_class.send(:define_method, name, &body)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
alias_method :load, :new
|
|
164
|
+
private :load
|
|
165
|
+
|
|
166
|
+
def new(attrs = {})
|
|
167
|
+
load(assign_defaults!(attrs.stringify_keys))
|
|
114
168
|
end
|
|
115
169
|
|
|
116
170
|
private
|
|
@@ -125,6 +179,18 @@ module FrozenRecord
|
|
|
125
179
|
@store ||= ThreadSafeStorage.new(name)
|
|
126
180
|
end
|
|
127
181
|
|
|
182
|
+
def assign_defaults!(record)
|
|
183
|
+
if default_attributes
|
|
184
|
+
default_attributes.each do |key, value|
|
|
185
|
+
unless record.key?(key)
|
|
186
|
+
record[key] = value
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
record
|
|
192
|
+
end
|
|
193
|
+
|
|
128
194
|
def method_missing(name, *args)
|
|
129
195
|
if name.to_s =~ FIND_BY_PATTERN
|
|
130
196
|
return dynamic_match($1, args, $2.present?)
|
|
@@ -140,17 +206,15 @@ module FrozenRecord
|
|
|
140
206
|
def list_attributes(records)
|
|
141
207
|
attributes = Set.new
|
|
142
208
|
records.each do |record|
|
|
143
|
-
record.keys
|
|
144
|
-
attributes.add(key.to_s)
|
|
145
|
-
end
|
|
209
|
+
attributes.merge(record.keys)
|
|
146
210
|
end
|
|
147
|
-
attributes
|
|
211
|
+
attributes
|
|
148
212
|
end
|
|
149
213
|
|
|
150
214
|
end
|
|
151
215
|
|
|
152
216
|
def initialize(attrs = {})
|
|
153
|
-
@attributes = attrs.
|
|
217
|
+
@attributes = attrs.freeze
|
|
154
218
|
end
|
|
155
219
|
|
|
156
220
|
def attributes
|
|
@@ -158,7 +222,7 @@ module FrozenRecord
|
|
|
158
222
|
end
|
|
159
223
|
|
|
160
224
|
def id
|
|
161
|
-
self[primary_key
|
|
225
|
+
self[self.class.primary_key]
|
|
162
226
|
end
|
|
163
227
|
|
|
164
228
|
def [](attr)
|
|
@@ -184,5 +248,8 @@ module FrozenRecord
|
|
|
184
248
|
FALSY_VALUES.exclude?(self[attribute_name]) && self[attribute_name].present?
|
|
185
249
|
end
|
|
186
250
|
|
|
251
|
+
def attribute_method?(attribute_name)
|
|
252
|
+
respond_to_without_attributes?(:attributes) && self.class.attributes.include?(attribute_name)
|
|
253
|
+
end
|
|
187
254
|
end
|
|
188
255
|
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,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FrozenRecord
|
|
4
|
+
class Index
|
|
5
|
+
EMPTY_ARRAY = [].freeze
|
|
6
|
+
private_constant :EMPTY_ARRAY
|
|
7
|
+
|
|
8
|
+
AttributeNonUnique = Class.new(StandardError)
|
|
9
|
+
|
|
10
|
+
attr_reader :attribute, :model
|
|
11
|
+
|
|
12
|
+
def initialize(model, attribute, unique: false)
|
|
13
|
+
@model = model
|
|
14
|
+
@attribute = -attribute.to_s
|
|
15
|
+
@index = nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def unique?
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def query(value)
|
|
23
|
+
case value
|
|
24
|
+
when Array, Range
|
|
25
|
+
lookup_multi(value)
|
|
26
|
+
else
|
|
27
|
+
lookup(value)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def lookup_multi(values)
|
|
32
|
+
values.flat_map { |v| lookup(v) }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def lookup(value)
|
|
36
|
+
@index.fetch(value, EMPTY_ARRAY)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def reset
|
|
40
|
+
@index = nil
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def build(records)
|
|
44
|
+
@index = records.each_with_object({}) do |record, index|
|
|
45
|
+
entry = (index[record[attribute]] ||= [])
|
|
46
|
+
entry << record
|
|
47
|
+
end
|
|
48
|
+
@index.values.each(&:freeze)
|
|
49
|
+
@index.freeze
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
class UniqueIndex < Index
|
|
54
|
+
def unique?
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def lookup_multi(values)
|
|
59
|
+
results = @index.values_at(*values)
|
|
60
|
+
results.compact!
|
|
61
|
+
results
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def lookup(value)
|
|
65
|
+
record = @index[value]
|
|
66
|
+
record ? [record] : EMPTY_ARRAY
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build(records)
|
|
70
|
+
@index = records.each_with_object({}) { |r, index| index[r[attribute]] = r }
|
|
71
|
+
if @index.size != records.size
|
|
72
|
+
raise AttributeNonUnique, "#{model}##{attribute.inspect} is not unique."
|
|
73
|
+
end
|
|
74
|
+
@index.freeze
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
data/lib/frozen_record/scope.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module FrozenRecord
|
|
2
4
|
class Scope
|
|
3
5
|
BLACKLISTED_ARRAY_METHODS = [
|
|
@@ -173,9 +175,20 @@ module FrozenRecord
|
|
|
173
175
|
def select_records(records)
|
|
174
176
|
return records if @where_values.empty? && @where_not_values.empty?
|
|
175
177
|
|
|
178
|
+
indices = @klass.index_definitions
|
|
179
|
+
indexed_where_values, unindexed_where_values = @where_values.partition { |criteria| indices.key?(criteria.first) }
|
|
180
|
+
|
|
181
|
+
unless indexed_where_values.empty?
|
|
182
|
+
attribute, value = indexed_where_values.shift
|
|
183
|
+
records = indices[attribute].query(value)
|
|
184
|
+
indexed_where_values.each do |(attribute, value)|
|
|
185
|
+
records &= indices[attribute].query(value)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
176
189
|
records.select do |record|
|
|
177
|
-
|
|
178
|
-
|
|
190
|
+
unindexed_where_values.all? { |attr, value| compare_value(record[attr], value) } &&
|
|
191
|
+
!@where_not_values.any? { |attr, value| compare_value(record[attr], value) }
|
|
179
192
|
end
|
|
180
193
|
end
|
|
181
194
|
|
|
@@ -224,12 +237,12 @@ module FrozenRecord
|
|
|
224
237
|
end
|
|
225
238
|
|
|
226
239
|
def where!(criterias)
|
|
227
|
-
@where_values += criterias.
|
|
240
|
+
@where_values += criterias.map { |k, v| [k.to_s, v] }
|
|
228
241
|
self
|
|
229
242
|
end
|
|
230
243
|
|
|
231
244
|
def where_not!(criterias)
|
|
232
|
-
@where_not_values += criterias.
|
|
245
|
+
@where_not_values += criterias.map { |k, v| [k.to_s, v] }
|
|
233
246
|
self
|
|
234
247
|
end
|
|
235
248
|
|
|
@@ -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
|
|
File without changes
|
data/spec/frozen_record_spec.rb
CHANGED
|
@@ -1,32 +1,21 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
2
|
|
|
3
|
-
|
|
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 =
|
|
8
|
+
previous_primary_key = country_model.primary_key
|
|
20
9
|
begin
|
|
21
10
|
example.run
|
|
22
11
|
ensure
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
expect(
|
|
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 =
|
|
40
|
-
|
|
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
|
-
|
|
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(
|
|
38
|
+
mtime = File.mtime(country_model.file_path)
|
|
50
39
|
expect {
|
|
51
|
-
File.utime(mtime + 1, mtime + 1,
|
|
52
|
-
}.to change {
|
|
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(
|
|
45
|
+
expect(country_model.first.object_id).to be == country_model.first.object_id
|
|
57
46
|
end
|
|
58
47
|
|
|
59
48
|
end
|
|
@@ -61,34 +50,65 @@ 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(
|
|
53
|
+
mtime = File.mtime(country_model.file_path)
|
|
65
54
|
expect {
|
|
66
|
-
File.utime(mtime + 1, mtime + 1,
|
|
67
|
-
}.to_not change {
|
|
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
|
|
71
60
|
|
|
72
61
|
end
|
|
73
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
|
+
|
|
74
83
|
describe '.scope' do
|
|
84
|
+
|
|
75
85
|
it 'defines a scope method' do
|
|
86
|
+
country_model.scope :north_american, -> { 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
|
|
76
94
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
expect(
|
|
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
|
|
80
99
|
end
|
|
100
|
+
|
|
81
101
|
end
|
|
82
102
|
|
|
83
103
|
describe '#load_records' do
|
|
84
104
|
|
|
85
105
|
it 'processes erb by default' do
|
|
86
|
-
country =
|
|
106
|
+
country = country_model.first
|
|
87
107
|
expect(country.capital).to be == 'Ottawa'
|
|
88
108
|
end
|
|
89
109
|
|
|
90
110
|
it 'loads records with a custom backend' do
|
|
91
|
-
animal =
|
|
111
|
+
animal = animal_model.first
|
|
92
112
|
expect(animal.name).to be == 'cat'
|
|
93
113
|
end
|
|
94
114
|
|
|
@@ -97,22 +117,22 @@ describe FrozenRecord::Base do
|
|
|
97
117
|
describe '#==' do
|
|
98
118
|
|
|
99
119
|
it 'returns true if both instances are from the same class and have the same id' do
|
|
100
|
-
country =
|
|
120
|
+
country = country_model.first
|
|
101
121
|
second_country = country.dup
|
|
102
122
|
|
|
103
123
|
expect(country).to be == second_country
|
|
104
124
|
end
|
|
105
125
|
|
|
106
126
|
it 'returns false if both instances are not from the same class' do
|
|
107
|
-
country =
|
|
108
|
-
car =
|
|
127
|
+
country = country_model.first
|
|
128
|
+
car = car_model.new(id: country.id)
|
|
109
129
|
|
|
110
130
|
expect(country).to_not be == car
|
|
111
131
|
end
|
|
112
132
|
|
|
113
133
|
it 'returns false if both instances do not have the same id' do
|
|
114
|
-
country =
|
|
115
|
-
second_country =
|
|
134
|
+
country = country_model.first
|
|
135
|
+
second_country = country_model.last
|
|
116
136
|
|
|
117
137
|
expect(country).to_not be == second_country
|
|
118
138
|
end
|
|
@@ -122,7 +142,7 @@ describe FrozenRecord::Base do
|
|
|
122
142
|
describe '#attributes' do
|
|
123
143
|
|
|
124
144
|
it 'returns a Hash of the record attributes' do
|
|
125
|
-
attributes =
|
|
145
|
+
attributes = country_model.first.attributes
|
|
126
146
|
expect(attributes).to be == {
|
|
127
147
|
'id' => 1,
|
|
128
148
|
'name' => 'Canada',
|
|
@@ -134,6 +154,8 @@ describe FrozenRecord::Base do
|
|
|
134
154
|
'king' => 'Elisabeth II',
|
|
135
155
|
'nato' => true,
|
|
136
156
|
'continent' => 'North America',
|
|
157
|
+
'available' => true,
|
|
158
|
+
'contemporary' => true,
|
|
137
159
|
}
|
|
138
160
|
end
|
|
139
161
|
|
|
@@ -141,9 +163,9 @@ describe FrozenRecord::Base do
|
|
|
141
163
|
|
|
142
164
|
describe '`attribute`?' do
|
|
143
165
|
|
|
144
|
-
let(:blank) {
|
|
166
|
+
let(:blank) { country_model.new(id: 0, name: '', nato: false, king: nil) }
|
|
145
167
|
|
|
146
|
-
let(:present) {
|
|
168
|
+
let(:present) { country_model.new(id: 42, name: 'Groland', nato: true, king: Object.new) }
|
|
147
169
|
|
|
148
170
|
it 'considers `0` as missing' do
|
|
149
171
|
expect(blank.id?).to be false
|
|
@@ -179,15 +201,50 @@ describe FrozenRecord::Base do
|
|
|
179
201
|
|
|
180
202
|
end
|
|
181
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
|
+
|
|
182
212
|
describe '#count' do
|
|
183
213
|
|
|
184
214
|
it 'can count objects with no records' do
|
|
185
|
-
expect(
|
|
215
|
+
expect(car_model.count).to be 0
|
|
186
216
|
end
|
|
187
217
|
|
|
188
218
|
it 'can count objects with records' do
|
|
189
|
-
expect(
|
|
219
|
+
expect(country_model.count).to be 3
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
end
|
|
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)
|
|
190
239
|
end
|
|
191
240
|
|
|
192
241
|
end
|
|
193
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
|
data/spec/scope_spec.rb
CHANGED
|
@@ -203,6 +203,18 @@ describe 'querying' do
|
|
|
203
203
|
expect(countries.length).to be == 2
|
|
204
204
|
end
|
|
205
205
|
|
|
206
|
+
it 'can combine indices' do
|
|
207
|
+
countries = Country.where(name: 'France', continent: 'Europe')
|
|
208
|
+
expect(countries.length).to be == 1
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it 'can use indices with inclusion query' do
|
|
212
|
+
countries = Country.where(continent: ['Europe', 'North America'])
|
|
213
|
+
expect(countries.length).to be == 3
|
|
214
|
+
|
|
215
|
+
countries = Country.where(name: ['France', 'Canada'])
|
|
216
|
+
expect(countries.length).to be == 2
|
|
217
|
+
end
|
|
206
218
|
end
|
|
207
219
|
|
|
208
220
|
describe '.where.not' do
|
|
@@ -428,8 +440,8 @@ describe 'querying' do
|
|
|
428
440
|
it 'returns true when the same scope has be rechained' do
|
|
429
441
|
scope_a = Country.nato.republics.nato.republics
|
|
430
442
|
scope_b = Country.republics.nato
|
|
431
|
-
expect(scope_a.instance_variable_get(:@where_values)).to be == [[
|
|
432
|
-
expect(scope_b.instance_variable_get(:@where_values)).to be == [[
|
|
443
|
+
expect(scope_a.instance_variable_get(:@where_values)).to be == [['nato', true], ['king', nil], ['nato', true], ['king', nil]]
|
|
444
|
+
expect(scope_b.instance_variable_get(:@where_values)).to be == [['king', nil], ['nato', true]]
|
|
433
445
|
expect(scope_a).to be == scope_b
|
|
434
446
|
end
|
|
435
447
|
end
|
data/spec/support/animal.rb
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
data/spec/support/car.rb
CHANGED
data/spec/support/country.rb
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
class Country < FrozenRecord::Base
|
|
2
|
+
self.default_attributes = { contemporary: true, available: true }
|
|
3
|
+
|
|
4
|
+
add_index :name, unique: true
|
|
5
|
+
add_index :continent
|
|
2
6
|
|
|
3
7
|
def self.republics
|
|
4
8
|
where(king: nil)
|
|
@@ -12,3 +16,12 @@ class Country < FrozenRecord::Base
|
|
|
12
16
|
name.reverse
|
|
13
17
|
end
|
|
14
18
|
end
|
|
19
|
+
|
|
20
|
+
module Compact
|
|
21
|
+
class Country < ::Country
|
|
22
|
+
include FrozenRecord::Compact
|
|
23
|
+
def self.file_path
|
|
24
|
+
superclass.file_path
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
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.
|
|
4
|
+
version: 0.19.2
|
|
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:
|
|
11
|
+
date: 2020-08-06 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,18 +108,27 @@ 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/index.rb
|
|
115
123
|
- lib/frozen_record/railtie.rb
|
|
116
124
|
- lib/frozen_record/scope.rb
|
|
117
125
|
- lib/frozen_record/test_helper.rb
|
|
118
126
|
- lib/frozen_record/version.rb
|
|
127
|
+
- spec/deduplication_spec.rb
|
|
119
128
|
- spec/fixtures/animals.json
|
|
120
129
|
- spec/fixtures/cars.yml
|
|
121
|
-
- spec/fixtures/countries.yml
|
|
122
|
-
- spec/fixtures/test_helper/countries.yml
|
|
130
|
+
- spec/fixtures/countries.yml.erb
|
|
131
|
+
- spec/fixtures/test_helper/countries.yml.erb
|
|
123
132
|
- spec/frozen_record_spec.rb
|
|
124
133
|
- spec/scope_spec.rb
|
|
125
134
|
- spec/spec_helper.rb
|
|
@@ -132,7 +141,7 @@ homepage: https://github.com/byroot/frozen_record
|
|
|
132
141
|
licenses:
|
|
133
142
|
- MIT
|
|
134
143
|
metadata: {}
|
|
135
|
-
post_install_message:
|
|
144
|
+
post_install_message:
|
|
136
145
|
rdoc_options: []
|
|
137
146
|
require_paths:
|
|
138
147
|
- lib
|
|
@@ -140,22 +149,23 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
140
149
|
requirements:
|
|
141
150
|
- - ">="
|
|
142
151
|
- !ruby/object:Gem::Version
|
|
143
|
-
version: '
|
|
152
|
+
version: '2.5'
|
|
144
153
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
145
154
|
requirements:
|
|
146
155
|
- - ">="
|
|
147
156
|
- !ruby/object:Gem::Version
|
|
148
157
|
version: '0'
|
|
149
158
|
requirements: []
|
|
150
|
-
rubygems_version: 3.0.
|
|
151
|
-
signing_key:
|
|
159
|
+
rubygems_version: 3.0.2
|
|
160
|
+
signing_key:
|
|
152
161
|
specification_version: 4
|
|
153
162
|
summary: ActiveRecord like interface to read only access and query static YAML files
|
|
154
163
|
test_files:
|
|
164
|
+
- spec/deduplication_spec.rb
|
|
155
165
|
- spec/fixtures/animals.json
|
|
156
166
|
- spec/fixtures/cars.yml
|
|
157
|
-
- spec/fixtures/countries.yml
|
|
158
|
-
- spec/fixtures/test_helper/countries.yml
|
|
167
|
+
- spec/fixtures/countries.yml.erb
|
|
168
|
+
- spec/fixtures/test_helper/countries.yml.erb
|
|
159
169
|
- spec/frozen_record_spec.rb
|
|
160
170
|
- spec/scope_spec.rb
|
|
161
171
|
- spec/spec_helper.rb
|