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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 146bcc09f8a4a9622397a90709cb33fa124baca6820547de51337a827124054d
4
- data.tar.gz: f9f9d76a732523aca217be6a76ab2c1a64ea3faf994dc3a2f94ad93100e9439e
3
+ metadata.gz: f2012cdafeeed41657b76dbe5b5489a47f5e38d863335d0e2db87609d2030974
4
+ data.tar.gz: 636ea1f081a595230c27a2a4abb40978b977d1e3b541d23e039aa790d5bdb62d
5
5
  SHA512:
6
- metadata.gz: 6f9a8ef22f72605fd9e3156e145522748fb7b75720c61e76bdffc157f8321cb2bb3ef260e9bf9b7eaac16761a2c207b6f81489c49b66c97a872619c11c665c7e
7
- data.tar.gz: c030a6f630f96d474f0d441a310dc1c79649fb3c03b5ac3fa558b04b22c7d8838f717383724efec2bbfdcaf3fe92f93fdc777ce5d96d4570fbb1039905675db9
6
+ metadata.gz: ae134c3c39dca424590880b06190656c2d03aa0516fa387a5742ecf2dc1943e33f4c6eee30cc7fa600e63bd3c14186c69dc348ed20783256e563657bf9058874
7
+ data.tar.gz: bad575675f9a764c2e84f67b9c550beee1506ef763907d32a87d5eaf967eedeb3521ec28d6d9e6dd4ebc790ce0400bf579fb1764af3d13e46f14e5a6343a1a44
@@ -1,5 +1,6 @@
1
1
  sudo: false
2
2
  rvm:
3
- - '2.4.5'
4
- - '2.5.3'
5
- - '2.6.1'
3
+ - '2.4'
4
+ - '2.5'
5
+ - '2.6'
6
+ - '2.7'
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::Yaml
53
+ self.backend = FrozenRecord::Backends::Json
54
54
  end
55
55
  ```
56
56
 
@@ -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,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FrozenRecord
4
+ module Backends
5
+ autoload :Json, 'frozen_record/backends/json'
6
+ autoload :Yaml, 'frozen_record/backends/yaml'
7
+ end
8
+ end
@@ -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
- yml_erb_data = File.read(file_path)
21
- yml_data = ERB.new(yml_erb_data).result
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
- YAML.load(yml_data) || []
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
@@ -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/yaml'
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
- File.join(base_path, backend.filename(name))
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
- define_attribute_methods(list_attributes(records))
105
- records.map(&method(:new)).freeze
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.each do |key|
144
- attributes.add(key.to_s)
145
- end
187
+ attributes.merge(record.keys)
146
188
  end
147
- attributes.to_a
189
+ attributes
148
190
  end
149
191
 
150
192
  end
151
193
 
152
194
  def initialize(attrs = {})
153
- @attributes = attrs.stringify_keys.freeze
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.to_s]
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FrozenRecord
2
4
  class Railtie < Rails::Railtie
3
5
  initializer "frozen_record.setup" do |app|
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FrozenRecord
2
4
  class Scope
3
5
  BLACKLISTED_ARRAY_METHODS = [
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FrozenRecord
2
4
  module TestHelper
3
5
  NoFixturesLoaded = Class.new(StandardError)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FrozenRecord
2
- VERSION = '0.16.0'
4
+ VERSION = '0.17.0'
3
5
  end
@@ -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
@@ -20,7 +20,6 @@
20
20
  nato: true
21
21
  continent: Europe
22
22
 
23
-
24
23
  - id: 3
25
24
  name: Austria
26
25
  capital: <%= 'Vienna' %>
@@ -29,3 +28,4 @@
29
28
  founded_on: 1156-01-01
30
29
  updated_at: 2014-02-12T19:02:03-02:00
31
30
  continent: Europe
31
+ available: false
@@ -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
@@ -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 = JsonBackend
2
+ self.backend = FrozenRecord::Backends::Json
16
3
  end
@@ -1,4 +1,5 @@
1
1
  class Country < FrozenRecord::Base
2
+ self.default_attributes = { contemporary: true, available: true }
2
3
 
3
4
  def self.republics
4
5
  where(king: nil)
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.16.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: 2019-09-03 00:00:00.000000000 Z
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