stark-rack 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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