├── .gitignore ├── README.textile ├── Rakefile ├── examples └── active_record │ ├── app.rb │ ├── db │ └── migrate │ │ └── 20090924173700_create_people.rb │ ├── setup_activerecord.rb │ └── views │ ├── layout.haml │ └── people │ ├── edit.haml │ ├── index.haml │ ├── new.haml │ └── show.haml ├── lib └── sinatra │ ├── rest.rb │ └── rest │ ├── adapters.rb │ └── rest.yaml ├── sinatra-rest.gemspec └── test ├── call_order_spec.rb ├── crud_spec.rb ├── helper.rb ├── helpers_spec.rb ├── inflection_spec.rb ├── routes_spec.rb ├── test_spec.rb └── views └── people ├── edit.haml ├── index.haml ├── new.haml └── show.haml /.gitignore: -------------------------------------------------------------------------------- 1 | TODO 2 | sinatra-rest-*.gem 3 | 4 | -------------------------------------------------------------------------------- /README.textile: -------------------------------------------------------------------------------- 1 | h1. Sinatra-REST 2 | 3 | Actually it's a set of templates to introduce RESTful routes in Sinatra. The 4 | only thing for you to do is to provide the views. The routes and some 5 | url helpers will be provided behind the scenes. 6 | 7 | 8 | h2. Installation 9 | 10 | Guess what! 11 | 12 | sudo gem source --add http://gems.github.com 13 | sudo gem install blindgaenger-sinatra-rest 14 | 15 | 16 | h2. Usage 17 | 18 | Of course you need to require the gem in your Sinatra application: 19 | 20 | require 'rubygems' 21 | require 'sinatra' 22 | require 'sinatra/rest' 23 | 24 | It's very similar to defining routes in Sinatra (@get@, @post@, ...). But this 25 | time you don't define the routes by yourself, but use the model's name for 26 | convention. 27 | 28 | For example, if the model's class is called @Person@ you only need to add this 29 | line: 30 | 31 | rest Person 32 | 33 | Which will add the following RESTful routes to your application. (Note the 34 | pluralization of @Person@ to the @/people/*@ routes.) 35 | 36 | * GET /people 37 | * GET /people/new 38 | * POST /people 39 | * GET /people/:id 40 | * GET /people/:id/edit 41 | * PUT /people/:id 42 | * DELETE /people/:id 43 | 44 | But the real benefit is, that these *routes define a restful standard behaviour* 45 | on your model, *appropriate routing and redirecting* and *named url helpers*. 46 | 47 | For instance, you can imagine the following code to be added for the @/people@ 48 | and @/people/:id@ routes. 49 | 50 |

