rack-amf 0.0.1

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.
@@ -0,0 +1,218 @@
1
+ require 'amf/pure/io_helpers'
2
+
3
+ module AMF
4
+ module Pure
5
+ # AMF0 implementation of serializer
6
+ class Serializer
7
+ def initialize
8
+ @ref_cache = SerializerCache.new
9
+ end
10
+
11
+ def version
12
+ 0
13
+ end
14
+
15
+ def serialize obj, stream = ""
16
+ if @ref_cache[obj] != nil
17
+ # Write reference header
18
+ end
19
+ end
20
+ end
21
+
22
+ # AMF3 implementation of serializer
23
+ class AMF3Serializer
24
+ attr_reader :string_cache
25
+
26
+ def initialize
27
+ @string_cache = SerializerCache.new
28
+ @object_cache = SerializerCache.new
29
+ end
30
+
31
+ def version
32
+ 3
33
+ end
34
+
35
+ def serialize obj, stream = ""
36
+ if obj.respond_to?(:to_amf)
37
+ stream << obj.to_amf(self)
38
+ elsif obj.is_a?(NilClass)
39
+ write_null stream
40
+ elsif obj.is_a?(TrueClass)
41
+ write_true stream
42
+ elsif obj.is_a?(FalseClass)
43
+ write_false stream
44
+ elsif obj.is_a?(Float)
45
+ write_float obj, stream
46
+ elsif obj.is_a?(Integer)
47
+ write_integer obj, stream
48
+ elsif obj.is_a?(Symbol) || obj.is_a?(String)
49
+ write_string obj.to_s, stream
50
+ elsif obj.is_a?(Time)
51
+ write_date obj, stream
52
+ elsif obj.is_a?(Array)
53
+ write_array obj, stream
54
+ elsif obj.is_a?(Hash) || obj.is_a?(Object)
55
+ write_object obj, stream
56
+ end
57
+ stream
58
+ end
59
+
60
+ def write_reference index, stream
61
+ header = index << 1 # shift value left to leave a low bit of 0
62
+ stream << pack_integer(header)
63
+ end
64
+
65
+ def write_null stream
66
+ stream << AMF3_NULL_MARKER
67
+ end
68
+
69
+ def write_true stream
70
+ stream << AMF3_TRUE_MARKER
71
+ end
72
+
73
+ def write_false stream
74
+ stream << AMF3_FALSE_MARKER
75
+ end
76
+
77
+ def write_integer int, stream
78
+ if int < MIN_INTEGER || int > MAX_INTEGER # Check valid range for 29 bits
79
+ write_float int.to_f, stream
80
+ else
81
+ stream << AMF3_INTEGER_MARKER
82
+ stream << pack_integer(int)
83
+ end
84
+ end
85
+
86
+ def write_float float, stream
87
+ stream << AMF3_DOUBLE_MARKER
88
+ stream << pack_double(float)
89
+ end
90
+
91
+ def write_string str, stream
92
+ stream << AMF3_STRING_MARKER
93
+ write_utf8_vr str, stream
94
+ end
95
+
96
+ def write_date date, stream
97
+ stream << AMF3_DATE_MARKER
98
+ if @object_cache[date] != nil
99
+ write_reference @object_cache[date], stream
100
+ else
101
+ # Cache date
102
+ @object_cache.add_obj date
103
+
104
+ # Build AMF string
105
+ date.utc unless date.utc?
106
+ seconds = (date.to_f * 1000).to_i
107
+ stream << pack_integer(AMF3_NULL_MARKER)
108
+ stream << pack_double(seconds)
109
+ end
110
+ end
111
+
112
+ def write_array array, stream
113
+ stream << AMF3_ARRAY_MARKER
114
+ if @object_cache[array] != nil
115
+ write_reference @object_cache[array], stream
116
+ else
117
+ # Cache array
118
+ @object_cache.add_obj array
119
+
120
+ # Build AMF string
121
+ header = array.length << 1 # make room for a low bit of 1
122
+ header = header | 1 # set the low bit to 1
123
+ stream << pack_integer(header)
124
+ stream << CLOSE_DYNAMIC_ARRAY
125
+ array.each do |elem|
126
+ serialize elem, stream
127
+ end
128
+ end
129
+ end
130
+
131
+ def write_object obj, stream
132
+ stream << AMF3_OBJECT_MARKER
133
+ if @object_cache[obj] != nil
134
+ write_reference @object_cache[obj], stream
135
+ else
136
+ # Cache object
137
+ @object_cache.add_obj obj
138
+
139
+ class_name = ClassMapper.get_as_class_name obj
140
+
141
+ # Any object that has a class name isn't dynamic
142
+ unless class_name
143
+ stream << DYNAMIC_OBJECT
144
+ end
145
+
146
+ # Write class name/anonymous
147
+ if class_name
148
+ write_utf8_vr class_name, stream
149
+ else
150
+ stream << ANONYMOUS_OBJECT
151
+ end
152
+
153
+ # Write out properties
154
+ props = ClassMapper.props_for_serialization obj
155
+ props.sort.each do |key, val| # Sort props until Ruby 1.9 becomes common
156
+ write_utf8_vr key.to_s, stream
157
+ serialize val, stream
158
+ end
159
+
160
+ # Write close
161
+ stream << CLOSE_DYNAMIC_OBJECT
162
+ end
163
+ end
164
+
165
+ private
166
+ include AMF::Pure::WriteIOHelpers
167
+
168
+ def write_utf8_vr str, stream
169
+ if str == ''
170
+ stream << EMPTY_STRING
171
+ elsif @string_cache[str] != nil
172
+ write_reference @string_cache[str], stream
173
+ else
174
+ # Cache string
175
+ @string_cache.add_obj str
176
+
177
+ # Build AMF string
178
+ header = str.length << 1 # make room for a low bit of 1
179
+ header = header | 1 # set the low bit to 1
180
+ stream << pack_integer(header)
181
+ stream << str
182
+ end
183
+ end
184
+ end
185
+
186
+ class SerializerCache #:nodoc:
187
+ def initialize
188
+ @cache_index = 0
189
+ @store = {}
190
+ end
191
+
192
+ def [] obj
193
+ @store[object_key(obj)]
194
+ end
195
+
196
+ def []= obj, value
197
+ @store[object_key(obj)] = value
198
+ end
199
+
200
+ def add_obj obj
201
+ key = object_key obj
202
+ if @store[key].nil?
203
+ @store[key] = @cache_index
204
+ @cache_index += 1
205
+ end
206
+ end
207
+
208
+ private
209
+ def object_key obj
210
+ if obj.is_a?(String)
211
+ obj
212
+ else
213
+ obj.object_id
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
data/lib/amf/pure.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'amf/constants'
2
+ require 'amf/pure/deserializer'
3
+ require 'amf/pure/serializer'
4
+ require 'amf/pure/remoting'
5
+
6
+ module AMF
7
+ # This module holds all the modules/classes that implement AMF's
8
+ # functionality in pure ruby.
9
+ module Pure
10
+ $DEBUG and warn "Using pure library for AMF."
11
+ end
12
+
13
+ include AMF::Pure
14
+ end
@@ -0,0 +1,9 @@
1
+ module AMF
2
+ module Values #:nodoc:
3
+ class ArrayCollection
4
+ def externalized_data=(data)
5
+ @data = data
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,133 @@
1
+ module AMF
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
+ def rand_uuid
15
+ [8,4,4,4,12].map {|n| rand_hex_3(n)}.join('-').to_s
16
+ end
17
+
18
+ private
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 AMF
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 AMF
2
+ # AMF version
3
+ VERSION = '0.0.1'
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
data/lib/amf.rb ADDED
@@ -0,0 +1,17 @@
1
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
2
+ $:.unshift "#{File.expand_path(File.dirname(__FILE__))}/amf/"
3
+
4
+ require 'rubygems'
5
+ require 'amf/version'
6
+ require 'amf/common'
7
+
8
+ module AMF
9
+ begin
10
+ raise LoadError, 'C extensions not implemented'
11
+ rescue LoadError
12
+ require 'amf/pure'
13
+ end
14
+ require 'amf/class_mapping'
15
+
16
+ ClassMapper = AMF::ClassMapping.new
17
+ end
@@ -0,0 +1,32 @@
1
+ require 'rack/amf/request'
2
+ require 'rack/amf/response'
3
+
4
+ module Rack::AMF
5
+ class Application
6
+ def initialize app, mode
7
+ @app = app
8
+ @mode = mode
9
+ end
10
+
11
+ def call env
12
+ if env['CONTENT_TYPE'] != APPLICATION_AMF
13
+ return [200, {"Content-Type" => "text/plain"}, ["Hello From Rack::AMF"]]
14
+ end
15
+
16
+ # Wrap request and response
17
+ env['amf.request'] = Request.new(env)
18
+ env['amf.response'] = Response.new(env['amf.request'])
19
+
20
+ # Handle request
21
+ if @mode == :pass_through
22
+ @app.call env
23
+ elsif @mode == :internal
24
+ # Have the service manager handle it
25
+ Services.handle(env)
26
+ end
27
+
28
+ response = env['amf.response'].to_s
29
+ [200, {"Content-Type" => APPLICATION_AMF, 'Content-Length' => response.length.to_s}, [response]]
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ module Rack::AMF
2
+ class Request
3
+ attr_reader :raw_request
4
+
5
+ def initialize env
6
+ env['rack.input'].rewind
7
+ @raw_request = ::AMF::Request.new.populate_from_stream(env['rack.input'].read)
8
+ end
9
+
10
+ # Returns all messages in the request
11
+ def messages
12
+ @raw_request.messages
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,54 @@
1
+ module Rack::AMF
2
+ class Response
3
+ attr_reader :raw_response
4
+
5
+ def initialize request
6
+ @request = request
7
+ @raw_response = ::AMF::Response.new
8
+ end
9
+
10
+ # Builds response, iterating over each method call and using the return value
11
+ # as the method call's return value
12
+ def each_method_call &block
13
+ @request.messages.each do |m|
14
+ target_uri = m.response_uri
15
+
16
+ rd = m.data
17
+ if rd.is_a?(::AMF::Values::CommandMessage)
18
+ if rd.operation == ::AMF::Values::CommandMessage::CLIENT_PING_OPERATION
19
+ data = ::AMF::Values::AcknowledgeMessage.new(rd)
20
+ else
21
+ data == ::AMF::Values::ErrorMessage.new(Exception.new("CommandMessage #{rd.operation} not implemented"), rd)
22
+ end
23
+ elsif rd.is_a?(::AMF::Values::RemotingMessage)
24
+ am = ::AMF::Values::AcknowledgeMessage.new(rd)
25
+ body = dispatch_call(rd.source+'.'+rd.operation, rd.body, rd, block)
26
+ if body.is_a?(::AMF::Values::ErrorMessage)
27
+ data = body
28
+ else
29
+ am.body = body
30
+ data = am
31
+ end
32
+ else
33
+ data = dispatch_call(m.target_uri, rd, m, block)
34
+ end
35
+
36
+ target_uri += data.is_a?(::AMF::Values::ErrorMessage) ? '/onStatus' : '/onResult'
37
+ @raw_response.messages << ::AMF::Message.new(target_uri, '', data)
38
+ end
39
+ end
40
+
41
+ def to_s
42
+ raw_response.serialize
43
+ end
44
+
45
+ private
46
+ def dispatch_call method, args, source_message, handler
47
+ begin
48
+ handler.call(method, args)
49
+ rescue Exception => e
50
+ ::AMF::Values::ErrorMessage.new(source_message, e)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,35 @@
1
+ module Rack::AMF
2
+ class ServiceManager
3
+ def initialize
4
+ @services = {}
5
+ end
6
+
7
+ def register path, service
8
+ @services ||= {}
9
+ @services[path] = service
10
+ end
11
+
12
+ def handle env
13
+ env['amf.response'].each_method_call do |method, args|
14
+ handle_method method, args
15
+ end
16
+ end
17
+
18
+ private
19
+ def handle_method method, args
20
+ path = method.split('.')
21
+ method_name = path.pop
22
+ path = path.join('.')
23
+
24
+ if @services[path]
25
+ if @services[path].respond_to?(method_name)
26
+ @services[path].send(method_name, *args)
27
+ else
28
+ raise "Service #{path} does not respond to #{method_name}"
29
+ end
30
+ else
31
+ raise "Service #{path} does not exist"
32
+ end
33
+ end
34
+ end
35
+ end
data/lib/rack/amf.rb ADDED
@@ -0,0 +1,17 @@
1
+ require 'rack'
2
+ require 'amf'
3
+
4
+ require 'rack/amf/application'
5
+ require 'rack/amf/service_manager'
6
+ require 'rack/amf/request'
7
+ require 'rack/amf/response'
8
+
9
+ module Rack::AMF
10
+ APPLICATION_AMF = 'application/x-amf'.freeze
11
+
12
+ Services = Rack::AMF::ServiceManager.new
13
+
14
+ def self.new app, mode=:internal
15
+ Rack::AMF::Application.new(app, mode)
16
+ end
17
+ end
@@ -0,0 +1,34 @@
1
+ require File.dirname(__FILE__) + '/../spec_helper.rb'
2
+
3
+ describe AMF::ClassMapping::MappingSet do
4
+ before :each do
5
+ @config = AMF::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