forest_liana 9.22.9 → 9.22.10

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: 1427f49be68c4df6bbcd4fd859c13544c59d6c8c24b810cacfe2ebfcf34b20a0
4
- data.tar.gz: 4987bbf28820237edab483d04752e068fe35e1a547b95784aa19cf2e70db17f4
3
+ metadata.gz: 23a1adeed07bf166b64953093122fc2e9c8c442d1ab52aec8227bec80f83f17d
4
+ data.tar.gz: 3cbea9923bf9350fbd8ad11c0a11479504c3a7b876b1c96e2c79a0cf602c88a2
5
5
  SHA512:
6
- metadata.gz: b0c64c511777459d427d6e33e75698004f96436894c668ffaf2333b63e3c8d539b354155708dd15c88ce1d1b8b6c856082df12bfa555455f14d43f2689c9223f
7
- data.tar.gz: bf3bf6469ac14740c7b1eeddd6e05a404ffc7805248ee6bd4974f614c2c7e6b2e7be172743c3b55fa6bcad8bbf4da474ee5ed4306103e3ce4232e2b84d5edd92
6
+ metadata.gz: d41d2034547eb686df156ac01a72cd9e85f3aa10fcaf272831013dcf0827f7bd1b216b35828b22cc699d88e6ed1541ddaae5769b76e16cf6caf6aace4a49edef
7
+ data.tar.gz: f26b8e3fb84c18d3ee198b0f474e2bf189254b0c47a32365ee775ffcb1652baee2c6eea641e22cdb753d3f552ef5cc360db7b6c76a3df3f5eef8357adec0ede0
@@ -31,6 +31,18 @@ module ForestLiana
31
31
  end
32
32
 
33
33
  def self.find_model_from_collection_name(collection_name, logs = false)
34
+ model_found = LookupCache.fetch(:model_from_collection_name, collection_name) do
35
+ scan_models_for_collection_name(collection_name)
36
+ end
37
+
38
+ if logs && model_found.nil?
39
+ FOREST_LOGGER.warn "No model found for collection #{collection_name}"
40
+ end
41
+
42
+ model_found
43
+ end
44
+
45
+ def self.scan_models_for_collection_name(collection_name)
34
46
  model_found = nil
35
47
  ForestLiana.models.each do |model|
36
48
  if model.abstract_class?
@@ -42,10 +54,6 @@ module ForestLiana
42
54
  break if model_found
43
55
  end
44
56
 
45
- if logs && model_found.nil?
46
- FOREST_LOGGER.warn "No model found for collection #{collection_name}"
47
- end
48
-
49
57
  model_found
50
58
  end
51
59
 
@@ -0,0 +1,37 @@
1
+ module ForestLiana
2
+ # Remembers lookups by name over ForestLiana.models, .apimap and .names_overriden: on a large
3
+ # schema each one is a linear scan, schema_for_resource a scan of scans, and every request runs
4
+ # several. Any of the three being replaced or growing (the bootstrapper, a Forest collection
5
+ # file, a code reload) drops everything remembered, so an answer never outlives what it read.
6
+ module LookupCache
7
+ # A miss is not remembered: the router resolves a request's collection before authenticating
8
+ # it, so caching unknown names would let any caller grow this cache without bound.
9
+ def self.fetch(namespace, key)
10
+ reset_if_stale
11
+ store = (@stores[namespace] ||= {})
12
+ return store[key] if store.key?(key)
13
+
14
+ value = yield
15
+ store[key] = value unless value.nil?
16
+ value
17
+ end
18
+
19
+ def self.reset!
20
+ @stores = nil
21
+ end
22
+
23
+ def self.reset_if_stale
24
+ models = ForestLiana.models
25
+ apimap = ForestLiana.apimap
26
+ names = ForestLiana.names_overriden
27
+ return if @stores && models.equal?(@models) && models.size == @models_size &&
28
+ apimap.equal?(@apimap) && apimap.size == @apimap_size &&
29
+ names.equal?(@names) && names.size == @names_size
30
+
31
+ @models, @models_size = models, models.size
32
+ @apimap, @apimap_size = apimap, apimap.size
33
+ @names, @names_size = names, names.size
34
+ @stores = {}
35
+ end
36
+ end
37
+ end
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "9.22.9"
2
+ VERSION = "9.22.10"
3
3
  end
data/lib/forest_liana.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'forest_liana/engine'
2
+ require 'forest_liana/lookup_cache'
2
3
 
3
4
  module Forest
4
5
  end
