waithook 0.1 → 0.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 119debffa9104d9f4b3b0c1d2a943840a0bb1aaa
4
- data.tar.gz: ee89ce35a2bc6a13a3517f9c524b108d53c36d16
3
+ metadata.gz: 2d3624c5bebd573d8f2e33f66d96d19b93acc3a9
4
+ data.tar.gz: 25d37ca7b8c923b24c04425ce35f75ed997d9033
5
5
  SHA512:
6
- metadata.gz: e75927f077b2fa65d3a3af0855f314f68a4039996ae11ab641b1b2bdc58b43aaefa35196d2f6bf5728edde917729de5ed67880e58abcd85f3c57f6065a656e28
7
- data.tar.gz: 07000675d72ef66ab760dde7d039e4fcbafa36186d4f06ecc18e0912ad4b829d89dcfb2454c110480f190dce8017cade2acba2adddac6548c69c48768e9f29c0
6
+ metadata.gz: 32a2181e21aa618a589c64d7dbd07132ebdc1942c4169bfe26019b8061d05ea18a494d0ffb8e2da343accba7307427c9c69742ed3a05477897ff3d91dd3de847
7
+ data.tar.gz: 3a96f332074cbd7a5e8364bcebbc42de580ca8575f39ad17c19c28d0f012320c09cd36e7df2d18ad664bd43a3852a682ad79bb608fd3ca7796fb4cf090f930b3
data/Gemfile.lock CHANGED
@@ -2,7 +2,7 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  waithook (0.1)
5
- websocket (~> 1.2.3)
5
+ websocket (~> 1.2)
6
6
 
7
7
  GEM
8
8
  remote: https://rubygems.org/
data/bin/waithook CHANGED
@@ -2,3 +2,49 @@
2
2
 
3
3
  $:.push(File.expand_path("../../lib", __FILE__))
4
4
 
