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