├── Gemfile ├── Gemfile.lock ├── MIT-LICENSE ├── Procfile ├── README.md ├── config.ru ├── repasties.rb └── views ├── layout.erb ├── list.erb ├── new.erb └── show.erb /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem "sinatra" 3 | gem "rethinkdb" 4 | gem "json" 5 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | json (2.3.0) 5 | mustermann (1.1.1) 6 | ruby2_keywords (~> 0.0.1) 7 | rack (2.2.3) 8 | rack-protection (2.2.0) 9 | rack 10 | rethinkdb (2.4.0.0) 11 | ruby2_keywords (0.0.5) 12 | sinatra (2.2.0) 13 | mustermann (~> 1.0) 14 | rack (~> 2.2) 15 | rack-protection (= 2.2.0) 16 | tilt (~> 2.0) 17 | tilt (2.0.10) 18 | 19 | PLATFORMS 20 | ruby 21 | 22 | DEPENDENCIES 23 | json 24 | rethinkdb 25 | sinatra 26 | 27 | BUNDLED WITH 28 | 2.1.4 29 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Nick Plante 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec rackup -p $PORT 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # What is it # 2 | 3 | A simple [Pastie.org](http://pastie.org)-like web application inspired by Nick Plante's [toopaste](https://github.com/zapnap/toopaste) project 4 | showing how to use **RethinkDB as a backend for Sinatra applications**. 5 | 6 | The app demos the following functionality: 7 | 8 | * Creating a new snippet (code highlighting included) 9 | * Retrieving a snippet 10 | * Listing snippets for a language 11 | 12 | The app could be easily extended to provide more interesting features like: 13 | 14 | * pagination 15 | * snippet expiration 16 | 17 | Fork it and send us a pull request. 18 | 19 | # Complete stack # 20 | 21 | * [Sinatra](http://www.sinatrarb.com/) 22 | * [RethinkDB](http://www.rethinkdb.com) 23 | 24 | # Installation # 25 | 26 | ``` 27 | git clone git://github.com/rethinkdb/rethinkdb-example-sinatra-pastie.git 28 | cd rethinkdb-example-sinatra-pastie 29 | bundle 30 | ``` 31 | 32 | _Note_: If you don't have RethinkDB installed, you can follow [these instructions to get it up and running](http://www.rethinkdb.com/docs/install/). 33 | 34 | # Running the application # 35 | 36 | Running a Sinatra app is as easy as: 37 | 38 | ``` 39 | rackup 40 | ``` 41 | 42 | # Credits # 43 | 44 | * This sample app was inspired by Nick Plante's [toopaste](https://github.com/zapnap/toopaste) project. 45 | * The snippets of code used for syntax highlighting are from Ryan Tomayko's [rocco.rb](https://github.com/rtomayko/rocco) project. 46 | * Code highlighting in snippets is done using [Pygments](http://pygments.org) or the [Pygments web service](http://pygments.appspot.com/) 47 | * The [Solarized dark Pygments stylesheet](https://gist.github.com/1573884) was created by Zameer Manji 48 | 49 | # License # 50 | 51 | This demo application is licensed under the [MIT license](http://opensource.org/licenses/mit-license.php). 52 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require './repasties' 2 | run Sinatra::Application 3 | -------------------------------------------------------------------------------- /repasties.rb: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/ruby -rubygems 2 | # 3 | # A simple [Pastie](http://pastie.org)-like app inspired by Nick Plante's 4 | # [toopaste](https://github.com/zapnap/toopaste) project showing how to use 5 | # **RethinkDB as a backend for Sinatra applications**. 6 | 7 | 8 | require 'sinatra' 9 | require 'rethinkdb' 10 | 11 | #### Connection details 12 | 13 | # If the application is deployed to Cloud Foundry, then a 14 | # `$VCAP_SERVICES` environment variable is available (JSON format) 15 | # to describe the binding to a RethinkDB service instance. 16 | # If no `$VCAP_SERVICES` variable then application is not running 17 | # on Cloud Foundry. 18 | if ENV['VCAP_SERVICES'] 19 | services = JSON.parse(ENV['VCAP_SERVICES']) 20 | if service = services["rethinkdb"].first 21 | creds = service["credentials"] 22 | rdb_config = { 23 | :host => creds["hostname"] || creds["host"], 24 | :port => creds["port"], 25 | :db => creds["name"] || 'repasties' 26 | } 27 | end 28 | end 29 | 30 | # If `rdb_config` not already setup, then look for environment 31 | # variables for location of RethinkDB server. Otherwise default 32 | # to a locally running server. 33 | rdb_config ||= { 34 | :host => ENV['RDB_HOST'] || 'localhost', 35 | :port => ENV['RDB_PORT'] || 28015, 36 | :db => ENV['RDB_DB'] || 'repasties' 37 | } 38 | 39 | # A friendly shortcut for accessing ReQL functions 40 | r = RethinkDB::RQL.new 41 | 42 | #### Setting up the database 43 | 44 | # The app will use a table `snippets` in the database defined by the 45 | # environment variable `RDB_DB` (defaults to `repasties`). 46 | # 47 | # We'll create the database and the table here using 48 | # [`db_create`](http://www.rethinkdb.com/api/ruby/db_create/) 49 | # and 50 | # [`table_create`](http://www.rethinkdb.com/api/ruby/table_create/) commands. 51 | configure do 52 | set :db, rdb_config[:db] 53 | begin 54 | connection = r.connect(:host => rdb_config[:host], 55 | :port => rdb_config[:port]) 56 | rescue Exception => err 57 | puts "Cannot connect to RethinkDB database #{rdb_config[:host]}:#{rdb_config[:port]} (#{err.message})" 58 | Process.exit(1) 59 | end 60 | 61 | begin 62 | r.db_create(rdb_config[:db]).run(connection) 63 | rescue RethinkDB::RqlRuntimeError => err 64 | puts "Database `repasties` already exists." 65 | end 66 | 67 | begin 68 | r.db(rdb_config[:db]).table_create('snippets').run(connection) 69 | rescue RethinkDB::RqlRuntimeError => err 70 | puts "Table `snippets` already exists." 71 | ensure 72 | connection.close 73 | end 74 | end 75 | 76 | 77 | #### Managing connections 78 | 79 | 80 | # The pattern we're using for managing database connections is to have 81 | # **a connection per request**. We're using Sinatra's `before` and `after` for 82 | # [opening a database connection](http://www.rethinkdb.com/api/ruby/connect/) and 83 | # [closing it](http://www.rethinkdb.com/api/ruby/close/) respectively. 84 | before do 85 | begin 86 | # When opening a connection we can also specify the database: 87 | @rdb_connection = r.connect(:host => rdb_config[:host], :port => 88 | rdb_config[:port], :db => settings.db) 89 | rescue Exception => err 90 | logger.error "Cannot connect to RethinkDB database #{rdb_config[:host]}:#{rdb_config[:port]} (#{err.message})" 91 | halt 501, 'This page could look nicer, unfortunately the error is the same: database not available.' 92 | end 93 | end 94 | 95 | # After each request we [close the database connection](http://www.rethinkdb.com/api/ruby/close/). 96 | after do 97 | begin 98 | @rdb_connection.close if @rdb_connection 99 | rescue 100 | logger.warn "Couldn't close connection" 101 | end 102 | end 103 | 104 | get '/' do 105 | @snippet = {} 106 | erb :new 107 | end 108 | 109 | 110 | # We create a new snippet in response to a POST request using 111 | # [`table.insert`](http://www.rethinkdb.com/api/ruby/insert/). 112 | post '/' do 113 | @snippet = { 114 | :title => params[:snippet_title], 115 | :body => params[:snippet_body], 116 | :lang => (params[:snippet_lang] || 'text').downcase, 117 | } 118 | if @snippet[:body].empty? 119 | erb :new 120 | end 121 | 122 | if @snippet[:title].empty? 123 | @snippet[:title] = @snippet[:body].scan(/\w+/)[0..2].join(' ') 124 | erb :new 125 | end 126 | 127 | @snippet[:created_at] = Time.now.to_i 128 | @snippet[:formatted_body] = pygmentize(@snippet[:body], @snippet[:lang]) 129 | 130 | result = r.table('snippets').insert(@snippet).run(@rdb_connection) 131 | 132 | # The `insert` operation returns a single object specifying the number 133 | # of successfully created objects and their corresponding IDs. 134 | # 135 | # ``` 136 | # { 137 | # "inserted": 1, 138 | # "errors": 0, 139 | # "generated_keys": [ 140 | # "fcb17a43-cda2-49f3-98ee-1efc1ac5631d" 141 | # ] 142 | # } 143 | # ``` 144 | if result['inserted'] == 1 145 | redirect "/#{result['generated_keys'][0]}" 146 | else 147 | logger.error result 148 | redirect '/' 149 | end 150 | end 151 | 152 | # Every new snippet automatically gets assigned a unique ID. The browser can 153 | # retrieve a specific snippet by GETing `/`. To query the database 154 | # for a single document by its ID, we use the 155 | # [`get`](http://www.rethinkdb.com/api/ruby/get/) command. 156 | get '/:id' do 157 | @snippet = r.table('snippets').get(params[:id]).run(@rdb_connection) 158 | 159 | if @snippet 160 | @snippet['created_at'] = Time.at(@snippet['created_at']) 161 | erb :show 162 | else 163 | redirect '/' 164 | end 165 | end 166 | 167 | # Retrieving the latest `max_results` (default 10) snippets by their language 168 | # by chaining together [`filter`](http://www.rethinkdb.com/api/ruby/filter/), 169 | # [`pluck`](http://www.rethinkdb.com/api/ruby/pluck/), and 170 | # [`order_by`](http://www.rethinkdb.com/api/ruby/order_by/). 171 | # All chained operations are executed on the database server and the results are 172 | # returned as a batched iterator. 173 | get '/lang/:lang' do 174 | @lang = params[:lang].downcase 175 | max_results = params[:limit] || 10 176 | results = r.table('snippets'). 177 | filter('lang' => @lang). 178 | pluck('id', 'title', 'created_at'). 179 | order_by(r.desc('created_at')). 180 | limit(max_results). 181 | run(@rdb_connection) 182 | 183 | @snippets = results.to_a 184 | @snippets.each { |s| s['created_at'] = Time.at(s['created_at']) } 185 | erb :list 186 | end 187 | 188 | 189 | # List of languages for which syntax highlighting is supported. 190 | SUPPORTED_LANGUAGES = ['Ruby', 'Python', 'Javascript', 'Bash', 191 | 'ActionScript', 'AppleScript', 'Awk', 'C', 'C++', 'Clojure', 192 | 'CoffeeScript', 'Lisp', 'Erlang', 'Fortran', 'Groovy', 193 | 'Haskell', 'Io', 'Java', 'Lua', 'Objective-C', 194 | 'OCaml', 'Perl', 'Prolog', 'Scala', 'Smalltalk'].sort 195 | 196 | # A Sinatra helper to expose the list of languages to views. 197 | helpers do 198 | def languages 199 | SUPPORTED_LANGUAGES 200 | end 201 | end 202 | 203 | # Code is run through [Pygments](http://pygments.org/) for syntax 204 | # highlighting. If it's not installed, locally, use a webservice 205 | # http://pygments.appspot.com/. 206 | # (code inspired by [rocco.rb](http://rtomayko.github.com/rocco/)) 207 | unless ENV['PATH'].split(':').any? { |dir| 208 | File.executable?("#{dir}/pygmentize") } 209 | warn "WARNING: Pygments not found. Using webservice." 210 | PYGMENTIZE=false 211 | else 212 | PYGMENTIZE=true 213 | end 214 | 215 | 216 | def pygmentize(code, lang) 217 | if lang.eql? 'text' 218 | return code 219 | end 220 | lang.downcase! 221 | if PYGMENTIZE 222 | highlight_pygmentize(code, lang) 223 | else 224 | highlight_webservice(code, lang) 225 | end 226 | end 227 | 228 | def highlight_pygmentize(code, lang) 229 | code_html = nil 230 | open("|pygmentize -l #{lang} -f html -O encoding=utf-8,style=colorful,linenos=1", 231 | 'r+') do |fd| 232 | pid = 233 | fork { 234 | fd.close_read 235 | fd.write code 236 | fd.close_write 237 | exit! 238 | } 239 | fd.close_write 240 | code_html = fd.read 241 | fd.close_read 242 | Process.wait(pid) 243 | end 244 | 245 | code_html 246 | end 247 | 248 | require 'net/http' 249 | 250 | def highlight_webservice(code, lang) 251 | url = URI.parse 'http://pygments.appspot.com/' 252 | options = {'lang' => lang, 'code' => code} 253 | Net::HTTP.post_form(url, options).body 254 | end 255 | 256 | # ### Best practices ### 257 | # 258 | # #### Managing connections: a connection per request #### 259 | # 260 | # The RethinkDB server doesn't use a thread-per-connnection approach 261 | # so opening connections per request will not slow down your database. 262 | # 263 | # #### Fetching multiple rows: batched iterators #### 264 | # 265 | # When fetching multiple rows from a table, RethinkDB returns a 266 | # batched iterator initially containing a subset of the complete 267 | # result. Once the end of the current batch is reached, a new batch is 268 | # automatically retrieved from the server. From a coding point of view 269 | # this is transparent: 270 | # 271 | # r.table('todos').run(g.rdb_conn).each do |result| 272 | # print result 273 | # end 274 | # 275 | 276 | #### Credit 277 | 278 | # * This sample app was inspired by Nick Plante's [toopaste](https://github.com/zapnap/toopaste) project. 279 | # * The snippets of code used for syntax highlighting are from Ryan Tomayko's [rocco.rb](https://github.com/rtomayko/rocco) project. 280 | # * Snippets code highlighting is done using [Pygments](http://pygments.org) or the [Pygments web service](http://pygments.appspot.com/) 281 | # * The [Solarized dark Pygments stylesheet](https://gist.github.com/1573884) was created by Zameer Manji 282 | 283 | #### License 284 | 285 | # This demo application is licensed under the MIT license: 286 | -------------------------------------------------------------------------------- /views/layout.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= @title || 'Repastie!' %> 5 | 124 | 125 | 126 | <%= yield %> 127 | 128 |