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