scotttam-RocketAMF 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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