thrurl 0.0.3

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