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 ⇒ Hash
readonly
The current context set by
#with_contextin the form of.
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
-
.sideload_whitelist(whitelist) ⇒ Object
Set the sideload whitelist.
- .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
-
#allowed_sideloads(namespace = nil) ⇒ Hash
An Include Directive Hash of all possible sideloads for the current context namespace, taking into account the sideload whitelist.
-
#association_names ⇒ Array<Symbol>
All possible sideload names, including nested names.
-
#build_scope(base, query, opts = {}) ⇒ Scope
Build a scope using this Resource configuration.
-
#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.
- #extra_fields ⇒ Object private
- #filters ⇒ Object private
- #model ⇒ Object private
- #pagination ⇒ Object private
- #persist_with_relationships(meta, attributes, relationships) ⇒ Object private
-
#resolve(scope) ⇒ Array
How do you want to resolve the scope?.
- #sideload ⇒ Object
- #sideload_whitelist ⇒ Object private
-
#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.
415 416 417 |
# File 'lib/jsonapi_compliable/resource.rb', line 415 def config @config end |
Instance Attribute Details
#context ⇒ Hash (readonly)
The current context set by #with_context in the form of
{ object: context_obj, namespace: :index }
92 93 94 |
# File 'lib/jsonapi_compliable/resource.rb', line 92 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.
201 202 203 204 205 206 207 208 209 |
# File 'lib/jsonapi_compliable/resource.rb', line 201 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
102 |
# File 'lib/jsonapi_compliable/resource.rb', line 102 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.
240 241 242 243 244 |
# File 'lib/jsonapi_compliable/resource.rb', line 240 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
111 |
# File 'lib/jsonapi_compliable/resource.rb', line 111 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:
268 269 270 271 272 |
# File 'lib/jsonapi_compliable/resource.rb', line 268 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.
395 396 397 |
# File 'lib/jsonapi_compliable/resource.rb', line 395 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.
406 407 408 |
# File 'lib/jsonapi_compliable/resource.rb', line 406 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 }]
367 368 369 |
# File 'lib/jsonapi_compliable/resource.rb', line 367 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.
343 344 345 |
# File 'lib/jsonapi_compliable/resource.rb', line 343 def self.extra_field(name, &blk) config[:extra_fields][name] = blk end |
.has_and_belongs_to_many ⇒ Object
114 |
# File 'lib/jsonapi_compliable/resource.rb', line 114 def_delegator :sideloading, :has_and_belongs_to_many |
.has_many ⇒ Object
105 |
# File 'lib/jsonapi_compliable/resource.rb', line 105 def_delegator :sideloading, :has_many |
.has_one ⇒ Object
108 |
# File 'lib/jsonapi_compliable/resource.rb', line 108 def_delegator :sideloading, :has_one |
.inherited(klass) ⇒ Object
128 129 130 |
# File 'lib/jsonapi_compliable/resource.rb', line 128 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 ;)
287 288 289 |
# File 'lib/jsonapi_compliable/resource.rb', line 287 def self.model(klass) config[:model] = klass end |
.paginate {|scope, current_page, per_page| ... } ⇒ Object
Define custom pagination logic
321 322 323 |
# File 'lib/jsonapi_compliable/resource.rb', line 321 def self.paginate(&blk) config[:pagination] = blk end |
.polymorphic_belongs_to ⇒ Object
117 |
# File 'lib/jsonapi_compliable/resource.rb', line 117 def_delegator :sideloading, :polymorphic_belongs_to |
.polymorphic_has_many ⇒ Object
120 |
# File 'lib/jsonapi_compliable/resource.rb', line 120 def_delegator :sideloading, :polymorphic_has_many |
.sideload_whitelist(whitelist) ⇒ Object
Set the sideload whitelist. You may want to omit sideloads for security or performance reasons.
Uses JSONAPI::IncludeDirective from jsonapi-rb}
163 164 165 |
# File 'lib/jsonapi_compliable/resource.rb', line 163 def self.sideload_whitelist(whitelist) config[:sideload_whitelist] = JSONAPI::IncludeDirective.new(whitelist).to_hash end |
.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.
133 134 135 |
# File 'lib/jsonapi_compliable/resource.rb', line 133 def self.sideloading @sideloading ||= Sideload.new(:base, resource: self) end |
.sort {|scope, att, dir| ... } ⇒ Object
Define custom sorting logic
306 307 308 |
# File 'lib/jsonapi_compliable/resource.rb', line 306 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.
389 390 391 |
# File 'lib/jsonapi_compliable/resource.rb', line 389 def self.type(value = nil) config[:type] = value end |
.use_adapter(klass) ⇒ Object
Configure the adapter you want to use.
354 355 356 |
# File 'lib/jsonapi_compliable/resource.rb', line 354 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.
709 710 711 |
# File 'lib/jsonapi_compliable/resource.rb', line 709 def adapter self.class.config[:adapter] end |
#allowed_sideloads(namespace = nil) ⇒ Hash
An Include Directive Hash of all possible sideloads for the current context namespace, taking into account the sideload whitelist.
In other words, say we have this resource:
class PostResource < ApplicationResource
sideload_whitelist({
index: :comments,
show: { comments: :author }
})
end
Expected behavior:
allowed_sideloads(:index) # => { comments: {} }
allowed_sideloads(:show) # => { comments: { author: {} }
instance.with_context({}, :index) do
instance.allowed_sideloads # => { comments: {} }
end
592 593 594 595 596 597 598 599 600 601 |
# File 'lib/jsonapi_compliable/resource.rb', line 592 def allowed_sideloads(namespace = nil) return {} unless sideloading namespace ||= context[:namespace] sideloads = sideloading.to_hash[:base] if !sideload_whitelist.empty? && namespace sideloads = Util::IncludeParams.scrub(sideloads, sideload_whitelist[namespace]) end sideloads end |
#association_names ⇒ Array<Symbol>
All possible sideload names, including nested names
{ comments: { author: {} } }
Becomes
[:comments, :author]
557 558 559 560 561 562 563 564 565 |
# File 'lib/jsonapi_compliable/resource.rb', line 557 def association_names @association_names ||= begin if sideloading Util::Hash.keys(sideloading.to_hash[:base]) else [] end end 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 |
#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.
697 698 699 |
# File 'lib/jsonapi_compliable/resource.rb', line 697 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.
642 643 644 |
# File 'lib/jsonapi_compliable/resource.rb', line 642 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.
648 649 650 |
# File 'lib/jsonapi_compliable/resource.rb', line 648 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.
636 637 638 |
# File 'lib/jsonapi_compliable/resource.rb', line 636 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 |
#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.
685 686 687 |
# File 'lib/jsonapi_compliable/resource.rb', line 685 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.
661 662 663 |
# File 'lib/jsonapi_compliable/resource.rb', line 661 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.
703 704 705 |
# File 'lib/jsonapi_compliable/resource.rb', line 703 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.
679 680 681 |
# File 'lib/jsonapi_compliable/resource.rb', line 679 def pagination self.class.config[:pagination] end |
#persist_with_relationships(meta, attributes, relationships) ⇒ 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.
541 542 543 544 545 |
# File 'lib/jsonapi_compliable/resource.rb', line 541 def persist_with_relationships(, attributes, relationships) persistence = JsonapiCompliable::Util::Persistence \ .new(self, , attributes, relationships) 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.
745 746 747 |
# File 'lib/jsonapi_compliable/resource.rb', line 745 def resolve(scope) adapter.resolve(scope) end |
#sideload ⇒ Object
125 |
# File 'lib/jsonapi_compliable/resource.rb', line 125 def_delegator :sideloading, :sideload |
#sideload_whitelist ⇒ 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.
691 692 693 |
# File 'lib/jsonapi_compliable/resource.rb', line 691 def sideload_whitelist self.class.config[:sideload_whitelist] end |
#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
630 631 632 |
# File 'lib/jsonapi_compliable/resource.rb', line 630 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.
667 668 669 |
# File 'lib/jsonapi_compliable/resource.rb', line 667 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.
622 623 624 625 626 |
# File 'lib/jsonapi_compliable/resource.rb', line 622 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.
673 674 675 |
# File 'lib/jsonapi_compliable/resource.rb', line 673 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.
765 766 767 768 769 770 771 772 773 774 |
# File 'lib/jsonapi_compliable/resource.rb', line 765 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.
655 656 657 |
# File 'lib/jsonapi_compliable/resource.rb', line 655 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.
448 449 450 451 452 453 454 455 456 |
# File 'lib/jsonapi_compliable/resource.rb', line 448 def with_context(object, namespace = nil) begin prior = context @context = { object: object, namespace: namespace } yield ensure @context = prior end end |