@@ -69,14 +70,16 @@ module ForestLiana
69
70
  end
70
71
 
71
72
  def self.schema_for_resource resource
72
- self.apimap.find do |collection|
73
- SchemaUtils.find_model_from_collection_name(collection.name)
74
- .try(:name) == resource.name
73
+ LookupCache.fetch(:schema_for_resource, resource.name) do
74
+ self.apimap.find do |collection|
75
+ SchemaUtils.find_model_from_collection_name(collection.name)
76
+ .try(:name) == resource.name
77
+ end
75
78
  end
76
79
  end
77
80
 
78
81
  def self.name_for(model)
79
- self.names_overriden[model] || model.try(:name).gsub('::', '__')
82
+ LookupCache.fetch(:name_for, model) { self.names_overriden[model] || model.try(:name).gsub('::', '__') }
80
83
  end
81
84
 
82
85
  # TODO: Remove once lianas prior to 2.0.0 are not supported anymore.
@@ -0,0 +1,68 @@
1
+ require 'rails_helper'
2
+
3
+ describe 'Lookups by name over the schema' do
4
+ describe 'ForestLiana.name_for' do
5
+ after { ForestLiana.names_overriden.delete(Tree) }
6
+
7
+ it 'answers an override registered after a first lookup' do
8
+ expect(ForestLiana.name_for(Tree)).to eq 'Tree'
9
+
10
+ ForestLiana.names_overriden[Tree] = 'Orchard'
11
+
12
+ expect(ForestLiana.name_for(Tree)).to eq 'Orchard'
13
+ end
14
+ end
15
+
16
+ describe 'ForestLiana::SchemaUtils.find_model_from_collection_name' do
17
+ let(:ghost) { Class.new(ActiveRecord::Base) { self.table_name = 'trees' } }
18
+
19
+ after do
20
+ ForestLiana.models.delete(ghost)
21
+ ForestLiana.names_overriden.delete(ghost)
22
+ end
23
+
24
+ it 'scans the models once for repeated lookups of a name' do
25
+ expect(ForestLiana::SchemaUtils).to receive(:scan_models_for_collection_name).with('Tree').once.and_call_original
26
+
27
+ 3.times { expect(ForestLiana::SchemaUtils.find_model_from_collection_name('Tree')).to eq Tree }
28
+ end
29
+
30
+ it 'does not remember an unknown name, which any caller can send before authenticating' do
31
+ expect(ForestLiana::SchemaUtils).to receive(:scan_models_for_collection_name).with('Ghost').twice.and_call_original
32
+
33
+ 2.times { expect(ForestLiana::SchemaUtils.find_model_from_collection_name('Ghost')).to be_nil }
34
+ end
35
+
36
+ it 'still warns on each lookup of an unknown name' do
37
+ expect(FOREST_LOGGER).to receive(:warn).with('No model found for collection Ghost').twice
38
+
39
+ 2.times { expect(ForestLiana::SchemaUtils.find_model_from_collection_name('Ghost', true)).to be_nil }
40
+ end
41
+
42
+ it 'finds a model added after a first lookup missed it' do
43
+ expect(ForestLiana::SchemaUtils.find_model_from_collection_name('Ghost')).to be_nil
44
+
45
+ ForestLiana.names_overriden[ghost] = 'Ghost'
46
+ ForestLiana.models << ghost
47
+
48
+ expect(ForestLiana::SchemaUtils.find_model_from_collection_name('Ghost')).to eq ghost
49
+ end
50
+ end
51
+
52
+ describe 'ForestLiana.schema_for_resource' do
53
+ it 'resolves the collection of a resource once for repeated lookups' do
54
+ expect(ForestLiana.schema_for_resource(Tree).name).to eq 'Tree'
55
+ expect(ForestLiana::SchemaUtils).not_to receive(:find_model_from_collection_name)
56
+
57
+ expect(ForestLiana.schema_for_resource(Tree).name).to eq 'Tree'
58
+ end
59
+
60
+ it 'answers from the apimap in place when it is replaced' do
61
+ expect(ForestLiana.schema_for_resource(Tree).name).to eq 'Tree'
62
+ replacement = ForestLiana::Model::Collection.new(name: 'Tree', fields: [])
63
+ allow(ForestLiana).to receive(:apimap).and_return([replacement])
64
+
65
+ expect(ForestLiana.schema_for_resource(Tree)).to equal replacement
66
+ end
67
+ end
68
+ end
data/spec/rails_helper.rb CHANGED
@@ -65,6 +65,7 @@ RSpec.configure do |config|
65
65
  # own comment) — cleared before every example so one spec's warning can't silence another's,
