scotttam-RocketAMF 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/README.rdoc +45 -0
  2. data/Rakefile +54 -0
  3. data/lib/rocketamf.rb +128 -0
  4. data/lib/rocketamf/class_mapping.rb +231 -0
  5. data/lib/rocketamf/constants.rb +46 -0
  6. data/lib/rocketamf/pure.rb +28 -0
  7. data/lib/rocketamf/pure/deserializer.rb +419 -0
  8. data/lib/rocketamf/pure/io_helpers.rb +94 -0
  9. data/lib/rocketamf/pure/remoting.rb +134 -0
  10. data/lib/rocketamf/pure/serializer.rb +433 -0
  11. data/lib/rocketamf/remoting.rb +144 -0
  12. data/lib/rocketamf/values/array_collection.rb +13 -0
  13. data/lib/rocketamf/values/messages.rb +133 -0
  14. data/lib/rocketamf/values/typed_hash.rb +13 -0
  15. data/spec/amf/class_mapping_spec.rb +150 -0
  16. data/spec/amf/deserializer_spec.rb +367 -0
  17. data/spec/amf/remoting_spec.rb +132 -0
  18. data/spec/amf/serializer_spec.rb +384 -0
  19. data/spec/amf/values/array_collection_spec.rb +19 -0
  20. data/spec/amf/values/messages_spec.rb +31 -0
  21. data/spec/fixtures/objects/amf0-boolean.bin +1 -0
  22. data/spec/fixtures/objects/amf0-complexEncodedStringArray.bin +0 -0
  23. data/spec/fixtures/objects/amf0-date.bin +0 -0
  24. data/spec/fixtures/objects/amf0-ecma-ordinal-array.bin +0 -0
  25. data/spec/fixtures/objects/amf0-hash.bin +0 -0
  26. data/spec/fixtures/objects/amf0-null.bin +1 -0
  27. data/spec/fixtures/objects/amf0-number.bin +0 -0
  28. data/spec/fixtures/objects/amf0-object.bin +0 -0
  29. data/spec/fixtures/objects/amf0-ref-test.bin +0 -0
  30. data/spec/fixtures/objects/amf0-strict-array.bin +0 -0
  31. data/spec/fixtures/objects/amf0-string.bin +0 -0
  32. data/spec/fixtures/objects/amf0-typed-object.bin +0 -0
  33. data/spec/fixtures/objects/amf0-undefined.bin +1 -0
  34. data/spec/fixtures/objects/amf0-untyped-object.bin +0 -0
  35. data/spec/fixtures/objects/amf0-xmlDoc.bin +0 -0
  36. data/spec/fixtures/objects/amf3-0.bin +0 -0
  37. data/spec/fixtures/objects/amf3-arrayCollection.bin +2 -0
  38. data/spec/fixtures/objects/amf3-arrayRef.bin +1 -0
  39. data/spec/fixtures/objects/amf3-bigNum.bin +0 -0
  40. data/spec/fixtures/objects/amf3-byteArray.bin +0 -0
  41. data/spec/fixtures/objects/amf3-byteArrayRef.bin +1 -0
  42. data/spec/fixtures/objects/amf3-complexEncodedStringArray.bin +1 -0
  43. data/spec/fixtures/objects/amf3-date.bin +0 -0
  44. data/spec/fixtures/objects/amf3-datesRef.bin +0 -0
  45. data/spec/fixtures/objects/amf3-dictionary.bin +0 -0
  46. data/spec/fixtures/objects/amf3-dynObject.bin +2 -0
  47. data/spec/fixtures/objects/amf3-emptyArray.bin +1 -0
  48. data/spec/fixtures/objects/amf3-emptyArrayRef.bin +1 -0
  49. data/spec/fixtures/objects/amf3-emptyDictionary.bin +0 -0
  50. data/spec/fixtures/objects/amf3-emptyStringRef.bin +1 -0
  51. data/spec/fixtures/objects/amf3-encodedStringRef.bin +0 -0
  52. data/spec/fixtures/objects/amf3-false.bin +1 -0
  53. data/spec/fixtures/objects/amf3-float.bin +0 -0
  54. data/spec/fixtures/objects/amf3-graphMember.bin +0 -0
  55. data/spec/fixtures/objects/amf3-hash.bin +2 -0
  56. data/spec/fixtures/objects/amf3-largeMax.bin +0 -0
  57. data/spec/fixtures/objects/amf3-largeMin.bin +0 -0
  58. data/spec/fixtures/objects/amf3-max.bin +1 -0
  59. data/spec/fixtures/objects/amf3-min.bin +0 -0
  60. data/spec/fixtures/objects/amf3-mixedArray.bin +11 -0
  61. data/spec/fixtures/objects/amf3-null.bin +1 -0
  62. data/spec/fixtures/objects/amf3-objRef.bin +0 -0
  63. data/spec/fixtures/objects/amf3-primArray.bin +1 -0
  64. data/spec/fixtures/objects/amf3-string.bin +1 -0
  65. data/spec/fixtures/objects/amf3-stringRef.bin +0 -0
  66. data/spec/fixtures/objects/amf3-symbol.bin +1 -0
  67. data/spec/fixtures/objects/amf3-traitRef.bin +3 -0
  68. data/spec/fixtures/objects/amf3-true.bin +1 -0
  69. data/spec/fixtures/objects/amf3-typedObject.bin +2 -0
  70. data/spec/fixtures/objects/amf3-xml.bin +1 -0
  71. data/spec/fixtures/objects/amf3-xmlDoc.bin +1 -0
  72. data/spec/fixtures/objects/amf3-xmlRef.bin +1 -0
  73. data/spec/fixtures/request/acknowledge-response.bin +0 -0
  74. data/spec/fixtures/request/amf0-error-response.bin +0 -0
  75. data/spec/fixtures/request/commandMessage.bin +0 -0
  76. data/spec/fixtures/request/remotingMessage.bin +0 -0
  77. data/spec/fixtures/request/simple-response.bin +0 -0
  78. data/spec/fixtures/request/unsupportedCommandMessage.bin +0 -0
  79. data/spec/spec.opts +1 -0
  80. data/spec/spec_helper.rb +31 -0
  81. metadata +153 -0
