frozen_record 0.14.0 → 0.19.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.travis.yml +3 -3
- data/Gemfile +2 -0
- data/README.md +23 -5
- 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 +97 -24
- data/lib/frozen_record/compact.rb +77 -0
- data/lib/frozen_record/deduplication.rb +57 -0
- data/lib/frozen_record/index.rb +58 -0
- data/lib/frozen_record/railtie.rb +9 -0
- data/lib/frozen_record/scope.rb +39 -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} +5 -0
- data/spec/fixtures/test_helper/{countries.yml → countries.yml.erb} +0 -0
- data/spec/frozen_record_spec.rb +105 -38
- data/spec/scope_spec.rb +21 -0
- data/spec/support/animal.rb +9 -13
- data/spec/support/car.rb +10 -0
- data/spec/support/country.rb +17 -0
- metadata +23 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e1006ddbdc9f21010a819e6085800b05fc549c9b513c75431b329f20d46e055a
|
4
|
+
data.tar.gz: b3f65c04430375bf7fe2be7ecd233515e9a8b4a1dc7ebc29a430fa4934fad8be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: db63b827feb659e1228812947551db15ef777b33c71b38d3d9c77aa304d148b2e23eaaa6075e3dda90bd759abd9774635030bd01947b6d7b61fa01a53114601c
|
7
|
+
data.tar.gz: a11f92b0a5ff35033c348b69735821969f33a93fdca861f58b3dd4c5a58263da643ce6efe6c9f7f473e754615e91c94712dce7901f7d49b2893bd3a808b9bc7b
|
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
|
@@ -95,10 +95,12 @@ Country.
|
|
95
95
|
|
96
96
|
### Scopes
|
97
97
|
|
98
|
-
|
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
|
@@ -138,6 +140,22 @@ Country.republics.part_of_nato.order(id: :desc)
|
|
138
140
|
- average
|
139
141
|
|
140
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
|
+
|
141
159
|
## Configuration
|
142
160
|
|
143
161
|
### Reloading
|
@@ -181,7 +199,7 @@ class CountryTest < ActiveSupport::TestCase
|
|
181
199
|
teardown do
|
182
200
|
FrozenRecord::TestHelper.unload_fixtures
|
183
201
|
end
|
184
|
-
|
202
|
+
|
185
203
|
test "countries have a valid name" do
|
186
204
|
# ...
|
187
205
|
```
|
@@ -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,11 +145,27 @@ 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
|
|
158
|
+
def scope(name, body)
|
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))
|
167
|
+
end
|
168
|
+
|
109
169
|
private
|
110
170
|
|
111
171
|
def file_changed?
|
@@ -118,6 +178,18 @@ module FrozenRecord
|
|
118
178
|
@store ||= ThreadSafeStorage.new(name)
|
119
179
|
end
|
120
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
|
+
|
121
193
|
def method_missing(name, *args)
|
122
194
|
if name.to_s =~ FIND_BY_PATTERN
|
123
195
|
return dynamic_match($1, args, $2.present?)
|
@@ -133,17 +205,15 @@ module FrozenRecord
|
|
133
205
|
def list_attributes(records)
|
134
206
|
attributes = Set.new
|
135
207
|
records.each do |record|
|
136
|
-
record.keys
|
137
|
-
attributes.add(key.to_s)
|
138
|
-
end
|
208
|
+
attributes.merge(record.keys)
|
139
209
|
end
|
140
|
-
attributes
|
210
|
+
attributes
|
141
211
|
end
|
142
212
|
|
143
213
|
end
|
144
214
|
|
145
215
|
def initialize(attrs = {})
|
146
|
-
@attributes = attrs.
|
216
|
+
@attributes = attrs.freeze
|
147
217
|
end
|
148
218
|
|
149
219
|
def attributes
|
@@ -151,7 +221,7 @@ module FrozenRecord
|
|
151
221
|
end
|
152
222
|
|
153
223
|
def id
|
154
|
-
self[primary_key
|
224
|
+
self[self.class.primary_key]
|
155
225
|
end
|
156
226
|
|
157
227
|
def [](attr)
|
@@ -177,5 +247,8 @@ module FrozenRecord
|
|
177
247
|
FALSY_VALUES.exclude?(self[attribute_name]) && self[attribute_name].present?
|
178
248
|
end
|
179
249
|
|
250
|
+
def attribute_method?(attribute_name)
|
251
|
+
respond_to_without_attributes?(:attributes) && self.class.attributes.include?(attribute_name)
|
252
|
+
end
|
180
253
|
end
|
181
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,58 @@
|
|
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
|
+
@index.fetch(value, EMPTY_ARRAY)
|
24
|
+
end
|
25
|
+
|
26
|
+
def reset
|
27
|
+
@index = nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def build(records)
|
31
|
+
@index = records.each_with_object({}) do |record, index|
|
32
|
+
entry = (index[record[attribute]] ||= [])
|
33
|
+
entry << record
|
34
|
+
end
|
35
|
+
@index.values.each(&:freeze)
|
36
|
+
@index.freeze
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class UniqueIndex < Index
|
41
|
+
def unique?
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def query(value)
|
46
|
+
record = @index[value]
|
47
|
+
record ? [record] : EMPTY_ARRAY
|
48
|
+
end
|
49
|
+
|
50
|
+
def build(records)
|
51
|
+
@index = records.to_h { |r| [r[attribute], r] }
|
52
|
+
if @index.size != records.size
|
53
|
+
raise AttributeNonUnique, "#{model}##{attribute.inspect} is not unique."
|
54
|
+
end
|
55
|
+
@index.freeze
|
56
|
+
end
|
57
|
+
end
|
58
|
+
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 = [
|
@@ -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
|
@@ -152,9 +175,20 @@ module FrozenRecord
|
|
152
175
|
def select_records(records)
|
153
176
|
return records if @where_values.empty? && @where_not_values.empty?
|
154
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
|
+
|
155
189
|
records.select do |record|
|
156
|
-
|
157
|
-
|
190
|
+
unindexed_where_values.all? { |attr, value| compare_value(record[attr], value) } &&
|
191
|
+
!@where_not_values.any? { |attr, value| compare_value(record[attr], value) }
|
158
192
|
end
|
159
193
|
end
|
160
194
|
|
@@ -203,12 +237,12 @@ module FrozenRecord
|
|
203
237
|
end
|
204
238
|
|
205
239
|
def where!(criterias)
|
206
|
-
@where_values += criterias.
|
240
|
+
@where_values += criterias.map { |k, v| [k.to_s, v] }
|
207
241
|
self
|
208
242
|
end
|
209
243
|
|
210
244
|
def where_not!(criterias)
|
211
|
-
@where_not_values += criterias.
|
245
|
+
@where_not_values += criterias.map { |k, v| [k.to_s, v] }
|
212
246
|
self
|
213
247
|
end
|
214
248
|
|
@@ -230,6 +264,7 @@ module FrozenRecord
|
|
230
264
|
end
|
231
265
|
|
232
266
|
private
|
267
|
+
|
233
268
|
def compare_value(actual, requested)
|
234
269
|
return actual == requested unless requested.is_a?(Array) || requested.is_a?(Range)
|
235
270
|
requested.include?(actual)
|
@@ -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
|
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,25 +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
|
+
|
83
|
+
describe '.scope' do
|
84
|
+
|
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
|
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
|
+
|
74
103
|
describe '#load_records' do
|
75
104
|
|
76
105
|
it 'processes erb by default' do
|
77
|
-
country =
|
106
|
+
country = country_model.first
|
78
107
|
expect(country.capital).to be == 'Ottawa'
|
79
108
|
end
|
80
109
|
|
81
110
|
it 'loads records with a custom backend' do
|
82
|
-
animal =
|
111
|
+
animal = animal_model.first
|
83
112
|
expect(animal.name).to be == 'cat'
|
84
113
|
end
|
85
114
|
|
@@ -88,22 +117,22 @@ describe FrozenRecord::Base do
|
|
88
117
|
describe '#==' do
|
89
118
|
|
90
119
|
it 'returns true if both instances are from the same class and have the same id' do
|
91
|
-
country =
|
120
|
+
country = country_model.first
|
92
121
|
second_country = country.dup
|
93
122
|
|
94
123
|
expect(country).to be == second_country
|
95
124
|
end
|
96
125
|
|
97
126
|
it 'returns false if both instances are not from the same class' do
|
98
|
-
country =
|
99
|
-
car =
|
127
|
+
country = country_model.first
|
128
|
+
car = car_model.new(id: country.id)
|
100
129
|
|
101
130
|
expect(country).to_not be == car
|
102
131
|
end
|
103
132
|
|
104
133
|
it 'returns false if both instances do not have the same id' do
|
105
|
-
country =
|
106
|
-
second_country =
|
134
|
+
country = country_model.first
|
135
|
+
second_country = country_model.last
|
107
136
|
|
108
137
|
expect(country).to_not be == second_country
|
109
138
|
end
|
@@ -113,7 +142,7 @@ describe FrozenRecord::Base do
|
|
113
142
|
describe '#attributes' do
|
114
143
|
|
115
144
|
it 'returns a Hash of the record attributes' do
|
116
|
-
attributes =
|
145
|
+
attributes = country_model.first.attributes
|
117
146
|
expect(attributes).to be == {
|
118
147
|
'id' => 1,
|
119
148
|
'name' => 'Canada',
|
@@ -124,6 +153,9 @@ describe FrozenRecord::Base do
|
|
124
153
|
'updated_at' => Time.parse('2014-02-24T19:08:06-05:00'),
|
125
154
|
'king' => 'Elisabeth II',
|
126
155
|
'nato' => true,
|
156
|
+
'continent' => 'North America',
|
157
|
+
'available' => true,
|
158
|
+
'contemporary' => true,
|
127
159
|
}
|
128
160
|
end
|
129
161
|
|
@@ -131,9 +163,9 @@ describe FrozenRecord::Base do
|
|
131
163
|
|
132
164
|
describe '`attribute`?' do
|
133
165
|
|
134
|
-
let(:blank) {
|
166
|
+
let(:blank) { country_model.new(id: 0, name: '', nato: false, king: nil) }
|
135
167
|
|
136
|
-
let(:present) {
|
168
|
+
let(:present) { country_model.new(id: 42, name: 'Groland', nato: true, king: Object.new) }
|
137
169
|
|
138
170
|
it 'considers `0` as missing' do
|
139
171
|
expect(blank.id?).to be false
|
@@ -169,15 +201,50 @@ describe FrozenRecord::Base do
|
|
169
201
|
|
170
202
|
end
|
171
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
|
+
|
172
212
|
describe '#count' do
|
173
213
|
|
174
214
|
it 'can count objects with no records' do
|
175
|
-
expect(
|
215
|
+
expect(car_model.count).to be 0
|
176
216
|
end
|
177
217
|
|
178
218
|
it 'can count objects with records' do
|
179
|
-
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)
|
180
239
|
end
|
181
240
|
|
182
241
|
end
|
183
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,10 @@ 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
|
206
210
|
end
|
207
211
|
|
208
212
|
describe '.where.not' do
|
@@ -417,6 +421,23 @@ describe 'querying' do
|
|
417
421
|
|
418
422
|
end
|
419
423
|
|
424
|
+
describe '#==' do
|
425
|
+
it 'returns true when two scopes share the same hashed attributes' do
|
426
|
+
scope_a = Country.republics.nato
|
427
|
+
scope_b = Country.republics.nato
|
428
|
+
expect(scope_a.object_id).not_to be == scope_b.object_id
|
429
|
+
expect(scope_a).to be == scope_b
|
430
|
+
end
|
431
|
+
|
432
|
+
it 'returns true when the same scope has be rechained' do
|
433
|
+
scope_a = Country.nato.republics.nato.republics
|
434
|
+
scope_b = Country.republics.nato
|
435
|
+
expect(scope_a.instance_variable_get(:@where_values)).to be == [['nato', true], ['king', nil], ['nato', true], ['king', nil]]
|
436
|
+
expect(scope_b.instance_variable_get(:@where_values)).to be == [['king', nil], ['nato', true]]
|
437
|
+
expect(scope_a).to be == scope_b
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
420
441
|
describe 'class methods delegation' do
|
421
442
|
|
422
443
|
it 'can be called from a scope' do
|
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,10 +1,27 @@
|
|
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)
|
5
9
|
end
|
6
10
|
|
11
|
+
def self.nato
|
12
|
+
where(nato: true)
|
13
|
+
end
|
14
|
+
|
7
15
|
def reverse_name
|
8
16
|
name.reverse
|
9
17
|
end
|
10
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.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:
|
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,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,23 +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
|
-
|
150
|
-
|
151
|
-
signing_key:
|
159
|
+
rubygems_version: 3.1.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
|