Jump To …

controller.rb

This file is where everything related to controllers is written.

require "base64"

class Rocket
  

We use a class variable on the Rocket class to hold a hash of all controllers.

  @@controllers = {}

  def self.controllers
    @@controllers
  end
  
  module Controller
    

This callback adds the new class to the controller hash. It strips “Controller” off the end of the class name in order to simplify the API. This way you can call “rocket(‘User.Show’: {id: 1})” rather than “rocket(‘UserController.Show’: {id: 1})”. It also sets up an attraccessor for args, params, and currentuser, as well as a class attr_reader for storing the controller actions.

    def self.included(other)
      Rocket.controllers[other.to_s.sub(/Controller$/, "").to_sym] = other
      other.send :attr_accessor, :args, :params, :current_user
      other.extend ClassMethods
      class << other
        attr_reader :actions
      end
    end
    
    module ClassMethods

This is how actions are defined. As you can see, it’s incredibly simple. Just take the block and put it into the actions class instance variable. Note that @action is not an instance variable on the object, but an instance variable on the class.

Everything is a class in Ruby, including classes. Since all objects can have instance variables, and classes are objects, classes can have instance variables. Make sense? If not, check out this great post by John Nunemaker: http://railstips.org/blog/archives/2006/11/18/class-and-instance-variables-in-ruby/

      def define_action(name, &blk)
        @actions ||= {}
        @actions[name.to_sym] = blk
      end
      

This is a controller helper that sets you with with a “File First” upload. It defines two controller actions for the user. Note that the “Simple Upload” does not need a controller action, as everything happens client side.

      def file_first(name, &blk)

The first action creates a tempfile and gives the client an ID to reference it by. Since a client could upload multiple files simultaneously, this is necessary.

        define_action "#{name}_request_tempfile".to_sym do
          id = "#{self.class.to_s}_#{name}_#{Time.now.to_i.to_s}_#{rand(9999999).to_s}"
          File.open(File.join(APP_ROOT, "tmp", id), "w") {|f| f.write ""}
          current_user.transmit("App.tempfile" => {id: id, req_id: params["req_id"]})
        end
        

The second action receives the base64 encoded file chunks, decodes them, and writes them to the temp file. Once the upload is complete, it sends a callback to the user to let them know the file is ready.

        define_action "#{name}_receive_file".to_sym do
          File.open(File.join(APP_ROOT, "tmp", params["id"]), "ab") {|f| f.write Base64.decode64(params["chunk"])}
          if params["complete"]
            current_user.transmit({"App.finished_upload" => {id: params["id"], req_id: params["req_id"]}})
          end
        end
      end
    end
    

This is the code that turns your params pseudo hash into a true hash. It always groups things as a hash, even things that would normally be an array. Arrays come out as as a hash with stringified numbers as keys. That will be fixed soon. In fact, I’m considering having the client take care of generating the params, as I don’t see any reason for the server to waste time performing the calculations.

    def paramify(hsh)
      return {} if hsh == ""
      t = {}
      hsh.each_pair do |k, v|
        chunks = k.split("[").map {|s| s.sub /\]$/, ""}
        if chunks.length > 1
          iter = t
          chunks.each_with_index do |c, i|
            if i+1 == chunks.length
              iter[c] = v
            else
              iter[c] ||= {}
            end
            iter = iter[c]
          end
        else
          t[k] = v
        end
      end
      t
    end
    

Link to the class instance variable that holds the actions.

    def actions
      self.class.actions
    end
    

Not a true HTTP redirect obviously, as we aren’t using HTTP. This is simply how you call one controller action from another.

    def redirect(command)
      ROCKET.parse_command(current_user, command)
    end
    

This is the method that runs the controller action. It hands over the current user, and the paramified args.

    def process_command(user, command, args, params = nil)
      command = command.to_sym
      if actions.include?(command)
        @current_user = user
        @params = paramify(args)
        puts "-> #{self.class}.#{command}"
        self.instance_exec &actions[command.to_sym]
      else
        raise "Class #{self.class} does not have an action named #{command}"
      end
    end
  end
  

Pulls in all of the controllers defined in app/controllers. This should probably be recursive to support namespacing.

  Dir[File.join(APP_ROOT, "app", "controllers", "*.rb")].each do |f|
    require File.expand_path(f)
  end
  
end