So in this text, I will actually build a very simple Webapp including data that is stored on the server. I will not use a database, so as not to complicate things.
The project is the following: I like to write stories in my free time, but with my notes all over time and space (I don't know, where they're getting lost), it gets quite annoying. Also the notes are completely disorganized, but that we will fix later.
So the idea is to have a web application that stores my stories. For now, lets assume, that the stories only have a title and a body.
In ruby, classes don't need no stupid type declarations, you just call it
@variablename
and the '@' tells us that it has instance scope and the
variable can hold a string, a number, or any object. Similarily
@@classvariable
has class scope. The first, we won't even see, because Ruby
can define getters and setters automatically with the attr_accessor
function, to which you give symbols - strings that signify a name. The
minimal class, that has a publicly accessable text and title is
class Text
attr_accessor :text, :title
end
But we want a constructor, which is called initialize
in ruby and some
management of all the entities, e.g. displaying all Texts.
The constructor only passes the arguments to the setters:
class Text
attr_accessor :text, :title
def initialize( title, text )
self.text = text
self.title = title
end
# ...
end
def
defines methods (or functions) in the current scope and self
is
always bound to the current object.
Now we need a class scoped set of all the Texts, preferably only the ones we save (think of tests, where we generate Texts that should not appear in the set).
class Text
# ...
@@texts = []
def save
@@texts << self unless @@texts.include? self
end
end
Ok, that's a bit to digest:
-
@@texts = []
initializes the class scoped variable with an empty list literal. -
save
does not take any arguments, we don't need to write the parens in method declarations. -
The shift operator adds the element to the list.
-
The postfix
unless
(orif
) is an idiomatic way to write one-line conditional statements. It is equivalent tounless @@texts.include? self @@texts << self end
but is much more readable.
-
include?
is a valid method name and indeed idiomatic for predicates (methods, that return true or false). Again, you don't need to write the parens on the method calls (fun fact: attr_accessor is a method too).
That said, the delete method should be simple
class Text
# ...
def delete
@@texts.delete self
end
def self.all
@@texts
end
end
but the all
method needs some explaining again:
- Remember how
self
is always bound to the current object? In the body of a class, that object is the class you're currently defining! That's whydef self.all
defines the methodall
in the class, not the instances.
Finally, lets add a unique identifier with what we have learned:
class Text
attr_accessor :text, :title
@@text_count = 0
def initialize( title, text )
self.title = title
self.text = text
@id = @@text_count
@@text_count += 1
end
def id
@id
end
# ...
end
and add a lookup:
def self.by_id id
@@texts.detect {|txt| txt.id == id}
end
- The
detect
method takes a block, that is an executable part of code. This block is evaluated and the method returns the first element, for which the block returnstrue
. In the same fashion, there areselect
: returns a list of all the elements, for which the block is true.collect
: returns a list of all the return values for the block executed on the elementsinject
: ... Lets not get into that.
This is all there is to do for the model part.
Now we can present that with Sinatra, we need to require the models file as well and pass the variables.
require 'rubygems'
require 'sinatra'
require './models.rb'
get "/" do
haml :index, :locals => {:texts => Text.all}
end
get "/text/:id" do
text = Text.by_id(params[:id].to_i)
return 404 if text.nil?
haml :show, :locals => {:text => text}
end
- Note that I converted the parameter
id
to an integer before handing it to the#by_id
method. - I can return the 'not found' code 404 directly, if... well... I didn't find the text with the id.
- We can extract parameters from a
get
request (or any request really), by marking the variable with a colon (like a symbol).
Finally, we can display all that with two haml files:
# index.haml
%ul
- for text in texts
%li
%a( href="/text/#{text.id}" )= text.title
This displays links to all texts in an unsorted list showing their title.
- In a ruby string, you can put a ruby expression between
#{
and}
, and it will insert the value into the string. - In HAML, you can define attributes of a block (in this example
a
) by putting them in parens. The right hand side of the equal signs are ruby expressions.
Finally, we need to add a file to show a single text:
# show.haml
%h1
= text.title
%div
:markdown
#{text.text}
- In HAML, when we prepose a paragraph with a filter name, like
markdown
, then that is processed through that. Note however, that prefixing something with=
does no longer insert it.
Of course, if we run that program, it will not show anything, in lack of any texts and also a way to add them. For the moment, we can fix that by adding a fixture to the controller file:
require 'rubygems'
require 'sinatra'
require 'rdiscount'
require './models.rb'
Text.new("Humpty Dumpty", "Humpty Dumpty sat on the wall...").save
get "/" do
haml :index, :locals => {:texts => Text.all}
end
get "/text/:id" do
text = Text.by_id(params[:id].to_i)
return 404 if text.nil?
haml :show, :locals => {:text => text}
end
- Ruby objects are initialized by the
#new
method of the class, which in turn runs the#initialize
method of the new object. - There is no magic "fixture" file, the fixture is just some code that adds objects to the collection.
That concludes this. We can now build a simple model and know how to build a in-memory database. The rest should be a breeze, right?
Next time, I'll show you, how to make the application understand request like "add this text" and "this text sucks, change it" and also "this text sucks so much, forget it ever existed". You will see, that the designers of the HTTP protocol had something like that in mind.