Skip to content

Commit

Permalink
Add `show_many' action helper
Browse files Browse the repository at this point in the history
  • Loading branch information
mwpastore committed Dec 6, 2016
1 parent 69e5b7a commit caae6fa
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 14 deletions.
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ helpers, and the `resource`, `has_one`, and `has_many` keywords build
[Sinatra::Namespace][21] blocks. You can manage caching directives, set
headers, and even `halt` (or `not_found`, although such cases are usually
handled transparently by returning `nil` values or empty collections from
action helpers) as desired.
action helpers) as appropriate.

```ruby
class App < Sinatra::Base
Expand Down Expand Up @@ -541,6 +541,11 @@ Return an array of zero or more objects to serialize on the response.
Take an ID and return the corresponding object (or `nil` if not found) to
serialize on the response.

##### `show_many {|ids| ..}` => Array

Take an array of IDs and return an equally-lengthed array of objects to
serialize on the response. See "Coalesced Find Requests" below.

##### `create {|attr, id| ..}` => id, Object?

With client-generated IDs: Take a hash of (dedasherized) attributes and a
Expand Down Expand Up @@ -1116,7 +1121,7 @@ either `graft` or `create`.

`create` and `update` are the only two action helpers that trigger sideloading;
`graft`, `merge`, and `clear` are the only action helpers invoked by
sideloading. You must indicate which combinations are valid using the
sideloading. You must indicate which combinations are valid using the
`:sideload_on` action helper option. (Note that if you want to sideload `merge`
on `update`, you must define a `clear` action helper as well.) For example:

Expand Down Expand Up @@ -1251,6 +1256,11 @@ are supported: `?filter[id]=1,2` and `?filter[id][]=1&filter[id][]=2`. If any
ID is not found (i.e. `show` returns `nil`), the route will halt with HTTP
status 404.

Optionally, to reduce round trips to the database, you may define a "special"
`show_many` action helper that takes an array of IDs to show. It does not take
`:roles` or any other options and will only be invoked if the current user has
access to `show`. This feature is still experimental.

### Patchless Clients

{json:api} [recommends][23] supporting patchless clients by using the
Expand Down
4 changes: 4 additions & 0 deletions demo-app/classes/author.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ def settable_fields

show

show_many do |ids|
Author.where(id: ids.map!(&:to_i)).all
end

index do
Author.dataset
end
Expand Down
4 changes: 4 additions & 0 deletions demo-app/classes/post.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ def settable_fields
next find(slug), include: %w[author comments tags]
end

show_many do |slugs|
next Post.where(slug: slugs.map!(&:to_s)).all, include: %i[author tags]
end

index do
Post.dataset
end
Expand Down
2 changes: 1 addition & 1 deletion lib/sinja/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def initialize
@error_logger = ->(h) { logger.error('sinja') { h } }

@default_roles = {
:resource=>RolesConfig.new(%i[show index create update destroy]),
:resource=>RolesConfig.new(%i[show show_many index create update destroy]),
:has_many=>RolesConfig.new(%i[fetch merge subtract clear]),
:has_one=>RolesConfig.new(%i[pluck graft prune])
}
Expand Down
6 changes: 4 additions & 2 deletions lib/sinja/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,16 @@ def def_action_helper(context, action, allow_opts=[])
required_arity = {
:create=>2,
:index=>-1,
:fetch=>-1
:fetch=>-1,
:show_many=>-1
}.freeze[action] || 1

define_method(action) do |*args|
raise ArgumentError, "Unexpected argument(s) for `#{action}' action helper" \
unless args.length == block.arity

public_send("before_#{action}", *args) if respond_to?("before_#{action}")
public_send("before_#{action}", *args) \
if respond_to?("before_#{action}")

case result = instance_exec(*args, &block)
when Array
Expand Down
24 changes: 16 additions & 8 deletions lib/sinja/resource_routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module Sinja
module ResourceRoutes
def self.registered(app)
app.def_action_helper(app, :show, :roles)
app.def_action_helper(app, :show_many)
app.def_action_helper(app, :index, %i[roles filter_by sort_by])
app.def_action_helper(app, :create, :roles)
app.def_action_helper(app, :update, :roles)
Expand All @@ -15,16 +16,23 @@ def self.registered(app)
app.get '', :qcapture=>{ :filter=>:id }, :qparams=>%i[include fields], :actions=>:show do
ids = @qcaptures.first # TODO: Get this as a block parameter?
ids = ids.split(',') if String === ids
ids = [*ids].tap(&:uniq!)

opts = {}
resources = [*ids].tap(&:uniq!).map! do |id|
tmp, opts = show(id)
raise NotFoundError, "Resource '#{id}' not found" unless tmp
tmp
end
if respond_to?(:show_many)
resources, opts = show_many(ids)
raise NotFoundError, "Resource(s) not found" \
unless ids.length == resources.length
serialize_models(resources, opts)
else
opts = {}
resources = ids.map! do |id|
tmp, opts = show(id)
raise NotFoundError, "Resource '#{id}' not found" unless tmp
tmp
end

# TODO: Serialize collection with opts from last model found?
serialize_models(resources, opts)
serialize_models(resources, opts)
end
end

app.head '' do
Expand Down
42 changes: 42 additions & 0 deletions test/integration/author_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,48 @@ def app
Sinatra::Application.new
end

def test_uncoalesced_find_head
head '/authors'
assert_ok
assert_equal 'GET,POST', last_response.headers['Allow']
end

def test_uncoalesced_find
DB[:authors].multi_insert [
{ :email=>'dilbert@example.com', :display_name=>'Dilbert' },
{ :email=>'dogbert@example.com', :display_name=>'Dogbert' },
{ :email=>'catbert@example.com', :display_name=>'Catbert' },
{ :email=>'wally@example.com', :display_name=>'Wally' }
]
get '/authors'
assert_ok
vals = json[:data].map do |t|
name = t[:attributes][:'display-name']
name = nil if name == 'Anonymous Coward'
{ :id=>t[:id].to_i, :display_name=>name }
end
assert_equal DB[:authors].select(:id, :display_name).all, vals
end

def test_coalesced_find_head
head '/tags?filter[id]=2,4'
assert_ok
assert_equal 'GET', last_response.headers['Allow']
end

def test_coalesced_find
DB[:authors].multi_insert [
{ :email=>'dilbert@example.com', :display_name=>'Dilbert' },
{ :email=>'dogbert@example.com', :display_name=>'Dogbert' },
{ :email=>'catbert@example.com', :display_name=>'Catbert' },
{ :email=>'wally@example.com', :display_name=>'Wally' }
]
get '/authors?filter[id]=2,4'
assert_ok
vals = json[:data].map { |t| { :id=>t[:id].to_i, :display_name=>t[:attributes][:'display-name'] } }
assert_equal DB[:authors].where(:id=>[2, 4]).select(:id, :display_name).all, vals
end

def test_disallow_client_generated_id
post '/authors', JSON.generate(:data=>{
:type=>'authors',
Expand Down
1 change: 0 additions & 1 deletion test/integration/tag_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ class PostTest < SequelTest
include Rack::Test::Methods

def app
Sinatra::Application.dump_errors = true
Sinatra::Application.new
end

Expand Down

0 comments on commit caae6fa

Please sign in to comment.