thrurl 0.0.3
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.
- 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: []
|