RocketAMF 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/README.rdoc +36 -0
  2. data/Rakefile +54 -0
  3. data/lib/rocketamf.rb +12 -0
  4. data/lib/rocketamf/class_mapping.rb +211 -0
  5. data/lib/rocketamf/common.rb +35 -0
  6. data/lib/rocketamf/constants.rb +47 -0
  7. data/lib/rocketamf/pure.rb +30 -0
  8. data/lib/rocketamf/pure/deserializer.rb +361 -0
  9. data/lib/rocketamf/pure/io_helpers.rb +94 -0
  10. data/lib/rocketamf/pure/remoting.rb +89 -0
  11. data/lib/rocketamf/pure/serializer.rb +327 -0
  12. data/lib/rocketamf/remoting.rb +133 -0
  13. data/lib/rocketamf/values/array_collection.rb +9 -0
  14. data/lib/rocketamf/values/messages.rb +133 -0
  15. data/lib/rocketamf/values/typed_hash.rb +13 -0
  16. data/lib/rocketamf/version.rb +9 -0
  17. data/spec/amf/class_mapping_set_spec.rb +34 -0
  18. data/spec/amf/class_mapping_spec.rb +120 -0
  19. data/spec/amf/deserializer_spec.rb +311 -0
  20. data/spec/amf/request_spec.rb +25 -0
  21. data/spec/amf/response_spec.rb +68 -0
  22. data/spec/amf/serializer_spec.rb +328 -0
  23. data/spec/amf/values/array_collection_spec.rb +6 -0
  24. data/spec/amf/values/messages_spec.rb +31 -0
  25. data/spec/fixtures/objects/amf0-boolean.bin +1 -0
  26. data/spec/fixtures/objects/amf0-date.bin +0 -0
  27. data/spec/fixtures/objects/amf0-ecma-ordinal-array.bin +0 -0
  28. data/spec/fixtures/objects/amf0-hash.bin +0 -0
  29. data/spec/fixtures/objects/amf0-null.bin +1 -0
  30. data/spec/fixtures/objects/amf0-number.bin +0 -0
  31. data/spec/fixtures/objects/amf0-object.bin +0 -0
  32. data/spec/fixtures/objects/amf0-ref-test.bin +0 -0
  33. data/spec/fixtures/objects/amf0-strict-array.bin +0 -0
  34. data/spec/fixtures/objects/amf0-string.bin +0 -0
  35. data/spec/fixtures/objects/amf0-typed-object.bin +0 -0
  36. data/spec/fixtures/objects/amf0-undefined.bin +1 -0
  37. data/spec/fixtures/objects/amf0-untyped-object.bin +0 -0
  38. data/spec/fixtures/objects/amf3-0.bin +0 -0
  39. data/spec/fixtures/objects/amf3-arrayRef.bin +1 -0
  40. data/spec/fixtures/objects/amf3-bigNum.bin +0 -0
  41. data/spec/fixtures/objects/amf3-date.bin +0 -0
  42. data/spec/fixtures/objects/amf3-datesRef.bin +0 -0
  43. data/spec/fixtures/objects/amf3-dynObject.bin +2 -0
  44. data/spec/fixtures/objects/amf3-emptyArray.bin +1 -0
  45. data/spec/fixtures/objects/amf3-emptyArrayRef.bin +1 -0
  46. data/spec/fixtures/objects/amf3-emptyStringRef.bin +1 -0
  47. data/spec/fixtures/objects/amf3-false.bin +1 -0
  48. data/spec/fixtures/objects/amf3-graphMember.bin +0 -0
  49. data/spec/fixtures/objects/amf3-hash.bin +2 -0
  50. data/spec/fixtures/objects/amf3-largeMax.bin +0 -0
  51. data/spec/fixtures/objects/amf3-largeMin.bin +0 -0
  52. data/spec/fixtures/objects/amf3-max.bin +1 -0
  53. data/spec/fixtures/objects/amf3-min.bin +0 -0
  54. data/spec/fixtures/objects/amf3-mixedArray.bin +11 -0
  55. data/spec/fixtures/objects/amf3-null.bin +1 -0
  56. data/spec/fixtures/objects/amf3-objRef.bin +0 -0
  57. data/spec/fixtures/objects/amf3-primArray.bin +1 -0
  58. data/spec/fixtures/objects/amf3-string.bin +1 -0
  59. data/spec/fixtures/objects/amf3-stringRef.bin +0 -0
  60. data/spec/fixtures/objects/amf3-symbol.bin +1 -0
  61. data/spec/fixtures/objects/amf3-true.bin +1 -0
  62. data/spec/fixtures/objects/amf3-typedObject.bin +2 -0
  63. data/spec/fixtures/request/acknowledge-response.bin +0 -0
  64. data/spec/fixtures/request/amf0-error-response.bin +0 -0
  65. data/spec/fixtures/request/commandMessage.bin +0 -0
  66. data/spec/fixtures/request/remotingMessage.bin +0 -0
  67. data/spec/fixtures/request/simple-response.bin +0 -0
  68. data/spec/spec.opts +1 -0
  69. data/spec/spec_helper.rb +32 -0
  70. 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,9 @@
1
+ module RocketAMF
2
+ module Values #:nodoc:
3
+ class ArrayCollection < Array
4
+ def externalized_data=(data)
5
+ push(*data)
6
+ end
7
+ end
8
+ end
9
+ 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