RocketAMF 0.0.5

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 (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