From 3ea58d643270eb63592ebdc460689ba414f563c7 Mon Sep 17 00:00:00 2001 From: Lee Richmond Date: Thu, 18 May 2017 14:16:06 -0400 Subject: [PATCH] Treat context like a global There are a number of use cases - such as getting the current user - where Resources and other objects need access to the context (in Rails, the context is the controller). Resource#with_context made sure the context was available for filter guards, for example. This is now the same dev-facing API, but the context is set on Thread.current rather than the Resource instance. This is because the same context needs to be available to all nested resources, serializers, etc. 'context' is more of a global concept, and this code treats it as such. The final product is you can do something like this in a Resource: ```ruby def create(attributes) raise 'not authorized!' unless context.current_user.role == 'admin' super end ``` ...regardless of whether this resource is being created from its corresponding endpoint, or side-posted 3 levels deep. --- lib/jsonapi_compliable.rb | 21 ++++++++++++++++++++ lib/jsonapi_compliable/base.rb | 6 ++---- lib/jsonapi_compliable/resource.rb | 14 ++++++------- lib/jsonapi_compliable/scoping/filterable.rb | 2 +- spec/jsonapi_compliable_spec.rb | 20 ++----------------- spec/resource_spec.rb | 15 +++++++++++--- spec/support/scope_helper.rb | 2 +- 7 files changed, 46 insertions(+), 34 deletions(-) diff --git a/lib/jsonapi_compliable.rb b/lib/jsonapi_compliable.rb index 3ac9c04..e9dbe80 100644 --- a/lib/jsonapi_compliable.rb +++ b/lib/jsonapi_compliable.rb @@ -42,4 +42,25 @@ def self.included(klass) include Base end end + + # @api private + def self.context + Thread.current[:context] ||= {} + end + + # @api private + def self.context=(val) + Thread.current[:context] = val + end + + # @api private + def self.with_context(obj, namespace) + begin + prior = self.context + self.context = { object: obj, namespace: namespace } + yield + ensure + self.context = prior + end + end end diff --git a/lib/jsonapi_compliable/base.rb b/lib/jsonapi_compliable/base.rb index 76dea9b..eb0080f 100644 --- a/lib/jsonapi_compliable/base.rb +++ b/lib/jsonapi_compliable/base.rb @@ -88,10 +88,8 @@ def query_hash # @api private # @yieldreturn Code to run within the current context def wrap_context - if self.class._jsonapi_compliable - jsonapi_resource.with_context(self, action_name.to_sym) do - yield - end + jsonapi_resource.with_context(self, action_name.to_sym) do + yield end end diff --git a/lib/jsonapi_compliable/resource.rb b/lib/jsonapi_compliable/resource.rb index e610d00..1f60c13 100644 --- a/lib/jsonapi_compliable/resource.rb +++ b/lib/jsonapi_compliable/resource.rb @@ -446,12 +446,8 @@ def self.config # @param object The context (Rails controller or equivalent) # @param namespace One of index/show/etc def with_context(object, namespace = nil) - begin - prior = context - @context = { object: object, namespace: namespace } + JsonapiCompliable.with_context(object, namespace) do yield - ensure - @context = prior end end @@ -462,7 +458,11 @@ def with_context(object, namespace = nil) # @see #with_context # @return [Hash] the context hash def context - @context || {} + JsonapiCompliable.context[:object] + end + + def context_namespace + JsonapiCompliable.context[:namespace] end # Build a scope using this Resource configuration @@ -592,7 +592,7 @@ def association_names def allowed_sideloads(namespace = nil) return {} unless sideloading - namespace ||= context[:namespace] + namespace ||= context_namespace sideloads = sideloading.to_hash[:base] if !sideload_whitelist.empty? && namespace sideloads = Util::IncludeParams.scrub(sideloads, sideload_whitelist[namespace]) diff --git a/lib/jsonapi_compliable/scoping/filterable.rb b/lib/jsonapi_compliable/scoping/filterable.rb index 5558a21..d15be17 100644 --- a/lib/jsonapi_compliable/scoping/filterable.rb +++ b/lib/jsonapi_compliable/scoping/filterable.rb @@ -14,7 +14,7 @@ def find_filter!(name) resource.filters.find { |_name, opts| opts[:aliases].include?(name.to_sym) } raise JsonapiCompliable::Errors::BadFilter unless filter_name if guard = filter_value[:if] - raise JsonapiCompliable::Errors::BadFilter if resource.context[:object].send(guard) == false + raise JsonapiCompliable::Errors::BadFilter if resource.context.send(guard) == false end { filter_name => filter_value } end diff --git a/spec/jsonapi_compliable_spec.rb b/spec/jsonapi_compliable_spec.rb index 41bcb1a..957b7d8 100644 --- a/spec/jsonapi_compliable_spec.rb +++ b/spec/jsonapi_compliable_spec.rb @@ -78,24 +78,8 @@ def config(obj) it 'wraps in the resource context' do instance.wrap_context do - expect(instance.jsonapi_resource.context).to eq({ - object: instance, - namespace: :index - }) - end - end - - context 'when the class does not have a resource' do - let(:klass) do - Class.new do - include JsonapiCompliable - end - end - - it 'does nothing' do - instance.wrap_context do - expect(instance.resource).to be_nil - end + expect(instance.jsonapi_resource.context).to eq(instance) + expect(instance.jsonapi_resource.context_namespace).to eq(:index) end end end diff --git a/spec/resource_spec.rb b/spec/resource_spec.rb index ff38ff8..7f0b33f 100644 --- a/spec/resource_spec.rb +++ b/spec/resource_spec.rb @@ -46,19 +46,28 @@ it 'sets/resets correct context' do dbl = double instance.with_context(dbl, :index) do - expect(instance.context).to eq(object: dbl, namespace: :index) + expect(instance.context).to eq(dbl) + expect(instance.context_namespace).to eq(:index) end - expect(instance.context).to eq({}) + expect(instance.context).to be_nil + expect(instance.context_namespace).to be_nil end context 'when an error' do + around do |e| + JsonapiCompliable.with_context('orig', 'orig namespace') do + e.run + end + end + it 'resets the context' do expect { instance.with_context({}, :index) do raise 'foo' end }.to raise_error('foo') - expect(instance.context).to eq({}) + expect(instance.context).to eq('orig') + expect(instance.context_namespace).to eq('orig namespace') end end end diff --git a/spec/support/scope_helper.rb b/spec/support/scope_helper.rb index 2993252..a85c6b6 100644 --- a/spec/support/scope_helper.rb +++ b/spec/support/scope_helper.rb @@ -14,7 +14,7 @@ let(:scope) { resource.build_scope(scope_object, query) } def render(object, opts = {}) - opts[:expose] = { context: resource.context[:object] } + opts[:expose] = { context: resource.context } opts = JsonapiCompliable::Util::RenderOptions.generate(object, query.to_hash[:authors], opts) resolved = opts.delete(:jsonapi) raw_json = JSONAPI::Serializable::Renderer.render(resolved, opts)