stark-rack 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.autotest +23 -0
- data/.gemtest +0 -0
- data/History.txt +6 -0
- data/Manifest.txt +23 -0
- data/README.txt +61 -0
- data/Rakefile +21 -0
- data/lib/stark/rack.rb +94 -0
- data/lib/stark/rack/content_negotiation.rb +34 -0
- data/lib/stark/rack/logging_processor.rb +50 -0
- data/lib/stark/rack/metadata.rb +89 -0
- data/lib/stark/rack/rest.rb +327 -0
- data/lib/stark/rack/verbose_protocol.rb +36 -0
- data/stark-rack.gemspec +42 -0
- data/test/calc-opt.rb +31 -0
- data/test/calc.thrift +13 -0
- data/test/config.ru +19 -0
- data/test/gen-rb/calc.rb +80 -0
- data/test/gen-rb/calc_constants.rb +8 -0
- data/test/gen-rb/calc_types.rb +7 -0
- data/test/helper.rb +60 -0
- data/test/test_metadata.rb +43 -0
- data/test/test_rack.rb +65 -0
- data/test/test_rest.rb +175 -0
- metadata +140 -0
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
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
|