@@ -0,0 +1,144 @@
1
+ module RocketAMF
2
+ # Container for the AMF request/response.
3
+ class Envelope
4
+ attr_reader :amf_version, :headers, :messages
5
+
6
+ def initialize props={}
7
+ @amf_version = props[:amf_version] || 0
8
+ @headers = props[:headers] || []
9
+ @messages = props[:messages] || []
10
+ end
11
+
12
+ # Populates the envelope from the given stream or string. Returns self for easy
13
+ # chaining.
14
+ #
15
+ # Example:
16
+ #
17
+ # req = RocketAMF::Envelope.new.populate_from_stream(env['rack.input'].read)
18
+ #--
19
+ # Implemented in pure/remoting.rb RocketAMF::Pure::Envelope
20
+ def populate_from_stream stream
21
+ raise AMFError, 'Must load "rocketamf/pure"'
22
+ end
23
+
24
+ # Serializes the envelope to a string and returns it
25
+ #--
26
+ # Implemented in pure/remoting.rb RocketAMF::Pure::Envelope
27
+ def serialize
28
+ raise AMFError, 'Must load "rocketamf/pure"'
29
+ end
30
+
31
+ # Builds response from the request, iterating over each method call and using
32
+ # the return value as the method call's return value
33
+ #--
34
+ # Iterate over all the sent messages. If they're somthing we can handle, like
35
+ # a command message, then simply add the response message ourselves. If it's
36
+ # a method call, then call the block with the method and args, catching errors
37
+ # for handling. Then create the appropriate response message using the return
38
+ # value of the block as the return value for the method call.
39
+ def each_method_call request, &block
40
+ raise 'Response already constructed' if @constructed
41
+
42
+ # Set version from response
43
+ # Can't just copy version because FMS sends version as 1
44
+ @amf_version = request.amf_version == 3 ? 3 : 0
45
+
46
+ request.messages.each do |m|
47
+ # What's the request body?
48
+ case m.data
49
+ when Values::CommandMessage
50
+ # Pings should be responded to with an AcknowledgeMessage built using the ping
51
+ # Everything else is unsupported
52
+ command_msg = m.data
53
+ if command_msg.operation == Values::CommandMessage::CLIENT_PING_OPERATION
54
+ response_value = Values::AcknowledgeMessage.new(command_msg)
55
+ else
56
+ e = Exception.new("CommandMessage #{command_msg.operation} not implemented")
57
+ e.set_backtrace ["RocketAMF::Envelope each_method_call"]
58
+ response_value = Values::ErrorMessage.new(command_msg, e)
59
+ end
60
+ when Values::RemotingMessage
61
+ # Using RemoteObject style message calls
62
+ remoting_msg = m.data
63
+ acknowledge_msg = Values::AcknowledgeMessage.new(remoting_msg)
64
+ method_base = remoting_msg.source.to_s.empty? ? '' : remoting_msg.source+'.'
65
+ body = dispatch_call :method => method_base+remoting_msg.operation, :args => remoting_msg.body, :source => remoting_msg, :block => block
66
+
67
+ # Response should be the bare ErrorMessage if there was an error
68
+ if body.is_a?(Values::ErrorMessage)
69
+ response_value = body
70
+ else
71
+ acknowledge_msg.body = body
72
+ response_value = acknowledge_msg
73
+ end
74
+ else
75
+ # Standard response message
76
+ response_value = dispatch_call :method => m.target_uri, :args => m.data, :source => m, :block => block
77
+ end
78
+
79
+ target_uri = m.response_uri
80
+ target_uri += response_value.is_a?(Values::ErrorMessage) ? '/onStatus' : '/onResult'
81
+ @messages << ::RocketAMF::Message.new(target_uri, '', response_value)
82
+ end
83
+
84
+ @constructed = true
85
+ end
86
+
87
+ # Whether or not the response has been constructed. Can be used to prevent
88
+ # serialization when no processing has taken place.
89
+ def constructed?
90
+ @constructed
91
+ end
92
+
93
+ # Return the serialized envelope as a string
94
+ def to_s
95
+ serialize
96
+ end
97
+
98
+ private
99
+ def dispatch_call p
100
+ begin
101
+ p[:block].call(p[:method], p[:args])
102
+ rescue Exception => e
103
+ # Create ErrorMessage object using the source message as the base
104
+ Values::ErrorMessage.new(p[:source], e)
105
+ end
106
+ end
107
+ end
108
+
109
+ class Request < Envelope #:nodoc:
110
+ def initialize props={}
111
+ $stderr.puts("DEPRECATION WARNING: Use RocketAMF::Envelope instead of RocketAMF::Request")
112
+ super(props)
113
+ end
114
+ end
115
+
116
+ class Response < Envelope #:nodoc:
117
+ def initialize props={}
118
+ $stderr.puts("DEPRECATION WARNING: Use RocketAMF::Envelope instead of RocketAMF::Request")
119
+ super(props)
120
+ end
121
+ end
122
+
123
+ # RocketAMF::Envelope header
124
+ class Header
125
+ attr_accessor :name, :must_understand, :data
126
+
127
+ def initialize name, must_understand, data
128
+ @name = name
129
+ @must_understand = must_understand
130
+ @data = data
131
+ end
132
+ end
133
+
134
+ # RocketAMF::Envelope message
135
+ class Message
136
+ attr_accessor :target_uri, :response_uri, :data
137
+
138
+ def initialize target_uri, response_uri, data
139
+ @target_uri = target_uri
140
+ @response_uri = response_uri
141
+ @data = data
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,13 @@
1
+ module RocketAMF
2
+ module Values #:nodoc:
3
+ class ArrayCollection < Array
4
+ def externalized_data
5
+ [] + self # Duplicate as an array
6
+ end
7
+
8
+ def externalized_data=(data)
9
+ push(*data)
10
+ end
11
+ end
12
+ end
13
+ 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=nil, exception=nil
109
+ super message
110
+
111
+ unless exception.nil?
112
+ @e = exception
113
+ @faultCode = @e.class.name
114
+ @faultDetail = @e.backtrace.join("\n")
115
+ @faultString = @e.message
116
+ end
117
+ end
118
+
119
+ def encode_amf serializer
120
+ if serializer.version == 0
121
+ data = {
122
+ :faultCode => @faultCode,
123
+ :faultDetail => @faultDetail,
124
+ :faultString => @faultString
125
+ }
126
+ serializer.write_hash(data)
127
+ else
128
+ serializer.write_object(self)
129
+ end
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,150 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper.rb'
2
+
3
+ class ClassMappingTest
4
+ attr_accessor :prop_a
5
+ attr_accessor :prop_b
6
+ attr_accessor :prop_c
7
+ end
8
+ class ClassMappingTest2 < ClassMappingTest; end;
9
+
10
+ module ANamespace; class TestRubyClass; end; end
11
+
12
+ describe RocketAMF::ClassMapping do
13
+ before :each do
14
+ @mapper = RocketAMF::ClassMapping.new
15
+ @mapper.define do |m|
16
+ m.map :as => 'ASClass', :ruby => 'ClassMappingTest'
17
+ end
18
+ end
19
+
20
+ describe "class name mapping" do
21
+ it "should allow resetting of mappings back to defaults" do
22
+ @mapper.reset
23
+ @mapper.get_as_class_name('ClassMappingTest').should be_nil
24
+ @mapper.get_as_class_name('RocketAMF::Values::AcknowledgeMessage').should_not be_nil
25
+ end
26
+
27
+ it "should return AS class name for ruby objects" do
28
+ @mapper.get_as_class_name(ClassMappingTest.new).should == 'ASClass'
29
+ @mapper.get_as_class_name('ClassMappingTest').should == 'ASClass'
30
+ @mapper.get_as_class_name('BadClass').should be_nil
31
+ end
32
+
33
+ it "should instantiate a ruby class" do
34
+ @mapper.get_ruby_obj('ASClass').should be_a(ClassMappingTest)
35
+ end
36
+
37
+ it "should properly instantiate namespaced classes" do
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
+
48
+ it "should map special classes from AS by default" do
49
+ as_classes = [
50
+ 'flex.messaging.messages.AcknowledgeMessage',
51
+ 'flex.messaging.messages.CommandMessage',
52
+ 'flex.messaging.messages.RemotingMessage',
53
+ 'flex.messaging.io.ArrayCollection'
54
+ ]
55
+
56
+ as_classes.each do |as_class|
57
+ @mapper.get_ruby_obj(as_class).should_not be_a(RocketAMF::Values::TypedHash)
58
+ end
59
+ end
60
+
61
+ it "should map special classes from ruby by default" do
62
+ ruby_classes = [
63
+ 'RocketAMF::Values::AcknowledgeMessage',
64
+ 'RocketAMF::Values::ErrorMessage',
65
+ 'RocketAMF::Values::ArrayCollection'
66
+ ]
67
+
68
+ ruby_classes.each do |obj|
69
+ @mapper.get_as_class_name(obj).should_not be_nil
70
+ end
71
+ end
72
+
73
+ it "should allow config modification" do
74
+ @mapper.define do |m|
75
+ m.map :as => 'SecondClass', :ruby => 'ClassMappingTest'
76
+ end
77
+ @mapper.get_as_class_name(ClassMappingTest.new).should == 'SecondClass'
78
+ end
79
+ end
80
+
81
+ describe "ruby object populator" do
82
+ it "should populate a ruby class" do
83
+ obj = @mapper.populate_ruby_obj ClassMappingTest.new, {:prop_a => 'Data'}
84
+ obj.prop_a.should == 'Data'
85
+ end
86
+
87
+ it "should populate a typed hash" do
88
+ obj = @mapper.populate_ruby_obj RocketAMF::Values::TypedHash.new('UnmappedClass'), {:prop_a => 'Data'}
89
+ obj[:prop_a].should == 'Data'
90
+ end
91
+
92
+ it "should allow custom populators" do
93
+ class CustomPopulator
94
+ def can_handle? obj
95
+ true
96
+ end
97
+ def populate obj, props, dynamic_props
98
+ obj[:populated] = true
99
+ obj.merge! props
100
+ obj.merge! dynamic_props if dynamic_props
101
+ end
102
+ end
103
+
104
+ @mapper.object_populators << CustomPopulator.new
105
+ obj = @mapper.populate_ruby_obj({}, {:prop_a => 'Data'})
106
+ obj[:populated].should == true
107
+ obj[:prop_a].should == 'Data'
108
+ end
109
+ end
110
+
111
+ describe "property extractor" do
112
+ it "should extract hash properties" do
113
+ hash = {:a => 'test1', 'b' => 'test2'}
114
+ props = @mapper.props_for_serialization(hash)
115
+ props.should == {'a' => 'test1', 'b' => 'test2'}
116
+ end
117
+
118
+ it "should extract object properties" do
119
+ obj = ClassMappingTest.new
120
+ obj.prop_a = 'Test A'
121
+ obj.prop_b = 'Test B'
122
+
123
+ hash = @mapper.props_for_serialization obj
124
+ hash.should == {'prop_a' => 'Test A', 'prop_b' => 'Test B', 'prop_c' => nil}
125
+ end
126
+
127
+ it "should extract inherited object properties" do
128
+ obj = ClassMappingTest2.new
129
+ obj.prop_a = 'Test A'
130
+ obj.prop_b = 'Test B'
131
+
132
+ hash = @mapper.props_for_serialization obj
133
+ hash.should == {'prop_a' => 'Test A', 'prop_b' => 'Test B', 'prop_c' => nil}
134
+ end
135
+
136
+ it "should allow custom serializers" do
137
+ class CustomSerializer
138
+ def can_handle? obj
139
+ true
140
+ end
141
+ def serialize obj
142
+ {:success => true}
143
+ end
144
+ end
145
+
146
+ @mapper.object_serializers << CustomSerializer.new
147
+ @mapper.props_for_serialization(nil).should == {:success => true}
148
+ end
149
+ end
150
+ end