Permalink
Browse files

First pass at rails action args

  • Loading branch information...
1 parent 4368ad5 commit f4f03979ee2b7c59d5127ac9c944c70efdff4f19 Andy Delcambre committed Aug 19, 2009
View
@@ -0,0 +1,2 @@
+require 'rails_action_args/get_args'
+require 'rails_action_args/abstract_controller'
@@ -0,0 +1,44 @@
+class AbstractController::Base
+
+ class << self
+ attr_accessor :action_argument_list
+ alias_method :old_inherited, :inherited
+
+ # Stores the argument lists for all methods for this class.
+ #
+ # ==== Parameters
+ # klass<Class>::
+ # The controller that is being inherited from Merb::AbstractController.
+ def inherited(klass)
+ klass.action_argument_list = Hash.new do |h,k|
+ args = klass.instance_method(k).get_args
+ arguments = args[0]
+ defaults = []
+ arguments.each {|a| defaults << a[0] if a.size == 2} if arguments
+ h[k] = [arguments || [], defaults]
+ end
+ old_inherited(klass)
+ end
+ end
+
+ # Calls an action and maps the params hash to the action parameters.
+ #
+ # ==== Parameters
+ # action<Symbol>:: The action to call
+ #
+ # ==== Raises
+ # BadRequest:: The params hash doesn't have a required parameter.
+ def send_action(action)
+ arguments, defaults = self.class.action_argument_list[action]
+
+ args = arguments.map do |arg, default|
+ p = params.key?(arg.to_sym)
+ unless p || (defaults && defaults.include?(arg))
+ missing = arguments.reject {|arg| params.key?(arg[0].to_sym || arg[1])}
+ raise BadRequest, "Your parameters (#{params.inspect}) were missing #{missing.join(", ")}"
+ end
+ p ? params[arg.to_sym] : default
+ end
+ __send__(action, *args)
+ end
+end
@@ -0,0 +1,15 @@
+if RUBY_PLATFORM == "java"
+ require File.join(File.dirname(__FILE__), "jruby_args")
+elsif RUBY_VERSION < "1.9"
+ require File.join(File.dirname(__FILE__), "mri_args")
+else
+ require File.join(File.dirname(__FILE__), "vm_args")
+end
+
+class UnboundMethod
+ include GetArgs
+end
+
+class Method
+ include GetArgs
+end
@@ -0,0 +1,67 @@
+require 'java'
+require 'jruby'
+
+module GetArgs
+ Methods = org.jruby.internal.runtime.methods
+
+ def get_args
+ real_method = JRuby.reference(self)
+
+ # hack to expose a protected field; could be improved in 1.1.5
+ method_field = org.jruby.RubyMethod.java_class.declared_field(:method)
+
+ method_field.accessible = true
+
+ dyn_method = method_field.value(real_method)
+
+ case dyn_method
+ when Methods.MethodArgs
+ return build_args(dyn_method.args_node)
+ else
+ raise "Can't get args from method: #{self}"
+ end
+ end
+
+ def build_args(args_node)
+ args = []
+ required = []
+ optional = []
+
+ # required args
+ if (args_node.args && args_node.args.size > 0)
+ required << args_node.args.child_nodes.map { |arg| [arg.name.to_s.intern] }
+ end
+
+ # optional args
+ if (args_node.opt_args && args_node.opt_args.size > 0)
+ optional << args_node.opt_args.child_nodes.map do |arg|
+ name = arg.name.to_s.intern
+ value_node = arg.value_node
+ case value_node
+ when org.jruby.ast::FixnumNode
+ value = value_node.value
+ when org.jruby.ast::SymbolNode
+ value = value_node.get_symbol(JRuby.runtime)
+ when org.jruby.ast::StrNode
+ value = value_node.value
+ else
+ value = nil
+ end
+ [name, value]
+ end
+ end
+
+ first_args = required.first
+ optional.first.each {|arg| first_args << arg} if optional.first
+
+ args = [first_args]
+
+ rest = args_node.rest_arg_node
+ args << (rest ? rest.name.to_s.intern : nil)
+
+ block = args_node.block_arg_node
+ args << (block ? block.name.to_s.intern : nil)
+
+ args
+ end
+end
@@ -0,0 +1,102 @@
+require 'parse_tree'
+require 'ruby2ruby'
+
+class Object
+ def full_const_get(name)
+ list = name.split("::")
+ list.shift if list.first.blank?
+ obj = self
+ list.each do |x|
+ # This is required because const_get tries to look for constants in the
+ # ancestor chain, but we only want constants that are HERE
+ obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
+ end
+ obj
+ end
+end
+
+class ParseTreeArray < Array
+ R2R = Object.const_defined?(:Ruby2Ruby) ? Ruby2Ruby : RubyToRuby
+
+ def self.translate(*args)
+ sexp = ParseTree.translate(*args)
+ # ParseTree.translate returns [nil] if called on an inherited method, so walk
+ # up the inheritance chain to find the class that the method was defined in
+ unless sexp.first
+ klass = args.first.ancestors.detect do |klass|
+ klass.public_instance_methods(false).include?(args.last.to_s)
+ end
+ sexp = ParseTree.translate(klass, args.last) if klass
+ end
+ sexp = Unifier.new.process(sexp)
+ self.new(sexp)
+ end
+
+ def deep_array_node(type = nil)
+ each do |node|
+ return ParseTreeArray.new(node) if node.is_a?(Array) && (!type || node[0] == type)
+ next unless node.is_a?(Array)
+ return ParseTreeArray.new(node).deep_array_node(type)
+ end
+ nil
+ end
+
+ def arg_nodes
+ self[1..-1].inject([]) do |sum,item|
+ sum << [item] unless item.is_a?(Array)
+ sum
+ end
+ end
+
+ def get_args
+ if arg_node = deep_array_node(:args)
+ # method defined with def keyword
+ args = arg_node.arg_nodes
+ default_node = arg_node.deep_array_node(:block)
+ return [args, []] unless default_node
+ else
+ # assuming method defined with Module#define_method
+ return [[],[]]
+ end
+
+ # if it was defined with def, and we found the default_node,
+ # that should bring us back to regularly scheduled programming..
+ lasgns = default_node[1..-1]
+ lasgns.each do |asgn|
+ args.assoc(asgn[1]) << eval(R2R.new.process(asgn[2]))
+ end
+ [args, (default_node[1..-1].map { |asgn| asgn[1] })]
+ end
+
+end
+
+# Used in mapping controller arguments to the params hash.
+# NOTE: You must have the 'ruby2ruby' gem installed for this to work.
+#
+# ==== Examples
+# # In PostsController
+# def show(id) #=> id is the same as params[:id]
+module GetArgs
+
+ # ==== Returns
+ # Array:: Method arguments and their default values.
+ #
+ # ==== Examples
+ # class Example
+ # def hello(one,two="two",three)
+ # end
+ #
+ # def goodbye
+ # end
+ # end
+ #
+ # Example.instance_method(:hello).get_args
+ # #=> [[:one], [:two, "two"], [:three, "three"]]
+ # Example.instance_method(:goodbye).get_args #=> nil
+ def get_args
+ klass, meth = self.to_s.split(/ /).to_a[1][0..-2].split("#")
+ # Remove stupidity for #<Method: Class(Object)#foo>
+ klass = $` if klass =~ /\(/
+ ParseTreeArray.translate(Object.full_const_get(klass), meth).get_args
+ end
+end
@@ -0,0 +1,27 @@
+begin
+ require "methopara"
+rescue
+ puts "make sure you have methora http://github.com/genki/methopara installed if you want to use action args on Ruby 1.9"
+end
+
+module GetArgs
+ def get_args
+ unless respond_to?(:parameters)
+ raise NotImplementedError, "Ruby #{RUBY_VERSION} doesn't support #{self.class}#parameters"
+ end
+
+ required = []
+ optional = []
+
+ parameters.each do |(type, name)|
+ if type == :opt
+ required << [name, nil]
+ optional << name
+ else
+ required << [name]
+ end
+ end
+
+ return [required, optional]
+ end
+end
@@ -0,0 +1,55 @@
+module ExtraActions
+ # def self.included(base)
+ # base.show_action(:funky_inherited_method)
+ # end
+
+ def funky_inherited_method(foo, bar)
+ render :text => "#{foo} #{bar}"
+ end
+end
+
+module Awesome
+ class ActionArgs < ActionController::Base
+ def index(foo)
+ render :text => foo.to_s
+ end
+ end
+end
+
+class ActionArgs < ActionController::Base
+ include ExtraActions
+
+ def nada
+ render :text => "NADA"
+ end
+
+ def index(foo)
+ render :text => foo
+ end
+
+ def multi(foo, bar)
+ render :text => "#{foo} #{bar}"
+ end
+
+ def defaults(foo, bar = "bar")
+ render :text => "#{foo} #{bar}"
+ end
+
+ def defaults_mixed(foo, bar ="bar", baz = "baz")
+ render :text => "#{foo} #{bar} #{baz}"
+ end
+
+ define_method :dynamic_define_method do
+ render :text => "mos def"
+ end
+
+ def with_default_nil(foo, bar = nil)
+ render :text => "#{foo} #{bar}"
+ end
+
+ def with_default_array(foo, bar = [])
+ render :text => "#{foo} #{bar.inspect}"
+ end
+
+end
+
@@ -1,7 +1,57 @@
-require 'spec_helper'
+require File.expand_path(File.join(File.dirname(__FILE__), "spec_helper"))
describe "RailsActionArgs" do
- it "fails" do
- fail "hey buddy, you should probably rename this file and start specing for real"
+ it "should be able to handle a nested class" do
+ Awesome::ActionArgs.action(:index).call(Rack::MockRequest.env_for("/?foo=bar"))[2].body.should == "bar"
end
-end
+
+ it "should be able to handle no arguments" do
+ ActionArgs.action(:nada).call(Rack::MockRequest.env_for("/"))[2].body.should == "NADA"
+ end
+
+ it "should be able to accept Action Arguments" do
+ ActionArgs.action(:index).call(Rack::MockRequest.env_for("/?foo=bar"))[2].body.should == "bar"
+ end
+
+ it "should be able to accept multiple Action Arguments" do
+ ActionArgs.action(:multi).call(Rack::MockRequest.env_for("/?foo=bar&bar=baz"))[2].body.should == "bar baz"
+ end
+
+ it "should be able to handle defaults in Action Arguments" do
+ ActionArgs.action(:defaults).call(Rack::MockRequest.env_for("/?foo=bar"))[2].body.should == "bar bar"
+ end
+
+ it "should be able to handle out of order defaults" do
+ ActionArgs.action(:defaults_mixed).call(Rack::MockRequest.env_for("/?foo=bar&baz=bar"))[2].body.should == "bar bar bar"
+ end
+
+ # it "should throw a BadRequest if the arguments are not provided" do
+ # lambda { ActionArgs.action(:index).call(Rack::MockRequest.env_for("/")) }.should raise_error(Merb::ControllerExceptions::BadRequest)
+ # end
+
+ it "should treat define_method actions as equal" do
+ ActionArgs.action(:dynamic_define_method).call(Rack::MockRequest.env_for("/"))[2].body.should == "mos def"
+ end
+
+ it "should be able to inherit actions for use with Action Arguments" do
+ ActionArgs.action(:funky_inherited_method).call(Rack::MockRequest.env_for("/?foo=bar&bar=baz"))[2].body.should == "bar baz"
+ end
+
+ it "should be able to handle nil defaults" do
+ ActionArgs.action(:with_default_nil).call(Rack::MockRequest.env_for("/?foo=bar"))[2].body.should == "bar "
+ end
+
+ it "should be able to handle [] defaults" do
+ ActionArgs.action(:with_default_array).call(Rack::MockRequest.env_for("/?foo=bar"))[2].body.should == "bar []"
+ end
+
+ # it "should print out the missing parameters if all are required" do
+ # lambda { ActionArgs.action(:multi).call(Rack::MockRequest.env_for("/")) }.should raise_error(
+ # Merb::ControllerExceptions::BadRequest, /were missing foo, bar/)
+ # end
+ #
+ # it "should only print out missing parameters" do
+ # lambda { ActionArgs.action(:multi).call(Rack::MockRequest.env_for("/?foo=Hello")) }.should raise_error(
+ # Merb::ControllerExceptions::BadRequest, /were missing bar/)
+ # end
+end
View
@@ -1,9 +1,11 @@
-require 'spec'
+$:.unshift File.join(File.dirname(__FILE__), '..', 'lib')
-$LOAD_PATH.unshift(File.dirname(__FILE__))
-$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
+require 'rubygems'
+require 'rack'
+require 'action_controller'
require 'rails_action_args'
+require 'spec'
+require File.expand_path(File.join(File.dirname(__FILE__), "controllers", "action_args"))
Spec::Runner.configure do |config|
-
end

0 comments on commit f4f0397

Please sign in to comment.