66
66
  # regardless of run order or which file exercises the valve.
67
67
  config.before { ForestLiana::MissingAttributeValve::WARNED_ONCE.clear }
68
+ config.before { ForestLiana::LookupCache.reset! }
68
69
 
69
70
  # Filter lines from Rails gems in backtraces.
70
71
  config.filter_rails_from_backtrace!
@@ -67,10 +67,9 @@ module ForestLiana
67
67
  expect { dummy_class.assert_can_read_query_fields(user, Tree) }.not_to raise_error
68
68
  end
69
69
 
70
- it 'reads the apimap once, however many paths are checked' do
70
+ it 'indexes the apimap once, however many paths are checked' do
71
71
  write_permissions('Tree' => true, 'User' => true, 'Island' => true)
72
- reads = 0
73
- allow(ForestLiana).to receive(:apimap).and_wrap_original { |original| reads += 1; original.call }
72
+ expect(ForestLiana::Ability::ApimapIndex).to receive(:new).with(ForestLiana.apimap).once.and_call_original
74
73
 
75
74
  expect do
76
75
  dummy_class.assert_can_read_query_fields(
@@ -78,7 +77,6 @@ module ForestLiana
78
77
  search_paths: %w[owner:title]
79
78
  )
80
79
  end.not_to raise_error
81
- expect(reads).to eq 1
82
80
  end
83
81
 
84
82
  it 'serves a filter, a sort and a search on an unreadable collection once the option is on' do
@@ -169,10 +169,9 @@ module ForestLiana
169
169
 
170
170
  # Resolving each field against the apimap rescans every collection: on a wide collection and
171
171
  # a large schema, that cost outweighs the request it guards.
172
- it 'reads the apimap once, however many fields are requested' do
172
+ it 'indexes the apimap once, however many fields are requested' do
173
173
  write_permissions('Tree' => true, 'User' => true, 'Island' => true)
174
- reads = 0
175
- allow(ForestLiana).to receive(:apimap).and_wrap_original { |original| reads += 1; original.call }
174
+ expect(ForestLiana::Ability::ApimapIndex).to receive(:new).with(ForestLiana.apimap).once.and_call_original
176
175
 
177
176
  redacted = dummy_class.redact_fields(
178
177
  user, Tree, { 'Tree' => 'id,name,age,owner,cutter,island', 'owner' => 'name', 'island' => 'name' },
@@ -180,7 +179,6 @@ module ForestLiana
180
179
  )
181
180
 
182
181
  expect(redacted).to eq('Tree' => 'id,name,age,owner,cutter,island', 'owner' => 'name', 'island' => 'name')
183
- expect(reads).to eq 1
184
182
  end
185
183
 
186
184
  it 'checks nothing at all when only root collection fields are requested' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forest_liana
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.22.9
4
+ version: 9.22.10
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sandro Munda
@@ -365,6 +365,7 @@ files:
365
365
  - lib/forest_liana/collection.rb
366
366
  - lib/forest_liana/engine.rb
367
367
  - lib/forest_liana/json_printer.rb
368
+ - lib/forest_liana/lookup_cache.rb
368
369
  - lib/forest_liana/mixpanel_event.rb
369
370
  - lib/forest_liana/schema_file_updater.rb
370
371
  - lib/forest_liana/version.rb
@@ -471,6 +472,7 @@ files:
471
472
  - spec/lib/forest_liana/bootstrapper_spec.rb
472
473
  - spec/lib/forest_liana/collection_spec.rb
473
474
  - spec/lib/forest_liana/engine_spec.rb
475
+ - spec/lib/forest_liana/lookup_cache_spec.rb
474
476
  - spec/lib/forest_liana/schema_file_updater_spec.rb
475
477
  - spec/models/forest_liana/model/action_spec.rb
476
478
  - spec/models/forest_liana/model/collection_spec.rb
@@ -829,6 +831,7 @@ test_files:
829
831
  - spec/lib/forest_liana/bootstrapper_spec.rb
830
832
  - spec/lib/forest_liana/collection_spec.rb
831
833
  - spec/lib/forest_liana/engine_spec.rb
834
+ - spec/lib/forest_liana/lookup_cache_spec.rb
832
835
  - spec/lib/forest_liana/schema_file_updater_spec.rb
833
836
  - spec/models/forest_liana/model/action_spec.rb
834
837
  - spec/models/forest_liana/model/collection_spec.rb