Sinatra is a very simple Ruby framework for generating web applications. Thanks to its small size and tight design, it makes a great backend for building the kind of simple REST-oriented services that drive SproutCore applications.
DataMapper is a fast Ruby-based ORM for managing data in a database. We'll be using DataMapper to interface with a SQLite database for storage in this tutorial.
Before you start coding with Sinatra, you will need to make sure it is installed on your machine. Enter the following command to get all the Ruby modules you need:
sudo gem install sinatra data_mapper data_objects do_sqlite3 dm-sqlite-adapter json
This will install or update all of the packages we'll be using.
NOTE: If you want to skip writing the code for this yourself, just install the ruby gems above then checkout the samples-todos-sinatra Git repository at Github.
Most Sinatra applications reside in a single Ruby file. This one will be no different. To setup your app, just create a new ruby file and call it "tasks.rb". The entire app is included below, with comment:
in tasks.rb:
#!/usr/bin/env ruby
# Implements Tasks spec defined at:
# http://wiki.sproutcore.com/Todos+06-Building+the+Backend
require 'rubygems'
require 'sinatra'
require 'dm-core'
require 'dm-migrations/adapters/dm-sqlite-adapter'
require 'json'
# connect DataMapper to a local sqlite file.
DataMapper::setup(:default, ENV['DATABASE_URL'] ||
"sqlite3://#{File.join(File.dirname(__FILE__), 'tmp', 'tasks.db')}")
# define the Task model object we will use to store data
# in the server. Note the three properties defined. "id"
# will be used as the GUID for the task.
class Task
include DataMapper::Resource
property :id, Serial
property :description, Text, :required => true
property :is_done, Boolean
# helper method returns the URL for a task based on id
def url
"/tasks/#{self.id}"
end
# helper method converts the Task to json. Anytime you
# call to_json on a data structure with a Task, this will
# be used to convert the task itself
def to_json(*a)
{
'guid' => self.url,
'description' => self.description,
'isDone' => self.is_done
}.to_json(*a)
end
# keys that MUST be found in the json
REQUIRED = [:description, :is_done]
# ensure json is safe. If invalid json is received returns nil
def self.parse_json(body)
json = JSON.parse(body)
ret = { :description => json['description'], :is_done => json['isDone'] }
return nil if REQUIRED.find { |r| ret[r].nil? }
ret
end
end
# instructs DataMapper to setup your database as needed
DataMapper.auto_upgrade!
# return list of all installed tasks. Just get all tasks and
# return as JSON
get '/tasks' do
content_type 'application/json'
{ 'content' => Array(Task.all) }.to_json
end
# create a new task. request body to contain json
post '/tasks' do
opts = Task.parse_json(request.body.read) rescue nil
halt(401, 'Invalid Format') if opts.nil?
task = Task.new(opts)
halt(500, 'Could not save task') unless task.save
response['Location'] = task.url
response.status = 201
end
# Get an individual task
get "/tasks/:id" do
task = Task.get(params[:id]) rescue nil
halt(404, 'Not Found') if task.nil?
content_type 'application/json'
{ 'content' => task }.to_json
end
# Update an individual task
put "/tasks/:id" do
task = Task.get(params[:id]) rescue nil
halt(404, 'Not Found') if task.nil?
opts = Task.parse_json(request.body.read) rescue nil
halt(401, 'Invalid Format') if opts.nil?
task.description = opts[:description]
task.is_done = opts[:is_done]
task.save
response['Content-Type'] = 'application/json'
{ 'content' => task }.to_json
end
# Delete an invidual task
delete '/tasks/:id' do
task = Task.get(params[:id]) rescue nil
task.destroy unless task.nil?
end
That's all there is to it. Put the above file into tasks.rb and then start your server with the following command:
$ ruby ./tasks.rb
You should now be able to test that your server works. We recommend using HTTPClient if you are on a Mac since it makes it easy to construct HTTP requests and see their results.
Here are some basic HTTP actions you can try along with the expected results:
This should return a list of tasks in JSON. If you try this first, the list of tasks should be empty. Once you've created a few tasks, the response should be something like:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 72
Connection: close
Server: thin 1.2.4 codename Flaming Astroboy
{"content":[{"isDone":true,"guid":"/tasks/8","description":"edit me "}]}
When you send this HTTP request, make sure you include task JSON in the body. The post body should look something like this:
"POST /tasks" post body:
{ "description": "test task", "isDone": true }
The response from your post should be empty except for a status code of 201 "Created" and a "Location" header with the new URL for the created task:
HTTP/1.1 201 Created
Location: /tasks/9
Content-Type: text/html
Content-Length: 0
Connection: close
Server: thin 1.2.4 codename Flaming Astroboy
Once you've created a task, you can try retrieving it. Just GET the URL returned by the Location header. The return value should be a content hash just like in /tasks:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 69
Connection: close
Server: thin 1.2.4 codename Flaming Astroboy
{"content":{"isDone":true,"guid":"/tasks/9","description":"test task"}}
This command should update the task. Like POST /tasks, you need to pass a post body with your new task. The post body should contain both the description and isDone properties to be valid. Be sure to PUT to a URL representing an existing task (like the one you created previously):
"PUT /tasks/(id)" post body:
{ "isDone": false, "description": "EDITED" }
The return value should indicate 200 OK and should also return the new task JSON. You should now be able to go GET /tasks/(id) and see the properties you just modified change.
This command should delete a task. Try calling this method several times on a URL for a task you have created. After the first request, GET /tasks/(id) should return a 404. Subsequent requests should not raise any kind of error.
Once you've verified your new server is working as expected, you have just one step left before you can wire it up to your client. Currently your new Sinatra server is running on port 4567 while your SproutCore app is on port 4020. This means that by itself your SproutCore app still can't access the Sinatra server.
To solve this, you need to add a "proxy" statement to your SproutCore buildfile. This will tell SproutCore that certain URLs should be sent to your backend server instead of generating code.
Open the Buildfile in your SproutCore Todos app and add the following line to the bottom:
in todos/Buildfile:
proxy "/tasks", :to => "localhost:4567"
Now any request beginning with /tasks on localhost:4020 will be proxied to your Sinatra backend. Make sure your Sinatra server is running then restart your sc-server in the SproutCore app and test it out by visiting:
In your web browser. You should see the JSON you expected to get from Sinatra. Congratulations, you have a simple web app up and running. Now you're ready to proceed with writing your Data Source.
Go to Step 7: Writing Your Data Source ยป