RocketAMF 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +36 -0
- data/Rakefile +54 -0
- data/lib/rocketamf.rb +12 -0
- data/lib/rocketamf/class_mapping.rb +211 -0
- data/lib/rocketamf/common.rb +35 -0
- data/lib/rocketamf/constants.rb +47 -0
- data/lib/rocketamf/pure.rb +30 -0
- data/lib/rocketamf/pure/deserializer.rb +361 -0
- data/lib/rocketamf/pure/io_helpers.rb +94 -0
- data/lib/rocketamf/pure/remoting.rb +89 -0
- data/lib/rocketamf/pure/serializer.rb +327 -0
- data/lib/rocketamf/remoting.rb +133 -0
- data/lib/rocketamf/values/array_collection.rb +9 -0
- data/lib/rocketamf/values/messages.rb +133 -0
- data/lib/rocketamf/values/typed_hash.rb +13 -0
- data/lib/rocketamf/version.rb +9 -0
- data/spec/amf/class_mapping_set_spec.rb +34 -0
- data/spec/amf/class_mapping_spec.rb +120 -0
- data/spec/amf/deserializer_spec.rb +311 -0
- data/spec/amf/request_spec.rb +25 -0
- data/spec/amf/response_spec.rb +68 -0
- data/spec/amf/serializer_spec.rb +328 -0
- data/spec/amf/values/array_collection_spec.rb +6 -0
- data/spec/amf/values/messages_spec.rb +31 -0
- data/spec/fixtures/objects/amf0-boolean.bin +1 -0
- data/spec/fixtures/objects/amf0-date.bin +0 -0
- data/spec/fixtures/objects/amf0-ecma-ordinal-array.bin +0 -0
- data/spec/fixtures/objects/amf0-hash.bin +0 -0
- data/spec/fixtures/objects/amf0-null.bin +1 -0
- data/spec/fixtures/objects/amf0-number.bin +0 -0
- data/spec/fixtures/objects/amf0-object.bin +0 -0
- data/spec/fixtures/objects/amf0-ref-test.bin +0 -0
- data/spec/fixtures/objects/amf0-strict-array.bin +0 -0
- data/spec/fixtures/objects/amf0-string.bin +0 -0
- data/spec/fixtures/objects/amf0-typed-object.bin +0 -0
- data/spec/fixtures/objects/amf0-undefined.bin +1 -0
- data/spec/fixtures/objects/amf0-untyped-object.bin +0 -0
- data/spec/fixtures/objects/amf3-0.bin +0 -0
- data/spec/fixtures/objects/amf3-arrayRef.bin +1 -0
- data/spec/fixtures/objects/amf3-bigNum.bin +0 -0
- data/spec/fixtures/objects/amf3-date.bin +0 -0
- data/spec/fixtures/objects/amf3-datesRef.bin +0 -0
- data/spec/fixtures/objects/amf3-dynObject.bin +2 -0
- data/spec/fixtures/objects/amf3-emptyArray.bin +1 -0
- data/spec/fixtures/objects/amf3-emptyArrayRef.bin +1 -0
- data/spec/fixtures/objects/amf3-emptyStringRef.bin +1 -0
- data/spec/fixtures/objects/amf3-false.bin +1 -0
- data/spec/fixtures/objects/amf3-graphMember.bin +0 -0
- data/spec/fixtures/objects/amf3-hash.bin +2 -0
- data/spec/fixtures/objects/amf3-largeMax.bin +0 -0
- data/spec/fixtures/objects/amf3-largeMin.bin +0 -0
- data/spec/fixtures/objects/amf3-max.bin +1 -0
- data/spec/fixtures/objects/amf3-min.bin +0 -0
- data/spec/fixtures/objects/amf3-mixedArray.bin +11 -0
- data/spec/fixtures/objects/amf3-null.bin +1 -0
- data/spec/fixtures/objects/amf3-objRef.bin +0 -0
- data/spec/fixtures/objects/amf3-primArray.bin +1 -0
- data/spec/fixtures/objects/amf3-string.bin +1 -0
- data/spec/fixtures/objects/amf3-stringRef.bin +0 -0
- data/spec/fixtures/objects/amf3-symbol.bin +1 -0
- data/spec/fixtures/objects/amf3-true.bin +1 -0
- data/spec/fixtures/objects/amf3-typedObject.bin +2 -0
- data/spec/fixtures/request/acknowledge-response.bin +0 -0
- data/spec/fixtures/request/amf0-error-response.bin +0 -0
- data/spec/fixtures/request/commandMessage.bin +0 -0
- data/spec/fixtures/request/remotingMessage.bin +0 -0
- data/spec/fixtures/request/simple-response.bin +0 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +32 -0
- metadata +133 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
module RocketAMF
|
2
|
+
# Container for the AMF request.
|
3
|
+
class Request
|
4
|
+
attr_reader :amf_version, :headers, :messages
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@amf_version = 0
|
8
|
+
@headers = []
|
9
|
+
@messages = []
|
10
|
+
end
|
11
|
+
|
12
|
+
# Populates the request from the given stream or string. Returns self for easy
|
13
|
+
# chaining
|
14
|
+
#
|
15
|
+
# Example:
|
16
|
+
#
|
17
|
+
# req = RocketAMF::Request.new.populate_from_stream(env['rack.input'].read)
|
18
|
+
#--
|
19
|
+
# Implemented in pure/remoting.rb RocketAMF::Pure::Request
|
20
|
+
def populate_from_stream stream
|
21
|
+
raise AMFError, 'Must load "rocketamf/pure"'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Container for the response of the AMF call. Includes serialization and request
|
26
|
+
# handling code.
|
27
|
+
class Response
|
28
|
+
attr_accessor :amf_version, :headers, :messages
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@amf_version = 0
|
32
|
+
@headers = []
|
33
|
+
@messages = []
|
34
|
+
end
|
35
|
+
|
36
|
+
# Serializes the response to a string and returns it.
|
37
|
+
#--
|
38
|
+
# Implemented in pure/remoting.rb RocketAMF::Pure::Response
|
39
|
+
def serialize
|
40
|
+
raise AMFError, 'Must load "rocketamf/pure"'
|
41
|
+
end
|
42
|
+
|
43
|
+
# Builds response from the request, iterating over each method call and using
|
44
|
+
# the return value as the method call's return value
|
45
|
+
#--
|
46
|
+
# Iterate over all the sent messages. If they're somthing we can handle, like
|
47
|
+
# a command message, then simply add the response message ourselves. If it's
|
48
|
+
# a method call, then call the block with the method and args, catching errors
|
49
|
+
# for handling. Then create the appropriate response message using the return
|
50
|
+
# value of the block as the return value for the method call.
|
51
|
+
def each_method_call request, &block
|
52
|
+
raise 'Response already constructed' if @constructed
|
53
|
+
|
54
|
+
# Set version from response
|
55
|
+
# Can't just copy version because FMS sends version as 1
|
56
|
+
@amf_version = request.amf_version == 3 ? 3 : 0
|
57
|
+
|
58
|
+
request.messages.each do |m|
|
59
|
+
# What's the request body?
|
60
|
+
case m.data
|
61
|
+
when Values::CommandMessage
|
62
|
+
# Pings should be responded to with an AcknowledgeMessage built using the ping
|
63
|
+
# Everything else is unsupported
|
64
|
+
command_msg = m.data
|
65
|
+
if command_msg.operation == Values::CommandMessage::CLIENT_PING_OPERATION
|
66
|
+
response_value = Values::AcknowledgeMessage.new(command_msg)
|
67
|
+
else
|
68
|
+
response_value = Values::ErrorMessage.new(Exception.new("CommandMessage #{command_msg.operation} not implemented"), command_msg)
|
69
|
+
end
|
70
|
+
when Values::RemotingMessage
|
71
|
+
# Using RemoteObject style message calls
|
72
|
+
remoting_msg = m.data
|
73
|
+
acknowledge_msg = Values::AcknowledgeMessage.new(remoting_msg)
|
74
|
+
body = dispatch_call :method => remoting_msg.source+'.'+remoting_msg.operation, :args => remoting_msg.body, :source => remoting_msg, :block => block
|
75
|
+
|
76
|
+
# Response should be the bare ErrorMessage if there was an error
|
77
|
+
if body.is_a?(Values::ErrorMessage)
|
78
|
+
response_value = body
|
79
|
+
else
|
80
|
+
acknowledge_msg.body = body
|
81
|
+
response_value = acknowledge_msg
|
82
|
+
end
|
83
|
+
else
|
84
|
+
# Standard response message
|
85
|
+
response_value = dispatch_call :method => m.target_uri, :args => m.data, :source => m, :block => block
|
86
|
+
end
|
87
|
+
|
88
|
+
target_uri = m.response_uri
|
89
|
+
target_uri += response_value.is_a?(Values::ErrorMessage) ? '/onStatus' : '/onResult'
|
90
|
+
@messages << ::RocketAMF::Message.new(target_uri, '', response_value)
|
91
|
+
end
|
92
|
+
|
93
|
+
@constructed = true
|
94
|
+
end
|
95
|
+
|
96
|
+
# Return the serialized response as a string
|
97
|
+
def to_s
|
98
|
+
serialize
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
def dispatch_call p
|
103
|
+
begin
|
104
|
+
p[:block].call(p[:method], p[:args])
|
105
|
+
rescue Exception => e
|
106
|
+
# Create ErrorMessage object using the source message as the base
|
107
|
+
Values::ErrorMessage.new(p[:source], e)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# RocketAMF::Request or RocketAMF::Response header
|
113
|
+
class Header
|
114
|
+
attr_accessor :name, :must_understand, :data
|
115
|
+
|
116
|
+
def initialize name, must_understand, data
|
117
|
+
@name = name
|
118
|
+
@must_understand = must_understand
|
119
|
+
@data = data
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# RocketAMF::Request or RocketAMF::Response message
|
124
|
+
class Message
|
125
|
+
attr_accessor :target_uri, :response_uri, :data
|
126
|
+
|
127
|
+
def initialize target_uri, response_uri, data
|
128
|
+
@target_uri = target_uri
|
129
|
+
@response_uri = response_uri
|
130
|
+
@data = data
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
module RocketAMF
|
2
|
+
module Values #:nodoc:
|
3
|
+
# Base class for all special AS3 response messages. Maps to
|
4
|
+
# <tt>flex.messaging.messages.AbstractMessage</tt>
|
5
|
+
class AbstractMessage
|
6
|
+
attr_accessor :clientId
|
7
|
+
attr_accessor :destination
|
8
|
+
attr_accessor :messageId
|
9
|
+
attr_accessor :timestamp
|
10
|
+
attr_accessor :timeToLive
|
11
|
+
attr_accessor :headers
|
12
|
+
attr_accessor :body
|
13
|
+
|
14
|
+
protected
|
15
|
+
def rand_uuid
|
16
|
+
[8,4,4,4,12].map {|n| rand_hex_3(n)}.join('-').to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def rand_hex_3(l)
|
20
|
+
"%0#{l}x" % rand(1 << l*4)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Maps to <tt>flex.messaging.messages.RemotingMessage</tt>
|
25
|
+
class RemotingMessage < AbstractMessage
|
26
|
+
# The name of the service to be called including package name
|
27
|
+
attr_accessor :source
|
28
|
+
|
29
|
+
# The name of the method to be called
|
30
|
+
attr_accessor :operation
|
31
|
+
|
32
|
+
# The arguments to call the method with
|
33
|
+
attr_accessor :parameters
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
@clientId = rand_uuid
|
37
|
+
@destination = nil
|
38
|
+
@messageId = rand_uuid
|
39
|
+
@timestamp = Time.new.to_i*100
|
40
|
+
@timeToLive = 0
|
41
|
+
@headers = {}
|
42
|
+
@body = nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Maps to <tt>flex.messaging.messages.AsyncMessage</tt>
|
47
|
+
class AsyncMessage < AbstractMessage
|
48
|
+
attr_accessor :correlationId
|
49
|
+
end
|
50
|
+
|
51
|
+
# Maps to <tt>flex.messaging.messages.CommandMessage</tt>
|
52
|
+
class CommandMessage < AsyncMessage
|
53
|
+
SUBSCRIBE_OPERATION = 0
|
54
|
+
UNSUSBSCRIBE_OPERATION = 1
|
55
|
+
POLL_OPERATION = 2
|
56
|
+
CLIENT_SYNC_OPERATION = 4
|
57
|
+
CLIENT_PING_OPERATION = 5
|
58
|
+
CLUSTER_REQUEST_OPERATION = 7
|
59
|
+
LOGIN_OPERATION = 8
|
60
|
+
LOGOUT_OPERATION = 9
|
61
|
+
SESSION_INVALIDATE_OPERATION = 10
|
62
|
+
MULTI_SUBSCRIBE_OPERATION = 11
|
63
|
+
DISCONNECT_OPERATION = 12
|
64
|
+
UNKNOWN_OPERATION = 10000
|
65
|
+
|
66
|
+
attr_accessor :operation
|
67
|
+
|
68
|
+
def initialize
|
69
|
+
@operation = UNKNOWN_OPERATION
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Maps to <tt>flex.messaging.messages.AcknowledgeMessage</tt>
|
74
|
+
class AcknowledgeMessage < AsyncMessage
|
75
|
+
def initialize message=nil
|
76
|
+
@clientId = rand_uuid
|
77
|
+
@destination = nil
|
78
|
+
@messageId = rand_uuid
|
79
|
+
@timestamp = Time.new.to_i*100
|
80
|
+
@timeToLive = 0
|
81
|
+
@headers = {}
|
82
|
+
@body = nil
|
83
|
+
|
84
|
+
if message.is_a?(AbstractMessage)
|
85
|
+
@correlationId = message.messageId
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Maps to <tt>flex.messaging.messages.ErrorMessage</tt> in AMF3 mode
|
91
|
+
class ErrorMessage < AcknowledgeMessage
|
92
|
+
# Extended data that will facilitate custom error processing on the client
|
93
|
+
attr_accessor :extendedData
|
94
|
+
|
95
|
+
# The fault code for the error, which defaults to the class name of the
|
96
|
+
# causing exception
|
97
|
+
attr_accessor :faultCode
|
98
|
+
|
99
|
+
# Detailed description of what caused the error
|
100
|
+
attr_accessor :faultDetail
|
101
|
+
|
102
|
+
# A simple description of the error
|
103
|
+
attr_accessor :faultString
|
104
|
+
|
105
|
+
# Optional "root cause" of the error
|
106
|
+
attr_accessor :rootCause
|
107
|
+
|
108
|
+
def initialize message, exception
|
109
|
+
super message
|
110
|
+
|
111
|
+
@e = exception
|
112
|
+
@faultCode = @e.class.name
|
113
|
+
@faultDetail = @e.backtrace.join("\n")
|
114
|
+
@faultString = @e.message
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_amf serializer
|
118
|
+
stream = ""
|
119
|
+
if serializer.version == 0
|
120
|
+
data = {
|
121
|
+
:faultCode => @faultCode,
|
122
|
+
:faultDetail => @faultDetail,
|
123
|
+
:faultString => @faultString
|
124
|
+
}
|
125
|
+
serializer.write_hash(data, stream)
|
126
|
+
else
|
127
|
+
serializer.write_object(self, stream)
|
128
|
+
end
|
129
|
+
stream
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module RocketAMF
|
2
|
+
module Values #:nodoc:
|
3
|
+
# Hash-like object that can store a type string. Used to preserve type information
|
4
|
+
# for unmapped objects after deserialization.
|
5
|
+
class TypedHash < Hash
|
6
|
+
attr_reader :type
|
7
|
+
|
8
|
+
def initialize type
|
9
|
+
@type = type
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module RocketAMF
|
2
|
+
# AMF version
|
3
|
+
VERSION = '0.0.4'
|
4
|
+
VERSION_ARRAY = VERSION.split(/\./).map { |x| x.to_i } # :nodoc:
|
5
|
+
VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
|
6
|
+
VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
|
7
|
+
VERSION_BUILD = VERSION_ARRAY[2] # :nodoc:
|
8
|
+
VARIANT_BINARY = false
|
9
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
|
+
|
3
|
+
describe RocketAMF::ClassMapping::MappingSet do
|
4
|
+
before :each do
|
5
|
+
@config = RocketAMF::ClassMapping::MappingSet.new
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should retrieve AS mapping for ruby class" do
|
9
|
+
@config.map :as => 'ASTest', :ruby => 'RubyTest'
|
10
|
+
@config.get_as_class_name('RubyTest').should == 'ASTest'
|
11
|
+
@config.get_as_class_name('BadClass').should be_nil
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should retrive ruby class name mapping for AS class" do
|
15
|
+
@config.map :as => 'ASTest', :ruby => 'RubyTest'
|
16
|
+
@config.get_ruby_class_name('ASTest').should == 'RubyTest'
|
17
|
+
@config.get_ruby_class_name('BadClass').should be_nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should map special classes by default" do
|
21
|
+
SPECIAL_CLASSES = [
|
22
|
+
'flex.messaging.messages.AcknowledgeMessage',
|
23
|
+
'flex.messaging.messages.ErrorMessage',
|
24
|
+
'flex.messaging.messages.CommandMessage',
|
25
|
+
'flex.messaging.messages.ErrorMessage',
|
26
|
+
'flex.messaging.messages.RemotingMessage',
|
27
|
+
'flex.messaging.io.ArrayCollection'
|
28
|
+
]
|
29
|
+
|
30
|
+
SPECIAL_CLASSES.each do |as_class|
|
31
|
+
@config.get_ruby_class_name(as_class).should_not be_nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
|
+
|
3
|
+
describe RocketAMF::ClassMapping do
|
4
|
+
before(:all) do
|
5
|
+
class ClassMappingTest
|
6
|
+
attr_accessor :prop_a
|
7
|
+
attr_accessor :prop_b
|
8
|
+
attr_accessor :prop_c
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
before :each do
|
13
|
+
@mapper = RocketAMF::ClassMapping.new
|
14
|
+
@mapper.define do |m|
|
15
|
+
m.map :as => 'ASClass', :ruby => 'ClassMappingTest'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should return AS class name for ruby objects" do
|
20
|
+
@mapper.get_as_class_name(ClassMappingTest.new).should == 'ASClass'
|
21
|
+
@mapper.get_as_class_name('ClassMappingTest').should == 'ASClass'
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should allow config modification" do
|
25
|
+
@mapper.define do |m|
|
26
|
+
m.map :as => 'SecondClass', :ruby => 'ClassMappingTest'
|
27
|
+
end
|
28
|
+
@mapper.get_as_class_name(ClassMappingTest.new).should == 'SecondClass'
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "ruby object generator" do
|
32
|
+
it "should instantiate a ruby class" do
|
33
|
+
@mapper.get_ruby_obj('ASClass').should be_a(ClassMappingTest)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should properly instantiate namespaced classes" do
|
37
|
+
module ANamespace; class TestRubyClass; end; end
|
38
|
+
@mapper.define {|m| m.map :as => 'ASClass', :ruby => 'ANamespace::TestRubyClass'}
|
39
|
+
@mapper.get_ruby_obj('ASClass').should be_a(ANamespace::TestRubyClass)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should return a hash with original type if not mapped" do
|
43
|
+
obj = @mapper.get_ruby_obj('UnmappedClass')
|
44
|
+
obj.should be_a(RocketAMF::Values::TypedHash)
|
45
|
+
obj.type.should == 'UnmappedClass'
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
describe "ruby object populator" do
|
50
|
+
it "should populate a ruby class" do
|
51
|
+
obj = @mapper.populate_ruby_obj ClassMappingTest.new, {:prop_a => 'Data'}
|
52
|
+
obj.prop_a.should == 'Data'
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should populate a typed hash" do
|
56
|
+
obj = @mapper.populate_ruby_obj RocketAMF::Values::TypedHash.new('UnmappedClass'), {:prop_a => 'Data'}
|
57
|
+
obj[:prop_a].should == 'Data'
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should allow custom populators" do
|
61
|
+
class CustomPopulator
|
62
|
+
def can_handle? obj
|
63
|
+
true
|
64
|
+
end
|
65
|
+
def populate obj, props, dynamic_props
|
66
|
+
obj[:populated] = true
|
67
|
+
obj.merge! props
|
68
|
+
obj.merge! dynamic_props if dynamic_props
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
@mapper.object_populators << CustomPopulator.new
|
73
|
+
obj = @mapper.populate_ruby_obj({}, {:prop_a => 'Data'})
|
74
|
+
obj[:populated].should == true
|
75
|
+
obj[:prop_a].should == 'Data'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "property extractor" do
|
80
|
+
it "should extract hash properties" do
|
81
|
+
hash = {:a => 'test1', :b => 'test2'}
|
82
|
+
props = @mapper.props_for_serialization(hash)
|
83
|
+
props.should == {'a' => 'test1', 'b' => 'test2'}
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should extract object properties" do
|
87
|
+
obj = ClassMappingTest.new
|
88
|
+
obj.prop_a = 'Test A'
|
89
|
+
obj.prop_b = 'Test B'
|
90
|
+
|
91
|
+
hash = @mapper.props_for_serialization obj
|
92
|
+
hash.should == {'prop_a' => 'Test A', 'prop_b' => 'Test B', 'prop_c' => nil}
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should extract inherited object properties" do
|
96
|
+
class ClassMappingTest2 < ClassMappingTest
|
97
|
+
end
|
98
|
+
obj = ClassMappingTest2.new
|
99
|
+
obj.prop_a = 'Test A'
|
100
|
+
obj.prop_b = 'Test B'
|
101
|
+
|
102
|
+
hash = @mapper.props_for_serialization obj
|
103
|
+
hash.should == {'prop_a' => 'Test A', 'prop_b' => 'Test B', 'prop_c' => nil}
|
104
|
+
end
|
105
|
+
|
106
|
+
it "should allow custom serializers" do
|
107
|
+
class CustomSerializer
|
108
|
+
def can_handle? obj
|
109
|
+
true
|
110
|
+
end
|
111
|
+
def serialize obj
|
112
|
+
{:success => true}
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
@mapper.object_serializers << CustomSerializer.new
|
117
|
+
@mapper.props_for_serialization(nil).should == {:success => true}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|