frozen_record 0.19.5 → 0.22.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/.github/workflows/main.yml +32 -0
- data/README.md +0 -2
- data/benchmark/querying +34 -0
- data/frozen_record.gemspec +2 -3
- data/lib/frozen_record.rb +2 -26
- data/lib/frozen_record/backends/empty.json +1 -0
- data/lib/frozen_record/backends/json.rb +20 -3
- data/lib/frozen_record/backends/yaml.rb +31 -3
- data/lib/frozen_record/base.rb +12 -15
- data/lib/frozen_record/compact.rb +5 -4
- data/lib/frozen_record/index.rb +4 -5
- data/lib/frozen_record/minimal.rb +27 -0
- data/lib/frozen_record/scope.rb +64 -9
- data/lib/frozen_record/serialization.rb +13 -0
- data/lib/frozen_record/version.rb +1 -1
- data/spec/frozen_record_spec.rb +26 -0
- data/spec/scope_spec.rb +22 -3
- data/spec/spec_helper.rb +10 -10
- data/spec/support/country.rb +4 -0
- metadata +10 -37
- data/.travis.yml +0 -5
- data/lib/frozen_record/deduplication.rb +0 -57
- data/spec/deduplication_spec.rb +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bfb835436fc605e43a6b928ae9e8149ec80f726dd0650801c3e1e2e4d46dba1d
|
4
|
+
data.tar.gz: 1b2d08e68b018cb8a0c18e68420dab5930c51c3cf42c559c841eb340b0b0226d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d7ae3739cc1a172d8a54e1f87513b307db69c937522032fbcf845a4f11141f7e373739f79c98e15216f6ce17ac4a0410cc6f72033b9a9cbb31ec260e650ea6c
|
7
|
+
data.tar.gz: 9f88ee2a8186533cbd8482704d2e02bc275e73af310b24801b7cebb37320ab6297038b3efa6368ea9ce0122de7aaaa56f1180fed4c9eefe1dc5de059e35ccbd0
|
@@ -0,0 +1,32 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
build:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
strategy:
|
9
|
+
matrix:
|
10
|
+
ruby: [ '2.5', '2.6', '2.7', '3.0' ]
|
11
|
+
minimal: [ false, true ]
|
12
|
+
name: Ruby ${{ matrix.ruby }} tests, minimal=${{ matrix.minimal }}
|
13
|
+
steps:
|
14
|
+
- uses: actions/checkout@v2
|
15
|
+
- name: Setup Ruby
|
16
|
+
uses: ruby/setup-ruby@v1
|
17
|
+
with:
|
18
|
+
ruby-version: ${{ matrix.ruby }}
|
19
|
+
- uses: actions/cache@v2
|
20
|
+
with:
|
21
|
+
path: vendor/bundle
|
22
|
+
key: ${{ runner.os }}-${{ matrix.ruby }}-gems-${{ hashFiles('Gemfile', 'frozen_record.gemspec') }}
|
23
|
+
restore-keys: |
|
24
|
+
${{ runner.os }}-${{ matrix.ruby }}-gems-
|
25
|
+
- name: Bundle install
|
26
|
+
run: |
|
27
|
+
gem install bundler
|
28
|
+
bundle install --jobs 4 --retry 3 --path=vendor/bundle
|
29
|
+
- name: Run tests
|
30
|
+
env:
|
31
|
+
MINIMAL: ${{ matrix.minimal }}
|
32
|
+
run: bundle exec rake
|
data/README.md
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
# FrozenRecord
|
2
2
|
|
3
3
|
[](http://travis-ci.org/byroot/frozen_record)
|
4
|
-
[](https://codeclimate.com/github/byroot/frozen_record)
|
5
|
-
[](https://coveralls.io/r/byroot/frozen_record)
|
6
4
|
[](http://badge.fury.io/rb/frozen_record)
|
7
5
|
|
8
6
|
ActiveRecord-like interface for **read only** access to static data files.
|
data/benchmark/querying
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative 'setup'
|
5
|
+
|
6
|
+
records = FrozenRecord::Backends::Yaml.load(Country.file_path)
|
7
|
+
large_records = 100.times.flat_map do |i|
|
8
|
+
records.map do |record|
|
9
|
+
new_record = record.dup
|
10
|
+
new_record['id'] += 10 * i
|
11
|
+
new_record['name'] += i.to_s
|
12
|
+
new_record
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
File.write('/tmp/frozen-record-bench-countries.yml', YAML.dump(large_records))
|
17
|
+
|
18
|
+
class LargeCountry < Country
|
19
|
+
class << self
|
20
|
+
def file_path
|
21
|
+
'/tmp/frozen-record-bench-countries.yml'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
puts "=== simple scalar match ==="
|
27
|
+
Benchmark.ips do |x|
|
28
|
+
x.report('simple') { LargeCountry.nato.size }
|
29
|
+
end
|
30
|
+
|
31
|
+
puts "=== range match ==="
|
32
|
+
Benchmark.ips do |x|
|
33
|
+
x.report('range') { LargeCountry.where(density: 100...200).size }
|
34
|
+
end
|
data/frozen_record.gemspec
CHANGED
@@ -20,9 +20,8 @@ Gem::Specification.new do |spec|
|
|
20
20
|
spec.required_ruby_version = '>= 2.5'
|
21
21
|
|
22
22
|
spec.add_runtime_dependency 'activemodel'
|
23
|
-
spec.
|
23
|
+
spec.add_runtime_dependency 'dedup'
|
24
|
+
|
24
25
|
spec.add_development_dependency 'rake'
|
25
26
|
spec.add_development_dependency 'rspec'
|
26
|
-
spec.add_development_dependency 'pry'
|
27
|
-
spec.add_development_dependency 'coveralls'
|
28
27
|
end
|
data/lib/frozen_record.rb
CHANGED
@@ -1,29 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
require '
|
5
|
-
require 'active_support/all'
|
6
|
-
require 'active_model'
|
7
|
-
|
8
|
-
require 'frozen_record/version'
|
9
|
-
require 'frozen_record/scope'
|
10
|
-
require 'frozen_record/index'
|
11
|
-
require 'frozen_record/base'
|
12
|
-
require 'frozen_record/compact'
|
13
|
-
require 'frozen_record/deduplication'
|
14
|
-
|
15
|
-
module FrozenRecord
|
16
|
-
RecordNotFound = Class.new(StandardError)
|
17
|
-
|
18
|
-
class << self
|
19
|
-
attr_accessor :deprecated_yaml_erb_backend
|
20
|
-
|
21
|
-
def eager_load!
|
22
|
-
Base.descendants.each(&:eager_load!)
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
self.deprecated_yaml_erb_backend = true
|
27
|
-
end
|
28
|
-
|
3
|
+
require 'frozen_record/minimal'
|
4
|
+
require 'frozen_record/serialization'
|
29
5
|
require 'frozen_record/railtie' if defined?(Rails)
|
@@ -0,0 +1 @@
|
|
1
|
+
true
|
@@ -9,9 +9,26 @@ module FrozenRecord
|
|
9
9
|
"#{model_name.underscore.pluralize}.json"
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
12
|
+
if JSON.respond_to?(:load_file)
|
13
|
+
supports_freeze = begin
|
14
|
+
JSON.load_file(File.expand_path('../empty.json', __FILE__), freeze: true)
|
15
|
+
rescue ArgumentError
|
16
|
+
false
|
17
|
+
end
|
18
|
+
|
19
|
+
if supports_freeze
|
20
|
+
def load(file_path)
|
21
|
+
JSON.load_file(file_path, freeze: true) || Dedup::EMPTY_ARRAY
|
22
|
+
end
|
23
|
+
else
|
24
|
+
def load(file_path)
|
25
|
+
Dedup.deep_intern!(JSON.load_file(file_path) || Dedup::EMPTY_ARRAY)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
else
|
29
|
+
def load(file_path)
|
30
|
+
Dedup.deep_intern!(JSON.parse(File.read(file_path)) || Dedup::EMPTY_ARRAY)
|
31
|
+
end
|
15
32
|
end
|
16
33
|
end
|
17
34
|
end
|
@@ -3,6 +3,8 @@
|
|
3
3
|
module FrozenRecord
|
4
4
|
module Backends
|
5
5
|
module Yaml
|
6
|
+
autoload :ERB, 'erb'
|
7
|
+
|
6
8
|
extend self
|
7
9
|
|
8
10
|
# Transforms the model name into a valid filename.
|
@@ -34,15 +36,41 @@ module FrozenRecord
|
|
34
36
|
end
|
35
37
|
end
|
36
38
|
|
37
|
-
|
39
|
+
load_string(yml_data)
|
38
40
|
else
|
39
41
|
if file_path.end_with?('.erb')
|
40
|
-
|
42
|
+
load_string(ERB.new(File.read(file_path)).result)
|
41
43
|
else
|
42
|
-
|
44
|
+
load_file(file_path)
|
43
45
|
end
|
44
46
|
end
|
45
47
|
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
supports_freeze = begin
|
52
|
+
YAML.load_file(File.expand_path('../empty.json', __FILE__), freeze: true)
|
53
|
+
rescue ArgumentError
|
54
|
+
false
|
55
|
+
end
|
56
|
+
|
57
|
+
if supports_freeze
|
58
|
+
def load_file(path)
|
59
|
+
YAML.load_file(path, freeze: true) || Dedup::EMPTY_ARRAY
|
60
|
+
end
|
61
|
+
|
62
|
+
def load_string(yaml)
|
63
|
+
YAML.load(yaml, freeze: true) || Dedup::EMPTY_ARRAY
|
64
|
+
end
|
65
|
+
else
|
66
|
+
def load_file(path)
|
67
|
+
Dedup.deep_intern!(YAML.load_file(path) || Dedup::EMPTY_ARRAY)
|
68
|
+
end
|
69
|
+
|
70
|
+
def load_string(yaml)
|
71
|
+
Dedup.deep_intern!(YAML.load(yaml) || Dedup::EMPTY_ARRAY)
|
72
|
+
end
|
73
|
+
end
|
46
74
|
end
|
47
75
|
end
|
48
76
|
end
|
data/lib/frozen_record/base.rb
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'set'
|
4
3
|
require 'active_support/descendants_tracker'
|
5
4
|
require 'frozen_record/backends'
|
6
|
-
require 'objspace'
|
7
5
|
|
8
6
|
module FrozenRecord
|
9
7
|
class Base
|
@@ -11,11 +9,6 @@ module FrozenRecord
|
|
11
9
|
extend ActiveModel::Naming
|
12
10
|
include ActiveModel::Conversion
|
13
11
|
include ActiveModel::AttributeMethods
|
14
|
-
include ActiveModel::Serializers::JSON
|
15
|
-
|
16
|
-
if defined? ActiveModel::Serializers::Xml
|
17
|
-
include ActiveModel::Serializers::Xml
|
18
|
-
end
|
19
12
|
|
20
13
|
FIND_BY_PATTERN = /\Afind_by_(\w+)(!?)/
|
21
14
|
FALSY_VALUES = [false, nil, 0, -''].to_set
|
@@ -52,7 +45,7 @@ module FrozenRecord
|
|
52
45
|
alias_method :set_default_attributes, :default_attributes=
|
53
46
|
private :set_default_attributes
|
54
47
|
def default_attributes=(default_attributes)
|
55
|
-
set_default_attributes(
|
48
|
+
set_default_attributes(Dedup.deep_intern!(default_attributes.transform_keys(&:to_s)))
|
56
49
|
end
|
57
50
|
|
58
51
|
alias_method :set_primary_key, :primary_key=
|
@@ -90,8 +83,9 @@ module FrozenRecord
|
|
90
83
|
store[:scope] = scope
|
91
84
|
end
|
92
85
|
|
93
|
-
delegate :find, :find_by_id, :find_by, :find_by!, :where, :first, :first!, :last, :last!,
|
94
|
-
:minimum, :maximum, :average, :sum, :count,
|
86
|
+
delegate :each, :find, :find_each, :find_by_id, :find_by, :find_by!, :where, :first, :first!, :last, :last!,
|
87
|
+
:pluck, :ids, :order, :limit, :offset, :minimum, :maximum, :average, :sum, :count,
|
88
|
+
to: :current_scope
|
95
89
|
|
96
90
|
def file_path
|
97
91
|
raise ArgumentError, "You must define `#{name}.base_path`" unless base_path
|
@@ -146,8 +140,9 @@ module FrozenRecord
|
|
146
140
|
|
147
141
|
@records ||= begin
|
148
142
|
records = backend.load(file_path)
|
149
|
-
|
150
|
-
|
143
|
+
if default_attributes
|
144
|
+
records = records.map { |r| assign_defaults!(r.dup).freeze }.freeze
|
145
|
+
end
|
151
146
|
@attributes = list_attributes(records).freeze
|
152
147
|
define_attribute_methods(@attributes.to_a)
|
153
148
|
records = records.map { |r| load(r) }.freeze
|
@@ -164,7 +159,7 @@ module FrozenRecord
|
|
164
159
|
private :load
|
165
160
|
|
166
161
|
def new(attrs = {})
|
167
|
-
load(assign_defaults!(attrs.
|
162
|
+
load(assign_defaults!(attrs.transform_keys(&:to_s)))
|
168
163
|
end
|
169
164
|
|
170
165
|
private
|
@@ -206,7 +201,9 @@ module FrozenRecord
|
|
206
201
|
def list_attributes(records)
|
207
202
|
attributes = Set.new
|
208
203
|
records.each do |record|
|
209
|
-
|
204
|
+
record.each_key do |key|
|
205
|
+
attributes.add(key)
|
206
|
+
end
|
210
207
|
end
|
211
208
|
attributes
|
212
209
|
end
|
@@ -245,7 +242,7 @@ module FrozenRecord
|
|
245
242
|
private
|
246
243
|
|
247
244
|
def attribute?(attribute_name)
|
248
|
-
FALSY_VALUES.
|
245
|
+
!FALSY_VALUES.include?(self[attribute_name]) && self[attribute_name].present?
|
249
246
|
end
|
250
247
|
|
251
248
|
def attribute_method?(attribute_name)
|
@@ -13,8 +13,9 @@ module FrozenRecord
|
|
13
13
|
|
14
14
|
@records ||= begin
|
15
15
|
records = backend.load(file_path)
|
16
|
-
|
17
|
-
|
16
|
+
if default_attributes
|
17
|
+
records = records.map { |r| assign_defaults!(r.dup).freeze }.freeze
|
18
|
+
end
|
18
19
|
@attributes = list_attributes(records).freeze
|
19
20
|
build_attributes_cache
|
20
21
|
define_attribute_methods(@attributes.to_a)
|
@@ -67,13 +68,13 @@ module FrozenRecord
|
|
67
68
|
|
68
69
|
def attributes=(attributes)
|
69
70
|
self.class.attributes.each do |attr|
|
70
|
-
instance_variable_set(self.class._attributes_cache[attr],
|
71
|
+
instance_variable_set(self.class._attributes_cache[attr], Dedup.deep_intern!(attributes[attr]))
|
71
72
|
end
|
72
73
|
end
|
73
74
|
|
74
75
|
def attribute?(attribute_name)
|
75
76
|
val = self[attribute_name]
|
76
|
-
Base::FALSY_VALUES.
|
77
|
+
!Base::FALSY_VALUES.include?(val) && val.present?
|
77
78
|
end
|
78
79
|
end
|
79
80
|
end
|
data/lib/frozen_record/index.rb
CHANGED
@@ -19,12 +19,11 @@ module FrozenRecord
|
|
19
19
|
false
|
20
20
|
end
|
21
21
|
|
22
|
-
def query(
|
23
|
-
|
24
|
-
|
25
|
-
lookup_multi(value)
|
22
|
+
def query(matcher)
|
23
|
+
if matcher.ranged?
|
24
|
+
lookup_multi(matcher.value)
|
26
25
|
else
|
27
|
-
lookup(value)
|
26
|
+
lookup(matcher.value)
|
28
27
|
end
|
29
28
|
end
|
30
29
|
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'yaml'
|
4
|
+
require 'set'
|
5
|
+
require 'active_model'
|
6
|
+
|
7
|
+
require 'dedup'
|
8
|
+
|
9
|
+
require 'frozen_record/version'
|
10
|
+
require 'frozen_record/scope'
|
11
|
+
require 'frozen_record/index'
|
12
|
+
require 'frozen_record/base'
|
13
|
+
require 'frozen_record/compact'
|
14
|
+
|
15
|
+
module FrozenRecord
|
16
|
+
RecordNotFound = Class.new(StandardError)
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_accessor :deprecated_yaml_erb_backend
|
20
|
+
|
21
|
+
def eager_load!
|
22
|
+
Base.descendants.each(&:eager_load!)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
self.deprecated_yaml_erb_backend = true
|
27
|
+
end
|
data/lib/frozen_record/scope.rb
CHANGED
@@ -2,13 +2,14 @@
|
|
2
2
|
|
3
3
|
module FrozenRecord
|
4
4
|
class Scope
|
5
|
-
|
5
|
+
DISALLOWED_ARRAY_METHODS = [
|
6
6
|
:compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
|
7
7
|
:shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
|
8
8
|
:keep_if, :pop, :shift, :delete_at, :compact
|
9
9
|
].to_set
|
10
10
|
|
11
11
|
delegate :first, :last, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to_json, :as_json, :count, to: :to_a
|
12
|
+
alias_method :find_each, :each
|
12
13
|
if defined? ActiveModel::Serializers::Xml
|
13
14
|
delegate :to_xml, to: :to_a
|
14
15
|
end
|
@@ -187,8 +188,8 @@ module FrozenRecord
|
|
187
188
|
end
|
188
189
|
|
189
190
|
records.select do |record|
|
190
|
-
unindexed_where_values.all? { |attr,
|
191
|
-
!@where_not_values.any? { |attr,
|
191
|
+
unindexed_where_values.all? { |attr, matcher| matcher.match?(record[attr]) } &&
|
192
|
+
!@where_not_values.any? { |attr, matcher| matcher.match?(record[attr]) }
|
192
193
|
end
|
193
194
|
end
|
194
195
|
|
@@ -227,22 +228,23 @@ module FrozenRecord
|
|
227
228
|
super
|
228
229
|
end
|
229
230
|
end
|
231
|
+
ruby2_keywords :method_missing if respond_to?(:ruby2_keywords, true)
|
230
232
|
|
231
233
|
def delegate_to_class(*args, &block)
|
232
234
|
scoping { @klass.public_send(*args, &block) }
|
233
235
|
end
|
234
236
|
|
235
237
|
def array_delegable?(method)
|
236
|
-
Array.method_defined?(method) &&
|
238
|
+
Array.method_defined?(method) && !DISALLOWED_ARRAY_METHODS.include?(method)
|
237
239
|
end
|
238
240
|
|
239
241
|
def where!(criterias)
|
240
|
-
@where_values += criterias.map { |k, v| [k.to_s, v] }
|
242
|
+
@where_values += criterias.map { |k, v| [k.to_s, Matcher.for(v)] }
|
241
243
|
self
|
242
244
|
end
|
243
245
|
|
244
246
|
def where_not!(criterias)
|
245
|
-
@where_not_values += criterias.map { |k, v| [k.to_s, v] }
|
247
|
+
@where_not_values += criterias.map { |k, v| [k.to_s, Matcher.for(v)] }
|
246
248
|
self
|
247
249
|
end
|
248
250
|
|
@@ -265,9 +267,62 @@ module FrozenRecord
|
|
265
267
|
|
266
268
|
private
|
267
269
|
|
268
|
-
|
269
|
-
|
270
|
-
|
270
|
+
class Matcher
|
271
|
+
class << self
|
272
|
+
def for(value)
|
273
|
+
case value
|
274
|
+
when Array
|
275
|
+
IncludeMatcher.new(value)
|
276
|
+
when Range
|
277
|
+
CoverMatcher.new(value)
|
278
|
+
else
|
279
|
+
new(value)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
attr_reader :value
|
285
|
+
|
286
|
+
def hash
|
287
|
+
self.class.hash ^ value.hash
|
288
|
+
end
|
289
|
+
|
290
|
+
def initialize(value)
|
291
|
+
@value = value
|
292
|
+
end
|
293
|
+
|
294
|
+
def ranged?
|
295
|
+
false
|
296
|
+
end
|
297
|
+
|
298
|
+
def match?(other)
|
299
|
+
@value == other
|
300
|
+
end
|
301
|
+
|
302
|
+
def ==(other)
|
303
|
+
self.class == other.class && value == other.value
|
304
|
+
end
|
305
|
+
alias_method :eql?, :==
|
306
|
+
end
|
307
|
+
|
308
|
+
class IncludeMatcher < Matcher
|
309
|
+
def ranged?
|
310
|
+
true
|
311
|
+
end
|
312
|
+
|
313
|
+
def match?(other)
|
314
|
+
@value.include?(other)
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
class CoverMatcher < Matcher
|
319
|
+
def ranged?
|
320
|
+
true
|
321
|
+
end
|
322
|
+
|
323
|
+
def match?(other)
|
324
|
+
@value.cover?(other)
|
325
|
+
end
|
271
326
|
end
|
272
327
|
end
|
273
328
|
end
|
data/spec/frozen_record_spec.rb
CHANGED
@@ -90,6 +90,32 @@ RSpec.shared_examples 'main' do
|
|
90
90
|
|
91
91
|
end
|
92
92
|
|
93
|
+
describe '.each' do
|
94
|
+
|
95
|
+
it 'yields the records one by one' do
|
96
|
+
count = 0
|
97
|
+
Country.each do |country|
|
98
|
+
expect(country).to be_a Country
|
99
|
+
count += 1
|
100
|
+
end
|
101
|
+
expect(count).to be == Country.count
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
106
|
+
describe '.find_each' do
|
107
|
+
|
108
|
+
it 'yields the records one by one' do
|
109
|
+
count = 0
|
110
|
+
Country.find_each do |country|
|
111
|
+
expect(country).to be_a Country
|
112
|
+
count += 1
|
113
|
+
end
|
114
|
+
expect(count).to be == Country.count
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
|
93
119
|
describe '.memsize' do
|
94
120
|
|
95
121
|
it 'retuns the records memory footprint' do
|
data/spec/scope_spec.rb
CHANGED
@@ -180,6 +180,20 @@ describe 'querying' do
|
|
180
180
|
|
181
181
|
end
|
182
182
|
|
183
|
+
describe '.find_each' do
|
184
|
+
|
185
|
+
it 'yields the records one by one' do
|
186
|
+
scope = Country.where(name: 'France')
|
187
|
+
count = 0
|
188
|
+
scope.find_each do |country|
|
189
|
+
expect(country).to be_a Country
|
190
|
+
count += 1
|
191
|
+
end
|
192
|
+
expect(count).to be == 1
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
183
197
|
describe '.where' do
|
184
198
|
|
185
199
|
it 'returns the records that match given criterias' do
|
@@ -193,6 +207,11 @@ describe 'querying' do
|
|
193
207
|
expect(countries.length).to be == 0
|
194
208
|
end
|
195
209
|
|
210
|
+
it 'is chainable with methods of the form `def method(*args, **kargs)' do
|
211
|
+
countries = Country.republics.continent_and_capital('Europe', capital: 'Paris')
|
212
|
+
expect(countries.length).to be == 1
|
213
|
+
end
|
214
|
+
|
196
215
|
it 'can be used with arrays' do
|
197
216
|
countries = Country.where(id: [1,2])
|
198
217
|
expect(countries.length).to be == 2
|
@@ -410,7 +429,7 @@ describe 'querying' do
|
|
410
429
|
|
411
430
|
end
|
412
431
|
|
413
|
-
describe '.as_json' do
|
432
|
+
describe '.as_json', exclude_minimal: true do
|
414
433
|
|
415
434
|
it 'serialize the results' do
|
416
435
|
json = Country.all.as_json
|
@@ -440,8 +459,8 @@ describe 'querying' do
|
|
440
459
|
it 'returns true when the same scope has be rechained' do
|
441
460
|
scope_a = Country.nato.republics.nato.republics
|
442
461
|
scope_b = Country.republics.nato
|
443
|
-
expect(scope_a.instance_variable_get(:@where_values)).to be == [['nato', true], ['king', nil], ['nato', true], ['king', nil]]
|
444
|
-
expect(scope_b.instance_variable_get(:@where_values)).to be == [['king', nil], ['nato', true]]
|
462
|
+
expect(scope_a.instance_variable_get(:@where_values).map { |k, v| [k, v.value] }).to be == [['nato', true], ['king', nil], ['nato', true], ['king', nil]]
|
463
|
+
expect(scope_b.instance_variable_get(:@where_values).map { |k, v| [k, v.value] }).to be == [['king', nil], ['nato', true]]
|
445
464
|
expect(scope_a).to be == scope_b
|
446
465
|
end
|
447
466
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,16 +1,15 @@
|
|
1
1
|
lib = File.expand_path('../lib', __FILE__)
|
2
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
3
|
|
4
|
-
|
5
|
-
require '
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
require 'frozen_record'
|
4
|
+
minimal = ENV['MINIMAL'] == 'true'
|
5
|
+
require 'objspace'
|
6
|
+
|
7
|
+
if minimal
|
8
|
+
require 'frozen_record/minimal'
|
9
|
+
else
|
10
|
+
require 'frozen_record'
|
11
|
+
end
|
12
|
+
|
14
13
|
require 'frozen_record/test_helper'
|
15
14
|
|
16
15
|
FrozenRecord::Base.base_path = File.join(File.dirname(__FILE__), 'fixtures')
|
@@ -22,6 +21,7 @@ FrozenRecord.eager_load!
|
|
22
21
|
RSpec.configure do |config|
|
23
22
|
config.run_all_when_everything_filtered = true
|
24
23
|
config.filter_run :focus
|
24
|
+
config.filter_run_excluding :exclude_minimal if minimal
|
25
25
|
|
26
26
|
config.order = 'random'
|
27
27
|
|
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.22.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: 2021-04-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -25,13 +25,13 @@ dependencies:
|
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
|
-
name:
|
28
|
+
name: dedup
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - ">="
|
32
32
|
- !ruby/object:Gem::Version
|
33
33
|
version: '0'
|
34
|
-
type: :
|
34
|
+
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
@@ -66,34 +66,6 @@ dependencies:
|
|
66
66
|
- - ">="
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: pry
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - ">="
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - ">="
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: coveralls
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - ">="
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: '0'
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '0'
|
97
69
|
description:
|
98
70
|
email:
|
99
71
|
- jean.boussier@gmail.com
|
@@ -101,30 +73,32 @@ executables: []
|
|
101
73
|
extensions: []
|
102
74
|
extra_rdoc_files: []
|
103
75
|
files:
|
76
|
+
- ".github/workflows/main.yml"
|
104
77
|
- ".gitignore"
|
105
78
|
- ".rspec"
|
106
|
-
- ".travis.yml"
|
107
79
|
- Gemfile
|
108
80
|
- LICENSE.txt
|
109
81
|
- README.md
|
110
82
|
- Rakefile
|
111
83
|
- benchmark/attribute-read
|
112
84
|
- benchmark/memory-usage
|
85
|
+
- benchmark/querying
|
113
86
|
- benchmark/setup.rb
|
114
87
|
- frozen_record.gemspec
|
115
88
|
- lib/frozen_record.rb
|
116
89
|
- lib/frozen_record/backends.rb
|
90
|
+
- lib/frozen_record/backends/empty.json
|
117
91
|
- lib/frozen_record/backends/json.rb
|
118
92
|
- lib/frozen_record/backends/yaml.rb
|
119
93
|
- lib/frozen_record/base.rb
|
120
94
|
- lib/frozen_record/compact.rb
|
121
|
-
- lib/frozen_record/deduplication.rb
|
122
95
|
- lib/frozen_record/index.rb
|
96
|
+
- lib/frozen_record/minimal.rb
|
123
97
|
- lib/frozen_record/railtie.rb
|
124
98
|
- lib/frozen_record/scope.rb
|
99
|
+
- lib/frozen_record/serialization.rb
|
125
100
|
- lib/frozen_record/test_helper.rb
|
126
101
|
- lib/frozen_record/version.rb
|
127
|
-
- spec/deduplication_spec.rb
|
128
102
|
- spec/fixtures/animals.json
|
129
103
|
- spec/fixtures/cars.yml
|
130
104
|
- spec/fixtures/countries.yml.erb
|
@@ -156,12 +130,11 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
156
130
|
- !ruby/object:Gem::Version
|
157
131
|
version: '0'
|
158
132
|
requirements: []
|
159
|
-
rubygems_version: 3.1.
|
133
|
+
rubygems_version: 3.1.4
|
160
134
|
signing_key:
|
161
135
|
specification_version: 4
|
162
136
|
summary: ActiveRecord like interface to read only access and query static YAML files
|
163
137
|
test_files:
|
164
|
-
- spec/deduplication_spec.rb
|
165
138
|
- spec/fixtures/animals.json
|
166
139
|
- spec/fixtures/cars.yml
|
167
140
|
- spec/fixtures/countries.yml.erb
|
data/.travis.yml
DELETED
@@ -1,57 +0,0 @@
|
|
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.freeze
|
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.freeze
|
48
|
-
else
|
49
|
-
-data.freeze
|
50
|
-
end
|
51
|
-
else
|
52
|
-
data.duplicable? ? data.freeze : data
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
end
|
57
|
-
end
|
data/spec/deduplication_spec.rb
DELETED
@@ -1,31 +0,0 @@
|
|
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
|