Now that we know what kind of an API we want to expose from our Link Model, let's define it.
Listing 7: app/models/link.rb
class Link include ActiveModel::Validations include ActiveModel::Conversion extend ActiveModel::Callbacks extend ActiveModel::Naming # Couch Model attr_accessor :url, :key, :views, :session_id, :created_at define_model_callbacks :save validates :url, :presence => true, :url => {:allow_nil => true, :message => "This is not a valid URL"} before_save :generate_key def generate_key while self.key.nil? random = SecureRandom.hex(2) self.key = random if self.class.find(random).nil? end end # ActiveModel def initialize(attributes = {}) @errors = ActiveModel::Errors.new(self) attributes.each do |name, value| setter = "#{name}=" next unless respond_to?(setter) send(setter, value) end self.views ||= 0 self.created_at ||= Time.zone.now end def to_param self.key end def persisted? return false unless (key && valid?) # TODO need a better way to track if an object is *dirty* or not... self.class.find(key).url == self.url end def save return false unless valid? run_callbacks :save do Couch.client.set(self.key, { :type => self.class.to_s.downcase, :url => self.url, :key => self.key, :views => self.views, :session_id => self.session_id, :created_at => self.created_at }) end true end def self.find(key) return nil unless key begin doc = Couch.client.get(key) self.new(doc) rescue Couchbase::Error::NotFound => e nil end end end
Might seem like a lot to take in at once, let's break it down into the three main responsibilities of the Class.
Creating a basic ActiveModel implementation to integrate with Couchbase
Defining some Model logic for the Link.
Listing 7a: app/models/link.rb
[line:3..6]
include ActiveModel::Validations include ActiveModel::Conversion extend ActiveModel::Callbacks extend ActiveModel::Naming
Firstly we include all the aspects of ActiveModel that we would like to use.
Listing 7b: app/models/link.rb [line:8]
attr_accessor :url, :key, :views, :session_id, :created_atOur model needs some Attributes so we define them.
Listing 7c: app/models/link.rb
[line:23..32]
def initialize(attributes = {}) @errors = ActiveModel::Errors.new(self) attributes.each do |name, value| next unless @@keys.include?(name.to_sym) send("#{name}=", value) end self.views ||= 0 self.created_at ||= Time.zone.now end
In our initialize method we are doing the following:
Initializing an ActiveModel::Errors object that will work with the validations we'll define later.
Iterating through the Hash of attributes passed in. We check if our list of attribute keys and if it matches we assign that attribute to the passed in value.
Listing 7d: app/models/link.rb
[line:34..36]
def to_param self.key end
To work with rails link_to methods we need to implement this method. As we would like to use our generated key.
Listing 7e: app/models/link.rb
[line:38..42]
def persisted? return false unless (key && valid?) # TODO use a better way to track if an object is *dirty* or not... self.class.find(key).url == self.url end
So that our Link objects know wether they are a new? object or not, we implemented this method. There are better ways to track this, just to demonstrate things working, once a Link is valid? we are checking to see if the object has been persisted and that the url matches.
Listing 7f: app/models/link.rb
[line:44..56]
def save return false unless valid? run_callbacks :save do # TODO should client.set return nil if successful? don't think so Couch.client.set(self.key, { :type => self.class.to_s.downcase, :url => self.url, :key => self.key, :views => self.views, :session_id => self.session_id, :created_at => self.created_at }) end end
Saving a complex document to Couchbase is a simple as passing a Hash of attributes as the second parameter to client.set(key, value).
Listing 7g: app/models/link.rb
[line:58..66]
def self.find(key) return nil unless key begin doc = Couch.client.get(key) self.new(doc) rescue Couchbase::Error::NotFound => e nil end end
Here we've implemented a simple find method that will get a given document from couchbase based on the key. It's worth noting that Couchbase will raise an error if the document you're trying to access doesn't exist in which case we rescue and return a nil.
Everything we've done up to now is providing a bit of framework to our code to allow us to create our Link object.
Listing 7h: app/models/link.rb
[line:8..19]
attr_accessor :url, :key, :views, :session_id, :created_at define_model_callbacks :save validates :url, :presence => true, :url => {:allow_nil => true} before_save :generate_key def generate_key while self.key.nil? random = SecureRandom.hex(2) self.key = random if self.class.find(random).nil? end end
What we are left with here is quite simple, we've already mentioned defining the attributes. Next, we're telling ActiveModel that it should give us a callback to bind to for the save method of a Link. Next we setup a validation on the url attribute, we're using the built-in presence validator and we've included a third party validation by way of thevalidate_url Gem. Finally we setup our save callback to call the generate_key method which takes care of creating a unique short key for our Links.