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
-
.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.
-
#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.
- #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.
413 414 415 |
# File 'lib/jsonapi_compliable/resource.rb', line 413 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+
459 460 461 |
# File 'lib/jsonapi_compliable/resource.rb', line 459 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.
199 200 201 202 203 204 205 206 207 |
# File 'lib/jsonapi_compliable/resource.rb', line 199 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*.
238 239 240 241 242 |
# File 'lib/jsonapi_compliable/resource.rb', line 238 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
:
266 267 268 269 270 |
# File 'lib/jsonapi_compliable/resource.rb', line 266 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.
393 394 395 |
# File 'lib/jsonapi_compliable/resource.rb', line 393 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.
404 405 406 |
# File 'lib/jsonapi_compliable/resource.rb', line 404 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 }]
365 366 367 |
# File 'lib/jsonapi_compliable/resource.rb', line 365 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.
341 342 343 |
# File 'lib/jsonapi_compliable/resource.rb', line 341 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 ;)
285 286 287 |
# File 'lib/jsonapi_compliable/resource.rb', line 285 def self.model(klass) config[:model] = klass end |
.paginate {|scope, current_page, per_page| ... } ⇒ Object
Define custom pagination logic
319 320 321 |
# File 'lib/jsonapi_compliable/resource.rb', line 319 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 |
.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}
161 162 163 |
# File 'lib/jsonapi_compliable/resource.rb', line 161 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.
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
304 305 306 |
# File 'lib/jsonapi_compliable/resource.rb', line 304 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.
387 388 389 |
# File 'lib/jsonapi_compliable/resource.rb', line 387 def self.type(value = nil) config[:type] = value end |
.use_adapter(klass) ⇒ Object
Configure the adapter you want to use.
352 353 354 |
# File 'lib/jsonapi_compliable/resource.rb', line 352 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.
715 716 717 |
# File 'lib/jsonapi_compliable/resource.rb', line 715 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
598 599 600 601 602 603 604 605 606 607 |
# File 'lib/jsonapi_compliable/resource.rb', line 598 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]
563 564 565 566 567 568 569 570 571 |
# File 'lib/jsonapi_compliable/resource.rb', line 563 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.
484 485 486 |
# File 'lib/jsonapi_compliable/resource.rb', line 484 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+
470 471 472 |
# File 'lib/jsonapi_compliable/resource.rb', line 470 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.
503 504 505 |
# File 'lib/jsonapi_compliable/resource.rb', line 503 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.
703 704 705 |
# File 'lib/jsonapi_compliable/resource.rb', line 703 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.
648 649 650 |
# File 'lib/jsonapi_compliable/resource.rb', line 648 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.
654 655 656 |
# File 'lib/jsonapi_compliable/resource.rb', line 654 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.
642 643 644 |
# File 'lib/jsonapi_compliable/resource.rb', line 642 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.
542 543 544 |
# File 'lib/jsonapi_compliable/resource.rb', line 542 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.
691 692 693 |
# File 'lib/jsonapi_compliable/resource.rb', line 691 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.
667 668 669 |
# File 'lib/jsonapi_compliable/resource.rb', line 667 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.
709 710 711 |
# File 'lib/jsonapi_compliable/resource.rb', line 709 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.
685 686 687 |
# File 'lib/jsonapi_compliable/resource.rb', line 685 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.
547 548 549 550 551 |
# File 'lib/jsonapi_compliable/resource.rb', line 547 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.
751 752 753 |
# File 'lib/jsonapi_compliable/resource.rb', line 751 def resolve(scope) adapter.resolve(scope) end |
#sideload ⇒ Object
123 |
# File 'lib/jsonapi_compliable/resource.rb', line 123 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.
697 698 699 |
# File 'lib/jsonapi_compliable/resource.rb', line 697 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
636 637 638 |
# File 'lib/jsonapi_compliable/resource.rb', line 636 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.
673 674 675 |
# File 'lib/jsonapi_compliable/resource.rb', line 673 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.
628 629 630 631 632 |
# File 'lib/jsonapi_compliable/resource.rb', line 628 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.
679 680 681 |
# File 'lib/jsonapi_compliable/resource.rb', line 679 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.
771 772 773 774 775 776 777 778 779 780 |
# File 'lib/jsonapi_compliable/resource.rb', line 771 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.
661 662 663 |
# File 'lib/jsonapi_compliable/resource.rb', line 661 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.
522 523 524 |
# File 'lib/jsonapi_compliable/resource.rb', line 522 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.
446 447 448 449 450 |
# File 'lib/jsonapi_compliable/resource.rb', line 446 def with_context(object, namespace = nil) JsonapiCompliable.with_context(object, namespace) do yield end end |