Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Binding of entire classes; constructors; property/attribute accessors…

… for bound objects
  • Loading branch information...
commit fc4980fb95dafc87216970b062a94af5fbe8d61e 1 parent 0248162
@sinisterchipmunk authored
View
19 README.rdoc
@@ -80,6 +80,25 @@ You can also easily bind an entire object to JavaScript:
tomato.bind_object(Time.now, "time.current")
tomato.run("time.current.to_s();")
#=> "2010-06-25 18:12:23 -0400"
+
+Even better, you can also bind a whole class and instantiate it from within JavaScript. If you return the object to
+Ruby, it'll be seamlessly converted into the corresponding Ruby object.
+
+ class Person
+ def initialize(name)
+ @name = name
+ end
+ attr_accessor :name, :age
+ end
+
+ tomato.bind_object(Person, "world.Person")
+ tomato.run <<-end_js
+ var colin = new world.Person("Colin");
+ colin.age = 25;
+ colin;
+ end_js
+
+ #=> #<Person:0x00000100dbbc50 @name="Colin", @age=25>
==== Error Handling
View
2  VERSION
@@ -1 +1 @@
-0.0.1.prealpha1
+0.0.1.prealpha2
View
4 ext/tomato/Makefile
@@ -179,11 +179,11 @@ $(DLLIB): $(OBJS) Makefile
###
-binding_methods.o: binding_methods.cpp tomato.h
+binding_methods.o: binding_methods.cpp tomato.h binding_methods.h
conversions_to_js.o: conversions_to_js.cpp tomato.h
conversions_to_rb.o: conversions_to_rb.cpp tomato.h
errors.o: errors.cpp tomato.h
-object_chain.o: object_chain.cpp tomato.h
+object_chain.o: object_chain.cpp tomato.h binding_methods.h
tomato.o: tomato.cpp tomato.h
v8.o: v8.cpp tomato.h
View
7 ext/tomato/binding_methods.cpp
@@ -1,9 +1,7 @@
#include "tomato.h"
+#include "binding_methods.h"
static VALUE bound_method_call(VALUE args);
-static Handle<Value> bound_method(const Arguments& args);
-static int store_rb_message(const Arguments &args, V8Tomato **out_tomato, VALUE *out_receiver, ID *out_method_id);
-static void store_args(V8Tomato *tomato, VALUE rbargs, const Arguments &args);
Handle<Value> bound_method(const Arguments& args)
{
@@ -35,7 +33,7 @@ Handle<Value> bound_method(const Arguments& args)
return js_value_of(tomato, result);
}
-static void store_args(V8Tomato *tomato, VALUE rbargs, const Arguments &args)
+void store_args(V8Tomato *tomato, VALUE rbargs, const Arguments &args)
{
int length = args.Length();
int offset = RARRAY_LEN(rbargs);
@@ -100,7 +98,6 @@ VALUE fTomato_bind_method(int argc, VALUE *argv, VALUE self)
if (value->IsObject())
{
Handle<Object> object = Handle<Object>::Cast(value);
- Handle<Value> proto_value = object->GetPrototype();
Handle<Function> function = FunctionTemplate::New(bound_method)->GetFunction();
function->Set(String::New("_tomato"), External::New(tomato));
View
8 ext/tomato/binding_methods.h
@@ -0,0 +1,8 @@
+#ifndef BINDING_METHODS_H
+#define BINDING_METHODS_H
+
+extern Handle<Value> bound_method(const Arguments& args);
+extern int store_rb_message(const Arguments &args, V8Tomato **out_tomato, VALUE *out_receiver, ID *out_method_id);
+extern void store_args(V8Tomato *tomato, VALUE rbargs, const Arguments &args);
+
+#endif//BINDING_METHODS_H
View
39 ext/tomato/conversions_to_rb.cpp
@@ -5,6 +5,7 @@ static VALUE ruby_numeric_from(const Handle<Value> &number);
static VALUE ruby_date_from(const Handle<Value> &date);
static VALUE ruby_string_from(const Handle<Value> &value);
static VALUE ruby_symbol_from(const Handle<Object> &value);
+static VALUE ruby_unwrapped_object_from(V8Tomato *tomato, const Handle<Object> &value);
static VALUE ruby_object_from(V8Tomato *tomato, Handle<Value> result);
static VALUE ruby_hash_from(V8Tomato *tomato, const Handle<Object> &object);
@@ -37,10 +38,9 @@ static VALUE ruby_object_from(V8Tomato *tomato, Handle<Value> result)
{
Handle<Object> object = Handle<Object>::Cast(result);
- if (object->Get(String::New("_tomato_hash"))->IsTrue())
- return ruby_hash_from(tomato, object);
- if (object->Get(String::New("_tomato_symbol"))->IsTrue())
- return ruby_symbol_from(object);
+ if (object->Get(String::New("_tomato_hash"))->IsTrue()) return ruby_hash_from(tomato, object);
+ if (object->Get(String::New("_tomato_symbol"))->IsTrue()) return ruby_symbol_from(object);
+ if (object->Get(String::New("_tomato_ruby_wrapper"))->IsTrue()) return ruby_unwrapped_object_from(tomato, object);
}
/* Call Javascript's JSON.stringify(object) method. If that can't be done for any reason, return nil. */
@@ -115,10 +115,33 @@ static VALUE ruby_numeric_from(const Handle<Value> &number)
return DBL2NUM(number->NumberValue());
}
-void inspect_js(V8Tomato *tomato, Handle<Value> obj)
+static VALUE ruby_unwrapped_object_from(V8Tomato *tomato, const Handle<Object> &value)
{
- VALUE rbObj = ruby_value_of(tomato, obj);
- VALUE rbStr = rb_funcall(rbObj, rb_intern("inspect"), 0);
- printf("%s\n", StringValuePtr(rbStr));
+ VALUE receivers = rb_funcall2(tomato->rb_instance, rb_intern("receivers"), 0, 0);
+ int len = RARRAY_LEN(receivers);
+ int index = value->Get(String::New("_tomato_receiver_index"))->Int32Value();
+ if (len <= index)
+ return Qnil;
+ return *(RARRAY_PTR(receivers)+index);
+}
+
+Handle<Value> inspect_js(V8Tomato *tomato, Handle<Value> obj)
+{
+ /* Call Javascript's JSON.stringify(object) method. If that can't be done for any reason, return an error. */
+ Handle<Value> json = tomato->context->Global()->Get(String::New("JSON"));
+ if (json->IsObject())
+ {
+ Handle<Value> stringify = Handle<Object>::Cast(json)->Get(String::New("stringify"));
+ if (stringify->IsFunction())
+ {
+ String::Utf8Value str(Handle<Function>::Cast(stringify)->Call(
+ Handle<Object>::Cast(json),
+ 1,
+ &obj
+ ));
+ return String::New(ToCString(str));
+ }
+ }
+ return ThrowException(String::New("Could not JSONify the object"));
}
View
4 ext/tomato/depend
@@ -1,7 +1,7 @@
-binding_methods.o: binding_methods.cpp tomato.h
+binding_methods.o: binding_methods.cpp tomato.h binding_methods.h
conversions_to_js.o: conversions_to_js.cpp tomato.h
conversions_to_rb.o: conversions_to_rb.cpp tomato.h
errors.o: errors.cpp tomato.h
-object_chain.o: object_chain.cpp tomato.h
+object_chain.o: object_chain.cpp tomato.h binding_methods.h
tomato.o: tomato.cpp tomato.h
v8.o: v8.cpp tomato.h
View
208 ext/tomato/object_chain.cpp
@@ -1,4 +1,212 @@
#include "tomato.h"
+#include "binding_methods.h"
+
+static VALUE create_new(VALUE args);
+static v8::Handle<v8::Value> ruby_class_constructor(const Arguments &args);
+static Handle<Value> bind_methods(Local<Object> js, VALUE rb, V8Tomato *tomato);
+static Handle<Value> bound_getter(Local<String> property, const AccessorInfo &info);
+static void bound_setter(Local<String> property, Local<Value> value, const AccessorInfo &info);
+static VALUE protected_get(VALUE args);
+static VALUE protected_set(VALUE args);
+
+VALUE fTomato_bind_class(VALUE self, VALUE receiver_index, VALUE chain)
+{
+ V8Tomato *tomato;
+ Data_Get_Struct(self, V8Tomato, tomato);
+
+ HandleScope handle_scope;
+ Context::Scope context_scope(tomato->context);
+ VALUE js_class_name = rb_ary_pop(chain);
+ // This is kind of a misnomer. We're creating a JavaScript function ("method") to stand in for
+ // the Ruby class. So the method_name has to be the Ruby class name. Consider: "new" is not a
+ // method in JS -- it's a keyword.
+ Handle<String> method_name = String::New(StringValuePtr(js_class_name));
+ Handle<Value> parent = find_or_create_object_chain(tomato, chain);
+
+ if (parent->IsObject())
+ {
+ Handle<Object> object = Handle<Object>::Cast(parent);
+ Handle<Function> function = FunctionTemplate::New(ruby_class_constructor)->GetFunction();
+
+ function->Set(String::New("_tomato"), External::New(tomato));
+ function->Set(String::New("_tomato_receiver_index"), Int32::New(FIX2INT(receiver_index)));
+ function->SetName(method_name);
+
+ Handle<Value> current_value = object->Get(method_name);
+ if (current_value->IsObject())
+ {
+ // we're about to overwrite an object. First clone any of its registered functions.
+ Handle<Object> current = Handle<Object>::Cast(current_value);
+ Local<Array> properties = current->GetPropertyNames();
+ int length = properties->Length();
+ for (int i = 0; i < length; i++)
+ {
+ Local<Value> property = properties->Get(i);
+ String::Utf8Value stra(inspect_js(tomato, property));
+ String::Utf8Value str(inspect_js(tomato, current->Get(property)));
+ function->Set(property, current->Get(property));
+ }
+ }
+ object->Set(method_name, function);
+ return Qtrue;
+ }
+ return Qfalse;
+}
+
+v8::Handle<v8::Value> ruby_class_constructor(const Arguments &args)
+{
+ // throw if called without `new'
+ if (!args.IsConstructCall())
+ return ThrowException(String::New("Cannot call constructor as function"));
+
+ // we get the method ID and then call it, so that any C++ destructors that need to fire before
+ // we do so, can.
+ VALUE receiver;
+ ID rb_method_id;
+ V8Tomato *tomato;
+ int code = store_rb_message(args, &tomato, &receiver, &rb_method_id);
+ switch(code)
+ {
+ case -1: return ThrowException(String::New("Error: _tomato is not an object (BUG: please report)"));
+ case -2: return ThrowException(String::New("Error: _tomato_receiver_index is not an Int32 (BUG: please report)"));
+ case -3: return ThrowException(String::New("Error: _tomato_receiver_index is greater than @receivers.length (BUG: please report)"));
+ };
+
+ VALUE rbargs = rb_ary_new2(1+args.Length());
+ rb_ary_store(rbargs, 0, receiver);
+ store_args(tomato, rbargs, args);
+
+ int error;
+ VALUE result = rb_protect(create_new, rbargs, &error);
+ if(error)
+ {
+ return ThrowException(js_error_from(rb_gv_get("$!")));
+ }
+
+ Local<Object> holder = args.Holder();
+ holder->Set(String::New("_tomato_ruby_wrapper"), Boolean::New(true), DontEnum);
+ holder->Set(String::New("_tomato_receiver_index"),
+ Int32::New(FIX2INT(rb_funcall(tomato->rb_instance, rb_intern("receiver_index"), 1, result))), DontEnum);
+ return bind_methods(holder, result, tomato);
+}
+
+Handle<Value> bind_methods(Local<Object> js, VALUE rb, V8Tomato *tomato)
+{
+ VALUE methods = rb_funcall(rb, rb_intern("public_methods"), 0);
+ int receiver_index = FIX2INT(rb_funcall(tomato->rb_instance, rb_intern("receiver_index"), 1, rb));
+
+ HandleScope handle_scope;
+ Context::Scope context_scope(tomato->context);
+ Handle<String> method_name;
+ js->Set(String::New("_tomato"), External::New(tomato));
+ for (int i = 0; i < RARRAY_LEN(methods); i++)
+ {
+ method_name = String::New(StringValuePtr(*(RARRAY_PTR(methods)+i)));
+ Handle<Function> function = FunctionTemplate::New(bound_method)->GetFunction();
+
+ function->Set(String::New("_tomato"), External::New(tomato));
+ function->Set(String::New("_tomato_receiver_index"), Int32::New(receiver_index));
+ function->SetName(method_name);
+
+ js->Set(method_name, function);
+ js->SetAccessor(method_name, bound_getter, bound_setter);
+ }
+ return js;
+}
+
+static VALUE protected_get(VALUE args)
+{
+ VALUE receiver = *(RARRAY_PTR(args));
+ VALUE method = *(RARRAY_PTR(args)+1);
+ return rb_funcall2(receiver, SYM2ID(method), 0, 0);
+}
+
+static VALUE protected_set(VALUE args)
+{
+ VALUE receiver = *(RARRAY_PTR(args));
+ VALUE method = *(RARRAY_PTR(args)+1);
+ VALUE value = *(RARRAY_PTR(args)+2);
+
+ method = rb_funcall(method, rb_intern("to_s"), 0);
+ method = rb_funcall(method, rb_intern("+"), 1, rb_str_new2("="));
+ return rb_funcall2(receiver, rb_intern(StringValuePtr(method)), 1, &value);
+ return Qnil;
+}
+
+static Handle<Value> bound_getter(Local<String> property, const AccessorInfo &info)
+{
+ int error;
+ Local<Object> self = info.Holder();
+
+ // pull the binding data from the function (stored there by fTomato_bind_method)
+ Local<Value> v8_tomato = self->Get(String::New("_tomato"));
+ Local<Value> v8_receiver_index = self->Get(String::New("_tomato_receiver_index"));
+
+ // make sure the data is what we expect it to be
+ if (!v8_tomato->IsExternal()) return ThrowException(String::New("_tomato is not an external! (bug: please report)"));
+ if (!v8_receiver_index->IsInt32()) return ThrowException(String::New("_tomato_receiver_index is not an Int32! (bug: please report)"));
+
+ // find the tomato
+ V8Tomato *tomato = (V8Tomato *)Local<External>::Cast(v8_tomato)->Value();
+
+ // find the receiver index, and make sure it's a valid index
+ int receiver_index = v8_receiver_index->Int32Value();
+ VALUE receivers = rb_iv_get(tomato->rb_instance, "@receivers"); //rb_funcall(tomato->rb_instance, rb_intern("receivers"));
+ if (RARRAY_LEN(receivers) < receiver_index) return ThrowException(String::New("_tomato_receiver_index is too small! (bug: please report)"));
+
+ // get the receiver
+ VALUE receiver = (RARRAY_PTR(receivers)[receiver_index]);
+
+ VALUE args = rb_ary_new();
+ String::Utf8Value property_name(property);
+ rb_ary_push(args, receiver);
+ rb_ary_push(args, ID2SYM(rb_intern(ToCString(property_name))));
+ VALUE result = rb_protect(protected_get, args, &error);
+ if (error)
+ return ThrowException(js_error_from(rb_gv_get("$!")));
+ return js_value_of(tomato, result);
+}
+
+static void bound_setter(Local<String> property, Local<Value> value, const AccessorInfo &info)
+{
+ int error;
+ Local<Object> self = info.Holder();
+
+ // pull the binding data from the function (stored there by fTomato_bind_method)
+ Local<Value> v8_tomato = self->Get(String::New("_tomato"));
+ Local<Value> v8_receiver_index = self->Get(String::New("_tomato_receiver_index"));
+
+ // make sure the data is what we expect it to be
+ if (!v8_tomato->IsExternal()) { ThrowException(String::New("_tomato is not an external! (bug: please report)")); return; }
+ if (!v8_receiver_index->IsInt32()) { ThrowException(String::New("_tomato_receiver_index is not an Int32! (bug: please report)")); return; }
+
+ // find the tomato
+ V8Tomato *tomato = (V8Tomato *)Local<External>::Cast(v8_tomato)->Value();
+
+ // find the receiver index, and make sure it's a valid index
+ int receiver_index = v8_receiver_index->Int32Value();
+ VALUE receivers = rb_iv_get(tomato->rb_instance, "@receivers"); //rb_funcall(tomato->rb_instance, rb_intern("receivers"));
+ if (RARRAY_LEN(receivers) < receiver_index) { ThrowException(String::New("_tomato_receiver_index is too small! (bug: please report)")); return; }
+
+ // get the receiver
+ VALUE receiver = (RARRAY_PTR(receivers)[receiver_index]);
+
+ VALUE args = rb_ary_new();
+ String::Utf8Value property_name(property);
+ rb_ary_push(args, receiver);
+ rb_ary_push(args, ID2SYM(rb_intern(ToCString(property_name))));
+ rb_ary_push(args, ruby_value_of(tomato, value));
+ rb_protect(protected_set, args, &error);
+ if (error)
+ ThrowException(js_error_from(rb_gv_get("$!")));
+}
+
+static VALUE create_new(VALUE args)
+{
+ VALUE receiver = rb_ary_shift(args);
+ VALUE result = rb_funcall2(receiver, rb_intern("new"), RARRAY_LEN(args), RARRAY_PTR(args));
+ return result;
+}
Handle<Value> find_or_create_object_chain(V8Tomato *tomato, VALUE chain)
{
View
23 ext/tomato/tomato.cpp
@@ -13,6 +13,20 @@ static VALUE fTomato_execute(VALUE self, const char *javascript, const char *fil
static void tomato_mark(V8Tomato *tomato);
static void tomato_free(V8Tomato *tomato);
+static v8::Handle<v8::Value> debug(const Arguments &args)
+{
+ Handle<Value> arg;
+ V8Tomato *tomato = (V8Tomato *)(Handle<External>::Cast(args.Holder()->Get(String::New("_tomato")))->Value());
+ for (int i = 0; i < args.Length(); i++)
+ {
+ arg = args[i];
+ if (i > 0) printf(", ");
+ String::Utf8Value str(inspect_js(tomato, args[i]));
+ printf("<%s>", ToCString(str));
+ }
+ printf("\n");
+ return Null();
+}
extern "C"
void Init_tomato(void)
@@ -37,7 +51,10 @@ void Init_tomato(void)
/* instance method "bind_method" */
rb_define_method(cTomato, "_bind_method", (ruby_method_vararg *)&fTomato_bind_method, -1);
-
+
+ /* instance method "_bind_class" */
+ rb_define_method(cTomato, "_bind_class", (ruby_method_vararg *)&fTomato_bind_class, 2);
+
/* init error-specific junk */
err_init();
}
@@ -50,9 +67,11 @@ static VALUE fTomato_allocate(VALUE klass)
HandleScope handle_scope;
Handle<ObjectTemplate> global = ObjectTemplate::New();
+ global->Set(String::New("debug"), FunctionTemplate::New(debug));
+ global->Set(String::New("_tomato"), External::New(tomato), DontEnum);
tomato->context = Context::New(NULL, global);
tomato->rb_instance = instance;
-
+
return instance;
}
View
3  ext/tomato/tomato.h
@@ -30,10 +30,11 @@ extern VALUE rb_cTime;
/* in object_chain.cpp */
extern Handle<Value> find_or_create_object_chain(V8Tomato *tomato, VALUE chain);
+extern VALUE fTomato_bind_class(VALUE self, VALUE klass, VALUE chain);
/* in conversions_to_rb.cpp */
extern VALUE ruby_value_of(V8Tomato *tomato, Handle<Value> result);
-extern void inspect_js(V8Tomato *tomato, Handle<Value> obj);
+extern Handle<Value> inspect_js(V8Tomato *tomato, Handle<Value> obj);
/* in conversions_to_js.cpp */
extern Handle<Value> js_value_of(V8Tomato *tomato, VALUE value);
View
38 lib/tomato.rb
@@ -32,10 +32,9 @@ class Tomato
def bind_method(method_name, *args)
options = args.extract_options!
receiver = options[:object] || args.first || self
- receivers << receiver unless receivers.include?(receiver)
- chain = options[:to] ? options[:to].split(/\./) : nil
+ chain = options[:to] ? split_chain(options[:to]) : nil
# Bind the method to JS.
- _bind_method(method_name, receivers.index(receiver), chain)
+ _bind_method(method_name, receiver_index(receiver), chain)
end
# Binds an entire Ruby object to the specified JavaScript object chain.
@@ -63,9 +62,17 @@ def bind_method(method_name, *args)
# tomato.run("ruby.time.to_s()")
# #=> "2010-06-25 18:08:29 -0400"
def bind_object(obj, chain = nil)
- if (obj.kind_of?(Class) || obj.kind_of?(Module)) && chain.nil?
- chain = obj.name.gsub(/\:\:/, '.')
- chain = chain[1..-1] if chain[0] == ?.
+ if (obj.kind_of?(Class) || obj.kind_of?(Module))
+ if chain.nil?
+ chain = obj.name.gsub(/\:\:/, '.')
+ chain = chain[1..-1] if chain[0] == ?.
+ end
+ # This sets up an object chain with the last created object as a Function, which can
+ # then be instiated with "new [Object]()" in JS.
+ #
+ # Objects of the same name in the chain are replaced, but their sub-objects are copied over
+ # so it should be transparent to the user.
+ return false unless _bind_class(receiver_index(obj), split_chain(chain))
elsif chain.nil?
unqualified_name = obj.class.name.gsub(/.*\:\:([^\:])?$/, '\1').underscore
chain = "ruby.#{unqualified_name}"
@@ -77,14 +84,31 @@ def bind_object(obj, chain = nil)
obj
end
- alias bind bind_method
+ # Attempts to bind this object to the JavaScript world. If it's a String or Symbol, it will be treated
+ # as a call to #bind_method; otherwise, a call to #bind_object.
+ def bind(target, *args)
+ if target.kind_of?(String) || target.kind_of?(Symbol)
+ bind_method(target, *args)
+ else
+ bind_object(target, *args)
+ end
+ end
def inspect
"#<Tomato>"
end
private
+ def split_chain(str)
+ str.split(/\./)
+ end
+
def receivers
@receivers ||= []
end
+
+ def receiver_index(receiver)
+ receivers << receiver unless receivers.include?(receiver)
+ receivers.index(receiver)
+ end
end
View
48 spec/lib/bound_class_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe "Tomato bound objects" do
+ subject { Tomato.new }
+
+ class ::TestObject
+ attr_accessor :i
+
+ def initialize; @i = 1; end
+
+ def ==(other)
+ other.kind_of?(TestObject)
+ end
+ end
+
+ it "should map attribute getters to corresponding Ruby methods" do
+ subject.bind_object(TestObject, "TO")
+ subject.run('var v = new TO(); v.i;').should == 1
+ end
+
+ it "should map attribute setters to corresponding Ruby methods" do
+ subject.bind_object(TestObject, "TO")
+ result = subject.run('var v = new TO(); v.i = 5; v;')
+ result.i.should == 5
+ end
+
+ it "should be bind-able with an explicit chain" do
+ subject.bind_object(TestObject, "TO")
+ proc { subject.run("new TO();") }.should_not raise_error
+ end
+
+ it "should be instantiatable" do
+ subject.bind_object(TestObject)
+ subject.run("new TestObject();").should == TestObject.new
+ end
+
+ it "should not lose sub-objects" do
+ subject.bind_object("hello", "TestObject.string")
+ subject.bind_object(TestObject)
+ subject.run("TestObject.string.inspect()").should == '"hello"'
+ end
+
+ it "should allow sub-bindings" do
+ subject.bind_object(TestObject)
+ subject.bind_object("hello", "TestObject.string")
+ subject.run("TestObject.string.inspect()").should == '"hello"'
+ end
+end
View
2  spec/lib/bound_object_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe "Tomato bound methods" do
+describe "Tomato bound objects" do
subject { Tomato.new }
it "should bind a Ruby class to an implicit chain" do
View
30 spec/lib/conversions_spec.rb
@@ -1,9 +1,23 @@
require 'spec_helper'
describe "Tomato conversions" do
- context "from Ruby to JS to Ruby" do
- subject { Tomato.new }
+ subject { Tomato.new }
+ context "from JS to Ruby" do
+ it "should handle objects" do
+ pending "how to do it? JSON?"
+ end
+
+ it "should handle classes" do
+ pending "how to do it? Prototype?"
+ end
+
+ it "should handle modules" do
+ pending "how to do it? Prototype?"
+ end
+ end
+
+ context "from Ruby to JS to Ruby" do
def handle(value)
$test_value = value
def subject.a_method; $test_value; end
@@ -27,18 +41,6 @@ def subject.a_method; raise ArgumentError, "err"; end
handle(nil).should == nil
end
- it "should handle objects" do
- pending "how to do it? JSON?"
- end
-
- it "should handle classes" do
- pending "how to do it? Prototype?"
- end
-
- it "should handle modules" do
- pending "how to do it? Prototype?"
- end
-
it "should handle floats" do
handle(1.05).should == 1.05
end
Please sign in to comment.
Something went wrong with that request. Please try again.