Created
April 10, 2012 19:22
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- 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 | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Some feedback:
cc.errors
output the hash on #L126 and markup on #L142?conduces
method on #L22 probably needs some explanationcase
statement on #L24 probably needs some explanationurl
example on #L35 might be a little contrived -- why not use thehelper
helperMy example app is here: https://github.com/cookrn/dao_conducer_example