build-reddit-in-ruby

We are using an application from a previous post to explore seldom used features of Ruby.

This tutorial will highlight concepts such as ternary operators, multiple assignment, and multiple argument passing.

1. Clone Reddit in Sinatra Repo

1
git clone git://github.com/DruRly/reddit-in-sinatra.git -b 1.0.x

2. Install Gems

1
2
cd reddit-in-sinatra
bundle install

Run this command from the reddit-in-sinatra directory.

This will reference the Gemfile and download all dependencies. If you have any issues installing dependencies on Ubuntu, please reference the initial post.

3. Start Server

1
ruby reddit.rb

Starting the server allows us to interact with the application as we complete each step of refactoring.

The application will be available at localhost:4567.

4. Open reddit.rb

reddit.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
require 'sinatra'
require 'data_mapper'
require 'haml'
require 'sinatra/reloader'

DataMapper::setup(:default,"sqlite3://#{Dir.pwd}/example.db")

class Link
  include DataMapper::Resource
  property :id, Serial
  property :title, String
  property :url, Text
  property :score, Integer
  property :points, Integer, :default => 0
  property :created_at, Time

  attr_accessor :score

  def calculate_score
    time_elapsed = (Time.now - self.created_at) / 3600
    self.score = ((self.points-1) / (time_elapsed+2)**1.8).real
  end

  def self.all_sorted_desc
    self.all.each { |item| item.calculate_score }.sort { |a,b| a.score <=> b.score }.reverse
  end
end

DataMapper.finalize.auto_upgrade!

get '/' do
  @links = Link.all :order => :id.desc
  haml :index
end

get '/hot' do
  @links = Link.all_sorted_desc
  haml :index
end

post '/' do
  l = Link.new
  l.title = params[:title]
  l.url = params[:url]
  l.created_at = Time.now
  l.save
  redirect back
end

put '/:id/vote/:type' do
  l = Link.get params[:id]
  l.points += params[:type].to_i
  l.save
  redirect back
end

We will refactor this code step-by-step until we have reached 10 lines.

A full explanation of what this code does is available in the initial post.

5. Require Gems on One Line

1
2
3
4
require 'sinatra'
require 'data_mapper'
require 'haml'
require 'sinatra/reloader'

becomes..

1
%w{sinatra data_mapper haml sinatra/reloader}.each { |lib| require lib}

We are iterating over each gem which eliminates duplicate require statements.

6. Declare Two Statements on One Line

1
2
3
include DataMapper::Resource
...
attr_accessor :score

becomes..

1
include DataMapper::Resource; attr_accessor :score

We are declaring two short statements on one line. This is also useful in irb.

Ruby interprets the semicolon as a line break in your code.

7. Pass Multiple Arguments

1
2
3
4
5
6
property :id, Serial
property :title, String
property :url, Text
property :score, Integer
property :points, Integer, :default => 0
property :created_at, Time

becomes..

1
[[:id, Serial],[:title, String],[:url, Text],[:score, Integer],[:points, Integer, :default => 0],[:created_at, Time]].each { |args| property *args; }

We are passing multiple arguments to the property method.

This is similar to our strategy used for requiring gems, however, we are passing multiple arguments to property by enclosing the attributes in arrays.

8. Write One Line Functions

1
2
3
4
5
6
7
8
def calculate_score
  time_elapsed = (Time.now - self.created_at) / 3600
  self.score = ((self.points-1) / (time_elapsed+2)**1.8).real
end

def self.all_sorted_desc
  self.all.each { |item| item.calculate_score }.sort { |a,b| a.score <=> b.score }.reverse
end

becomes..

1
2
def calculate_score; self.score = ((self.points-1) / (((Time.now - self.created_at) / 3600) +2)**1.8).real end
def self.all_sorted_desc; self.all.each { |item| item.calculate_score }.sort { |a,b| a.score <=> b.score }.reverse end

We are flattening our functions by using semicolons. calculate_score is now composed of one equation instead of two.

9. Write One Line Blocks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
get '/' do
  @links = Link.all :order => :id.desc
  haml :index
end

get '/hot' do
  @links = Link.all_sorted_desc
  haml :index
end

post '/' do
  l = Link.new
  l.title = params[:title]
  l.url = params[:url]
  l.created_at = Time.now
  l.save
  redirect back
end

put '/:id/vote/:type' do
  l = Link.get params[:id]
  l.points += params[:type].to_i
  l.save
  redirect back
end

becomes..

1
2
3
4
get ('/') { @links = Link.all :order => :id.desc; haml :index; }
get ('/hot') { @links = Link.all_sorted_desc; haml :index; }
post ('/') { l = Link.new; l.title = params[:title]; l.url = params[:url]; l.save; redirect back }
put ('/:id/vote/:type') { l = Link.get params[:id]; l.points += (params[:type].to_i); l.save; redirect back; }

We are using curly braces to declare ruby blocks instead of using do-end syntax.

10. Use Multiple Assignment

1
post ('/') { l = Link.new; l.title = params[:title]; l.url = params[:url]; l.created_at = Time.now; l.save; redirect back }

becomes..

1
post ('/') { l, l.title, l.url, l.created_at = Link.new, params[:title], params[:url], Time.now; l.save; redirect back }

We are using multiple assignment to define variables with a single = sign.

11. Use Ternary Operators

1
2
3
4
get ('/') { @links = Link.all :order => :id.desc; haml :index; }
get ('/hot') { @links = Link.all_sorted_desc; haml :index; }
post ('/') { l, l.title, l.url, l.created_at = Link.new, params[:title], params[:url], Time.now; l.save; redirect back }
put ('/:id/vote/:type') { l = Link.get params[:id]; l.points += (params[:type].to_i); l.save; redirect back; }

becomes..

1
2
['/', '/hot'].each {|path| get (path) { @links = (path == '/' ? (Link.all :order => :id.desc) : (Link.all_sorted_desc)); haml :index }}
['/', '/:id/vote/:type'].each {|path| path == '/:id/vote/:type' ? (put (path) { l = Link.get params[:id]; l.points += params[:type].to_i; l.save; redirect back }) : (post (path) {l, l.title, l.url, l.created_at = Link.new, params[:title], params[:url], Time.now; l.save!; redirect back})}

We are using ternary operators as if-else statements.

Review the use of simple ternary opertors and then revisit this code if you are having trouble grasping the syntax.

12. Use Nested Ternary Operators

1
2
['/', '/hot'].each {|path| get (path) { @links = (path == '/' ? (Link.all :order => :id.desc) : (Link.all_sorted_desc)); haml :index }}
['/', '/:id/vote/:type'].each {|path| path == '/:id/vote/:type' ? (put (path) { l = Link.get params[:id]; l.points += params[:type].to_i; l.save; redirect back }) : (post (path) {l, l.title, l.url, l.created_at = Link.new, params[:title], params[:url], Time.now; l.save!; redirect back})}

becomes..

1
['/', '/hot', '/:id/vote/:type'].each { |path| path == '/' ? (((get (path) {@links = Link.all :order => :id.desc; haml :index}) && (post (path) {l, l.title, l.url, l.created_at = Link.new, params[:title], params[:url], Time.now; l.save!; redirect back}))) : path == '/hot' ? (get (path) { @links = Link.all_sorted_desc; haml :index}) : (put (path) {l = Link.get params[:id]; l.points += params[:type].to_i; l.save; redirect back})}

We are using nested ternary operators as complex if-elsif-else statements.

More examples of nested ternary operators are available.

Final Result

reddit.rb
1
2
3
4
5
6
7
8
9
10
%w{sinatra data_mapper haml sinatra/reloader}.each { |lib| require lib}
DataMapper::setup(:default,"sqlite3://#{Dir.pwd}/example.db")
class Link
  include DataMapper::Resource; attr_accessor :score
  [[:id, Serial],[:title, String],[:url, Text],[:score, Integer],[:points, Integer, :default => 0],[:created_at, Time]].each { |args| property *args; }
  def calculate_score; self.score = ((self.points-1) / (((Time.now - self.created_at) / 3600) +2)**1.8).real end
  def self.all_sorted_desc; self.all.each { |item| item.calculate_score }.sort { |a,b| a.score <=> b.score }.reverse end
end
DataMapper.finalize.auto_upgrade!
['/', '/hot', '/:id/vote/:type'].each { |path| path == '/' ? (((get (path) {@links = Link.all :order => :id.desc; haml :index}) && (post (path) {l, l.title, l.url, l.created_at = Link.new, params[:title], params[:url], Time.now; l.save!; redirect back}))) : path == '/hot' ? (get (path) { @links = Link.all_sorted_desc; haml :index}) : (put (path) {l = Link.get params[:id]; l.points += params[:type].to_i; l.save; redirect back})}

Although the code is abstract, the individual ideas are rock-solid and I encourage you to explore them further.

Github

Code Avaiable at Reddit-in-10-Lines

Conclusion

This was an excercise created to explore seldom used features of Ruby. There are appropriate use cases for these concepts in your everyday work.

Check out my initial post and consider using Sinatra for your next project. Thanks for reading.

Comments