thrurl 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +23 -0
- data/bin/thrurl +14 -0
- data/lib/args_reader.rb +44 -0
- data/lib/client.rb +22 -0
- data/lib/method_call.rb +60 -0
- data/lib/object_inspector.rb +101 -0
- data/lib/thrift_generator.rb +34 -0
- data/lib/type_adapter.rb +48 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5a8ea299751ef830fd45369afb732a79b6d2ccd3
|
4
|
+
data.tar.gz: e63a443330de3020e67962597cf891c8cc8cbd3f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d24d0f51a83bd2a430bc32a82873282640b39490844aaa6ac2d44182f2638ae9015ad1010243703617019cbf9127262d4ba6bfceecaa8f1cac509a44e21aff5d
|
7
|
+
data.tar.gz: 295ee639580cda621f0d9fb697f1b30c1cd5baf8fb267d2089b661a4773101cf8184b02f5178d2e797e5f06aa8811632edbafa231a29c650708e25ca52a9f781
|
data/README.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Thrurl - Thrift meets cURL
|
2
|
+
|
3
|
+
A small utility for accessing [Thrift](http://en.wikipedia.org/wiki/Apache_Thrift) services from the command line, without having to write a service-specific client. Useful for debugging purposes, trying out services, writing integration tests, etc.
|
4
|
+
|
5
|
+
## Dependencies
|
6
|
+
|
7
|
+
You need to have [`thrift`](http://thrift.apache.org/) installed. You also need to have the IDL files (`.thrift`) related to the service you want to call. Thrurl will compile them into Ruby code and parse your parameters to match the expected types automatically.
|
8
|
+
|
9
|
+
## How to use
|
10
|
+
|
11
|
+
Thrurl is a command line client, much like cURL. Using it is simple:
|
12
|
+
|
13
|
+
```
|
14
|
+
thrurl -h "my-thrift-server" -p 5000 -m "checkinsPerLocation" -s "CheckinService" -a "{ user: { id: 1 } }"
|
15
|
+
|
16
|
+
```
|
17
|
+
|
18
|
+
Thrurl will parse the response and display it in JSON format (in case you want to use the output of the script, and because it's a nice human readable format).
|
19
|
+
|
20
|
+
## Things to improve
|
21
|
+
|
22
|
+
- The error messages when e.g. fields are missing or have the wrong type is still *very* cryptic
|
23
|
+
- If you use enums, you have to pass in the *value* of the enum
|
data/bin/thrurl
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'args_reader'
|
4
|
+
require 'client'
|
5
|
+
require 'thrift_generator'
|
6
|
+
require 'pp'
|
7
|
+
|
8
|
+
configs = ArgsReader.new(ARGV).configs
|
9
|
+
|
10
|
+
thrift_module = ThriftGenerator.new(configs[:service]).generate_if_needed
|
11
|
+
|
12
|
+
resp = Client.new(thrift_module::Client, configs).call
|
13
|
+
|
14
|
+
puts ObjectInspector.new(resp).response_map.to_json
|
data/lib/args_reader.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
class ArgsReader
|
2
|
+
attr_accessor :configs
|
3
|
+
|
4
|
+
def initialize(args)
|
5
|
+
arg_map = Hash[*args]
|
6
|
+
validate_configs!(arg_map)
|
7
|
+
|
8
|
+
self.configs = {
|
9
|
+
host: arg_map["-h"],
|
10
|
+
port: arg_map["-p"],
|
11
|
+
method: arg_map["-m"],
|
12
|
+
service: arg_map["-s"],
|
13
|
+
params: eval(arg_map["-a"])
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
17
|
+
def args_and_help
|
18
|
+
{
|
19
|
+
"-h" => "Service host",
|
20
|
+
"-p" => "Service port",
|
21
|
+
"-s" => "The name of the service (as defined on the IDL)",
|
22
|
+
"-m" => "The method getting called (as defined on the IDL)",
|
23
|
+
"-a" => "The arguments for the call as a Ruby Hash"
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def usage_string
|
28
|
+
args_and_help.map { |k, v|
|
29
|
+
" #{k.ljust(4)} #{v}"
|
30
|
+
}
|
31
|
+
end
|
32
|
+
|
33
|
+
def validate_configs!(arg_map)
|
34
|
+
missing = args_and_help.keys.select { |k| arg_map[k].nil? }
|
35
|
+
if !missing.empty?
|
36
|
+
puts "Missing required parameters: #{missing.join(', ')}\n\n"
|
37
|
+
puts "Required parameters:"
|
38
|
+
puts usage_string
|
39
|
+
puts "\nExample usage:\n"
|
40
|
+
puts "thrurl -h \"localhost\" -p 5000 -m \"checkinsPerLocation\" -s \"CheckinService\" -a \"{ user: { id: 1 } }\"\n\n"
|
41
|
+
exit 1
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/client.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'thrift'
|
2
|
+
require 'json'
|
3
|
+
require_relative 'method_call'
|
4
|
+
require_relative 'type_adapter'
|
5
|
+
|
6
|
+
class Client
|
7
|
+
def initialize(client_thrift_class, config)
|
8
|
+
@transport = ::Thrift::FramedTransport.new(::Thrift::Socket.new(config[:host], config[:port]))
|
9
|
+
protocol = ::Thrift::BinaryProtocol.new(@transport)
|
10
|
+
|
11
|
+
@client = client_thrift_class.new(protocol)
|
12
|
+
@call = MethodCall.new(config[:service], config[:method], config[:params])
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
@transport.open
|
17
|
+
resp = @client.send(*@call.args)
|
18
|
+
@transport.close
|
19
|
+
resp
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
data/lib/method_call.rb
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require_relative 'object_inspector'
|
2
|
+
require 'pp'
|
3
|
+
|
4
|
+
class MethodCall
|
5
|
+
attr_accessor :args
|
6
|
+
|
7
|
+
def initialize(service_name, method, values)
|
8
|
+
eval(service_name)
|
9
|
+
|
10
|
+
args_type = eval("#{service_name}::#{upcase_first(method)}_args") rescue nil
|
11
|
+
|
12
|
+
evaluate_method(args_type, service_name, method)
|
13
|
+
@inspector = ObjectInspector.new(args_type.new)
|
14
|
+
|
15
|
+
args = evaluate_args(args_type, values)
|
16
|
+
|
17
|
+
self.args = [method] + args
|
18
|
+
end
|
19
|
+
|
20
|
+
def evaluate_method(args_type, service_name, method)
|
21
|
+
if args_type.nil?
|
22
|
+
puts "\nMethod not found: #{method}\n\nThe following methods are available:"
|
23
|
+
puts inspect_methods(service_name)
|
24
|
+
exit 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def evaluate_args(args_type, values)
|
29
|
+
args = TypeAdapter.new(
|
30
|
+
{ type: ::Thrift::Types::STRUCT, class: args_type },
|
31
|
+
values
|
32
|
+
).field_values rescue nil
|
33
|
+
|
34
|
+
if args.nil?
|
35
|
+
puts "Error processing args: #{values.to_json}\nExpected args:\n"
|
36
|
+
pp inspect_args
|
37
|
+
puts "\nExample argument with all optional fields:\n\n"
|
38
|
+
puts sample_call
|
39
|
+
exit 1
|
40
|
+
end
|
41
|
+
|
42
|
+
args
|
43
|
+
end
|
44
|
+
|
45
|
+
def inspect_args
|
46
|
+
@inspector.args_tree
|
47
|
+
end
|
48
|
+
|
49
|
+
def sample_call
|
50
|
+
@inspector.sample_call
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect_methods(base_class)
|
54
|
+
ObjectInspector.new(eval(base_class)).available_methods.join("\n")
|
55
|
+
end
|
56
|
+
|
57
|
+
def upcase_first(str)
|
58
|
+
str[0].upcase + str[1..-1]
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# this is rather hacky, but...
|
2
|
+
require 'thrift/types'
|
3
|
+
|
4
|
+
class ObjectInspector
|
5
|
+
def initialize(obj)
|
6
|
+
@obj = obj
|
7
|
+
end
|
8
|
+
|
9
|
+
def sample_call
|
10
|
+
type_tree_for(@obj.class, {}, sample_values)
|
11
|
+
end
|
12
|
+
|
13
|
+
def args_tree
|
14
|
+
type_tree_for(@obj.class, {}, type_names)
|
15
|
+
end
|
16
|
+
|
17
|
+
def response_map
|
18
|
+
response_tree(@obj, {})
|
19
|
+
end
|
20
|
+
|
21
|
+
def available_methods
|
22
|
+
@obj.constants.map { |c|
|
23
|
+
c.to_s.split("_args").first if c.to_s.end_with?("_args")
|
24
|
+
}.compact
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def response_tree(obj, tree)
|
30
|
+
obj.struct_fields.values.each { |i|
|
31
|
+
tree[i[:name]] = response_fields(obj.send(i[:name]))
|
32
|
+
}
|
33
|
+
tree
|
34
|
+
end
|
35
|
+
|
36
|
+
def response_fields(obj)
|
37
|
+
if obj.is_a?(Array)
|
38
|
+
obj.map { |f| response_fields(f) }
|
39
|
+
elsif obj.is_a?(Hash)
|
40
|
+
Hash[*
|
41
|
+
obj.each { |k, v|
|
42
|
+
[k, response_fields(v)]
|
43
|
+
}
|
44
|
+
]
|
45
|
+
elsif obj.is_a?(::Thrift::Struct)
|
46
|
+
response_tree(obj, {})
|
47
|
+
else
|
48
|
+
obj
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def type_tree_for(obj, tree, value_mapping)
|
53
|
+
obj::FIELDS.values.map { |f| type_annotation_for(f, tree, value_mapping) }
|
54
|
+
tree
|
55
|
+
end
|
56
|
+
|
57
|
+
def type_annotation_for(field, tree, value_mapping)
|
58
|
+
if(field[:class])
|
59
|
+
tree[field_name(field)] = type_tree_for(field[:class], {}, value_mapping)
|
60
|
+
elsif field[:type] == ::Thrift::Types::MAP
|
61
|
+
tree[field_name(field)] = {
|
62
|
+
type_annotation_for(field[:key], {}, value_mapping).values.first =>
|
63
|
+
type_annotation_for(field[:value], {}, value_mapping).values.first
|
64
|
+
}
|
65
|
+
elsif field[:type] == ::Thrift::Types::SET || field[:type] == ::Thrift::Types::LIST
|
66
|
+
tree[field_name(field)] = type_annotation_for(field[:element], {}, value_mapping).values
|
67
|
+
else
|
68
|
+
tree[field_name(field)] = value_mapping[field[:type]]
|
69
|
+
end
|
70
|
+
tree
|
71
|
+
end
|
72
|
+
|
73
|
+
def field_name(field)
|
74
|
+
field[:name]
|
75
|
+
end
|
76
|
+
|
77
|
+
def type_names
|
78
|
+
{
|
79
|
+
::Thrift::Types::BOOL => "boolean",
|
80
|
+
::Thrift::Types::BYTE => "integer",
|
81
|
+
::Thrift::Types::I16 => "integer",
|
82
|
+
::Thrift::Types::I32 => "integer",
|
83
|
+
::Thrift::Types::I64 => "integer",
|
84
|
+
::Thrift::Types::DOUBLE => "double",
|
85
|
+
::Thrift::Types::STRING => "string"
|
86
|
+
}
|
87
|
+
end
|
88
|
+
|
89
|
+
def sample_values
|
90
|
+
{
|
91
|
+
::Thrift::Types::BOOL => true,
|
92
|
+
::Thrift::Types::BYTE => 1,
|
93
|
+
::Thrift::Types::I16 => 12,
|
94
|
+
::Thrift::Types::I32 => 123,
|
95
|
+
::Thrift::Types::I64 => 1234,
|
96
|
+
::Thrift::Types::DOUBLE => 12.34,
|
97
|
+
::Thrift::Types::STRING => "foo"
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
class ThriftGenerator
|
2
|
+
|
3
|
+
GEN_FOLDER = "gen-rb"
|
4
|
+
|
5
|
+
def initialize(module_name)
|
6
|
+
@base_dir = File.expand_path Dir.getwd
|
7
|
+
@module_name = module_name
|
8
|
+
@idl = "#{module_name.downcase}.thrift"
|
9
|
+
end
|
10
|
+
|
11
|
+
def generate_if_needed(&block)
|
12
|
+
# puts "Generating Ruby classes for all available thrift files in #{@base_dir}/#{GEN_FOLDER}"
|
13
|
+
Dir.glob("*.thrift").each { |f|
|
14
|
+
# puts "Compiling #{f}"
|
15
|
+
`thrift -r --gen rb #{f}`
|
16
|
+
}
|
17
|
+
|
18
|
+
service_file = @base_dir + "/#{GEN_FOLDER}/#{@module_name.downcase}.rb"
|
19
|
+
|
20
|
+
if !File.exists?(service_file)
|
21
|
+
puts "Could not generate code for module #{@module_name} - can't find .thrift file"
|
22
|
+
exit 1
|
23
|
+
end
|
24
|
+
|
25
|
+
load_module(service_file)
|
26
|
+
end
|
27
|
+
|
28
|
+
def load_module(service_file)
|
29
|
+
$LOAD_PATH.unshift(GEN_FOLDER)
|
30
|
+
require service_file
|
31
|
+
eval(@module_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
data/lib/type_adapter.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
class TypeAdapter
|
2
|
+
attr_accessor :adapted
|
3
|
+
|
4
|
+
def initialize(metadata, values)
|
5
|
+
case metadata[:type]
|
6
|
+
when ::Thrift::Types::STRUCT
|
7
|
+
fields = adapt(metadata[:class]::FIELDS.values, values)
|
8
|
+
self.adapted = metadata[:class].new(fields)
|
9
|
+
|
10
|
+
when ::Thrift::Types::LIST
|
11
|
+
self.adapted = values.map { |v|
|
12
|
+
adapt_field(metadata[:element], v)
|
13
|
+
}
|
14
|
+
|
15
|
+
when ::Thrift::Types::MAP
|
16
|
+
self.adapted = Hash[*values.map { |v|
|
17
|
+
[
|
18
|
+
adapt_field(metadata[:key], v),
|
19
|
+
adapt_field(metadata[:value], v)
|
20
|
+
]
|
21
|
+
}]
|
22
|
+
|
23
|
+
else
|
24
|
+
self.adapted = values
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def field_values
|
29
|
+
self.adapted.struct_fields.values.collect { |f| self.adapted.send(f[:name]) }
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def adapt(metadata, values)
|
36
|
+
adapted_params = {}
|
37
|
+
values.each_with_index { |v, i|
|
38
|
+
meta = metadata.find { |m| m[:name] == v.first.to_s }
|
39
|
+
adapted_params[v.first] = adapt_field(meta, v.last)
|
40
|
+
}
|
41
|
+
adapted_params
|
42
|
+
end
|
43
|
+
|
44
|
+
def adapt_field(metadata, value)
|
45
|
+
TypeAdapter.new(metadata, value).adapted
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: thrurl
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Herval Freire
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: thrift
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: Call Thrift services from the command line, as easily as a cURL
|
28
|
+
email: hervalfreire@gmail.com
|
29
|
+
executables:
|
30
|
+
- thrurl
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- README.md
|
35
|
+
- bin/thrurl
|
36
|
+
- lib/args_reader.rb
|
37
|
+
- lib/client.rb
|
38
|
+
- lib/method_call.rb
|
39
|
+
- lib/object_inspector.rb
|
40
|
+
- lib/thrift_generator.rb
|
41
|
+
- lib/type_adapter.rb
|
42
|
+
homepage: https://github.com/herval/thrurl
|
43
|
+
licenses:
|
44
|
+
- MIT
|
45
|
+
metadata: {}
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - '>='
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubyforge_project:
|
62
|
+
rubygems_version: 2.4.5
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Call Thrift services from the command line, as easily as a cURL
|
66
|
+
test_files: []
|