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 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