frozen_record 0.15.0 → 0.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.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 +11 -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 +94 -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 +9 -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 +23 -12
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d75dfc07a86429e4dcc6a17c076aa87081a19e812b3d9b8d9722281d8a14aba9
|
|
4
|
+
data.tar.gz: a445c878d8cddaa87cb59db1213cf4099e3cbf10fa854d92fbd28669867dad4a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 54b3c787e7c0f028c8c58a2a1fb214a5b3ac0c4b8b06ac4a42d4a35526baa0c50573967a7aa7faad0921c028d19d985dda831257f2244b1ba5af56960187ced1
|
|
7
|
+
data.tar.gz: 8159f7e45d32256ab9565bbf5ff81c6685e912c57603f97b335f8c5dbb2529b4153bb0e2937e4c709da9a78d0f303f485e33daf4933aa129be9599313964ec8d
|
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,14 +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
|
|
28
|
+
|
|
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,13 @@ 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
|
-
class_attribute :primary_key
|
|
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, default: {}.freeze
|
|
31
25
|
|
|
32
26
|
self.primary_key = 'id'
|
|
33
27
|
|
|
34
|
-
class_attribute :backend
|
|
35
28
|
self.backend = FrozenRecord::Backends::Yaml
|
|
36
29
|
|
|
37
|
-
class_attribute :auto_reloading
|
|
38
|
-
|
|
39
30
|
attribute_method_suffix -'?'
|
|
40
31
|
|
|
41
32
|
class ThreadSafeStorage
|
|
@@ -57,8 +48,34 @@ module FrozenRecord
|
|
|
57
48
|
end
|
|
58
49
|
|
|
59
50
|
class << self
|
|
51
|
+
alias_method :set_default_attributes, :default_attributes=
|
|
52
|
+
private :set_default_attributes
|
|
53
|
+
def default_attributes=(default_attributes)
|
|
54
|
+
set_default_attributes(Deduplication.deep_deduplicate!(default_attributes.stringify_keys))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
alias_method :set_primary_key, :primary_key=
|
|
58
|
+
private :set_primary_key
|
|
59
|
+
def primary_key=(primary_key)
|
|
60
|
+
set_primary_key(-primary_key.to_s)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
alias_method :set_base_path, :base_path=
|
|
64
|
+
private :set_base_path
|
|
65
|
+
def base_path=(base_path)
|
|
66
|
+
@file_path = nil
|
|
67
|
+
set_base_path(base_path)
|
|
68
|
+
end
|
|
69
|
+
|
|
60
70
|
attr_accessor :abstract_class
|
|
61
71
|
|
|
72
|
+
def attributes
|
|
73
|
+
@attributes ||= begin
|
|
74
|
+
load_records
|
|
75
|
+
@attributes
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
62
79
|
def abstract_class?
|
|
63
80
|
defined?(@abstract_class) && @abstract_class
|
|
64
81
|
end
|
|
@@ -77,7 +94,34 @@ module FrozenRecord
|
|
|
77
94
|
|
|
78
95
|
def file_path
|
|
79
96
|
raise ArgumentError, "You must define `#{name}.base_path`" unless base_path
|
|
80
|
-
|
|
97
|
+
@file_path ||= begin
|
|
98
|
+
file_path = File.join(base_path, backend.filename(name))
|
|
99
|
+
if !File.exist?(file_path) && File.exist?("#{file_path}.erb")
|
|
100
|
+
"#{file_path}.erb"
|
|
101
|
+
else
|
|
102
|
+
file_path
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def add_index(attribute, unique: false)
|
|
108
|
+
index = unique ? UniqueIndex.new(self, attribute) : Index.new(self, attribute)
|
|
109
|
+
self.index_definitions = index_definitions.merge(index.attribute => index).freeze
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def memsize(object = self, seen = Set.new.compare_by_identity)
|
|
113
|
+
return 0 unless seen.add?(object)
|
|
114
|
+
|
|
115
|
+
size = ObjectSpace.memsize_of(object)
|
|
116
|
+
object.instance_variables.each { |v| size += memsize(object.instance_variable_get(v), seen) }
|
|
117
|
+
|
|
118
|
+
case object
|
|
119
|
+
when Hash
|
|
120
|
+
object.each { |k, v| size += memsize(k, seen) + memsize(v, seen) }
|
|
121
|
+
when Array
|
|
122
|
+
object.each { |i| size += memsize(i, seen) }
|
|
123
|
+
end
|
|
124
|
+
size
|
|
81
125
|
end
|
|
82
126
|
|
|
83
127
|
def respond_to_missing?(name, *)
|
|
@@ -101,16 +145,25 @@ module FrozenRecord
|
|
|
101
145
|
|
|
102
146
|
@records ||= begin
|
|
103
147
|
records = backend.load(file_path)
|
|
104
|
-
|
|
105
|
-
records.
|
|
148
|
+
records.each { |r| assign_defaults!(r) }
|
|
149
|
+
records = Deduplication.deep_deduplicate!(records)
|
|
150
|
+
@attributes = list_attributes(records).freeze
|
|
151
|
+
define_attribute_methods(@attributes.to_a)
|
|
152
|
+
records = records.map { |r| load(r) }.freeze
|
|
153
|
+
index_definitions.values.each { |index| index.build(records) }
|
|
154
|
+
records
|
|
106
155
|
end
|
|
107
156
|
end
|
|
108
157
|
|
|
109
158
|
def scope(name, body)
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
159
|
+
singleton_class.send(:define_method, name, &body)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
alias_method :load, :new
|
|
163
|
+
private :load
|
|
164
|
+
|
|
165
|
+
def new(attrs = {})
|
|
166
|
+
load(assign_defaults!(attrs.stringify_keys))
|
|
114
167
|
end
|
|
115
168
|
|
|
116
169
|
private
|
|
@@ -125,6 +178,18 @@ module FrozenRecord
|
|
|
125
178
|
@store ||= ThreadSafeStorage.new(name)
|
|
126
179
|
end
|
|
127
180
|
|
|
181
|
+
def assign_defaults!(record)
|
|
182
|
+
if default_attributes
|
|
183
|
+
default_attributes.each do |key, value|
|
|
184
|
+
unless record.key?(key)
|
|
185
|
+
record[key] = value
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
record
|
|
191
|
+
end
|
|
192
|
+
|
|
128
193
|
def method_missing(name, *args)
|
|
129
194
|
if name.to_s =~ FIND_BY_PATTERN
|
|
130
195
|
return dynamic_match($1, args, $2.present?)
|
|
@@ -140,17 +205,15 @@ module FrozenRecord
|
|
|
140
205
|
def list_attributes(records)
|
|
141
206
|
attributes = Set.new
|
|
142
207
|
records.each do |record|
|
|
143
|
-
record.keys
|
|
144
|
-
attributes.add(key.to_s)
|
|
145
|
-
end
|
|
208
|
+
attributes.merge(record.keys)
|
|
146
209
|
end
|
|
147
|
-
attributes
|
|
210
|
+
attributes
|
|
148
211
|
end
|
|
149
212
|
|
|
150
213
|
end
|
|
151
214
|
|
|
152
215
|
def initialize(attrs = {})
|
|
153
|
-
@attributes = attrs.
|
|
216
|
+
@attributes = attrs.freeze
|
|
154
217
|
end
|
|
155
218
|
|
|
156
219
|
def attributes
|
|
@@ -158,7 +221,7 @@ module FrozenRecord
|
|
|
158
221
|
end
|
|
159
222
|
|
|
160
223
|
def id
|
|
161
|
-
self[primary_key
|
|
224
|
+
self[self.class.primary_key]
|
|
162
225
|
end
|
|
163
226
|
|
|
164
227
|
def [](attr)
|
|
@@ -184,5 +247,8 @@ module FrozenRecord
|
|
|
184
247
|
FALSY_VALUES.exclude?(self[attribute_name]) && self[attribute_name].present?
|
|
185
248
|
end
|
|
186
249
|
|
|
250
|
+
def attribute_method?(attribute_name)
|
|
251
|
+
respond_to_without_attributes?(:attributes) && self.class.attributes.include?(attribute_name)
|
|
252
|
+
end
|
|
187
253
|
end
|
|
188
254
|
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.to_h { |r| [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.1
|
|
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-07-16 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,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
|
|
123
|
+
- lib/frozen_record/railtie.rb
|
|
115
124
|
- lib/frozen_record/scope.rb
|
|
116
125
|
- lib/frozen_record/test_helper.rb
|
|
117
126
|
- lib/frozen_record/version.rb
|
|
127
|
+
- spec/deduplication_spec.rb
|
|
118
128
|
- spec/fixtures/animals.json
|
|
119
129
|
- spec/fixtures/cars.yml
|
|
120
|
-
- spec/fixtures/countries.yml
|
|
121
|
-
- spec/fixtures/test_helper/countries.yml
|
|
130
|
+
- spec/fixtures/countries.yml.erb
|
|
131
|
+
- spec/fixtures/test_helper/countries.yml.erb
|
|
122
132
|
- spec/frozen_record_spec.rb
|
|
123
133
|
- spec/scope_spec.rb
|
|
124
134
|
- spec/spec_helper.rb
|
|
@@ -131,7 +141,7 @@ homepage: https://github.com/byroot/frozen_record
|
|
|
131
141
|
licenses:
|
|
132
142
|
- MIT
|
|
133
143
|
metadata: {}
|
|
134
|
-
post_install_message:
|
|
144
|
+
post_install_message:
|
|
135
145
|
rdoc_options: []
|
|
136
146
|
require_paths:
|
|
137
147
|
- lib
|
|
@@ -139,22 +149,23 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
139
149
|
requirements:
|
|
140
150
|
- - ">="
|
|
141
151
|
- !ruby/object:Gem::Version
|
|
142
|
-
version: '
|
|
152
|
+
version: '2.5'
|
|
143
153
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
144
154
|
requirements:
|
|
145
155
|
- - ">="
|
|
146
156
|
- !ruby/object:Gem::Version
|
|
147
157
|
version: '0'
|
|
148
158
|
requirements: []
|
|
149
|
-
rubygems_version: 3.
|
|
150
|
-
signing_key:
|
|
159
|
+
rubygems_version: 3.1.2
|
|
160
|
+
signing_key:
|
|
151
161
|
specification_version: 4
|
|
152
162
|
summary: ActiveRecord like interface to read only access and query static YAML files
|
|
153
163
|
test_files:
|
|
164
|
+
- spec/deduplication_spec.rb
|
|
154
165
|
- spec/fixtures/animals.json
|
|
155
166
|
- spec/fixtures/cars.yml
|
|
156
|
-
- spec/fixtures/countries.yml
|
|
157
|
-
- spec/fixtures/test_helper/countries.yml
|
|
167
|
+
- spec/fixtures/countries.yml.erb
|
|
168
|
+
- spec/fixtures/test_helper/countries.yml.erb
|
|
158
169
|
- spec/frozen_record_spec.rb
|
|
159
170
|
- spec/scope_spec.rb
|
|
160
171
|
- spec/spec_helper.rb
|