frozen_record 0.16.0 → 0.17.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|