5
+ require 'optparse'
6
+
7
+ def help_message
8
+ [
9
+ "Waithook command line client.",
10
+ "",
11
+ "Example usage:",
12
+ " waithook waithook.herokuapp.com/my_path",
13
+ " waithook waithook.herokuapp.com/my_path --forward http://localhost:3000/notify"
14
+ ].join("\n")
15
+ end
16
+
17
+ options = {}
18
+
19
+ begin
20
+ OptionParser.new do |opts|
21
+ opts.banner = help_message + "\nOptions:"
22
+
23
+ opts.on("-v", "--[no-]verbose", "Run verbosely") do |v|
24
+ options[:verbose] = v
25
+ end
26
+ opts.on("-f URL", "--forward URL", "Forward to HTTP server") do |value|
27
+ options[:forward] = value
28
+ end
29
+ end.parse!
30
+ rescue OptionParser::InvalidOption => error
31
+ require 'waithook'
32
+ puts Waithook::WithColor.red(error.message)
33
+ puts "Please use --help to list of arguments"
34
+ exit 1
35
+ end
36
+
37
+ begin
38
+ require 'waithook'
39
+ if ARGV.first
40
+ Waithook::CLI.listen(ARGV.first, options)
41
+ else
42
+ puts Waithook::WithColor.red("URL is required\n")
43
+ puts help_message
44
+ exit 1
45
+ end
46
+ rescue Waithook::CLI::ArgError => error
47
+ puts Waithook::WithColor.red(error.message)
48
+ puts help_message
49
+ exit 1
50
+ end
@@ -0,0 +1,64 @@
1
+ class Waithook
2
+ module WithColor
3
+ extend self
4
+ def black(s); "\033[30m#{s}\033[0m" end
5
+ def red(s); "\033[31m#{s}\033[0m" end
6
+ def green(s); "\033[32m#{s}\033[0m" end
7
+ def brown(s); "\033[33m#{s}\033[0m" end
8
+ def blue(s); "\033[34m#{s}\033[0m" end
9
+ def magenta(s); "\033[35m#{s}\033[0m" end
10
+ def cyan(s); "\033[36m#{s}\033[0m" end
11
+ def gray(s); "\033[37m#{s}\033[0m" end
12
+ def yellow(s); "\033[93m#{s}\033[0m" end
13
+ def bold(s); "\e[1m#{s}\e[m" end
14
+ end
15
+
16
+ module CLI
17
+ class ArgError < ArgumentError; end
18
+
19
+ extend self
20
+
21
+ def listen(url, options)
22
+ puts "Run with options: #{options}" if options[:verbose]
23
+ unless url.start_with?('ws://', 'wss://')
24
+ url = 'wss://' + url
25
+ end
26
+
27
+ unless url =~ /\A#{URI::regexp(['ws', 'wss'])}\z/
28
+ raise ArgError, "#{url.inspect} is not a valid websocket URL"
29
+ end
30
+
31
+ uri = URI.parse(url)
32
+ port = uri.scheme == 'wss' ? 443 : uri.port
33
+ path = uri.path.start_with?('/') ? uri.path.sub(/^\//, '') : uri.path
34
+ logger_level = options[:verbose] ? 'trace' : 'warn'
35
+
36
+ puts WithColor.green("Connecting to #{url}")
37
+ waithook = Waithook.new(host: uri.host, port: port, path: path, logger_level: logger_level)
38
+ puts WithColor.green("Connected! Waiting to for message...") if waithook.client.wait_connected
39
+
40
+ while true
41
+ message = waithook.wait_message
42
+ puts message.message
43
+ if options[:forward]
44
+ Thread.new do
45
+ begin
46
+ forward_url = if options[:forward].start_with?('http://', 'https://')
47
+ forward_url
48
+ else
49
+ "http://#{options[:forward]}"
50
+ end
51
+
52
+ puts WithColor.brown("Sending as HTTP to #{forward_url}")
53
+ response = message.send_to(forward_url)
54
+ puts WithColor.brown("Reponse from #{forward_url} -> #{WithColor.bold("#{response.code} #{response.message}")}")
55
+ rescue => error
56
+ puts WithColor.red("#{error.message} (#{error.class})")
57
+ puts WithColor.red(error.backtrace.join("\n"))
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,49 @@
1
+ require 'logger'
2
+
3
+ # Just add trace level
4
+ class LoggerWithTrace < ::Logger
5
+ module Severity
6
+ include ::Logger::Severity
7
+ TRACE = -1
8
+ end
9
+
10
+ TRACE = Severity::TRACE
11
+
12
+ def trace(progname = nil, &block)
13
+ add(TRACE, nil, progname, &block)
14
+ end
15
+
16
+ def trace?
17
+ @level <= TRACE
18
+ end
19
+
20
+ def level=(severity)
21
+ if severity.is_a?(Integer)
22
+ @level = severity
23
+ else
24
+ @level = case severity.to_s.downcase
25
+ when 'trace'.freeze then TRACE
26
+ when 'debug'.freeze then DEBUG
27
+ when 'info'.freeze then INFO
28
+ when 'warn'.freeze then WARN
29
+ when 'error'.freeze then ERROR
30
+ when 'fatal'.freeze then FATAL
31
+ when 'unknown'.freeze then UNKNOWN
32
+ else
33
+ raise ArgumentError, "invalid log level: #{severity}"
34
+ end
35
+ end
36
+ end
37
+
38
+ def setup(options)
39
+ self.progname = options[:progname]
40
+ self.formatter = proc do |serverity, time, progname, msg|
41
+ msg.lines.map do |line|
42
+ "#{progname} :: #{line}"
43
+ end.join("") + "\n"
44
+ end
45
+ self.level = options[:level]
46
+
47
+ self
48
+ end
49
+ end
@@ -1,3 +1,3 @@
1
1
  class Waithook
2
- VERSION = "0.1"
2
+ VERSION = "0.2"
3
3
  end
@@ -1,6 +1,8 @@
1
1
  require 'socket'
2
- require 'logger'
3
2
  require 'websocket'
3
+ require 'stringio'
4
+
5
+ require_relative 'logger_with_trace'
4
6
 
5
7
  class Waithook
6
8
  class WebsocketClient
@@ -29,26 +31,24 @@ class Waithook
29
31
  @options = options
30
32
 
31
33
  @waiters = []
34
+ @connect_waiters = []
32
35
  @handshake_received = false
33
36
  @messages = Queue.new
34
37
  @is_open = false
35
38
 
36
- @output = options[:output] || STDOUT
39
+ @output = options[:output] || $stdout
37
40
 
38
41
  if options[:logger] === false
39
42
  @output = StringIO.new
40
43
  end
41
44
 
42
- if options[:logger]
45
+ if options[:logger] && options[:logger] != true
43
46
  @logger = options[:logger]
44
47
  else
45
- @logger = Logger.new(@output)
46
- @logger.progname = self.class.name
47
- @logger.formatter = proc do |serverity, time, progname, msg|
48
- msg.lines.map do |line|
49
- "#{progname} :: #{line}"
50
- end.join("") + "\n"
51
- end
48
+ @logger = LoggerWithTrace.new(@output).setup(
49
+ progname: self.class.name,
50
+ level: options[:logger_level] || :info
51
+ )
52
52
  end
53
53
  end
54
54
 
@@ -74,7 +74,7 @@ class Waithook
74
74
  @is_open = true
75
75
  @handshake = WebSocket::Handshake::Client.new(url: "ws://#{@host}/#{@path}")
76
76
 
77
- logger.debug "Sending handshake:\n#{@handshake}"
77
+ logger.trace "Sending handshake:\n#{@handshake}"
78
78
 
79
79
  @socket.print(@handshake)
80
80
  _start_parser!
@@ -94,10 +94,10 @@ class Waithook
94
94
  logger.debug "Start reading in thread"
95
95
  handshake_response = _wait_handshake_response
96
96
  @handshake << handshake_response
97
- logger.debug "Handshake received:\n #{handshake_response}"
97
+ logger.trace "Handshake received:\n #{handshake_response}"
98
98
 
99
99
  @frame_parser = WebSocket::Frame::Incoming::Client.new
100
- @handshake_received = true
100
+ _handshake_recieved!
101
101
  _wait_frames!
102
102
  rescue Object => error
103
103
  logger.error "#{error.class}: #{error.message}\n#{error.backtrace.join("\n")}"
@@ -135,6 +135,20 @@ class Waithook
135
135
  @messages.pop
136
136
  end
137
137
 
138
+ def wait_connected
139
+ return true if @handshake_received
140
+ waiter = Waiter.new
141
+ @connect_waiters << waiter
142
+ waiter.wait
143
+ end
144
+
145
+ def _handshake_recieved!
146
+ @handshake_received = true
147
+ while waiter = @connect_waiters.shift
148
+ waiter.notify(true)
149
+ end
150
+ end
151
+
138
152
  def _notify_waiters(type, payload)
139
153
  while waiter = @waiters.shift
140
154
  waiter.notify([type, payload])
@@ -144,11 +158,13 @@ class Waithook
144
158
  def _send_frame(type, payload = nil)
145
159
  wait_handshake!
146
160
  frame = WebSocket::Frame::Outgoing::Client.new(version: @handshake.version, data: payload, type: type)
147
- logger.debug "Sending :#{frame.type} #{payload ? "DATA: #{frame.data}" : "(no data)"}"
161
+ logger.trace "Sending :#{frame.type} #{payload ? "DATA: #{frame.data}" : "(no data)"}"
148
162
  @socket.write(frame.to_s)
149
163
  end
150
164
 
151
165
  def _process_frame(message)
166
+ logger.trace "Received :#{message.type} #{message.data ? "DATA: #{message.data}" : "(no data)"}"
167
+
152
168
  if message.type == :ping
153
169
  send_pong!
154
170
  end
data/lib/waithook.rb CHANGED
@@ -1,15 +1,20 @@
1
1
  require 'net/http'
2
+ require 'uri'
3
+ require 'timeout'
2
4
  require 'json'
5
+ require 'stringio'
3
6
 
7
+ require_relative 'waithook/logger_with_trace'
4
8
  require_relative 'waithook/websocket_client'
9
+ require_relative 'waithook/cli'
5
10
 
6
11
  class Waithook
7
12
 
8
13
  SERVER_HOST = "waithook.herokuapp.com"
9
14
  SERVER_PORT = 443
10
15
 
11
- def self.subscribe(path, options = {}, &block)
12
- instance = new(path, options)
16
+ def self.subscribe(options = {}, &block)
17
+ instance = new(options)
13
18
  if block
14
19
  instance.filter = block
15
20
  end
@@ -17,30 +22,52 @@ class Waithook
17
22
  instance
18
23
  end
19
24
 
25
+ def self.default_path
26
+ @default_path
27
+ end
28
+
29
+ def self.default_path=(value)
30
+ @default_path = value
31
+ end
32
+
20
33
  attr_accessor :filter
21
34
  attr_accessor :messages
22
35
  attr_reader :client
36
+ attr_reader :options
23
37
 
24
- def initialize(path, options = {})
25
- options = {
38
+ def initialize(options = {})
39
+ @options = {
26
40
  host: SERVER_HOST,
27
41
  port: SERVER_PORT,
28
- auto_connect: true
42
+ auto_connect: true,
43
+ path: self.class.default_path
29
44
  }.merge(options)
30
45
 
31
- @path = path
46
+ if @options[:path] == nil
47
+ raise ArgumentError, ":path is missing. Please add :path to options argument or set Waithook.default_path = 'foo'"
48
+ end
49
+
32
50
  @client = WebsocketClient.new(
33
- path: path,
34
- host: options[:host],
35
- port: options[:port],
36
- logger: options[:logger]
51
+ path: @options[:path],
52
+ host: @options[:host],
53
+ port: @options[:port],
54
+ logger: @options[:logger],
55
+ logger_level: @options[:logger_level],
56
+ output: @options[:output]
37
57
  )
38
58
 
39
59
  @messages = []
40
60
  @filter = nil
41
61
  @started = false
42
62
 
43
- connect! if options[:auto_connect]
63
+ connect! if @options[:auto_connect]
64
+ end
65
+
66
+ def logger
67
+ @logger ||= LoggerWithTrace.new(@options[:logger] ? $stdout : StringIO.new).setup(
68
+ progname: self.class.name,
69
+ level: @options[:logger_level] || :info
70
+ )
44
71
  end
45
72
 
46
73
  def connect!
@@ -54,50 +81,31 @@ class Waithook
54
81
  !!@started
55
82
  end
56
83
 
57
- def forward_to(url)
58
- webhook = wait_message
59
-
60
- uri = URI.parse(url)
61
- response = nil
62
-
63
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
64
- http_klass = case webhook.method
65
- when "GET" then Net::HTTP::Get
66
- when "POST" then Net::HTTP::Post
67
- when "PUT" then Net::HTTP::Put
68
- when "PATCH" then Net::HTTP::Patch
69
- when "HEAD" then Net::HTTP::Head
70
- when "DELETE" then Net::HTTP::Delete
71
- when "MOVE" then Net::HTTP::Move
72
- when "COPY" then Net::HTTP::Copy
73
- when "HEAD" then Net::HTTP::Head
74
- else Net::HTTP::Post
75
- end
76
-
77
- request = http_klass.new(uri)
78
- webhook.headers.each do |key, value|
79
- request[key] = value
80
- end
81
-
82
- if webhook.body
83
- request.body = webhook.body
84
- end
85
-
86
- response = http.request(request)
87
- end
88
-
89
- response
84
+ def forward_to(url, options = {})
85
+ webhook = wait_message(options)
86
+ webhook.send_to(url) unless webhook.nil?
90
87
  end
91
88
 
92
- def wait_message
93
- while true
94
- type, data = @client.wait_message
95
- webhook = Webhook.new(data)
96
- if @filter && @filter.call(webhook) || !@filter
97
- @messages << webhook
98
- return webhook
89
+ def wait_message(options = {})
90
+ raise_timeout_error = options.has_key?(:raise_on_timeout) ? options[:raise_on_timeout] : true
91
+ timeout = options[:timeout] || @options[:timeout] || 0
92
+
93
+ start_time = Time.new.to_f
94
+ Timeout.timeout(timeout) do
95
+ while true
96
+ type, data = @client.wait_message
97
+ webhook = Webhook.new(data)
98
+ if @filter && @filter.call(webhook) || !@filter
99
+ @messages << webhook
100
+ return webhook
101
+ end
99
102
  end
100
103
  end
104
+ rescue Timeout::Error => error
105
+ time_diff = (Time.now.to_f - start_time).round(3)
106
+ logger.error "#{error.class}: #{error.message} (after #{time_diff} seconds)"
107
+ raise error if raise_timeout_error
108
+ return nil
101
109
  end
102
110
 
103
111
  def close!
@@ -110,9 +118,11 @@ class Waithook
110
118
  attr_reader :headers
111
119
  attr_reader :body
112
120
  attr_reader :method
121
+ attr_reader :message
113
122
 
114
123
  def initialize(payload)
115
- data = JSON.parse(payload)
124
+ @message = payload
125
+ data = JSON.parse(@message)
116
126
  @url = data['url']
117
127
  @headers = data['headers']
118
128
  @body = data['body']
@@ -124,5 +134,38 @@ class Waithook
124
134
  @json_body ||= JSON.parse(@body)
125
135
  end
126
136
  end
137
+
138
+ def send_to(url)
139
+ uri = URI.parse(url)
140
+ response = nil
141
+
142
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
143
+ http_klass = case method
144
+ when "GET" then Net::HTTP::Get
145
+ when "POST" then Net::HTTP::Post
146
+ when "PUT" then Net::HTTP::Put
147
+ when "PATCH" then Net::HTTP::Patch
148
+ when "HEAD" then Net::HTTP::Head
149
+ when "DELETE" then Net::HTTP::Delete
150
+ when "MOVE" then Net::HTTP::Move
151
+ when "COPY" then Net::HTTP::Copy
152
+ when "HEAD" then Net::HTTP::Head
153
+ else Net::HTTP::Post
154
+ end
155
+
156
+ request = http_klass.new(uri)
157
+ headers.each do |key, value|
158
+ request[key] = value
159
+ end
160
+
161
+ if body
162
+ request.body = body
163
+ end
164
+
165
+ response = http.request(request)
166
+ end
167
+
168
+ response
169
+ end
127
170
  end
128
171
  end
@@ -11,7 +11,7 @@ describe "Waithook" do
11
11
  end
12
12
 
13
13
  def default_client(options = {})
14
- client = Waithook.subscribe(options[:path] || 'my-super-test', host: HOST, port: PORT, logger: false)
14
+ client = Waithook.subscribe({path: 'my-super-test', host: HOST, port: PORT, logger: false}.merge(options))
15
15
  @waithook_instances.push(client)
16
16
  client
17
17
  end
@@ -54,4 +54,46 @@ describe "Waithook" do
54
54
  assert_equal(message, webhook.json_body)
55
55
  assert_equal("POST", webhook.method)
56
56
  end
57
+
58
+ it "should have trace log level" do
59
+ out, err = capture_io do
60
+ default_client(logger_level: :trace, logger: true)
61
+ end
62
+
63
+ assert_includes(out, 'Sec-WebSocket-Version')
64
+ end
65
+
66
+ it "should be more quiet generally" do
67
+ out, err = capture_io do
68
+ default_client(logger: true)
69
+ end
70
+
71
+ refute_includes(out, 'Sec-WebSocket-Version')
72
+ end
73
+
74
+ it "wait_message should raise exception after timeout" do
75
+ assert_raises(Timeout::Error) do
76
+ default_client.wait_message(timeout: 0.1)
77
+ end
78
+ end
79
+
80
+ it "wait_message should return nil after timeout" do
81
+ waithook = default_client
82
+ assert_equal(nil, waithook.wait_message(timeout: 0.1, raise_on_timeout: false))
83
+ end
84
+
85
+ it "forward_to should raise exception after timeout" do
86
+ assert_raises(Timeout::Error) do
87
+ default_client.forward_to('', timeout: 0.1)
88
+ end
89
+ end
90
+
91
+ it "forward_to should return nil after timeout" do
92
+ out, err = capture_io do
93
+ waithook = default_client(logger: true)
94
+ assert_equal(nil, waithook.forward_to('', timeout: 0.1, raise_on_timeout: false))
95
+ end
96
+
97
+ assert_includes(out, "Timeout::Error: execution expired")
98
+ end
57
99
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: waithook
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '0.2'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pavel Evstigneev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-11-27 00:00:00.000000000 Z
11
+ date: 2016-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: websocket
@@ -42,6 +42,8 @@ files:
42
42
  - example/real_server_test.rb
43
43
  - example/waithook_example.rb
44
44
  - lib/waithook.rb
45
+ - lib/waithook/cli.rb
46
+ - lib/waithook/logger_with_trace.rb
45
47
  - lib/waithook/version.rb
46
48
  - lib/waithook/websocket_client.rb
47
49
  - tests/server_test.rb