Skip to content

Instantly share code, notes, and snippets.

@ahoward
Created April 10, 2012 19:22

Revisions

  1. ahoward revised this gist Apr 11, 2012. 1 changed file with 21 additions and 20 deletions.
    41 changes: 21 additions & 20 deletions conducer.rb
    Original file line number Diff line number Diff line change
    @@ -19,8 +19,10 @@ class CommentConducer < Dao::Conducer
    def initialize(user, post, comment, params = {})
    @user, @post, @comment = user, post, comment

    conduces(@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>')
    @@ -33,7 +35,7 @@ def initialize(user, post, comment, params = {})
    end

    def url
    url_for('/comments/%d' % @comment.id)
    url_for('/comments/%d' % @comment.id) # you have access to all of rails' route helpers...
    end

    def to_s
    @@ -55,8 +57,9 @@ def save

    cc = CommentConducer.for('new', user, post, comment, params)

    # the state is determined by one model - the front model - the one being
    # 'conduced', but all the models are available...
    # 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
    @@ -132,27 +135,25 @@ def save
    __

    # and we can display errors over a group of models, related or note,
    # trivially...
    # trivially using Errors#to_s/to_html
    #
    puts
    puts cc.errors


    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>
    <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>
    <dt class="title field">Teh Api Call</dt>
    <dd class="message field">did not work</dd>
    </dl>
    </div>
    __
    __



  2. ahoward created this gist Apr 10, 2012.
    259 changes: 259 additions & 0 deletions conducer.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,259 @@
    # -*- 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

    conduces(@comment)

    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)
    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', 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...
    #
    puts
    puts cc.errors



    <<-__
    <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
    }