/paris-rb-grape-talk

paris-rb grape talk

Primary LanguageRuby

#Short presentation on Grape for your Rails bases API ##Paris.rb April 2nd

I don't really have the time to write a proper blog post, I hope this will be at least somewhat useful to those who saw my talk. We examined in this talk the interest of adding grape as an API DSL compared to using vanilla Rails.

To do so we are in a very quick and dirty manner recreating the website for talk submissions for paris.rb .... giving it a nice API.

We are using Rails 4.0 beta1. And to speed things up we are using an application template. We will be using Alex Klepa's example Rails 4.0 app with Devise, CanCan, OmniAuth and Twitter Bootstrap, minimally hacked for our needs.

Let's first take care of our dependencies. I ran this on ruby 2.0 with Rails 4.0. You should also install and have running an instance of Mongo db (http://docs.mongodb.org/manual/installation/).

As a quick reminder:

$  gem install rails --version 4.0.0.beta1 --no-ri --no-rdoc

First lets create the fullblown rails application. Boom.

$ rails new parisrb -m https://raw.github.com/OriPekelman/rails4-bootstrap-devise-cancan-omniauth/master/rails4-bootstrap.rb --skip-bundle

$ cd parisrb

To create a first admnistrative user(admin@example.com, password:administrator) we can:

$ rake db:seed

Let's test this is working

$ rails s

Note that lines starting with $EDITOR mean you want to replace this with your own (or configure your shell)

 $ $EDITOR app/views/layouts/_navigation.html.erb

Real geeks don't like facebook, but they do love branding.

Now let's generate our domain objects. Talks are given by a person, at a meetup that has a data, a starting time and a location.

$ rails g scaffold location title:string
$ rails g scaffold meetup title:string description:text location_id:string scheduled:date start_hour:time end_hour:time meetup_url:string
$ rails g scaffold talk  title:string description:text proposed_on:date meetup_id:string user_id:string level:string duration:integer

Now let's tell mongoing what is what and establish some relations between our models. Locations will have many meetups, meetups will have a single location but many talks. Talks belong to a meetup and a user. A user can have many talks.

 $ $EDITOR app/models/location.rb

 has_many :meetups

 $ $EDITOR   app/models/meetup.rb

 has_many :talks
 belongs_to :location

 $ $EDITOR  app/models/talk.rb

 belongs_to :meetup
 belongs_to :user

 $ $EDITOR  app/models/user.rb

 has_many :talks

At this point you might want to visit your site again and see everything is cool.

Ok, now for some better looking entry forms. Please remember all this is very q&r, and might very well be half broken

 $ $EDITOR  app/views/meetups/_form.html.erb

<%= f.input :title %>
<%= f.input :description, as: :text %>
<%= f.input :location_id, collection: Location.all, as: :select%>
<%= f.date_select :scheduled, default: Date.today + 30, label: 'Date' %>
<%= f.time_select :start_hour, default: Date.today + 1 , label: 'Heure de début' %>
<%= f.time_select :end_hour, default: Date.today + 4, label: 'Heure de fin'  %>

 $ $EDITOR  app/views/talks/_form.html.erb

 <%= f.input :title %>
 <%= f.input :description, as: :text %>
 <%= f.input :proposed_on, as: :date, disabled: :true, default: Date.today  %>
 <%= f.input :meetup_id, collection: Meetup.all, as: :select%>
 <fieldset><legend>Niveau</legend>
 <%= f.collection_radio_buttons :level, [[1, 'Débutant'] ,[5, 'Intermédiaire'],[10, 'Avancé']], :first, :last %>
 </fieldset>
 <fieldset><legend>Durée</legend>
 <%= f.collection_radio_buttons :duration, [[5, '4 min super court'] ,[20, '20 mins un peu de temps'],[60, 'Une heure!?']], :first, :last %>
 </fieldset>

Now time to see our beautiful application again. This is nice. But this is an admin party, only authenticated users should edit all this stuff

 $ $EDITOR  config/routes.rb

 devise_for :users, :controllers => {
   registrations: "users/registrations", 
   passwords: "users/passwords", 
   omniauth_callbacks: "users/omniauth_callbacks"
 }
  
 authenticated :user do
   root :to => "talks#index"
   resources :talks
   resources :meetups
   resources :locations
   resources :users
 end
 root :to => "home#index", :as => :anon_home

Before we forget, we said that a talk belongs to the user that proposes it, so here goes

 $ $EDITOR   app/controllers/talks_controller.rb
@talk = Talk.new(talk_params.merge({:user_id => current_user.id}))

Please log-in (or you will get a bunch of 404s). We can now visit http://localhost:3000/locations/new and create a new location then add our new meetup http://localhost:3000/meetups/new and even submit a new talk http://localhost:3000/talks/new

We almost also have a working API! we used jbuilder and we need to tweak the generated json views.

Now visiting http://localhost:3000/talks.json makes you wan't to cry right? 10 minutes later and we have our API! We can even do "PATCH" against it!

Or do we? Not really. Because we don't have an API documentation. And more importantly we are not exposing something that is rational, useful and designed. This was quick, but this is dirty. APIs want to be designed. Even more importantly the API portion of our application is spaghettied all over. Let's add the very nice https://github.com/intridea/grape from the great folks at Intridea (that also give us Omniauth used in this little demonstration).

 $ $EDITOR Gemfile

 gem 'grape'

We need to ctrl-c the rails we have been running and:

 $ bundle install

and restart it now

$ rails s

Now we want grape because we wanted to separate our API, decouple it from our main implementation. Let's honour it by letting it have it's own space.

mkdir app/api

We need rails to load our new creations so:

 $ $EDITOR config/application.rb

 config.paths.add "app/api", :glob => "**/*.rb"
 config.autoload_paths += Dir["#{Rails.root}/app/api/*"]

Time for a small hello-world

 $ $EDITOR  app/api/paris.rb

module Paris
  class RB < Grape::API
    desc "Root of api"
    get "/" do
      {:link=>"Home sweet home"}
    end
  end
end

We need to mount this as an engine.

 $ $EDITOR   config/routes.rb

 mount Paris::RB => '/api'  

Visiting http://localhost:3000/api should fill you with bliss. But we wanted to have our own specific API not just hello world.

Let's first show some grape goodness and add versioning while defaulting to json. Your api should now be at http://localhost:3000/api/v1 In th presenation I told you how versioning APIs using the url prefix can be a very bad idea. Still it is cool to have it for "free".

Ok, our own custom API logic.

 $ $EDITOR  app/api/paris.rb

 module Paris
   class RB < Grape::API
     version 'v1', :using => :path, :vendor => 'parisrb'
     format :json     
     
     resource :talks do  
       desc "Return the collection of current talks"
       get :current do
         Talk.limit(20)
       end
 
       desc "Return a talk."
       params do
         requires :id, :type => String, :desc => "talk id."
       end
       get ':id' do
         Talk.find(params[:id])
       end
     end     
     
   end
 end

Visiting http://localhost:3000/api/v1/talks/current should make you happy

But we haven't finished, have we? Without documentation an API is of no use.. grape comes with a great ecosystem of gems to do Caching, Hypermedia ... and docs.

 $ $EDITOR    Gemfile

 gem 'grape-swagger'

Let's ctrl-c again

 $ bundle install
 $ rails s

Time to add documentation . We add a second module that will mount inside it the API and add swagger style documentation generation. We will now mount this second module as out API.

 $ $EDITOR  app/api/paris.rb

 module Paris      
   class Root < Grape::API   
     mount Paris::RB
     add_swagger_documentation :base_path=>"http://localhost:3000/api", :mount_path =>"v1/doc", :markdown => false, :api_version => Paris::RB.version, :hide_documentation_path =>false
   end
 end

 $ $EDITOR  config/routes.rb

 mount Paris::Root => '/api'

Finished. Look at http://localhost:3000/api/v1/doc.json and.. go to swagger-ui demo http://petstore.swagger.wordnik.com and put this url in!

You got a free API console! This is great. But probably the major benefit here is that everything that is related to the API is in a single location. It is mostly self contained and understandable.