Class: JsonapiCompliable::Resource
- Inherits:
-
Object
- Object
- JsonapiCompliable::Resource
- Extended by:
- Forwardable
- Defined in:
- lib/jsonapi_compliable/resource.rb
Overview
Resources hold configuration: How do you want to process incoming JSONAPI requests?
Let's say we start with an empty hash as our scope object:
render_jsonapi({})
Let's define the behavior of various parameters. Here we'll merge options into our hash when the user filters, sorts, and paginates. Then, we'll pass that hash off to an HTTP Client:
class PostResource < ApplicationResource
type :posts
use_adapter JsonapiCompliable::Adapters::Null
# What do do when filter[active] parameter comes in
allow_filter :active do |scope, value|
scope.merge(active: value)
end
# What do do when sorting parameters come in
sort do |scope, attribute, direction|
scope.merge(order: { attribute => direction })
end
# What do do when pagination parameters come in
page do |scope, current_page, per_page|
scope.merge(page: current_page, per_page: per_page)
end
# Resolve the scope by passing the hash to an HTTP Client
def resolve(scope)
MyHttpClient.get(scope)
end
end
This code can quickly become duplicative - we probably want to reuse this logic for other objects that use the same HTTP client.
That's why we also have Adapters. Adapters encapsulate common, reusable
resource configuration. That's why we don't need to specify the above code
when using ActiveRecord - the default logic is already in the adapter.
class PostResource < ApplicationResource
type :posts
use_adapter JsonapiCompliable::Adapters::ActiveRecord
allow_filter :title
end
Of course, we can always override the Resource directly for one-off customizations:
class PostResource < ApplicationResource
type :posts
use_adapter JsonapiCompliable::Adapters::ActiveRecord
allow_filter :title_prefix do |scope, value|
scope.where(["title LIKE ?", "#{value}%"])
end
end
Resources can also define Sideloads. Sideloads define the relationships between resources:
allow_sideload :comments, resource: CommentResource do
# How to fetch the associated objects
# This will be further chained down the line
scope do |posts|
Comment.where(post_id: posts.map(&:id))
end
# Now that we've resolved everything, how to assign the objects
assign do |posts, comments|
posts.each do |post|
relevant_comments = comments.select { |c| c.post_id === post.id }
post.comments = relevant_comments
end
end
end
Once again, we can DRY this up using an Adapter:
use_adapter JsonapiCompliable::Adapters::ActiveRecord
has_many :comments,
scope: -> { Comment.all },
resource: CommentResource,
foreign_key: :post_id
Class Attribute Summary collapse
-
.config ⇒ Hash
This is where we store all information set via DSL.
Instance Attribute Summary collapse
-
#context ⇒ Object
readonly
The current context object set by
#with_context.
Class Method Summary collapse
-
.allow_filter(name, options = {}) ⇒ Object
Whitelist a filter.
- .allow_sideload ⇒ Object
-
.allow_stat(symbol_or_hash) {|scope, attr| ... } ⇒ Object
Whitelist a statistic.
- .belongs_to ⇒ Object
-
.default_filter(name) {|scope| ... } ⇒ Object
When you want a filter to always apply, on every request.
-
.default_page_number(val) ⇒ Object
Set an alternative default page number.
-
.default_page_size(val) ⇒ Object
Set an alternate default page size, when not specified in query parameters.
-
.default_sort(val) ⇒ Object
Override default sort applied when not present in the query parameters.
-
.extra_field(name) {|scope, current_page, per_page| ... } ⇒ Object
Perform special logic when an extra field is requested.
- .has_and_belongs_to_many ⇒ Object
- .has_many ⇒ Object
- .has_one ⇒ Object
- .inherited(klass) ⇒ Object
-
.model(klass) ⇒ Object
The Model object associated with this class.
-
.paginate {|scope, current_page, per_page| ... } ⇒ Object
Define custom pagination logic.
- .polymorphic_belongs_to ⇒ Object
- .polymorphic_has_many ⇒ Object
- .sideloading ⇒ Object private
-
.sort {|scope, att, dir| ... } ⇒ Object
Define custom sorting logic.
-
.type(value = nil) ⇒ Object
The JSONAPI Type.
-
.use_adapter(klass) ⇒ Object
Configure the adapter you want to use.
Instance Method Summary collapse
- #adapter ⇒ Object private
-
#associate(parent, child, association_name, type) ⇒ Object
Delegates #associate to adapter.
- #association_names ⇒ Object
-
#build_scope(base, query, opts = {}) ⇒ Scope
Build a scope using this Resource configuration.
-
#context_namespace ⇒ Symbol
The current context namespace set by
#with_context. -
#create(create_params) ⇒ Object
Create the relevant model.
- #default_filters ⇒ Object private
- #default_page_number ⇒ Object private
- #default_page_size ⇒ Object private
- #default_sort ⇒ Object private
-
#destroy(id) ⇒ Object
Destroy the relevant model.
-
#disassociate(parent, child, association_name, type) ⇒ Object
Delegates #disassociate to adapter.
- #extra_fields ⇒ Object private
- #filters ⇒ Object private
- #model ⇒ Object private
- #pagination ⇒ Object private
- #persist_with_relationships(meta, attributes, relationships, caller_model = nil) ⇒ Object private
-
#resolve(scope) ⇒ Array
How do you want to resolve the scope?.
- #sideload ⇒ Object
-
#sideloading ⇒ Object
private
Interface to the sideloads for this Resource.
- #sorting ⇒ Object private
-
#stat(attribute, calculation) ⇒ Proc
The relevant proc for the given attribute and calculation.
- #stats ⇒ Object private
-
#transaction ⇒ Object
How to run write requests within a transaction.
-
#type ⇒ Object
private
Returns :undefined_jsonapi_type when not configured.
-
#update(update_params) ⇒ Object
Update the relevant model.
-
#with_context(object, namespace = nil) ⇒ Object
Run code within a given context.
Class Attribute Details
.config ⇒ Hash
This is where we store all information set via DSL. Useful for introspection. Gets dup'd when inherited.
384 385 386 |
# File 'lib/jsonapi_compliable/resource.rb', line 384 def config @config end |
Instance Attribute Details
#context ⇒ Object (readonly)
The current context object set by #with_context. If you are
using Rails, this is a controller instance.
This method is equivalent to JsonapiCompliable.context[:object]
429 430 431 |
# File 'lib/jsonapi_compliable/resource.rb', line 429 def context @context end |
Class Method Details
.allow_filter(name, options = {}) ⇒ Object
Whitelist a filter
If a filter is not allowed, a Jsonapi::Errors::BadFilter error will be raised.
169 170 171 172 173 174 175 176 177 178 |
# File 'lib/jsonapi_compliable/resource.rb', line 169 def self.allow_filter(name, *args, &blk) opts = args. aliases = [name, opts[:aliases]].flatten.compact config[:filters][name.to_sym] = { aliases: aliases, if: opts[:if], filter: blk, required: opts[:required].respond_to?(:call) ? opts[:required] : !!opts[:required] } end |
.allow_sideload ⇒ Object
100 |
# File 'lib/jsonapi_compliable/resource.rb', line 100 def_delegator :sideloading, :allow_sideload |
.allow_stat(symbol_or_hash) {|scope, attr| ... } ⇒ Object
Whitelist a statistic.
Statistics are requested like
GET /posts?stats[total]=count
And returned in meta:
{
data: [...],
meta: { stats: { total: { count: 100 } } }
}
Statistics take into account the current scope, without pagination.
209 210 211 212 213 |
# File 'lib/jsonapi_compliable/resource.rb', line 209 def self.allow_stat(symbol_or_hash, &blk) dsl = Stats::DSL.new(config[:adapter], symbol_or_hash) dsl.instance_eval(&blk) if blk config[:stats][dsl.name] = dsl end |
.belongs_to ⇒ Object
109 |
# File 'lib/jsonapi_compliable/resource.rb', line 109 def_delegator :sideloading, :belongs_to |
.default_filter(name) {|scope| ... } ⇒ Object
When you want a filter to always apply, on every request.
Default filters can be overridden if there is a corresponding allow_filter:
237 238 239 240 241 |
# File 'lib/jsonapi_compliable/resource.rb', line 237 def self.default_filter(name, &blk) config[:default_filters][name.to_sym] = { filter: blk } end |
.default_page_number(val) ⇒ Object
Set an alternative default page number. Defaults to 1.
364 365 366 |
# File 'lib/jsonapi_compliable/resource.rb', line 364 def self.default_page_number(val) config[:default_page_number] = val end |
.default_page_size(val) ⇒ Object
Set an alternate default page size, when not specified in query parameters.
375 376 377 |
# File 'lib/jsonapi_compliable/resource.rb', line 375 def self.default_page_size(val) config[:default_page_size] = val end |
.default_sort(val) ⇒ Object
Override default sort applied when not present in the query parameters.
Default: [{ id: :asc }]
336 337 338 |
# File 'lib/jsonapi_compliable/resource.rb', line 336 def self.default_sort(val) config[:default_sort] = val end |
.extra_field(name) {|scope, current_page, per_page| ... } ⇒ Object
Perform special logic when an extra field is requested. Often used to eager load data that will be used to compute the extra field.
This is not required if you have no custom logic.
312 313 314 |
# File 'lib/jsonapi_compliable/resource.rb', line 312 def self.extra_field(name, &blk) config[:extra_fields][name] = blk end |
.has_and_belongs_to_many ⇒ Object
112 |
# File 'lib/jsonapi_compliable/resource.rb', line 112 def_delegator :sideloading, :has_and_belongs_to_many |
.has_many ⇒ Object
103 |
# File 'lib/jsonapi_compliable/resource.rb', line 103 def_delegator :sideloading, :has_many |
.has_one ⇒ Object
106 |
# File 'lib/jsonapi_compliable/resource.rb', line 106 def_delegator :sideloading, :has_one |
.inherited(klass) ⇒ Object
126 127 128 |
# File 'lib/jsonapi_compliable/resource.rb', line 126 def self.inherited(klass) klass.config = Util::Hash.deep_dup(self.config) end |
.model(klass) ⇒ Object
The Model object associated with this class.
This model will be utilized on write requests.
Models need not be ActiveRecord ;)
256 257 258 |
# File 'lib/jsonapi_compliable/resource.rb', line 256 def self.model(klass) config[:model] = klass end |
.paginate {|scope, current_page, per_page| ... } ⇒ Object
Define custom pagination logic
290 291 292 |
# File 'lib/jsonapi_compliable/resource.rb', line 290 def self.paginate(&blk) config[:pagination] = blk end |
.polymorphic_belongs_to ⇒ Object
115 |
# File 'lib/jsonapi_compliable/resource.rb', line 115 def_delegator :sideloading, :polymorphic_belongs_to |
.polymorphic_has_many ⇒ Object
118 |
# File 'lib/jsonapi_compliable/resource.rb', line 118 def_delegator :sideloading, :polymorphic_has_many |
.sideloading ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
131 132 133 |
# File 'lib/jsonapi_compliable/resource.rb', line 131 def self.sideloading @sideloading ||= Sideload.new(:base, resource: self) end |
.sort {|scope, att, dir| ... } ⇒ Object
Define custom sorting logic
275 276 277 |
# File 'lib/jsonapi_compliable/resource.rb', line 275 def self.sort(&blk) config[:sorting] = blk end |
.type(value = nil) ⇒ Object
The JSONAPI Type. For instance if you queried:
GET /employees?fields=title
And/Or got back in the response
{ id: '1', type: 'positions' }
The type would be :positions
This should match the type set in your serializer.
358 359 360 |
# File 'lib/jsonapi_compliable/resource.rb', line 358 def self.type(value = nil) config[:type] = value end |
.use_adapter(klass) ⇒ Object
Configure the adapter you want to use.
323 324 325 |
# File 'lib/jsonapi_compliable/resource.rb', line 323 def self.use_adapter(klass) config[:adapter] = klass.new end |
Instance Method Details
#adapter ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
646 647 648 |
# File 'lib/jsonapi_compliable/resource.rb', line 646 def adapter self.class.config[:adapter] end |
#associate(parent, child, association_name, type) ⇒ Object
Delegates #associate to adapter. Built for overriding.
521 522 523 |
# File 'lib/jsonapi_compliable/resource.rb', line 521 def associate(parent, child, association_name, type) adapter.associate(parent, child, association_name, type) end |
#association_names ⇒ Object
542 543 544 |
# File 'lib/jsonapi_compliable/resource.rb', line 542 def association_names sideloading.association_names end |
#build_scope(base, query, opts = {}) ⇒ Scope
Build a scope using this Resource configuration
Essentially "api private", but can be useful for testing.
454 455 456 |
# File 'lib/jsonapi_compliable/resource.rb', line 454 def build_scope(base, query, opts = {}) Scope.new(base, self, query, opts) end |
#context_namespace ⇒ Symbol
The current context namespace set by #with_context. If you
are using Rails, this is the controller method name (e.g. :index)
This method is equivalent to JsonapiCompliable.context[:namespace]
440 441 442 |
# File 'lib/jsonapi_compliable/resource.rb', line 440 def context_namespace JsonapiCompliable.context[:namespace] end |
#create(create_params) ⇒ Object
Create the relevant model. You must configure a model (see .model) to create. If you override, you must return the created instance.
473 474 475 |
# File 'lib/jsonapi_compliable/resource.rb', line 473 def create(create_params) adapter.create(model, create_params) end |
#default_filters ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
634 635 636 |
# File 'lib/jsonapi_compliable/resource.rb', line 634 def default_filters self.class.config[:default_filters] end |
#default_page_number ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
585 586 587 |
# File 'lib/jsonapi_compliable/resource.rb', line 585 def default_page_number self.class.config[:default_page_number] || 1 end |
#default_page_size ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
591 592 593 |
# File 'lib/jsonapi_compliable/resource.rb', line 591 def default_page_size self.class.config[:default_page_size] || 20 end |
#default_sort ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
579 580 581 |
# File 'lib/jsonapi_compliable/resource.rb', line 579 def default_sort self.class.config[:default_sort] || [{ id: :asc }] end |
#destroy(id) ⇒ Object
Destroy the relevant model. You must configure a model (see .model) to destroy. If you override, you must return the destroyed instance.
512 513 514 |
# File 'lib/jsonapi_compliable/resource.rb', line 512 def destroy(id) adapter.destroy(model, id) end |
#disassociate(parent, child, association_name, type) ⇒ Object
Delegates #disassociate to adapter. Built for overriding.
530 531 532 |
# File 'lib/jsonapi_compliable/resource.rb', line 530 def disassociate(parent, child, association_name, type) adapter.disassociate(parent, child, association_name, type) end |
#extra_fields ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
628 629 630 |
# File 'lib/jsonapi_compliable/resource.rb', line 628 def extra_fields self.class.config[:extra_fields] end |
#filters ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
604 605 606 |
# File 'lib/jsonapi_compliable/resource.rb', line 604 def filters self.class.config[:filters] end |
#model ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
640 641 642 |
# File 'lib/jsonapi_compliable/resource.rb', line 640 def model self.class.config[:model] end |
#pagination ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
622 623 624 |
# File 'lib/jsonapi_compliable/resource.rb', line 622 def pagination self.class.config[:pagination] end |
#persist_with_relationships(meta, attributes, relationships, caller_model = nil) ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
535 536 537 538 539 |
# File 'lib/jsonapi_compliable/resource.rb', line 535 def persist_with_relationships(, attributes, relationships, caller_model = nil) persistence = JsonapiCompliable::Util::Persistence \ .new(self, , attributes, relationships, caller_model) persistence.run end |
#resolve(scope) ⇒ Array
How do you want to resolve the scope?
For ActiveRecord, when we want to actually fire SQL, it's
#to_a.
This method must return an array of resolved model objects.
By default, delegates to the adapter. You likely want to alter your adapter rather than override this directly.
682 683 684 |
# File 'lib/jsonapi_compliable/resource.rb', line 682 def resolve(scope) adapter.resolve(scope) end |
#sideload ⇒ Object
123 |
# File 'lib/jsonapi_compliable/resource.rb', line 123 def_delegator :sideloading, :sideload |
#sideloading ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Interface to the sideloads for this Resource
573 574 575 |
# File 'lib/jsonapi_compliable/resource.rb', line 573 def sideloading self.class.sideloading end |
#sorting ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
610 611 612 |
# File 'lib/jsonapi_compliable/resource.rb', line 610 def sorting self.class.config[:sorting] end |
#stat(attribute, calculation) ⇒ Proc
The relevant proc for the given attribute and calculation.
Raises JsonapiCompliable::Errors::StatNotFound if not corresponding
stat has been configured.
565 566 567 568 569 |
# File 'lib/jsonapi_compliable/resource.rb', line 565 def stat(attribute, calculation) stats_dsl = stats[attribute] || stats[attribute.to_sym] raise Errors::StatNotFound.new(attribute, calculation) unless stats_dsl stats_dsl.calculation(calculation) end |
#stats ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
616 617 618 |
# File 'lib/jsonapi_compliable/resource.rb', line 616 def stats self.class.config[:stats] end |
#transaction ⇒ Object
How to run write requests within a transaction.
Should roll back the transaction, but avoid bubbling up the error,
if JsonapiCompliable::Errors::ValidationError is raised within
the block.
By default, delegates to the adapter. You likely want to alter your adapter rather than override this directly.
702 703 704 705 706 707 708 709 710 711 |
# File 'lib/jsonapi_compliable/resource.rb', line 702 def transaction response = nil begin adapter.transaction(model) do response = yield end rescue Errors::ValidationError end response end |
#type ⇒ Object
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Returns :undefined_jsonapi_type when not configured.
598 599 600 |
# File 'lib/jsonapi_compliable/resource.rb', line 598 def type self.class.config[:type] || :undefined_jsonapi_type end |
#update(update_params) ⇒ Object
Update the relevant model. You must configure a model (see .model) to update. If you override, you must return the updated instance.
492 493 494 |
# File 'lib/jsonapi_compliable/resource.rb', line 492 def update(update_params) adapter.update(model, update_params) end |
#with_context(object, namespace = nil) ⇒ Object
Run code within a given context. Useful for running code within, say, a Rails controller context
When using Rails, controller actions are wrapped this way.
416 417 418 419 420 |
# File 'lib/jsonapi_compliable/resource.rb', line 416 def with_context(object, namespace = nil) JsonapiCompliable.with_context(object, namespace) do yield end end |