51 | # simply add this line
52 | 
53 | rest Person, :renderer => :erb
54 | 
55 | # and this is generated for you
56 | 
57 | get '/people' do
58 |   @people = Person.all
59 |   erb :"people/index", options
60 | end
61 | 
62 | put '/people/:id' do
63 |   @person = Person.find_by_id(params[:id])
64 |   redirect url_for_people_show(@person), 'person updated'
65 | end
66 | 
67 | # further restful routes for Person ...
68 | 
69 | 70 | That's only half the truth! The routes are generated dynamically, so all 71 | defaults can be overridden (the behaviour, after/before callbacks, used renderer, 72 | which routes are added). 73 | 74 | For more details and options, please have a look at the pages in the 75 | "Sinatra-REST Wiki":http://github.com/blindgaenger/sinatra-rest/wikis on Github. 76 | 77 | h2. Links 78 | 79 | * "Homepage @ GitHub Pages":http://blindgaenger.github.com/sinatra-rest/ 80 | * "Source Code @ GitHub":http://blindgaenger.github.com/sinatra-rest/ 81 | * "Documentation @ rdoc.info":http://rdoc.info/projects/blindgaenger/sinatra-rest 82 | * "Continuous Integration @ RunCodeRun":http://runcoderun.com/blindgaenger/sinatra-rest 83 | * "Gem hosting @ Gemcutter":http://gemcutter.org/gems/sinatra-rest 84 | 85 | h2. Contact 86 | 87 | You can contact me via mail at blindgaenger at gmail dot com, or leave me a 88 | message on my "Github profile":http://github.com/blindgaenger. 89 | 90 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'spec/rake/spectask' 2 | 3 | task :default => :test 4 | 5 | desc "Run tests" 6 | Spec::Rake::SpecTask.new :test do |t| 7 | t.spec_opts = %w(--format specdoc --color) #--backtrace 8 | t.spec_files = FileList['test/*_spec.rb'] 9 | end 10 | 11 | -------------------------------------------------------------------------------- /examples/active_record/app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | 3 | 4 | # add some data the first time or use an already existing db 5 | configure :development do 6 | load 'setup_activerecord.rb' 7 | 8 | Person.create(:name => 'foo') 9 | Person.create(:name => 'bar') 10 | Person.create(:name => 'baz') 11 | end 12 | 13 | # sinatra-rest needs to be loaded after ActiveRecord 14 | $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '../../lib') 15 | require 'sinatra/rest' 16 | 17 | rest Person 18 | 19 | get '/' do 20 | redirect '/people' 21 | end 22 | 23 | -------------------------------------------------------------------------------- /examples/active_record/db/migrate/20090924173700_create_people.rb: -------------------------------------------------------------------------------- 1 | class CreatePeople < ActiveRecord::Migration 2 | def self.up 3 | create_table :people do |t| 4 | t.string :name 5 | t.timestamps 6 | end 7 | end 8 | 9 | def self.down 10 | drop_table :people 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /examples/active_record/setup_activerecord.rb: -------------------------------------------------------------------------------- 1 | require 'activerecord' 2 | 3 | ActiveRecord::Base.logger = Logger.new(STDOUT) 4 | ActiveRecord::Base.logger.level = Logger::WARN 5 | 6 | config = { 7 | :adapter => 'sqlite3', 8 | :database => "db/#{Sinatra::Application.environment.to_s}.sqlite3" 9 | } 10 | ActiveRecord::Base.establish_connection(config) 11 | ActiveRecord::Migrator.up('db/migrate') 12 | 13 | class Person < ActiveRecord::Base 14 | end 15 | 16 | -------------------------------------------------------------------------------- /examples/active_record/views/layout.haml: -------------------------------------------------------------------------------- 1 | !!! XML 2 | !!! Strict 3 | %html{html_attrs} 4 | %head 5 | %meta{:"http-equiv"=>"content-type", :content=>"text/html; charset=UTF-8"}/ 6 | %title People 7 | %style{:type=>"text/css", :media=>"screen"} 8 | :sass 9 | body 10 | font-family: "Lucida Grande", sans-serif 11 | font-size: 12px 12 | background-color: #E9ECEB 13 | color: #555555 14 | 15 | #container 16 | width: 400px 17 | margin: 25px auto 18 | padding: 10px 25px 19 | background-color: #FFFFFF 20 | border: 1px solid #D4D4D4 21 | :-moz-border-radius 8px 22 | :-webkit-border-radius 8px 23 | 24 | #footer 25 | text-align: center 26 | color: #909090 27 | 28 | h1 29 | color: #383838 30 | font-size: 2em 31 | font-weight: bold 32 | 33 | a 34 | color: #075FB2 35 | text-decoration: none 36 | 37 | .buttons * 38 | padding: 0 0.5em 39 | text-align: center 40 | margin: 1em 0 41 | display: inline 42 | 43 | %body 44 | 45 | #container 46 | = yield 47 | 48 | #footer 49 | powered by 50 | %a{:href=>'http://blindgaenger.github.com/sinatra-rest/'} sinatra-rest 51 | -------------------------------------------------------------------------------- /examples/active_record/views/people/edit.haml: -------------------------------------------------------------------------------- 1 | %h1 Edit 2 | 3 | %form{:action=>url_for_people_update(@person), :method=>"post"} 4 | %input{:type=>"hidden", :name=>"_method", :value=>"put"} 5 | Name: 6 | %input{:name=>"name", :value=>@person.name} 7 | .buttons 8 | %a{:href => url_for_people_show(@person)} Cancel 9 | %button{:type => :submit} Update 10 | -------------------------------------------------------------------------------- /examples/active_record/views/people/index.haml: -------------------------------------------------------------------------------- 1 | %h1 Index 2 | 3 | %ul 4 | - @people.each do |person| 5 | %li 6 | = person.id 7 | %a{:href => url_for_people_show(person)} 8 | = person.name 9 | 10 | .buttons 11 | %a{:href => url_for_people_new} New 12 | -------------------------------------------------------------------------------- /examples/active_record/views/people/new.haml: -------------------------------------------------------------------------------- 1 | %h1 New 2 | 3 | %form{:action=>url_for_people_create, :method=>"post"} 4 | Name: 5 | %input{:name=>"name"} 6 | .buttons 7 | %a{:href => url_for_people_index} Cancel 8 | %button{:type => :submit} Create 9 | 10 | -------------------------------------------------------------------------------- /examples/active_record/views/people/show.haml: -------------------------------------------------------------------------------- 1 | %h1 Show 2 | 3 | %table 4 | %tr 5 | %th Name 6 | %th Value 7 | %tr 8 | %td id 9 | %td= @person.id 10 | %tr 11 | %td Name 12 | %td= @person.name 13 | 14 | .buttons 15 | %a{:href => url_for_people_index} « Index 16 | %a{:href => url_for_people_edit(@person)} Edit 17 | %form{:action=>url_for_people_update(@person), :method=>"post"} 18 | %input{:type=>"hidden", :name=>"_method", :value=>"delete"} 19 | %button{:type => :submit} Delete 20 | 21 | -------------------------------------------------------------------------------- /lib/sinatra/rest.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra/base' 2 | require 'english/inflect' 3 | 4 | libdir = File.dirname(__FILE__) + "/rest" 5 | $LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir) 6 | require 'adapters' 7 | require 'yaml' 8 | 9 | module Sinatra 10 | 11 | module REST 12 | 13 | # 14 | # adds restful routes and url helpers for the model 15 | def rest(model_class, options={}, &block) 16 | parse_args(model_class, options) 17 | read_config('rest/rest.yaml') 18 | 19 | # register model specific helpers 20 | helpers generate_helpers 21 | 22 | # create an own module, to override the template with custom methods 23 | # this way, you can still use #super# in the overridden methods 24 | controller = generate_controller 25 | if block_given? 26 | custom = CustomController.new(@plural) 27 | custom.instance_eval &block 28 | custom.module.send :include, controller 29 | controller = custom.module 30 | end 31 | helpers controller 32 | 33 | # register routes as DSL extension 34 | instance_eval generate_routes 35 | end 36 | 37 | protected 38 | 39 | ROUTES = { 40 | :all => [:index, :new, :create, :show, :edit, :update, :destroy], 41 | :readable => [:index, :show], 42 | :writeable => [:index, :show, :create, :update, :destroy], 43 | :editable => [:index, :show, :create, :update, :destroy, :new, :edit], 44 | } 45 | 46 | def parse_args(model_class, options) 47 | @model, @singular, @plural = conjugate(model_class) 48 | @renderer = (options.delete(:renderer) || :haml).to_s 49 | @route_flags = parse_routes(options.delete(:routes) || :all) 50 | end 51 | 52 | def parse_routes(routes) 53 | routes = [*routes].map {|route| ROUTES[route] || route}.flatten.uniq 54 | # keep the order of :all routes 55 | ROUTES[:all].select{|route| routes.include? route} 56 | end 57 | 58 | def read_config(filename) 59 | file = File.read(File.join(File.dirname(__FILE__), filename)) 60 | @config = YAML.load file 61 | end 62 | 63 | # 64 | # creates the necessary forms of the model name 65 | # pretty much like ActiveSupport's inflections, but don't like to depend on 66 | def conjugate(model_class) 67 | model = model_class.to_s.match(/(\w+)$/)[0] 68 | singular = model.gsub(/([a-z])([A-Z])/, '\1_\2').downcase 69 | return model, singular, singular.pluralize 70 | end 71 | 72 | def replace_variables(t, route=nil) 73 | if route 74 | t.gsub!('NAME', route.to_s) 75 | t.gsub!('VERB', @config[route][:verb].downcase) 76 | t.gsub!('URL', @config[route][:url]) 77 | t.gsub!('CONTROL', @config[route][:control]) 78 | t.gsub!('RENDER', @config[route][:render]) 79 | end 80 | t.gsub!(/PLURAL/, @plural) 81 | t.gsub!(/SINGULAR/, @singular) 82 | t.gsub!(/MODEL/, @model) 83 | t.gsub!(/RENDERER/, @renderer) 84 | t 85 | end 86 | 87 | def generate_routes 88 | @route_flags.map{|r| route_template(r)}.join("\n\n") 89 | end 90 | 91 | def route_template(route) 92 | t = <<-RUBY 93 | VERB 'URL' do 94 | PLURAL_before :NAME 95 | PLURAL_NAME 96 | PLURAL_after :NAME 97 | RENDER 98 | end 99 | RUBY 100 | replace_variables(t, route) 101 | end 102 | 103 | def generate_helpers 104 | m = Module.new 105 | @route_flags.each {|r| 106 | m.module_eval helpers_template(r) 107 | } 108 | m 109 | end 110 | 111 | def helpers_template(route) 112 | t = <<-RUBY 113 | def url_for_PLURAL_NAME(model=nil) 114 | "URL" 115 | end 116 | RUBY 117 | helper_route = @config[route][:url].gsub(':id', '#{escape_model_id(model)}') 118 | t.gsub!('URL', helper_route) 119 | replace_variables(t, route) 120 | end 121 | 122 | def generate_controller 123 | m = Module.new 124 | t = <<-RUBY 125 | def PLURAL_before(name); end 126 | def PLURAL_after(name); end 127 | RUBY 128 | m.module_eval replace_variables(t) 129 | 130 | @route_flags.each {|route| 131 | m.module_eval controller_template(route) 132 | } 133 | m 134 | end 135 | 136 | def controller_template(route) 137 | t = <<-RUBY 138 | def PLURAL_NAME(options=params) 139 | mp = filter_model_params(options) 140 | CONTROL 141 | end 142 | RUBY 143 | replace_variables(t, route) 144 | end 145 | 146 | # 147 | # model unspecific helpers, will be included once 148 | module Helpers 149 | # for example _method will be removed 150 | def filter_model_params(params) 151 | params.reject {|k, v| k =~ /^_/} 152 | end 153 | 154 | def escape_model_id(model) 155 | if model.nil? 156 | raise 'can not generate url for nil' 157 | elsif model.kind_of?(String) 158 | Rack::Utils.escape(model) 159 | elsif model.kind_of?(Fixnum) 160 | model 161 | elsif model.id.kind_of? String 162 | Rack::Utils.escape(model.id) 163 | else 164 | model.id 165 | end 166 | end 167 | 168 | def call_model_method(model_class, name, options={}) 169 | method = model_class.method(name) 170 | if options.nil? || method.arity == 0 171 | Kernel.warn "warning: calling #{model_class.to_s}##{name} with args, although it doesn't take args" if options 172 | method.call 173 | else 174 | method.call(options) 175 | end 176 | end 177 | end 178 | 179 | # 180 | # used as context to evaluate the controller's module 181 | class CustomController 182 | attr_reader :module 183 | 184 | def initialize(prefix) 185 | @prefix = prefix 186 | @module = Module.new 187 | end 188 | 189 | def before(options={}, &block) prefix :before, █ end 190 | def after(options={}, &block) prefix :after, █ end 191 | def index(options={}, &block) prefix :index, █ end 192 | def new(options={}, &block) prefix :new, █ end 193 | def create(options={}, &block) prefix :create, █ end 194 | def show(options={}, &block) prefix :show, █ end 195 | def edit(options={}, &block) prefix :edit, █ end 196 | def update(options={}, &block) prefix :update, █ end 197 | def destroy(options={}, &block) prefix :destroy, █ end 198 | 199 | private 200 | def prefix(name, &block) 201 | @module.send :define_method, "#{@prefix}_#{name}", &block if block_given? 202 | end 203 | end 204 | 205 | end # REST 206 | 207 | helpers REST::Helpers 208 | register REST 209 | 210 | end # Sinatra 211 | -------------------------------------------------------------------------------- /lib/sinatra/rest/adapters.rb: -------------------------------------------------------------------------------- 1 | module Stone 2 | module Resource 3 | def find_by_id(id) 4 | get(id) 5 | end 6 | 7 | def delete(id) 8 | model = self.find_by_id(id) 9 | model.destroy if model 10 | end 11 | end 12 | end 13 | 14 | module DataMapper 15 | module Model 16 | def find_by_id(id) 17 | get(id) 18 | end 19 | 20 | def delete(id) 21 | model = self.find_by_id(id) 22 | model.destroy if model 23 | end 24 | end 25 | end 26 | 27 | module ActiveRecord 28 | class Base 29 | class << self 30 | def find_by_id(id) 31 | find(id) 32 | end 33 | end 34 | end 35 | end 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/sinatra/rest/rest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | :index: 3 | :verb: GET 4 | :url: /PLURAL 5 | :control: |- 6 | @PLURAL = call_model_method(MODEL, :all, mp) 7 | :render: |- 8 | RENDERER :'PLURAL/index', options 9 | 10 | :new: 11 | :verb: GET 12 | :url: /PLURAL/new 13 | :control: |- 14 | @SINGULAR = call_model_method(MODEL, :new, mp) 15 | :render: |- 16 | RENDERER :'PLURAL/new', options 17 | 18 | :create: 19 | :verb: POST 20 | :url: /PLURAL 21 | :control: |- 22 | @SINGULAR = call_model_method(MODEL, :new, mp) 23 | @SINGULAR.save 24 | :render: |- 25 | redirect url_for_PLURAL_show(@SINGULAR), 'SINGULAR created' 26 | 27 | :show: 28 | :verb: GET 29 | :url: /PLURAL/:id 30 | :control: |- 31 | @SINGULAR = call_model_method(MODEL, :find_by_id, mp[:id]) 32 | :render: |- 33 | if @SINGULAR.nil? 34 | throw :halt, [404, 'SINGULAR not found'] 35 | else 36 | RENDERER :'PLURAL/show', options 37 | end 38 | 39 | :edit: 40 | :verb: GET 41 | :url: /PLURAL/:id/edit 42 | :control: |- 43 | @SINGULAR = call_model_method(MODEL, :find_by_id, mp[:id]) 44 | :render: |- 45 | RENDERER :'PLURAL/edit', options 46 | 47 | :update: 48 | :verb: PUT 49 | :url: /PLURAL/:id 50 | :control: |- 51 | @SINGULAR = call_model_method(MODEL, :find_by_id, mp[:id]) 52 | @SINGULAR.update_attributes(mp) unless @SINGULAR.nil? 53 | :render: |- 54 | if @SINGULAR.nil? 55 | throw :halt, [404, 'SINGULAR not found'] 56 | else 57 | redirect url_for_PLURAL_show(@SINGULAR), 'SINGULAR updated' 58 | end 59 | 60 | :destroy: 61 | :verb: DELETE 62 | :url: /PLURAL/:id 63 | :control: |- 64 | call_model_method(MODEL, :delete, mp[:id]) 65 | :render: |- 66 | redirect url_for_PLURAL_index, 'SINGULAR destroyed' 67 | 68 | -------------------------------------------------------------------------------- /sinatra-rest.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.platform = Gem::Platform::RUBY 3 | s.name = "sinatra-rest" 4 | s.version = "0.3.3" 5 | s.date = "2009-09-25" 6 | s.authors = ["blindgaenger"] 7 | s.email = "blindgaenger@gmail.com" 8 | s.homepage = "http://github.com/blindgaenger/sinatra-rest" 9 | s.summary = "Generates RESTful routes for the models of a Sinatra application (ActiveRecord, DataMapper, Stone)" 10 | 11 | s.files = [ 12 | "Rakefile", 13 | "README.textile", 14 | "lib/sinatra/rest.rb", 15 | "lib/sinatra/rest/adapters.rb", 16 | "lib/sinatra/rest/rest.yaml", 17 | "test/call_order_spec.rb", 18 | "test/crud_spec.rb", 19 | "test/helper.rb", 20 | "test/helpers_spec.rb", 21 | "test/inflection_spec.rb", 22 | "test/routes_spec.rb", 23 | "test/test_spec.rb", 24 | "test/views/people/edit.haml", 25 | "test/views/people/index.haml", 26 | "test/views/people/new.haml", 27 | "test/views/people/show.haml" 28 | ] 29 | 30 | s.require_paths = ["lib"] 31 | s.add_dependency "sinatra", [">= 0.9.0.5"] 32 | s.add_dependency "english", [">= 0.3.1"] 33 | 34 | s.has_rdoc = "false" 35 | end 36 | 37 | -------------------------------------------------------------------------------- /test/call_order_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'call order' do 4 | 5 | def called_routes 6 | @app.call_order.map {|r, m| r}.uniq 7 | end 8 | 9 | def called_methods 10 | @app.call_order.map {|r, m| m} 11 | end 12 | 13 | before(:each) do 14 | mock_app { 15 | configure do 16 | set :call_order, [] 17 | end 18 | 19 | rest Person do 20 | before do |route| 21 | options.call_order << [route, :before] 22 | super 23 | end 24 | 25 | after do |route| 26 | options.call_order << [route, :after] 27 | super 28 | end 29 | 30 | index do 31 | options.call_order << [:index, :index] 32 | super 33 | end 34 | 35 | new do 36 | options.call_order << [:new, :new] 37 | super 38 | end 39 | 40 | create do 41 | options.call_order << [:create, :create] 42 | super 43 | end 44 | 45 | show do 46 | options.call_order << [:show, :show] 47 | super 48 | end 49 | 50 | edit do 51 | options.call_order << [:edit, :edit] 52 | super 53 | end 54 | 55 | update do 56 | options.call_order << [:update, :update] 57 | super 58 | end 59 | 60 | destroy do 61 | options.call_order << [:destroy, :destroy] 62 | super 63 | end 64 | end 65 | } 66 | end 67 | 68 | it 'should call :index in the right order' do 69 | index '/people' 70 | called_methods.should == [:before, :index, :after] 71 | called_routes.should == [:index] 72 | end 73 | 74 | it 'should call :new in the right order' do 75 | new '/people/new' 76 | called_methods.should == [:before, :new, :after] 77 | called_routes.should == [:new] 78 | end 79 | 80 | it 'should call :create in the right order' do 81 | create('/people', :name => 'initial name') 82 | called_methods.should == [:before, :create, :after] 83 | called_routes.should == [:create] 84 | end 85 | 86 | it 'should call :show in the right order' do 87 | show '/people/1' 88 | called_methods.should == [:before, :show, :after] 89 | called_routes.should == [:show] 90 | end 91 | 92 | it 'should call :edit in the right order' do 93 | edit '/people/1/edit' 94 | called_methods.should == [:before, :edit, :after] 95 | called_routes.should == [:edit] 96 | end 97 | 98 | it 'should call :update in the right order' do 99 | update '/people/1', :name => 'new name' 100 | called_methods.should == [:before, :update, :after] 101 | called_routes.should == [:update] 102 | end 103 | 104 | it 'should call :destroy in the right order' do 105 | destroy '/people/1' 106 | called_methods.should == [:before, :destroy, :after] 107 | called_routes.should == [:destroy] 108 | end 109 | 110 | end 111 | 112 | -------------------------------------------------------------------------------- /test/crud_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'some use cases' do 4 | 5 | def total_models 6 | Person.all.size 7 | end 8 | 9 | require "rexml/document" 10 | def doc(xml) 11 | REXML::Document.new(xml.gsub(/>\s+<').strip) 12 | end 13 | 14 | before(:each) do 15 | Person.reset! 16 | mock_rest Person 17 | end 18 | 19 | it 'should list all persons' do 20 | get '/people' 21 | normalized_response.should == [200, '123'] 22 | total_models.should == 3 23 | end 24 | 25 | it 'should create a new person' do 26 | get '/people' 27 | normalized_response.should == [200, '123'] 28 | total_models.should == 3 29 | 30 | get '/people/new' 31 | normalized_response.should == [200, ''] 32 | total_models.should == 3 33 | 34 | post '/people', {:name => 'four'} 35 | normalized_response.should == [302, 'person created'] 36 | total_models.should == 4 37 | 38 | get '/people' 39 | normalized_response.should == [200, '1234'] 40 | total_models.should == 4 41 | end 42 | 43 | it 'should read all persons' do 44 | get '/people' 45 | 46 | el_people = doc(body).elements.to_a("*/person/id") 47 | el_people.size.should == 3 48 | total_models.should == 3 49 | 50 | get "/people/#{el_people[0].text}" 51 | normalized_response.should == [200, '1one'] 52 | total_models.should == 3 53 | 54 | get "/people/#{el_people[1].text}" 55 | normalized_response.should == [200, '2two'] 56 | total_models.should == 3 57 | 58 | get "/people/#{el_people[2].text}" 59 | normalized_response.should == [200, '3three'] 60 | total_models.should == 3 61 | 62 | get "/people/99" 63 | normalized_response.should == [404, 'route not found'] 64 | total_models.should == 3 65 | end 66 | 67 | it 'should update a person' do 68 | get '/people/2' 69 | normalized_response.should == [200, '2two'] 70 | total_models.should == 3 71 | 72 | put '/people/2', {:name => 'tomorrow'} 73 | normalized_response.should == [302, 'person updated'] 74 | total_models.should == 3 75 | 76 | get '/people/2' 77 | normalized_response.should == [200, '2tomorrow'] 78 | total_models.should == 3 79 | end 80 | 81 | it 'should destroy a person' do 82 | get '/people' 83 | normalized_response.should == [200, '123'] 84 | total_models.should == 3 85 | 86 | delete '/people/2' 87 | normalized_response.should == [302, 'person destroyed'] 88 | total_models.should == 2 89 | 90 | get '/people' 91 | normalized_response.should == [200, '13'] 92 | total_models.should == 2 93 | 94 | get '/people/2' 95 | normalized_response.should == [404, 'route not found'] 96 | total_models.should == 2 97 | end 98 | 99 | end 100 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'spec' 2 | require 'spec/interop/test' 3 | require 'sinatra/test' 4 | 5 | $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib' 6 | require 'sinatra/base' 7 | require 'sinatra/rest' 8 | 9 | Sinatra::Default.set(:environment, :test) 10 | Test::Unit::TestCase.send :include, Sinatra::Test 11 | 12 | # 13 | # Sets up a Sinatra::Base subclass defined with the block 14 | # given. Used in setup or individual spec methods to establish 15 | # the application. 16 | def mock_app(&block) 17 | base = Sinatra::Application 18 | @app = Sinatra.new(base) do 19 | set :views, File.dirname(__FILE__) + '/views' 20 | 21 | not_found do 22 | 'route not found' 23 | end 24 | end 25 | @app.instance_eval(&block) if block_given? 26 | end 27 | 28 | # 29 | # sets rest in a sinatra instance 30 | # and returns the block's result, if a block is given 31 | def mock_rest(model, options={}, &block) 32 | mock_app do 33 | rest model, options 34 | 35 | self.new.instance_eval do 36 | @app.instance_eval(&block) if block_given? 37 | end 38 | end 39 | end 40 | 41 | 42 | # 43 | # normalize for easier testing 44 | def normalized_response 45 | return status, body.gsub(/>\s+<').strip 46 | end 47 | 48 | # index GET /models 49 | def index(url) 50 | get url 51 | normalized_response 52 | end 53 | 54 | # new GET /models/new 55 | def new(url) 56 | get url 57 | normalized_response 58 | end 59 | 60 | # create POST /models 61 | def create(url, params={}) 62 | post url, params 63 | normalized_response 64 | end 65 | 66 | # show GET /models/1 67 | def show(url) 68 | get url 69 | normalized_response 70 | end 71 | 72 | # edit GET /models/1/edit 73 | def edit(url) 74 | get url 75 | normalized_response 76 | end 77 | 78 | # update PUT /models/1 79 | def update(url, params={}) 80 | put url, params 81 | normalized_response 82 | end 83 | 84 | # destroy DELETE /models/1 85 | def destroy(url) 86 | delete url 87 | normalized_response 88 | end 89 | 90 | 91 | ## 92 | ## kind of a 'minimal model' 93 | class Person 94 | attr_accessor :id 95 | attr_accessor :name 96 | 97 | def initialize(*args) 98 | #puts "new #{args.inspect}" 99 | if args.size == 0 100 | @id = nil 101 | @name = nil 102 | elsif args.size == 2 103 | @id = args[0].to_i 104 | @name = args[1] 105 | else args.size == 1 106 | update_attributes(args[0]) 107 | end 108 | end 109 | 110 | def save 111 | #puts "save #{@id}" 112 | @@people << self 113 | self.id = @@people.size 114 | end 115 | 116 | def update_attributes(hash) 117 | #puts "update_attributes #{hash.inspect}" 118 | unless hash.empty? 119 | @id = hash['id'].to_i if hash.include?('id') 120 | @name = hash['name'] if hash.include?('name') 121 | end 122 | end 123 | 124 | def self.delete(id) 125 | #puts "delete #{id}" 126 | @@people.delete_if {|person| person.id == id.to_i} 127 | end 128 | 129 | @@people = [] 130 | 131 | def self.all(criteria={}) 132 | #puts 'all' 133 | return @@people 134 | end 135 | 136 | def self.find_by_id(id) 137 | #puts "find_by_id #{id}" 138 | all.find {|f| f.id == id.to_i} 139 | end 140 | 141 | def self.clear! 142 | @@people = [] 143 | end 144 | 145 | def self.reset! 146 | clear! 147 | Person.new(1, 'one').save 148 | Person.new(2, 'two').save 149 | Person.new(3, 'three').save 150 | end 151 | end 152 | 153 | 154 | -------------------------------------------------------------------------------- /test/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'url helpers' do 4 | 5 | it 'should generate the correct urls for the model' do 6 | mock_rest Person do 7 | person = Person.new(99, 'foo') 8 | url_for_people_create.should == '/people' 9 | url_for_people_destroy(person).should == '/people/99' 10 | url_for_people_edit(person).should == '/people/99/edit' 11 | url_for_people_index.should == '/people' 12 | url_for_people_new.should == '/people/new' 13 | url_for_people_show(person).should == '/people/99' 14 | url_for_people_update(person).should == '/people/99' 15 | end 16 | end 17 | 18 | it 'should add :all helpers' do 19 | mock_rest(Person) { methods.grep(/^url_for_people_/).sort }.should == [ 20 | "url_for_people_create", 21 | "url_for_people_destroy", 22 | "url_for_people_edit", 23 | "url_for_people_index", 24 | "url_for_people_new", 25 | "url_for_people_show", 26 | "url_for_people_update", 27 | ] 28 | end 29 | 30 | it 'should add :readable helpers' do 31 | mock_rest(Person, :routes => :readable) { methods.grep(/^url_for_people_/).sort }.should == [ 32 | "url_for_people_index", 33 | "url_for_people_show", 34 | ] 35 | end 36 | 37 | it 'should add :writeable helpers' do 38 | mock_rest(Person, :routes => :writeable) { methods.grep(/^url_for_people_/).sort }.should == [ 39 | "url_for_people_create", 40 | "url_for_people_destroy", 41 | "url_for_people_index", 42 | "url_for_people_show", 43 | "url_for_people_update", 44 | ] 45 | end 46 | 47 | it 'should add :editable helpers' do 48 | mock_rest(Person, :routes => :editable) { methods.grep(/^url_for_people_/).sort }.should == [ 49 | "url_for_people_create", 50 | "url_for_people_destroy", 51 | "url_for_people_edit", 52 | "url_for_people_index", 53 | "url_for_people_new", 54 | "url_for_people_show", 55 | "url_for_people_update", 56 | ] 57 | end 58 | 59 | it 'should add helpers by name' do 60 | mock_rest(Person, :routes => [:new, :create, :destroy]) { methods.grep(/^url_for_people_/).sort }.should == [ 61 | "url_for_people_create", 62 | "url_for_people_destroy", 63 | "url_for_people_new", 64 | ] 65 | end 66 | 67 | it 'should add helpers by mixing aliases and names' do 68 | mock_rest(Person, :routes => [:readable, :create, :destroy]) { methods.grep(/^url_for_people_/).sort }.should == [ 69 | "url_for_people_create", 70 | "url_for_people_destroy", 71 | "url_for_people_index", 72 | "url_for_people_show", 73 | ] 74 | end 75 | 76 | end 77 | -------------------------------------------------------------------------------- /test/inflection_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'model inflection' do 4 | 5 | def conjugate(model) 6 | mock_app { 7 | include Sinatra::REST 8 | conjugate(model) 9 | } 10 | end 11 | 12 | it "should conjugate a simple model name" do 13 | conjugate(Person).should == %w(Person person people) 14 | end 15 | 16 | it "should conjugate a String as model name" do 17 | conjugate('Person').should == %w(Person person people) 18 | end 19 | 20 | it "should conjugate a model name in camel cases" do 21 | conjugate('SomePerson').should == %w(SomePerson some_person some_people) 22 | end 23 | 24 | it "should conjugate a model name without module" do 25 | conjugate('MyModule::ModulePerson').should == %w(ModulePerson module_person module_people) 26 | end 27 | 28 | end 29 | 30 | -------------------------------------------------------------------------------- /test/routes_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'routes' do 4 | 5 | before(:each) do 6 | Person.reset! 7 | end 8 | 9 | describe 'one by one' do 10 | 11 | before(:each) do 12 | mock_rest Person 13 | end 14 | 15 | it 'should list all people on index by their id' do 16 | index('/people').should == [200, '123'] 17 | end 18 | 19 | it 'should prepare an empty item on new' do 20 | new('/people/new').should == [200, ''] 21 | end 22 | 23 | it 'should create an item on post' do 24 | create('/people', :name => 'new resource').should == [302, 'person created'] 25 | end 26 | 27 | it 'should show an item on get' do 28 | show('/people/1').should == [200, '1one'] 29 | end 30 | 31 | it 'should get the item for editing' do 32 | edit('/people/1/edit').should == [200, '1one'] 33 | end 34 | 35 | it 'should update an item on put' do 36 | update('/people/1', :name => 'another name').should == [302, 'person updated'] 37 | end 38 | 39 | it 'should destroy an item on delete' do 40 | destroy('/people/1').should == [302, 'person destroyed'] 41 | end 42 | 43 | end 44 | 45 | describe 'options' do 46 | 47 | it 'should add :all routes' do 48 | mock_rest Person 49 | 50 | index('/people').should == [200, '123'] 51 | new('/people/new').should == [200, ''] 52 | create('/people', :name => 'new person').should == [302, "person created"] 53 | show('/people/1').should == [200, '1one'] 54 | edit('/people/1/edit').should == [200, "1one"] 55 | update('/people/1', :name => 'new name').should == [302, "person updated"] 56 | destroy('/people/1').should == [302, "person destroyed"] 57 | end 58 | 59 | it 'should add :readable routes' do 60 | mock_rest Person, :routes => :readable 61 | 62 | index('/people').should == [200, '123'] 63 | show('/people/1').should == [200, '1one'] 64 | 65 | new('/people/new').should == [404, "route not found"] 66 | create('/people', :name => 'new person').should == [404, "route not found"] 67 | edit('/people/1/edit').should == [404, "route not found"] 68 | update('/people/1', :name => 'new name').should == [404, "route not found"] 69 | destroy('/people/1').should == [404, "route not found"] 70 | end 71 | 72 | it 'should add :writeable routes' do 73 | mock_rest Person, :routes => :writeable 74 | 75 | index('/people').should == [200, '123'] 76 | show('/people/1').should == [200, '1one'] 77 | create('/people', :name => 'new person').should == [302, "person created"] 78 | update('/people/1', :name => 'new name').should == [302, "person updated"] 79 | destroy('/people/1').should == [302, "person destroyed"] 80 | 81 | new('/people/new').should == [404, "route not found"] 82 | edit('/people/1/edit').should == [404, "route not found"] 83 | end 84 | 85 | it 'should add :editable routes' do 86 | mock_rest Person, :routes => :editable 87 | 88 | index('/people').should == [200, '123'] 89 | new('/people/new').should == [200, ''] 90 | create('/people', :name => 'new person').should == [302, "person created"] 91 | show('/people/1').should == [200, '1one'] 92 | edit('/people/1/edit').should == [200, "1one"] 93 | update('/people/1', :name => 'new name').should == [302, "person updated"] 94 | destroy('/people/1').should == [302, "person destroyed"] 95 | end 96 | 97 | it 'should add routes by name' do 98 | mock_rest Person, :routes => [:readable, :new, :create] 99 | 100 | index('/people').should == [200, '123'] 101 | show('/people/1').should == [200, '1one'] 102 | new('/people/new').should == [200, ''] 103 | create('/people', :name => 'new person').should == [302, "person created"] 104 | 105 | edit('/people/1/edit').should == [404, "route not found"] 106 | update('/people/1', :name => 'new name').should == [404, "route not found"] 107 | destroy('/people/1').should == [404, "route not found"] 108 | end 109 | 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /test/test_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | describe 'test helpers' do 4 | 5 | it 'should work with mock_app' do 6 | Person.clear! 7 | mock_app { 8 | rest Person 9 | } 10 | index('/people').should == [200, ''] 11 | end 12 | 13 | it 'should work with mock_rest' do 14 | Person.clear! 15 | mock_rest Person 16 | index('/people').should == [200, ''] 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /test/views/people/edit.haml: -------------------------------------------------------------------------------- 1 | %person 2 | %id= @person.id 3 | %name= @person.name 4 | 5 | -------------------------------------------------------------------------------- /test/views/people/index.haml: -------------------------------------------------------------------------------- 1 | %people 2 | - @people.each do |person| 3 | %person 4 | %id= person.id 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/views/people/new.haml: -------------------------------------------------------------------------------- 1 | %person 2 | %id= @person.id 3 | %name= @person.name 4 | 5 | -------------------------------------------------------------------------------- /test/views/people/show.haml: -------------------------------------------------------------------------------- 1 | %person 2 | %id= @person.id 3 | %name= @person.name 4 | 5 | --------------------------------------------------------------------------------