frozen_record 0.14.0 → 0.19.0
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 +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
|