stark-rack 1.0.0

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.
data/.autotest ADDED
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'autotest/restart'
4
+
5
+ # Autotest.add_hook :initialize do |at|
6
+ # at.extra_files << "../some/external/dependency.rb"
7
+ #
8
+ # at.libs << ":../some/external"
9
+ #
10
+ # at.add_exception 'vendor'
11
+ #
12
+ # at.add_mapping(/dependency.rb/) do |f, _|
13
+ # at.files_matching(/test_.*rb$/)
14
+ # end
15
+ #
16
+ # %w(TestA TestB).each do |klass|
17
+ # at.extra_class_map[klass] = "test/test_misc.rb"
18
+ # end
19
+ # end
20
+
21
+ # Autotest.add_hook :run_command do |at|
22
+ # system "rake build"
23
+ # end
data/.gemtest ADDED
File without changes
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 1.0.0 / 2013-05-22
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,23 @@
1
+ .autotest
2
+ .gemtest
3
+ History.txt
4
+ Manifest.txt
5
+ README.txt
6
+ Rakefile
7
+ lib/stark/rack.rb
8
+ lib/stark/rack/content_negotiation.rb
9
+ lib/stark/rack/logging_processor.rb
10
+ lib/stark/rack/metadata.rb
11
+ lib/stark/rack/rest.rb
12
+ lib/stark/rack/verbose_protocol.rb
13
+ stark-rack.gemspec
14
+ test/calc-opt.rb
15
+ test/calc.thrift
16
+ test/config.ru
17
+ test/gen-rb/calc.rb
18
+ test/gen-rb/calc_constants.rb
19
+ test/gen-rb/calc_types.rb
20
+ test/helper.rb
21
+ test/test_metadata.rb
22
+ test/test_rack.rb
23
+ test/test_rest.rb
data/README.txt ADDED
@@ -0,0 +1,61 @@
1
+ = stark-rack
2
+
3
+ * https://github.com/evanphx/stark-rack
4
+
5
+ == DESCRIPTION:
6
+
7
+ Provides middleware for mounting Stark/Thrift services as Rack endpoints.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * Thrift binary protocol by default
12
+ * JSON protocol with 'Accept: application/json' in request headers
13
+
14
+ == SYNOPSIS:
15
+
16
+ # config.ru
17
+ use Stark::Rack
18
+ use MyService::Processor # Stark/Thrift processor
19
+ run MyHandler.new # Handler that implements service
20
+
21
+ == REQUIREMENTS:
22
+
23
+ * Rack (optional; only required with Stark::Rack::REST middleware)
24
+
25
+ == INSTALL:
26
+
27
+ * gem install stark-rack
28
+
29
+ == DEVELOPERS:
30
+
31
+ After checking out the source, run:
32
+
33
+ $ rake newb
34
+
35
+ This task will install any missing dependencies, run the tests/specs,
36
+ and generate the RDoc.
37
+
38
+ == LICENSE:
39
+
40
+ (The MIT License)
41
+
42
+ Copyright (c) 2012, 2013 Evan Phoenix
43
+
44
+ Permission is hereby granted, free of charge, to any person obtaining
45
+ a copy of this software and associated documentation files (the
46
+ 'Software'), to deal in the Software without restriction, including
47
+ without limitation the rights to use, copy, modify, merge, publish,
48
+ distribute, sublicense, and/or sell copies of the Software, and to
49
+ permit persons to whom the Software is furnished to do so, subject to
50
+ the following conditions:
51
+
52
+ The above copyright notice and this permission notice shall be
53
+ included in all copies or substantial portions of the Software.
54
+
55
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
56
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
57
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
58
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
59
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
60
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
61
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+
6
+ # Don't turn on warnings, output is very ugly w/ generated code
7
+ Hoe::RUBY_FLAGS.sub! /-w/, ''
8
+ # Add stark to path if we have it checked out locally
9
+ stark_local_path = File.expand_path('../../stark/lib', __FILE__)
10
+ Hoe::RUBY_FLAGS.concat " -I#{stark_local_path}" if File.directory?(stark_local_path)
11
+
12
+ Hoe.plugin :git
13
+ Hoe.plugin :gemspec
14
+
15
+ Hoe.spec 'stark-rack' do
16
+ developer('Evan Phoenix', 'evan@phx.io')
17
+ dependency 'stark', '< 2.0.0'
18
+ dependency 'rack', '>= 1.5.0', :dev
19
+ end
20
+
21
+ # vim: syntax=ruby
data/lib/stark/rack.rb ADDED
@@ -0,0 +1,94 @@
1
+ require 'stark'
2
+ require 'stark/rack/content_negotiation'
3
+ require 'stark/rack/logging_processor'
4
+ require 'stark/rack/verbose_protocol'
5
+
6
+ class Stark::Rack
7
+
8
+ VERSION = '1.0.0'
9
+
10
+ FORMAT = %{when: %0.4f, client: "%s", path: "%s%s", type: "%s", name: "%s", seqid: %d, error: %s\n}
11
+
12
+ TYPES = {
13
+ 1 => "CALL",
14
+ 2 => "REPLY",
15
+ 3 => "EXCEPTION",
16
+ 4 => "ONEWAY"
17
+ }
18
+
19
+ include ContentNegotiation
20
+
21
+ def initialize(processor, options={})
22
+ @log = options[:log]
23
+ @logger = STDERR
24
+ @app_processor = processor
25
+ end
26
+
27
+ attr_accessor :log
28
+
29
+ def processor
30
+ @processor ||= LoggingProcessor.new(@app_processor, error_capture)
31
+ end
32
+
33
+ def call(env)
34
+ dup._call(env)
35
+ end
36
+
37
+ def _call(env)
38
+ path = env['PATH_INFO'] || ""
39
+ path << "/" if path.empty?
40
+
41
+ if env["REQUEST_METHOD"] != "POST"
42
+ return [405, {"Content-Type" => "text/plain"},
43
+ ["Method #{env["REQUEST_METHOD"]} not allowed, must be POST\n"]]
44
+ end
45
+
46
+ unless path == "/"
47
+ return [404, {"Content-Type" => "text/plain"}, ["Nothing at #{path}\n"]]
48
+ end
49
+
50
+ out = StringIO.new
51
+
52
+ transport = Thrift::IOStreamTransport.new env['rack.input'], out
53
+ protocol = protocol_factory(env).get_protocol transport
54
+
55
+ if @log
56
+ name, type, seqid, err = processor.process protocol, protocol
57
+
58
+ @logger.write FORMAT % [
59
+ Time.now.to_f,
60
+ env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-",
61
+ env["PATH_INFO"],
62
+ env["QUERY_STRING"].empty? ? "" : "?"+env["QUERY_STRING"],
63
+ TYPES[type],
64
+ name,
65
+ seqid,
66
+ err ? "'#{err.message} (#{err.class})'" : "null"
67
+ ]
68
+ else
69
+ processor.process protocol, protocol
70
+ end
71
+
72
+ [status_from_last_error, headers(env), [out.string]]
73
+ end
74
+
75
+ def error_capture
76
+ lambda do |m,*args|
77
+ @last_error = [m, args]
78
+ end.tap do |x|
79
+ (class << x; self; end).instance_eval {
80
+ alias_method :method_missing, :call }
81
+ end
82
+ end
83
+
84
+ def status_from_last_error
85
+ return 200 if @last_error.nil? || @last_error.first == :success
86
+ x = @last_error.last[3]
87
+ case x.type
88
+ when Thrift::ApplicationException::UNKNOWN_METHOD
89
+ 404
90
+ else
91
+ 500
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,34 @@
1
+ class Stark::Rack
2
+ module ContentNegotiation
3
+ THRIFT_CONTENT_TYPE = 'application/x-thrift'
4
+ THRIFT_JSON_CONTENT_TYPE = 'application/vnd.thrift+json'
5
+
6
+ def accept_json?(env)
7
+ env['HTTP_ACCEPT'] == THRIFT_JSON_CONTENT_TYPE ||
8
+ env['HTTP_CONTENT_TYPE'] == THRIFT_JSON_CONTENT_TYPE
9
+ end
10
+
11
+ def headers(env)
12
+ headers = { 'Content-Type' => THRIFT_CONTENT_TYPE }
13
+ if accept_json?(env)
14
+ headers['Content-Type'] = THRIFT_JSON_CONTENT_TYPE
15
+ end
16
+ headers
17
+ end
18
+
19
+ def protocol_factory(env)
20
+ if env['stark.protocol.factory']
21
+ env['stark.protocol.factory']
22
+ else
23
+ if accept_json?(env)
24
+ f = Thrift::JsonProtocolFactory.new
25
+ env['stark.protocol'] = :json
26
+ else
27
+ f = Thrift::BinaryProtocolFactory.new
28
+ env['stark.protocol'] = :binary
29
+ end
30
+ env['stark.protocol.factory'] = f
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,50 @@
1
+ class Stark::Rack
2
+ class LoggingProcessor
3
+ def initialize(handler, secondary=nil)
4
+ @handler = handler
5
+ @secondary = secondary
6
+ end
7
+
8
+ def process(iprot, oprot)
9
+ name, type, seqid = iprot.read_message_begin
10
+ x = nil
11
+ if @handler.respond_to?("process_#{name}")
12
+ begin
13
+ @handler.send("process_#{name}", seqid, iprot, oprot)
14
+ rescue StandardError => e
15
+ Stark.logger.error "#{@handler.class.name}#process_#{name}: #{e.message}\n " + e.backtrace.join("\n ")
16
+ x = Thrift::ApplicationException.new(
17
+ Thrift::ApplicationException::UNKNOWN,
18
+ "#{e.message} (#{e.class})")
19
+ oprot.write_message_begin(name, Thrift::MessageTypes::EXCEPTION, seqid)
20
+ x.write(oprot)
21
+ oprot.write_message_end
22
+ oprot.trans.flush
23
+ end
24
+
25
+ if s = @secondary
26
+ if x
27
+ s.error name, type, seqid, x
28
+ else
29
+ s.success name, type, seqid
30
+ end
31
+ end
32
+
33
+ [name, type, seqid, x]
34
+ else
35
+ iprot.skip(Thrift::Types::STRUCT)
36
+ iprot.read_message_end
37
+ x = Thrift::ApplicationException.new(Thrift::ApplicationException::UNKNOWN_METHOD,
38
+ "Unknown function: #{name}")
39
+ oprot.write_message_begin(name, Thrift::MessageTypes::EXCEPTION, seqid)
40
+ x.write(oprot)
41
+ oprot.write_message_end
42
+ oprot.trans.flush
43
+ if s = @secondary
44
+ s.error name, type, seqid, x
45
+ end
46
+ false
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,89 @@
1
+ require 'stark'
2
+ require 'composite_io'
3
+
4
+ class Stark::Rack
5
+ module Metadata
6
+ DEFAULT_METADATA = { 'version' => Stark::Rack::VERSION }
7
+ METADATA_IDL = "service Metadata {\nmap<string,string> metadata()\n}\n"
8
+ Stark.materialize StringIO.new(METADATA_IDL), Stark::Rack
9
+
10
+ def self.new(app, metadata = {})
11
+ Middleware.new app, metadata
12
+ end
13
+
14
+ class Middleware
15
+ include ContentNegotiation
16
+
17
+ def initialize(app, metadata)
18
+ @app = app
19
+ @handler = Handler.new metadata
20
+ @processor = Processor.new @handler
21
+ end
22
+
23
+ def call(env)
24
+ env['rack.input'] = RewindableInput.new(env['rack.input'])
25
+ status, hdr, body = @app.call env
26
+
27
+ if status == 404
28
+ env['rack.input'].rewind
29
+
30
+ out = StringIO.new
31
+ transport = Thrift::IOStreamTransport.new env['rack.input'], out
32
+ protocol = protocol_factory(env).get_protocol transport
33
+
34
+ if @processor.process(protocol, protocol)
35
+ return [200, headers(env), [out.string]]
36
+ end
37
+ end
38
+
39
+ [status, hdr, body]
40
+ end
41
+ end
42
+
43
+ class Handler
44
+ attr_reader :metadata
45
+ def initialize(metadata)
46
+ @metadata = DEFAULT_METADATA.merge(metadata || {})
47
+ @metadata.keys.each do |k|
48
+ unless String === @metadata[k]
49
+ @metadata[k] = @metadata[k].to_s # stringify values
50
+ end
51
+ unless String === k # stringify keys
52
+ @metadata[k.to_s] = @metadata.delete k
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ class RewindableInput
59
+ def initialize(io)
60
+ @io = io
61
+ @buffered = StringIO.new
62
+ end
63
+
64
+ def read(n)
65
+ if @io
66
+ @io.read(n).tap do |s|
67
+ if s
68
+ @buffered.write s
69
+ else
70
+ @io.close
71
+ @io = nil
72
+ end
73
+ end
74
+ else
75
+ @buffered.read(n)
76
+ end
77
+ end
78
+
79
+ def rewind
80
+ if @io
81
+ @io = CompositeReadIO.new([StringIO.new(@buffered.string), @io])
82
+ @buffered = StringIO.new
83
+ else
84
+ @buffered.rewind
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,327 @@
1
+ require 'rack/request'
2
+ require 'rack/utils/okjson'
3
+ require 'stark/rack'
4
+
5
+ class Stark::Rack
6
+ # Public: This middleware translates "REST-y" requests into Thrift requests
7
+ # and Thrift responses to a simplified, intuitive JSON response.
8
+ #
9
+ # In order for a request to qualify as "REST-y" its PATH_INFO must contain a
10
+ # method name and must either be:
11
+ #
12
+ # - a GET request, possibly with a query string
13
+ # - a POST or PUT request with body containing form data
14
+ # (application/x-www-form-urlencoded or multipart/form-data) or JSON
15
+ # (application/json)
16
+ #
17
+ # When converting parameters or JSON into Thrift, several conventions and
18
+ # assumptions are made. If your input does not follow these conventions, the
19
+ # conversion may succeed but the underlying thrift call may fail.
20
+ #
21
+ # - Hashes are converted into maps.
22
+ # - Arrays are converted into lists.
23
+ # - Structs can be passed as a hash that includes a '_struct_' key.
24
+ # - Everything else is converted into strings and makes use of Stark's
25
+ # facility to coerce strings into other types (numbers, booleans).
26
+ #
27
+ # Structs additionally must follow these conventions.
28
+ # - The keys of a struct hash should start with the field numbers. Anything
29
+ # after the number is discarded (e.g., for "1" or "1last_result" or
30
+ # "1-last_result", "last_result" or "-last_result" are discarded).
31
+ # - Field names not prefixed with a number can be used provided that the
32
+ # fields declared in the IDL are numbered starting at 1, increase
33
+ # monotonically, and the request struct key/values appear in the order in
34
+ # which they are declared in the IDL.
35
+ #
36
+ # Given the following thrift definition:
37
+ #
38
+ # struct State {
39
+ # 1: i32 last_result
40
+ # 2: map<string,i32> vars
41
+ # }
42
+ #
43
+ # service Calc {
44
+ # i32 add(1: i32 lhs, 2: i32 rhs)
45
+ # i32 last_result()
46
+ # void store_vars(1: map<string,i32> vars)
47
+ # i32 get_var(1: string name)
48
+ # void set_state(1: State state)
49
+ # State get_state()
50
+ # }
51
+ #
52
+ # When the Calc service is mounted in a Stark::Rack endpoint at the `/calc` path,
53
+ # all of the examples below demonstrate valid requests.
54
+ #
55
+ # Examples
56
+ #
57
+ # # Calls last_result()
58
+ # GET /calc/last_result
59
+ #
60
+ # # Also calls last_result(). Non-alphanumeric are converted to
61
+ # # underscore.
62
+ # GET /calc/last-result
63
+ #
64
+ # # Calls get_var("a") using an indexed-hash parameter with keys
65
+ # # corresponding to argument field numbers.
66
+ # # Effective params hash: {"arg" => {"1" => "a"}}
67
+ # GET /calc/get_var?arg[1]=a
68
+ #
69
+ # # Calls get_var("a") using open-ended array parameter.
70
+ # # Argument field numbers are assumed to start with 1 and increase
71
+ # # monotonically.
72
+ # # Effective params hash: {"arg" => ["a"]}
73
+ # GET /calc/get_var?arg[]=a
74
+ #
75
+ # # Calls add(1, 1) using an open-ended array parameter.
76
+ # # Effective params hash: {"arg" => ["1", "1"]}
77
+ # GET /calc/add?arg[]=1&arg[]=2
78
+ #
79
+ # # Calls store_vars({"a" => 1, "b" => 2, "c" => 3}),
80
+ # # treating query parameters as a single map argument.
81
+ # GET /calc/store_vars?a=1&b=2&c=3
82
+ #
83
+ # # Calls set_state(State.new(:last_result => 0, :vars => {"a" => 1, "b"=> 2})).
84
+ # # Note the presence of a "_struct_" key in the parameters, marking a
85
+ # # struct instead of a map.
86
+ # GET /calc/set_state?_struct_=State&last_result=0&vars[a]=1&vars[b]=2
87
+ #
88
+ # # Calls set_state(State.new(:last_result => 0, :vars => {"a" => 1, "b"=> 2})),
89
+ # # using indexed-hash format.
90
+ # # Effective params hash:
91
+ # # {"arg" => {"1" => {"_struct_" => "State", "last_result" => "0", "vars" => {"a" => "1", "b" => "2"}}}}
92
+ # GET /calc/set_state?arg[1][_struct_]=State&arg[1][last_result]=0&arg[1][vars][a]=1&arg[1][vars][b]=2
93
+ #
94
+ # # Calls set_state(State.new(:last_result => 0, :vars => {"a" => 1, "b"=> 2})),
95
+ # # using JSON.
96
+ # POST /calc/set_state
97
+ # Content-Type: application/json
98
+ #
99
+ # [{"_struct_":"State","last_result":0,"vars":{"a":1,"b":2}}]
100
+ #
101
+ # # Calls set_state(State.new(:last_result => 0, :vars => {"a" => 1, "b"=> 2})),
102
+ # # using JSON with indexed-hash format.
103
+ # POST /calc/set_state
104
+ # Content-Type: application/json
105
+ #
106
+ # {"arg":{"1":{"_struct_":"State","last_result":0,"vars":{"a":1,"b":2}}}}
107
+ #
108
+ class REST
109
+ include ContentNegotiation
110
+
111
+ # Name of marker key in a hash that indicates it represents a struct. The
112
+ # struct name is the corresponding value.
113
+ STRUCT = '_struct_'
114
+
115
+ def initialize(app)
116
+ @app = app
117
+ end
118
+
119
+ def call(env)
120
+ if applies?(env)
121
+ env['stark.protocol.factory'] = VerboseProtocolFactory.new
122
+ if send("create_thrift_call_from_#{env['stark.rest.input.format']}", env)
123
+ status, headers, body = @app.call env
124
+ headers["Content-Type"] = 'application/json'
125
+ [status, headers, unmarshal_result(env, body)]
126
+ else
127
+ [400, {}, []]
128
+ end
129
+ else
130
+ @app.call env
131
+ end
132
+ end
133
+
134
+ def applies?(env)
135
+ path = env["PATH_INFO"]
136
+ content_type = env["HTTP_CONTENT_TYPE"]
137
+
138
+ if path && path.length > 1 # need a method name
139
+ env['stark.rest.method.name'] = path.split('/')[1].gsub(/[^a-zA-Z0-9_]/, '_')
140
+ if content_type == 'application/json'
141
+ env['stark.rest.input.format'] = 'json'
142
+ elsif env["REQUEST_METHOD"] == "GET" || # pure GET, no body
143
+ # posted content looks like form data
144
+ Rack::Request::FORM_DATA_MEDIA_TYPES.include?(content_type)
145
+ env['stark.rest.input.format'] = 'params'
146
+ end
147
+ end
148
+ end
149
+
150
+ def create_thrift_call_from_json(env)
151
+ params = Rack::Utils::OkJson.decode(env['rack.input'].read)
152
+ params = { 'args' => params } if Array === params
153
+ encode_thrift_call env, params
154
+ rescue
155
+ false
156
+ end
157
+
158
+ def create_thrift_call_from_params(env)
159
+ encode_thrift_call env, ::Rack::Request.new(env).params
160
+ end
161
+
162
+ def encode_thrift_call(env, params)
163
+ name = env['stark.rest.method.name']
164
+ input = StringIO.new
165
+ proto = protocol_factory(env).get_protocol(Thrift::IOStreamTransport.new(input, input))
166
+ proto.write_message_begin name, Thrift::MessageTypes::CALL, 0
167
+
168
+ obj = { STRUCT => "#{name}_args" }
169
+ if !params.empty?
170
+ arguments = params['arg'] || params['args']
171
+ if Hash === arguments
172
+ obj.update(arguments)
173
+ elsif Array === arguments
174
+ arguments.each_with_index do |v,i|
175
+ obj["#{i+1}"] = v
176
+ end
177
+ else
178
+ obj["1"] = params
179
+ end
180
+ end
181
+
182
+ encode_thrift_obj proto, obj
183
+
184
+ proto.write_message_end
185
+ proto.trans.flush
186
+
187
+ input.rewind
188
+ env['rack.input'] = input
189
+ env['PATH_INFO'] = '/'
190
+ env['REQUEST_METHOD'] = 'POST'
191
+ true
192
+ end
193
+
194
+ # Determine a thrift type to use from an array of values.
195
+ def value_type(vals)
196
+ types = vals.map {|v| v.class }.uniq
197
+
198
+ # Convert everything to string if there isn't a single unique type
199
+ return Thrift::Types::STRING if types.size > 1
200
+
201
+ type = types.first
202
+
203
+ # Array -> LIST
204
+ return Thrift::Types::LIST if type == Array
205
+
206
+ # Hash can be a MAP or STRUCT
207
+ if type == Hash
208
+ if vals.first.has_key?(STRUCT)
209
+ return Thrift::Types::STRUCT
210
+ else
211
+ return Thrift::Types::MAP
212
+ end
213
+ end
214
+
215
+ Thrift::Types::STRING
216
+ end
217
+
218
+ def encode_thrift_obj(proto, obj)
219
+ case obj
220
+ when Hash
221
+ if struct = obj.delete(STRUCT)
222
+ proto.write_struct_begin struct
223
+ idx = 1
224
+ obj.each do |k,v|
225
+ _, number, name = /^(\d*)(.*?)$/.match(k).to_a
226
+ if number.nil? || number.empty?
227
+ number = idx
228
+ else
229
+ number = number.to_i
230
+ end
231
+ name = "field#{number}" if name.nil? || name.empty?
232
+
233
+ proto.write_field_begin name, value_type([v]), number
234
+
235
+ encode_thrift_obj proto, v
236
+ proto.write_field_end
237
+ idx += 1
238
+ end
239
+ proto.write_field_stop
240
+ proto.write_struct_end
241
+ else
242
+ proto.write_map_begin Thrift::Types::STRING, value_type(obj.values), obj.size
243
+ obj.each do |k,v|
244
+ proto.write_string k
245
+ encode_thrift_obj proto, v
246
+ end
247
+ proto.write_map_end
248
+ end
249
+ when Array
250
+ proto.write_list_begin value_type(obj), obj.size
251
+ obj.each {|v| encode_thrift_obj proto, v }
252
+ proto.write_list_end
253
+ else
254
+ proto.write_string obj.to_s
255
+ end
256
+ end
257
+
258
+ def decode_thrift_obj(proto, type)
259
+ case type
260
+ when Thrift::Types::BOOL
261
+ proto.read_bool
262
+ when Thrift::Types::BYTE
263
+ proto.read_i8
264
+ when Thrift::Types::I16
265
+ proto.read_i16
266
+ when Thrift::Types::I32
267
+ proto.read_i32
268
+ when Thrift::Types::I64
269
+ proto.read_i64
270
+ when Thrift::Types::DOUBLE
271
+ proto.read_double
272
+ when Thrift::Types::STRING
273
+ proto.read_string
274
+ when Thrift::Types::STRUCT
275
+ Hash.new.tap do |hash|
276
+ struct = proto.read_struct_begin
277
+ hash[STRUCT] = struct if struct
278
+ while true
279
+ name, type, id = proto.read_field_begin
280
+ break if type == Thrift::Types::STOP
281
+ hash["#{id}#{':'+name if name}"] = decode_thrift_obj proto, type
282
+ proto.read_field_end
283
+ end
284
+ proto.read_struct_end
285
+ end
286
+ when Thrift::Types::MAP
287
+ Hash.new.tap do |hash|
288
+ kt, vt, size = proto.read_map_begin
289
+ size.times do
290
+ hash[decode_thrift_obj(proto, kt).to_s] = decode_thrift_obj(proto, vt)
291
+ end
292
+ proto.read_map_end
293
+ end
294
+ when Thrift::Types::SET
295
+ Set.new.tap do |set|
296
+ vt, size = proto.read_set_begin
297
+ size.times do
298
+ set << decode_thrift_obj(proto, vt)
299
+ end
300
+ proto.read_set_end
301
+ end
302
+ when Thrift::Types::LIST
303
+ Array.new.tap do |list|
304
+ vt, size = proto.read_list_begin
305
+ size.times do
306
+ list << decode_thrift_obj(proto, vt)
307
+ end
308
+ proto.read_list_end
309
+ end
310
+ when Thrift::Types::STOP
311
+ nil
312
+ else
313
+ raise NotImplementedError, type
314
+ end
315
+ end
316
+
317
+ def unmarshal_result(env, body)
318
+ out = StringIO.new(body.join)
319
+ proto = protocol_factory(env).get_protocol(Thrift::IOStreamTransport.new(out, out))
320
+ proto.read_message_begin
321
+ proto.read_struct_begin
322
+ _, type, id = proto.read_field_begin
323
+ result = decode_thrift_obj(proto, type)
324
+ [Rack::Utils::OkJson.encode((id == 0 ? "result" : "error") => result)]
325
+ end
326
+ end
327
+ end