frozen_record 0.16.0 → 0.17.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 +4 -4
- data/.travis.yml +4 -3
- data/README.md +1 -1
- data/lib/frozen_record.rb +7 -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 +69 -24
- data/lib/frozen_record/deduplication.rb +57 -0
- data/lib/frozen_record/railtie.rb +2 -0
- data/lib/frozen_record/scope.rb +2 -0
- 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 +29 -0
- data/spec/support/animal.rb +1 -14
- data/spec/support/country.rb +1 -0
- metadata +11 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f2012cdafeeed41657b76dbe5b5489a47f5e38d863335d0e2db87609d2030974
|
4
|
+
data.tar.gz: 636ea1f081a595230c27a2a4abb40978b977d1e3b541d23e039aa790d5bdb62d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ae134c3c39dca424590880b06190656c2d03aa0516fa387a5742ecf2dc1943e33f4c6eee30cc7fa600e63bd3c14186c69dc348ed20783256e563657bf9058874
|
7
|
+
data.tar.gz: bad575675f9a764c2e84f67b9c550beee1506ef763907d32a87d5eaf967eedeb3521ec28d6d9e6dd4ebc790ce0400bf579fb1764af3d13e46f14e5a6343a1a44
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
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'
|
@@ -6,15 +8,20 @@ require 'active_model'
|
|
6
8
|
require 'frozen_record/version'
|
7
9
|
require 'frozen_record/scope'
|
8
10
|
require 'frozen_record/base'
|
11
|
+
require 'frozen_record/deduplication'
|
9
12
|
|
10
13
|
module FrozenRecord
|
11
14
|
RecordNotFound = Class.new(StandardError)
|
12
15
|
|
13
16
|
class << self
|
17
|
+
attr_accessor :deprecated_yaml_erb_backend
|
18
|
+
|
14
19
|
def eager_load!
|
15
20
|
Base.descendants.each(&:eager_load!)
|
16
21
|
end
|
17
22
|
end
|
23
|
+
|
24
|
+
self.deprecated_yaml_erb_backend = true
|
18
25
|
end
|
19
26
|
|
20
27
|
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,8 @@
|
|
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'
|
4
6
|
|
5
7
|
module FrozenRecord
|
6
8
|
class Base
|
@@ -17,25 +19,12 @@ module FrozenRecord
|
|
17
19
|
FIND_BY_PATTERN = /\Afind_by_(\w+)(!?)/
|
18
20
|
FALSY_VALUES = [false, nil, 0, -''].to_set
|
19
21
|
|
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
|
22
|
+
class_attribute :base_path, :primary_key, :backend, :auto_reloading, :default_attributes, instance_accessor: false
|
31
23
|
|
32
24
|
self.primary_key = 'id'
|
33
25
|
|
34
|
-
class_attribute :backend
|
35
26
|
self.backend = FrozenRecord::Backends::Yaml
|
36
27
|
|
37
|
-
class_attribute :auto_reloading
|
38
|
-
|
39
28
|
attribute_method_suffix -'?'
|
40
29
|
|
41
30
|
class ThreadSafeStorage
|
@@ -57,8 +46,34 @@ module FrozenRecord
|
|
57
46
|
end
|
58
47
|
|
59
48
|
class << self
|
49
|
+
alias_method :set_default_attributes, :default_attributes=
|
50
|
+
private :set_default_attributes
|
51
|
+
def default_attributes=(default_attributes)
|
52
|
+
set_default_attributes(Deduplication.deep_deduplicate!(default_attributes.stringify_keys))
|
53
|
+
end
|
54
|
+
|
55
|
+
alias_method :set_primary_key, :primary_key=
|
56
|
+
private :set_primary_key
|
57
|
+
def primary_key=(primary_key)
|
58
|
+
set_primary_key(-primary_key.to_s)
|
59
|
+
end
|
60
|
+
|
61
|
+
alias_method :set_base_path, :base_path=
|
62
|
+
private :set_base_path
|
63
|
+
def base_path=(base_path)
|
64
|
+
@file_path = nil
|
65
|
+
set_base_path(base_path)
|
66
|
+
end
|
67
|
+
|
60
68
|
attr_accessor :abstract_class
|
61
69
|
|
70
|
+
def attributes
|
71
|
+
@attributes ||= begin
|
72
|
+
load_records
|
73
|
+
@attributes
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
62
77
|
def abstract_class?
|
63
78
|
defined?(@abstract_class) && @abstract_class
|
64
79
|
end
|
@@ -77,7 +92,14 @@ module FrozenRecord
|
|
77
92
|
|
78
93
|
def file_path
|
79
94
|
raise ArgumentError, "You must define `#{name}.base_path`" unless base_path
|
80
|
-
|
95
|
+
@file_path ||= begin
|
96
|
+
file_path = File.join(base_path, backend.filename(name))
|
97
|
+
if !File.exist?(file_path) && File.exist?("#{file_path}.erb")
|
98
|
+
"#{file_path}.erb"
|
99
|
+
else
|
100
|
+
file_path
|
101
|
+
end
|
102
|
+
end
|
81
103
|
end
|
82
104
|
|
83
105
|
def respond_to_missing?(name, *)
|
@@ -101,8 +123,11 @@ module FrozenRecord
|
|
101
123
|
|
102
124
|
@records ||= begin
|
103
125
|
records = backend.load(file_path)
|
104
|
-
|
105
|
-
records.
|
126
|
+
records.each { |r| assign_defaults!(r) }
|
127
|
+
records = Deduplication.deep_deduplicate!(records)
|
128
|
+
@attributes = list_attributes(records).freeze
|
129
|
+
define_attribute_methods(@attributes.to_a)
|
130
|
+
records.map { |r| load(r) }.freeze
|
106
131
|
end
|
107
132
|
end
|
108
133
|
|
@@ -113,6 +138,13 @@ module FrozenRecord
|
|
113
138
|
singleton_class.send(:define_method, name) { |*args| body.call(*args) }
|
114
139
|
end
|
115
140
|
|
141
|
+
alias_method :load, :new
|
142
|
+
private :load
|
143
|
+
|
144
|
+
def new(attrs = {})
|
145
|
+
load(assign_defaults!(attrs.stringify_keys))
|
146
|
+
end
|
147
|
+
|
116
148
|
private
|
117
149
|
|
118
150
|
def file_changed?
|
@@ -125,6 +157,18 @@ module FrozenRecord
|
|
125
157
|
@store ||= ThreadSafeStorage.new(name)
|
126
158
|
end
|
127
159
|
|
160
|
+
def assign_defaults!(record)
|
161
|
+
if default_attributes
|
162
|
+
default_attributes.each do |key, value|
|
163
|
+
unless record.key?(key)
|
164
|
+
record[key] = value
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
record
|
170
|
+
end
|
171
|
+
|
128
172
|
def method_missing(name, *args)
|
129
173
|
if name.to_s =~ FIND_BY_PATTERN
|
130
174
|
return dynamic_match($1, args, $2.present?)
|
@@ -140,17 +184,15 @@ module FrozenRecord
|
|
140
184
|
def list_attributes(records)
|
141
185
|
attributes = Set.new
|
142
186
|
records.each do |record|
|
143
|
-
record.keys
|
144
|
-
attributes.add(key.to_s)
|
145
|
-
end
|
187
|
+
attributes.merge(record.keys)
|
146
188
|
end
|
147
|
-
attributes
|
189
|
+
attributes
|
148
190
|
end
|
149
191
|
|
150
192
|
end
|
151
193
|
|
152
194
|
def initialize(attrs = {})
|
153
|
-
@attributes = attrs.
|
195
|
+
@attributes = attrs.freeze
|
154
196
|
end
|
155
197
|
|
156
198
|
def attributes
|
@@ -158,7 +200,7 @@ module FrozenRecord
|
|
158
200
|
end
|
159
201
|
|
160
202
|
def id
|
161
|
-
self[primary_key
|
203
|
+
self[self.class.primary_key]
|
162
204
|
end
|
163
205
|
|
164
206
|
def [](attr)
|
@@ -184,5 +226,8 @@ module FrozenRecord
|
|
184
226
|
FALSY_VALUES.exclude?(self[attribute_name]) && self[attribute_name].present?
|
185
227
|
end
|
186
228
|
|
229
|
+
def attribute_method?(attribute_name)
|
230
|
+
respond_to_without_attributes?(:attributes) && self.class.attributes.include?(attribute_name)
|
231
|
+
end
|
187
232
|
end
|
188
233
|
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
|
data/lib/frozen_record/scope.rb
CHANGED
@@ -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
@@ -71,6 +71,25 @@ describe FrozenRecord::Base do
|
|
71
71
|
|
72
72
|
end
|
73
73
|
|
74
|
+
describe '.default_attributes' do
|
75
|
+
|
76
|
+
it 'define the attribute' do
|
77
|
+
expect(Country.new).to respond_to :contemporary
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'sets the value as default' do
|
81
|
+
expect(Country.find_by(name: 'Austria').contemporary).to be == true
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'gives precedence to the data file' do
|
85
|
+
expect(Country.find_by(name: 'Austria').available).to be == false
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'is also set in the initializer' do
|
89
|
+
expect(Country.new.contemporary).to be == true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
74
93
|
describe '.scope' do
|
75
94
|
it 'defines a scope method' do
|
76
95
|
|
@@ -134,6 +153,8 @@ describe FrozenRecord::Base do
|
|
134
153
|
'king' => 'Elisabeth II',
|
135
154
|
'nato' => true,
|
136
155
|
'continent' => 'North America',
|
156
|
+
'available' => true,
|
157
|
+
'contemporary' => true,
|
137
158
|
}
|
138
159
|
end
|
139
160
|
|
@@ -179,6 +200,14 @@ describe FrozenRecord::Base do
|
|
179
200
|
|
180
201
|
end
|
181
202
|
|
203
|
+
describe '#present?' do
|
204
|
+
|
205
|
+
it 'returns true' do
|
206
|
+
expect(Country.first).to be_present
|
207
|
+
end
|
208
|
+
|
209
|
+
end
|
210
|
+
|
182
211
|
describe '#count' do
|
183
212
|
|
184
213
|
it 'can count objects with no records' do
|
data/spec/support/animal.rb
CHANGED
@@ -1,16 +1,3 @@
|
|
1
|
-
module JsonBackend
|
2
|
-
extend self
|
3
|
-
|
4
|
-
def filename(model_name)
|
5
|
-
"#{model_name.underscore.pluralize}.json"
|
6
|
-
end
|
7
|
-
|
8
|
-
def load(file_path)
|
9
|
-
json_data = File.read(file_path)
|
10
|
-
JSON.parse(json_data) || []
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
1
|
class Animal < FrozenRecord::Base
|
15
|
-
self.backend =
|
2
|
+
self.backend = FrozenRecord::Backends::Json
|
16
3
|
end
|
data/spec/support/country.rb
CHANGED
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.17.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jean Boussier
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-01-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -110,16 +110,20 @@ files:
|
|
110
110
|
- Rakefile
|
111
111
|
- frozen_record.gemspec
|
112
112
|
- lib/frozen_record.rb
|
113
|
+
- lib/frozen_record/backends.rb
|
114
|
+
- lib/frozen_record/backends/json.rb
|
113
115
|
- lib/frozen_record/backends/yaml.rb
|
114
116
|
- lib/frozen_record/base.rb
|
117
|
+
- lib/frozen_record/deduplication.rb
|
115
118
|
- lib/frozen_record/railtie.rb
|
116
119
|
- lib/frozen_record/scope.rb
|
117
120
|
- lib/frozen_record/test_helper.rb
|
118
121
|
- lib/frozen_record/version.rb
|
122
|
+
- spec/deduplication_spec.rb
|
119
123
|
- spec/fixtures/animals.json
|
120
124
|
- spec/fixtures/cars.yml
|
121
|
-
- spec/fixtures/countries.yml
|
122
|
-
- spec/fixtures/test_helper/countries.yml
|
125
|
+
- spec/fixtures/countries.yml.erb
|
126
|
+
- spec/fixtures/test_helper/countries.yml.erb
|
123
127
|
- spec/frozen_record_spec.rb
|
124
128
|
- spec/scope_spec.rb
|
125
129
|
- spec/spec_helper.rb
|
@@ -152,10 +156,11 @@ signing_key:
|
|
152
156
|
specification_version: 4
|
153
157
|
summary: ActiveRecord like interface to read only access and query static YAML files
|
154
158
|
test_files:
|
159
|
+
- spec/deduplication_spec.rb
|
155
160
|
- spec/fixtures/animals.json
|
156
161
|
- spec/fixtures/cars.yml
|
157
|
-
- spec/fixtures/countries.yml
|
158
|
-
- spec/fixtures/test_helper/countries.yml
|
162
|
+
- spec/fixtures/countries.yml.erb
|
163
|
+
- spec/fixtures/test_helper/countries.yml.erb
|
159
164
|
- spec/frozen_record_spec.rb
|
160
165
|
- spec/scope_spec.rb
|
161
166
|
- spec/spec_helper.rb
|