From f83fad789920e6d6236a9dc0623320a0f1f82dcd Mon Sep 17 00:00:00 2001 From: Ari Summer Date: Fri, 28 Nov 2014 15:19:49 -0700 Subject: [PATCH 1/3] Add PageRepresenter --- lib/roar-rails.rb | 1 + lib/roar/rails/page_representer.rb | 36 ++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 lib/roar/rails/page_representer.rb diff --git a/lib/roar-rails.rb b/lib/roar-rails.rb index 4a2c570..fa87b0b 100644 --- a/lib/roar-rails.rb +++ b/lib/roar-rails.rb @@ -39,3 +39,4 @@ def self.rails_version end require "roar/rails/controller_additions" +require "roar/rails/page_representer" diff --git a/lib/roar/rails/page_representer.rb b/lib/roar/rails/page_representer.rb new file mode 100644 index 0000000..b628db8 --- /dev/null +++ b/lib/roar/rails/page_representer.rb @@ -0,0 +1,36 @@ +module Roar + module Rails + module PageRepresenter + extend ActiveSupport::Concern + + def page_url(args) + raise NotImplementedError + end + + included do + property :total_entries + + link :self do |opts| + page_url( + :page => represented.current_page, + :per_page => represented.per_page + ) + end + + link :next do |opts| + page_url( + :page => represented.next_page, + :per_page => represented.per_page + ) if represented.next_page + end + + link :previous do |opts| + page_url( + :page => represented.previous_page, + :per_page => represented.per_page + ) if represented.previous_page + end + end + end + end +end From 4cff838b0c74ac19e8cf3c8bf3898e5451604973 Mon Sep 17 00:00:00 2001 From: Ari Summer Date: Fri, 28 Nov 2014 15:25:41 -0700 Subject: [PATCH 2/3] PageRepresenter tests --- roar-rails.gemspec | 2 + .../app/controllers/venues_controller.rb | 32 +++++++++++ .../app/representers/venue_representer.rb | 6 ++ .../app/representers/venues_representer.rb | 15 +++++ test/dummy/config/routes.rb | 1 + test/representer_test.rb | 56 ++++++++++++++++++- 6 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 test/dummy/app/controllers/venues_controller.rb create mode 100644 test/dummy/app/representers/venue_representer.rb create mode 100644 test/dummy/app/representers/venues_representer.rb diff --git a/roar-rails.gemspec b/roar-rails.gemspec index 0f810d8..8bfaed9 100644 --- a/roar-rails.gemspec +++ b/roar-rails.gemspec @@ -29,4 +29,6 @@ Gem::Specification.new do |s| s.add_development_dependency "activerecord" s.add_development_dependency "sqlite3" s.add_development_dependency "tzinfo" # FIXME: why the hell do we need this for 3.1? + s.add_development_dependency "will_paginate" + s.add_development_dependency "kaminari" end diff --git a/test/dummy/app/controllers/venues_controller.rb b/test/dummy/app/controllers/venues_controller.rb new file mode 100644 index 0000000..f39346d --- /dev/null +++ b/test/dummy/app/controllers/venues_controller.rb @@ -0,0 +1,32 @@ +Venue = Struct.new(:name) + +class VenuesController < ActionController::Base + include Roar::Rails::ControllerAdditions + represents :json, Venue + + def index + venues = [ + Venue.new("Red Rocks"), + Venue.new("The Gorge"), + Venue.new("Jazz Club") + ] + + if defined? WillPaginate + venues = venues.paginate( + :page => params[:page], + :per_page => params[:per_page] + ) + elsif defined? Kaminari + venues = Kaminari + .paginate_array(venues) + .page(params[:page]) + .per(params[:per_page]) + end + + respond_with venues + end + + def show + respond_with Venue.new("Red Rocks") + end +end diff --git a/test/dummy/app/representers/venue_representer.rb b/test/dummy/app/representers/venue_representer.rb new file mode 100644 index 0000000..8866a6e --- /dev/null +++ b/test/dummy/app/representers/venue_representer.rb @@ -0,0 +1,6 @@ +class VenueRepresenter < Roar::Decorator + include Roar::Representer::JSON + include Roar::Representer::Feature::Hypermedia + + property :name +end diff --git a/test/dummy/app/representers/venues_representer.rb b/test/dummy/app/representers/venues_representer.rb new file mode 100644 index 0000000..8ba9643 --- /dev/null +++ b/test/dummy/app/representers/venues_representer.rb @@ -0,0 +1,15 @@ +class VenuesRepresenter < Roar::Decorator + include Roar::Representer::JSON + include Roar::Representer::Feature::Hypermedia + include Roar::Rails::PageRepresenter + + collection :venues, :exec_context => :decorator, :decorator => VenueRepresenter + + def venues + represented + end + + def page_url(args) + venues_url args + end +end diff --git a/test/dummy/config/routes.rb b/test/dummy/config/routes.rb index 856a8b3..be91aeb 100644 --- a/test/dummy/config/routes.rb +++ b/test/dummy/config/routes.rb @@ -5,4 +5,5 @@ delete ':controller(/:action(/:id(.:format)))' resources :singers resources :bands + resources :venues end diff --git a/test/representer_test.rb b/test/representer_test.rb index 77cd3b2..96485f1 100644 --- a/test/representer_test.rb +++ b/test/representer_test.rb @@ -36,4 +36,58 @@ class DecoratorTest < ActionController::TestCase get :show, :id => 1, :format => :json assert_body "{\"name\":\"Bodyjar\",\"links\":[{\"rel\":\"self\",\"href\":\"http://roar.apotomo.de/bands/Bodyjar\"}]}" end -end \ No newline at end of file + + class PageRepresenterTest < ActionController::TestCase + include Roar::Rails::TestCase + + tests VenuesController + + class WillPaginateTest < PageRepresenterTest + require "will_paginate/array" + + test "it renders a paginated response with no previous or next page" do + get :index, :format => :json + assert_body "{\"total_entries\":3,\"links\":[{\"rel\":\"self\",\"href\":\"http://roar.apotomo.de/venues?page=1\\u0026per_page=30\"}],\"venues\":[{\"name\":\"Red Rocks\"},{\"name\":\"The Gorge\"},{\"name\":\"Jazz Club\"}]}" + end + + test "it renders a paginated response with a previous and next page" do + get :index, :format => :json, :page => 2, :per_page => 1 + assert_body "{\"total_entries\":3,\"links\":[{\"rel\":\"self\",\"href\":\"http://roar.apotomo.de/venues?page=2\\u0026per_page=1\"},{\"rel\":\"next\",\"href\":\"http://roar.apotomo.de/venues?page=3\\u0026per_page=1\"},{\"rel\":\"previous\",\"href\":\"http://roar.apotomo.de/venues?page=1\\u0026per_page=1\"}],\"venues\":[{\"name\":\"The Gorge\"}]}" + end + + test "it renders a paginated response with a previous and no next page" do + get :index, :format => :json, :page => 3, :per_page => 1 + assert_body "{\"total_entries\":3,\"links\":[{\"rel\":\"self\",\"href\":\"http://roar.apotomo.de/venues?page=3\\u0026per_page=1\"},{\"rel\":\"previous\",\"href\":\"http://roar.apotomo.de/venues?page=2\\u0026per_page=1\"}],\"venues\":[{\"name\":\"Jazz Club\"}]}" + end + + test "it renders a paginated response with a next page and no previous page" do + get :index, :format => :json, :page => 1, :per_page => 1 + assert_body "{\"total_entries\":3,\"links\":[{\"rel\":\"self\",\"href\":\"http://roar.apotomo.de/venues?page=1\\u0026per_page=1\"},{\"rel\":\"next\",\"href\":\"http://roar.apotomo.de/venues?page=2\\u0026per_page=1\"}],\"venues\":[{\"name\":\"Red Rocks\"}]}" + end + end + + class KaminariTest < PageRepresenterTest + require "kaminari" + + test "it renders a paginated response with no previous or next page" do + get :index, :format => :json + assert_body "{\"total_entries\":3,\"links\":[{\"rel\":\"self\",\"href\":\"http://roar.apotomo.de/venues?page=1\\u0026per_page=30\"}],\"venues\":[{\"name\":\"Red Rocks\"},{\"name\":\"The Gorge\"},{\"name\":\"Jazz Club\"}]}" + end + + test "it renders a paginated response with a previous and next page" do + get :index, :format => :json, :page => 2, :per_page => 1 + assert_body "{\"total_entries\":3,\"links\":[{\"rel\":\"self\",\"href\":\"http://roar.apotomo.de/venues?page=2\\u0026per_page=1\"},{\"rel\":\"next\",\"href\":\"http://roar.apotomo.de/venues?page=3\\u0026per_page=1\"},{\"rel\":\"previous\",\"href\":\"http://roar.apotomo.de/venues?page=1\\u0026per_page=1\"}],\"venues\":[{\"name\":\"The Gorge\"}]}" + end + + test "it renders a paginated response with a previous and no next page" do + get :index, :format => :json, :page => 3, :per_page => 1 + assert_body "{\"total_entries\":3,\"links\":[{\"rel\":\"self\",\"href\":\"http://roar.apotomo.de/venues?page=3\\u0026per_page=1\"},{\"rel\":\"previous\",\"href\":\"http://roar.apotomo.de/venues?page=2\\u0026per_page=1\"}],\"venues\":[{\"name\":\"Jazz Club\"}]}" + end + + test "it renders a paginated response with a next page and no previous page" do + get :index, :format => :json, :page => 1, :per_page => 1 + assert_body "{\"total_entries\":3,\"links\":[{\"rel\":\"self\",\"href\":\"http://roar.apotomo.de/venues?page=1\\u0026per_page=1\"},{\"rel\":\"next\",\"href\":\"http://roar.apotomo.de/venues?page=2\\u0026per_page=1\"}],\"venues\":[{\"name\":\"Red Rocks\"}]}" + end + end + end +end From 734ba940032d9e4b1ba653cfe95f6c8c78956886 Mon Sep 17 00:00:00 2001 From: Ari Summer Date: Fri, 28 Nov 2014 22:15:51 -0700 Subject: [PATCH 3/3] Update README for PageRepresenter --- README.markdown | 112 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/README.markdown b/README.markdown index c9dac40..b7c3536 100644 --- a/README.markdown +++ b/README.markdown @@ -181,6 +181,118 @@ end In decorators' link blocks you currently have to use `represented` to get the actual represented model (this is `self` in module representers). +### Pagination with Decorators +Roar-rails pagination works with either [will_paginate](https://github.com/mislav/will_paginate) or [Kaminari](https://github.com/amatsuda/kaminari). It is based on Nick Sutterer's [blog post](http://nicksda.apotomo.de/2012/05/ruby-on-rest-6-pagination-with-roar/). You must install one of these for roar-rails pagination to work. In your Gemfile, include either + +```ruby +gem 'will_paginate' +``` +or + +```ruby +gem 'kaminari' +``` + +To use roar-rails pagination, include `Roar::Rails::PageRepresenter` in the paginated Representer and define `page_url`. + +```ruby +class VenuesRepresenter < Roar::Decorator + include Roar::Representer::JSON + include Roar::Representer::Feature::Hypermedia + include Roar::Rails::PageRepresenter + + collection :venues, :exec_context => :decorator, :decorator => VenueRepresenter + + def venues + represented + end + + def page_url(args) + venues_url args # Using Rails URL helpers + end +end + +class VenueRepresenter < Roar::Decorator + include Roar::Representer::JSON + include Roar::Representer::Feature::Hypermedia + + property :name +end + +# Using will_paginate +class VenuesController < ActionController::Base + include Roar::Rails::ControllerAdditions + represents :json, Venue + + def index + @venues = Venue.paginate(:page => params[:page], :per_page => params[:per_page]) + + respond_with @venues + end +end + +# Using Kaminari +class VenuesController < ActionController::Base + include Roar::Rails::ControllerAdditions + represents :json, Venue + + def index + @venues = Venue.page(params[:page]).per(params[:per_page]) + + respond_with @venues + end +end +``` + +**GET** `/venues?page=2&per_page=1` would give you a response similar to: + +```json +{ + "total_entries": 3, + "links": [ + { + "rel": "self", + "href": "http://roar.apotomo.de/venues?page=2&per_page=1" + }, + { + "rel": "next", + "href": "http://roar.apotomo.de/venues?page=3&per_page=1" + }, + { + "rel": "previous", + "href": "http://roar.apotomo.de/venues?page=1&per_page=1" + } + ], + "venues": [ + { + "name": "The Gorge" + } + ] +} +``` + +You can define additional pagination properties, such as `per_page`, that are provided by [will_paginate](https://github.com/mislav/will_paginate) and [Kaminari](https://github.com/amatsuda/kaminari) in your response by defining the property in your paginated representer. + +```ruby +class VenuesRepresenter < Roar::Decorator + include Roar::Representer::JSON + include Roar::Representer::Feature::Hypermedia + include Roar::Rails::PageRepresenter + + property :per_page + + collection :venues, :exec_context => :decorator, :decorator => VenueRepresenter + + def venues + represented + end + + def page_url(args) + venues_url args + end +end + +``` ## Passing Options