Skip to content

Instantly share code, notes, and snippets.

@ahoward
Created April 10, 2012 19:22
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ahoward/2353800 to your computer and use it in GitHub Desktop.
Save ahoward/2353800 to your computer and use it in GitHub Desktop.
sketching out a high level explanation of dao's conducer: presenter X conductor
# -*- encoding : utf-8 -*-
#
# conducers combine the presenter pattern with the conductor pattern. they
# can present and conduct an arbitrary number of models. they can also unify
# models with api calls and other non-persisted data in way that supports
# restufl interfaces, validations, form helpers, etc. consider conducers and
# being 'conductive to' writing web applications: you can prepare arbirtray
# data for the view and serialize arbirtray data out of it into a data store.
#
# to your controllers and views a conducer looks *just like a model* except
# that they are not limited to showing flat data: they can show any arbitrary
# graph of data - they do not suffer from the 'nested model' problem.
#
require 'rubygems'
require 'dao'
class CommentConducer < Dao::Conducer
def initialize(user, post, comment, params = {})
@user, @post, @comment = user, post, comment
# conducers can be specialized based on the current request/action, which
# is known to them but which can also be specificed using:
# Conducer.call(action, ...)
#
case action
when 'new', 'create'
update_attributes(:content => 'default comment <blink> content </blink>')
when 'edit', 'update', 'show'
:et_cetera
end
update_attributes(params)
end
def url
url_for('/comments/%d' % @comment.id) # you have access to all of rails' route helpers...
end
def to_s
sanitize attributes.content
end
def save
errors.add :teh_api_call, 'did not work'
return false
end
end
# conducers can unify a few models for presentation...
#
user = User.new(:email => 'ara@dojo4.com')
post = Post.new(:body => 'teh post...')
comment = Comment.new(:text => 'teh comment', :id => 42)
params = {:key => :val}
cc = CommentConducer.for('new', user, post, comment, params)
# the state is determined by one model - the front model - the one being #
# 'conduced' (default is the *last* model given to #initialize) , but all the
# models are available...
#
p 'cc.attributes' => cc.attributes
p 'cc.action' => cc.action
p 'cc.new_record?' => cc.new_record?
p 'cc.persisted?' => cc.persisted?
<<-__
{"cc.attributes"=>{"content"=>"default comment <blink> content </blink>", "key"=>:val} }
{"cc.action"=>"new"}
{"cc.new_record?"=>true}
{"cc.persisted?"=>false}
__
# being a presenter, of course we have access to helper-y methods...
#
p 'cc.url' => cc.url
p 'cc.to_s' => cc.to_s
<<-__
{"cc.url"=>"/comments/42"}
{"cc.to_s"=>"default comment content "}
__
# yes yes, we are a form too. notice how building a nest form is *exactly* as
# hard as building a non-nested one...
#
p cc.form.input :user, :name
p cc.form.input :comment, :text
puts
<<-__
"<input type=\"text\" name=\"dao[comment][user.name]\" class=\"dao\" id=\"comment_user-name\"/>"
"<input type=\"text\" name=\"dao[comment][comment.text]\" class=\"dao\" id=\"comment_comment-text\"/>"
__
# in the end, we want to validate and conduct data into the database... we can
# easily have loop-y or nested validations...
#
cc.errors.add :user, :name, 'is fucked'
<<-__
"<input type=\"text\" name=\"dao[comment][user.name]\" class=\"dao errors\" id=\"comment_user-name\" data-error=\"User Name: is fucked\"/>"
__
# note that data can be invalid on the form independently from model
# validations, and that form helpers know this...
#
p cc.form.input :user, :name
puts
# saving is a no-op in this example (we'd simply save models) but note that we
# can unify model errors with other, tangential errors such as api errrors
# seamlessly for the view...
#
cc.save
p cc.errors
<<-__
{"user"=>{"name"=>["is fucked"]}, "teh_api_call"=>["did not work"]}
__
# and we can display errors over a group of models, related or note,
# trivially using Errors#to_s/to_html
#
puts
puts cc.errors.to_html # errors.to_s == errors.to_html. you can override it.
<<-__
<div class="dao errors summary">
<h3 class="caption">We're so sorry, but can you please fix the following errors?</h3>
<dl class="list">
<dt class="title field">User Name</dt>
<dd class="message field">is fucked</dd>
<dt class="title field">Teh Api Call</dt>
<dd class="message field">did not work</dd>
</dl>
</div>
__
# MOCK-Y teh modelz...
BEGIN {
##
#
class Model
{
:new_record => true,
:persisted => false,
:destroyed => false,
}.each do |state, value|
class_eval <<-__
attr_writer :#{ state }
def #{ state }
@#{ state } = #{ value } unless defined?(@#{ state })
@#{ state }
end
def #{ state }?
#{ state }
end
__
end
def initialize(attributes = {})
self.attributes.update(attributes)
end
def attributes
@attributes ||= Map.new
end
def [](key)
attributes[key]
end
def []=(key, val)
attributes[key] = val
end
def update_attributes(hash = {})
hash.each{|k,v| attributes[k] = v }
end
def method_missing(method, *args, &block)
re = /^([^=!?]+)([=!?])?$/imox
matched, key, suffix = re.match(method.to_s).to_a
case suffix
when '=' then attributes.set(key, args.first)
when '!' then attributes.set(key, args.size > 0 ? args.first : true)
when '?' then attributes.has?(key)
else
attributes.has?(key) ? attributes.get(key) : super
end
end
def inspect(*args, &block)
"#{ self.class.name }( #{ attributes.inspect } )"
end
def errors
@errors ||= Map.new
end
def valid?
errors.empty?
end
def save
return false unless valid?
self.new_record = false
self.persisted = true
return true
end
def destroy
true
ensure
self.new_record = false
self.destroyed = true
end
end
##
#
class User < Model
end
class Post < Model
end
class Comment < Model
end
}
@cookrn
Copy link

cookrn commented Apr 11, 2012

Some feedback:

  • why does cc.errors output the hash on #L126 and markup on #L142?
  • the conduces method on #L22 probably needs some explanation
  • the reason for the case statement on #L24 probably needs some explanation
  • the url example on #L35 might be a little contrived -- why not use the helper helper

My example app is here: https://github.com/cookrn/dao_conducer_example

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment