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.
-
.before_commit(only: [:create, :update, :destroy], &blk) ⇒ Object
Register a hook that fires AFTER all validation logic has run - including validation of nested objects - but BEFORE the transaction has closed.
- .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
-
#before_commit(model, method) ⇒ Object
private
Actually fire the before commit hooks.
-
#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.
407 408 409 |
# File 'lib/jsonapi_compliable/resource.rb', line 407 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]
453 454 455 |
# File 'lib/jsonapi_compliable/resource.rb', line 453 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 |
.before_commit(only: [:create, :update, :destroy], &blk) ⇒ Object
Register a hook that fires AFTER all validation logic has run - including validation of nested objects - but BEFORE the transaction has closed.
Helpful for things like “contact this external service after persisting data, but roll everything back if there’s an error making the service call”
268 269 270 271 272 |
# File 'lib/jsonapi_compliable/resource.rb', line 268 def self.before_commit(only: [:create, :update, :destroy], &blk) Array(only).each do |verb| config[:before_commit][verb] = blk end 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.
387 388 389 |
# File 'lib/jsonapi_compliable/resource.rb', line 387 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.
398 399 400 |
# File 'lib/jsonapi_compliable/resource.rb', line 398 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 }]
359 360 361 |
# File 'lib/jsonapi_compliable/resource.rb', line 359 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.
335 336 337 |
# File 'lib/jsonapi_compliable/resource.rb', line 335 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
313 314 315 |
# File 'lib/jsonapi_compliable/resource.rb', line 313 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
298 299 300 |
# File 'lib/jsonapi_compliable/resource.rb', line 298 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.
381 382 383 |
# File 'lib/jsonapi_compliable/resource.rb', line 381 def self.type(value = nil) config[:type] = value end |
.use_adapter(klass) ⇒ Object
Configure the adapter you want to use.
346 347 348 |
# File 'lib/jsonapi_compliable/resource.rb', line 346 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.
670 671 672 |
# File 'lib/jsonapi_compliable/resource.rb', line 670 def adapter self.class.config[:adapter] end |
#associate(parent, child, association_name, type) ⇒ Object
Delegates #associate to adapter. Built for overriding.
545 546 547 |
# File 'lib/jsonapi_compliable/resource.rb', line 545 def associate(parent, child, association_name, type) adapter.associate(parent, child, association_name, type) end |
#association_names ⇒ Object
566 567 568 |
# File 'lib/jsonapi_compliable/resource.rb', line 566 def association_names sideloading.association_names end |
#before_commit(model, method) ⇒ 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.
Actually fire the before commit hooks
278 279 280 281 |
# File 'lib/jsonapi_compliable/resource.rb', line 278 def before_commit(model, method) hook = self.class.config[:before_commit][method] hook.call(model) if hook end |
#build_scope(base, query, opts = {}) ⇒ Scope
Build a scope using this Resource configuration
Essentially “api private”, but can be useful for testing.
478 479 480 |
# File 'lib/jsonapi_compliable/resource.rb', line 478 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]
464 465 466 |
# File 'lib/jsonapi_compliable/resource.rb', line 464 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.
497 498 499 |
# File 'lib/jsonapi_compliable/resource.rb', line 497 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.
658 659 660 |
# File 'lib/jsonapi_compliable/resource.rb', line 658 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.
609 610 611 |
# File 'lib/jsonapi_compliable/resource.rb', line 609 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.
615 616 617 |
# File 'lib/jsonapi_compliable/resource.rb', line 615 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.
603 604 605 |
# File 'lib/jsonapi_compliable/resource.rb', line 603 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.
536 537 538 |
# File 'lib/jsonapi_compliable/resource.rb', line 536 def destroy(id) adapter.destroy(model, id) end |
#disassociate(parent, child, association_name, type) ⇒ Object
Delegates #disassociate to adapter. Built for overriding.
554 555 556 |
# File 'lib/jsonapi_compliable/resource.rb', line 554 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.
652 653 654 |
# File 'lib/jsonapi_compliable/resource.rb', line 652 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.
628 629 630 |
# File 'lib/jsonapi_compliable/resource.rb', line 628 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.
664 665 666 |
# File 'lib/jsonapi_compliable/resource.rb', line 664 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.
646 647 648 |
# File 'lib/jsonapi_compliable/resource.rb', line 646 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.
559 560 561 562 563 |
# File 'lib/jsonapi_compliable/resource.rb', line 559 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.
706 707 708 |
# File 'lib/jsonapi_compliable/resource.rb', line 706 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
597 598 599 |
# File 'lib/jsonapi_compliable/resource.rb', line 597 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.
634 635 636 |
# File 'lib/jsonapi_compliable/resource.rb', line 634 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.
589 590 591 592 593 |
# File 'lib/jsonapi_compliable/resource.rb', line 589 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.
640 641 642 |
# File 'lib/jsonapi_compliable/resource.rb', line 640 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.
726 727 728 729 730 731 732 733 734 735 736 |
# File 'lib/jsonapi_compliable/resource.rb', line 726 def transaction response = nil begin adapter.transaction(model) do response = yield end rescue Errors::ValidationError => e response = e.validation_response 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.
622 623 624 |
# File 'lib/jsonapi_compliable/resource.rb', line 622 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.
516 517 518 |
# File 'lib/jsonapi_compliable/resource.rb', line 516 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.
440 441 442 443 444 |
# File 'lib/jsonapi_compliable/resource.rb', line 440 def with_context(object, namespace = nil) JsonapiCompliable.with_context(object, namespace) do yield end end |