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.
383 384 385 |
# File 'lib/jsonapi_compliable/resource.rb', line 383 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+
428 429 430 |
# File 'lib/jsonapi_compliable/resource.rb', line 428 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 |
# 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 } 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*.
208 209 210 211 212 |
# File 'lib/jsonapi_compliable/resource.rb', line 208 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
:
236 237 238 239 240 |
# File 'lib/jsonapi_compliable/resource.rb', line 236 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.
363 364 365 |
# File 'lib/jsonapi_compliable/resource.rb', line 363 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.
374 375 376 |
# File 'lib/jsonapi_compliable/resource.rb', line 374 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 }]
335 336 337 |
# File 'lib/jsonapi_compliable/resource.rb', line 335 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.
311 312 313 |
# File 'lib/jsonapi_compliable/resource.rb', line 311 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 ;)
255 256 257 |
# File 'lib/jsonapi_compliable/resource.rb', line 255 def self.model(klass) config[:model] = klass end |
.paginate {|scope, current_page, per_page| ... } ⇒ Object
Define custom pagination logic
289 290 291 |
# File 'lib/jsonapi_compliable/resource.rb', line 289 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
274 275 276 |
# File 'lib/jsonapi_compliable/resource.rb', line 274 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.
357 358 359 |
# File 'lib/jsonapi_compliable/resource.rb', line 357 def self.type(value = nil) config[:type] = value end |
.use_adapter(klass) ⇒ Object
Configure the adapter you want to use.
322 323 324 |
# File 'lib/jsonapi_compliable/resource.rb', line 322 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.
645 646 647 |
# File 'lib/jsonapi_compliable/resource.rb', line 645 def adapter self.class.config[:adapter] end |
#associate(parent, child, association_name, type) ⇒ Object
Delegates #associate to adapter. Built for overriding.
520 521 522 |
# File 'lib/jsonapi_compliable/resource.rb', line 520 def associate(parent, child, association_name, type) adapter.associate(parent, child, association_name, type) end |
#association_names ⇒ Object
541 542 543 |
# File 'lib/jsonapi_compliable/resource.rb', line 541 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.
453 454 455 |
# File 'lib/jsonapi_compliable/resource.rb', line 453 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+
439 440 441 |
# File 'lib/jsonapi_compliable/resource.rb', line 439 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.
472 473 474 |
# File 'lib/jsonapi_compliable/resource.rb', line 472 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.
633 634 635 |
# File 'lib/jsonapi_compliable/resource.rb', line 633 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.
584 585 586 |
# File 'lib/jsonapi_compliable/resource.rb', line 584 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.
590 591 592 |
# File 'lib/jsonapi_compliable/resource.rb', line 590 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.
578 579 580 |
# File 'lib/jsonapi_compliable/resource.rb', line 578 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.
511 512 513 |
# File 'lib/jsonapi_compliable/resource.rb', line 511 def destroy(id) adapter.destroy(model, id) end |
#disassociate(parent, child, association_name, type) ⇒ Object
Delegates #disassociate to adapter. Built for overriding.
529 530 531 |
# File 'lib/jsonapi_compliable/resource.rb', line 529 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.
627 628 629 |
# File 'lib/jsonapi_compliable/resource.rb', line 627 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.
603 604 605 |
# File 'lib/jsonapi_compliable/resource.rb', line 603 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.
639 640 641 |
# File 'lib/jsonapi_compliable/resource.rb', line 639 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.
621 622 623 |
# File 'lib/jsonapi_compliable/resource.rb', line 621 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.
534 535 536 537 538 |
# File 'lib/jsonapi_compliable/resource.rb', line 534 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.
681 682 683 |
# File 'lib/jsonapi_compliable/resource.rb', line 681 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
572 573 574 |
# File 'lib/jsonapi_compliable/resource.rb', line 572 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.
609 610 611 |
# File 'lib/jsonapi_compliable/resource.rb', line 609 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.
564 565 566 567 568 |
# File 'lib/jsonapi_compliable/resource.rb', line 564 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.
615 616 617 |
# File 'lib/jsonapi_compliable/resource.rb', line 615 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.
701 702 703 704 705 706 707 708 709 710 |
# File 'lib/jsonapi_compliable/resource.rb', line 701 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.
597 598 599 |
# File 'lib/jsonapi_compliable/resource.rb', line 597 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.
491 492 493 |
# File 'lib/jsonapi_compliable/resource.rb', line 491 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.
415 416 417 418 419 |
# File 'lib/jsonapi_compliable/resource.rb', line 415 def with_context(object, namespace = nil) JsonapiCompliable.with_context(object, namespace) do yield end end |