uv-rays 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +53 -0
- data/Rakefile +22 -0
- data/lib/uv-rays/abstract_tokenizer.rb +100 -0
- data/lib/uv-rays/buffered_tokenizer.rb +97 -0
- data/lib/uv-rays/connection.rb +175 -0
- data/lib/uv-rays/http/encoding.rb +119 -0
- data/lib/uv-rays/http/request.rb +150 -0
- data/lib/uv-rays/http/response.rb +119 -0
- data/lib/uv-rays/http_endpoint.rb +253 -0
- data/lib/uv-rays/scheduler/cron.rb +386 -0
- data/lib/uv-rays/scheduler/time.rb +275 -0
- data/lib/uv-rays/scheduler.rb +319 -0
- data/lib/uv-rays/tcp_server.rb +48 -0
- data/lib/uv-rays/version.rb +3 -0
- data/lib/uv-rays.rb +96 -0
- data/spec/abstract_tokenizer_spec.rb +87 -0
- data/spec/buffered_tokenizer_spec.rb +253 -0
- data/spec/connection_spec.rb +137 -0
- data/spec/http_endpoint_spec.rb +279 -0
- data/spec/scheduler_cron_spec.rb +429 -0
- data/spec/scheduler_spec.rb +90 -0
- data/spec/scheduler_time_spec.rb +132 -0
- data/uv-rays.gemspec +31 -0
- metadata +217 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 3800c7d8ef446765e933dbf2f732dd037d4185f2
|
4
|
+
data.tar.gz: 8cb367445a00f73ad52e16ab4d26dac7453c1cd6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a4812d336b5366a07b4ee4f77c888cc323a24d9f700c93ea0c0335aa7e18bc08918328ddeabd47b858ad2e6773c78ce9ebba6c4cb4a468bc449697dfd4b41b63
|
7
|
+
data.tar.gz: 07285a027794ac7f3d4332595f936ea56000b251909665e4dd02edeb5e77a7b6f74fe901617883199fd07332329c48bd03ea152db7fe40ec25bfe5515e831d48
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 CoTag Media
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# uv-rays
|
2
|
+
|
3
|
+
[![Build Status](https://travis-ci.org/cotag/uv-rays.png?branch=master)](https://travis-ci.org/cotag/uv-rays)
|
4
|
+
|
5
|
+
UV-Rays was designed to eliminate the complexities of high-performance threaded network programming, allowing engineers to concentrate on their application logic.
|
6
|
+
|
7
|
+
|
8
|
+
## Core Features
|
9
|
+
|
10
|
+
1. TCP (and UDP) Connection abstractions
|
11
|
+
2. Advanced stream tokenization
|
12
|
+
3. Scheduled events (in, at, every, cron)
|
13
|
+
4. HTTP 1.1 compatible client support
|
14
|
+
|
15
|
+
This adds to the features already available from [Libuv](https://github.com/cotag/libuv) on which the gem is based
|
16
|
+
|
17
|
+
|
18
|
+
## Support
|
19
|
+
|
20
|
+
UV-Rays supports all platforms where ruby is available. Linux, OSX, BSD and Windows. MRI, jRuby and Rubinius.
|
21
|
+
|
22
|
+
Run `gem install uv-rays` to install
|
23
|
+
|
24
|
+
|
25
|
+
## Getting Started
|
26
|
+
|
27
|
+
Here's a fully-functional echo server written with UV-Rays:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
|
31
|
+
require 'uv-rays'
|
32
|
+
|
33
|
+
module EchoServer
|
34
|
+
def post_init
|
35
|
+
puts "-- someone connected to the echo server!"
|
36
|
+
end
|
37
|
+
|
38
|
+
def on_read data, *args
|
39
|
+
write ">>>you sent: #{data}"
|
40
|
+
close_connection if data =~ /quit/i
|
41
|
+
end
|
42
|
+
|
43
|
+
def on_close
|
44
|
+
puts "-- someone disconnected from the echo server!"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Note that this will block current thread.
|
49
|
+
Libuv::Loop.default.run {
|
50
|
+
UV.start_server "127.0.0.1", 8081, EchoServer
|
51
|
+
}
|
52
|
+
|
53
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rspec/core/rake_task' # testing framework
|
3
|
+
require 'yard' # yard documentation
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
# By default we don't run network tests
|
8
|
+
task :default => :limited_spec
|
9
|
+
RSpec::Core::RakeTask.new(:limited_spec) do |t|
|
10
|
+
# Exclude network tests
|
11
|
+
# t.rspec_opts = "--tag ~network"
|
12
|
+
end
|
13
|
+
RSpec::Core::RakeTask.new(:spec)
|
14
|
+
|
15
|
+
|
16
|
+
desc "Run all tests"
|
17
|
+
task :test => [:spec]
|
18
|
+
|
19
|
+
|
20
|
+
YARD::Rake::YardocTask.new do |t|
|
21
|
+
t.files = ['lib/**/*.rb', '-', 'ext/README.md', 'README.md']
|
22
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
|
2
|
+
module UvRays
|
3
|
+
|
4
|
+
# AbstractTokenizer is similar to BufferedTokernizer however should
|
5
|
+
# only be used when there is no delimiter to work with. It uses a
|
6
|
+
# callback based system for application level tokenization without
|
7
|
+
# the heavy lifting.
|
8
|
+
class AbstractTokenizer
|
9
|
+
|
10
|
+
attr_accessor :callback, :indicator, :size_limit, :verbose
|
11
|
+
|
12
|
+
# @param [Hash] options
|
13
|
+
def initialize(options)
|
14
|
+
@callback = options[:callback]
|
15
|
+
@indicator = options[:indicator]
|
16
|
+
@size_limit = options[:size_limit]
|
17
|
+
@verbose = options[:verbose] if @size_limit
|
18
|
+
|
19
|
+
raise ArgumentError, 'no indicator provided' unless @indicator
|
20
|
+
raise ArgumentError, 'no callback provided' unless @callback
|
21
|
+
|
22
|
+
@input = ''
|
23
|
+
end
|
24
|
+
|
25
|
+
# Extract takes an arbitrary string of input data and returns an array of
|
26
|
+
# tokenized entities using a message start indicator
|
27
|
+
#
|
28
|
+
# @example
|
29
|
+
#
|
30
|
+
# tokenizer.extract(data).
|
31
|
+
# map { |entity| Decode(entity) }.each { ... }
|
32
|
+
#
|
33
|
+
# @param [String] data
|
34
|
+
def extract(data)
|
35
|
+
@input << data
|
36
|
+
|
37
|
+
messages = @input.split(@indicator, -1)
|
38
|
+
if messages.length > 1
|
39
|
+
messages.shift # the first item will always be junk
|
40
|
+
last = messages.pop # the last item may require buffering
|
41
|
+
|
42
|
+
entities = []
|
43
|
+
messages.each do |msg|
|
44
|
+
entities << msg if @callback.call(msg)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Check if buffering is required
|
48
|
+
result = @callback.call(last)
|
49
|
+
if result
|
50
|
+
# Check for multi-byte indicator edge case
|
51
|
+
if result.is_a? Fixnum
|
52
|
+
entities << last[0...result]
|
53
|
+
@input = last[result..-1]
|
54
|
+
else
|
55
|
+
@input = ''
|
56
|
+
entities << last
|
57
|
+
end
|
58
|
+
else
|
59
|
+
# This will work with a regex
|
60
|
+
index = messages.last.nil? ? 0 : @input[0...-last.length].rindex(messages.last) + messages.last.length
|
61
|
+
indicator_val = @input[index...-last.length]
|
62
|
+
@input = indicator_val + last
|
63
|
+
end
|
64
|
+
else
|
65
|
+
@input = messages.pop
|
66
|
+
entities = messages
|
67
|
+
end
|
68
|
+
|
69
|
+
# Check to see if the buffer has exceeded capacity, if we're imposing a limit
|
70
|
+
if @size_limit && @input.size > @size_limit
|
71
|
+
if @indicator.respond_to?(:length) # check for regex
|
72
|
+
# save enough of the buffer that if one character of the indicator were
|
73
|
+
# missing we would match on next extract (very much an edge case) and
|
74
|
+
# best we can do with a full buffer.
|
75
|
+
@input = @input[-(@indicator.length - 1)..-1]
|
76
|
+
else
|
77
|
+
@input = ''
|
78
|
+
end
|
79
|
+
raise 'input buffer exceeded limit' if @verbose
|
80
|
+
end
|
81
|
+
|
82
|
+
return entities
|
83
|
+
end
|
84
|
+
|
85
|
+
# Flush the contents of the input buffer, i.e. return the input buffer even though
|
86
|
+
# a token has not yet been encountered.
|
87
|
+
#
|
88
|
+
# @return [String]
|
89
|
+
def flush
|
90
|
+
buffer = @input
|
91
|
+
@input = ''
|
92
|
+
buffer
|
93
|
+
end
|
94
|
+
|
95
|
+
# @return [Boolean]
|
96
|
+
def empty?
|
97
|
+
@input.empty?
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
|
2
|
+
# BufferedTokenizer takes a delimiter upon instantiation.
|
3
|
+
# It allows input to be spoon-fed from some outside source which receives
|
4
|
+
# arbitrary length datagrams which may-or-may-not contain the token by which
|
5
|
+
# entities are delimited.
|
6
|
+
#
|
7
|
+
# @example Using BufferedTokernizer to parse lines out of incoming data
|
8
|
+
#
|
9
|
+
# module LineBufferedConnection
|
10
|
+
# def receive_data(data)
|
11
|
+
# (@buffer ||= BufferedTokenizer.new(delimiter: "\n")).extract(data).each do |line|
|
12
|
+
# receive_line(line)
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
module UvRays
|
17
|
+
class BufferedTokenizer
|
18
|
+
|
19
|
+
attr_accessor :delimiter, :indicator, :size_limit, :verbose
|
20
|
+
|
21
|
+
# @param [Hash] options
|
22
|
+
def initialize(options)
|
23
|
+
@delimiter = options[:delimiter]
|
24
|
+
@indicator = options[:indicator]
|
25
|
+
@size_limit = options[:size_limit]
|
26
|
+
@verbose = options[:verbose] if @size_limit
|
27
|
+
|
28
|
+
raise ArgumentError, 'no delimiter provided' unless @delimiter
|
29
|
+
|
30
|
+
@input = ''
|
31
|
+
end
|
32
|
+
|
33
|
+
# Extract takes an arbitrary string of input data and returns an array of
|
34
|
+
# tokenized entities, provided there were any available to extract.
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
#
|
38
|
+
# tokenizer.extract(data).
|
39
|
+
# map { |entity| Decode(entity) }.each { ... }
|
40
|
+
#
|
41
|
+
# @param [String] data
|
42
|
+
def extract(data)
|
43
|
+
@input << data
|
44
|
+
|
45
|
+
# Extract token-delimited entities from the input string with the split command.
|
46
|
+
# There's a bit of craftiness here with the -1 parameter. Normally split would
|
47
|
+
# behave no differently regardless of if the token lies at the very end of the
|
48
|
+
# input buffer or not (i.e. a literal edge case) Specifying -1 forces split to
|
49
|
+
# return "" in this case, meaning that the last entry in the list represents a
|
50
|
+
# new segment of data where the token has not been encountered
|
51
|
+
messages = @input.split(@delimiter, -1)
|
52
|
+
|
53
|
+
if @indicator
|
54
|
+
@input = messages.pop
|
55
|
+
entities = []
|
56
|
+
messages.each do |msg|
|
57
|
+
res = msg.split(@indicator, -1)
|
58
|
+
entities << res.last if res.length > 1
|
59
|
+
end
|
60
|
+
else
|
61
|
+
entities = messages
|
62
|
+
@input = entities.pop
|
63
|
+
end
|
64
|
+
|
65
|
+
# Check to see if the buffer has exceeded capacity, if we're imposing a limit
|
66
|
+
if @size_limit && @input.size > @size_limit
|
67
|
+
if @indicator && @indicator.respond_to?(:length) # check for regex
|
68
|
+
# save enough of the buffer that if one character of the indicator were
|
69
|
+
# missing we would match on next extract (very much an edge case) and
|
70
|
+
# best we can do with a full buffer. If we were one char short of a
|
71
|
+
# delimiter it would be unfortunate
|
72
|
+
@input = @input[-(@indicator.length - 1)..-1]
|
73
|
+
else
|
74
|
+
@input = ''
|
75
|
+
end
|
76
|
+
raise 'input buffer exceeded limit' if @verbose
|
77
|
+
end
|
78
|
+
|
79
|
+
return entities
|
80
|
+
end
|
81
|
+
|
82
|
+
# Flush the contents of the input buffer, i.e. return the input buffer even though
|
83
|
+
# a token has not yet been encountered.
|
84
|
+
#
|
85
|
+
# @return [String]
|
86
|
+
def flush
|
87
|
+
buffer = @input
|
88
|
+
@input = ''
|
89
|
+
buffer
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Boolean]
|
93
|
+
def empty?
|
94
|
+
@input.empty?
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
|
2
|
+
module UvRays
|
3
|
+
def self.try_connect(tcp, handler, server, port)
|
4
|
+
if IPAddress.valid? server
|
5
|
+
tcp.finally handler.method(:on_close)
|
6
|
+
tcp.progress handler.method(:on_read)
|
7
|
+
tcp.connect server, port do
|
8
|
+
tcp.enable_nodelay
|
9
|
+
tcp.start_tls(handler.using_tls) unless handler.using_tls == false
|
10
|
+
|
11
|
+
# on_connect could call use_tls so must come after start_tls
|
12
|
+
handler.on_connect(tcp)
|
13
|
+
tcp.start_read
|
14
|
+
end
|
15
|
+
else
|
16
|
+
tcp.loop.lookup(server).then(
|
17
|
+
proc { |result|
|
18
|
+
UvRays.try_connect(tcp, handler, result[0][0], port)
|
19
|
+
},
|
20
|
+
proc { |failure|
|
21
|
+
# TODO:: Log error on loop
|
22
|
+
handler.on_close
|
23
|
+
}
|
24
|
+
)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
# @abstract
|
30
|
+
class Connection
|
31
|
+
attr_reader :using_tls
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@send_queue = []
|
35
|
+
@paused = false
|
36
|
+
@using_tls = false
|
37
|
+
end
|
38
|
+
|
39
|
+
def pause
|
40
|
+
@paused = true
|
41
|
+
@transport.stop_read
|
42
|
+
end
|
43
|
+
|
44
|
+
def paused?
|
45
|
+
@paused
|
46
|
+
end
|
47
|
+
|
48
|
+
def resume
|
49
|
+
@paused = false
|
50
|
+
@transport.start_read
|
51
|
+
end
|
52
|
+
|
53
|
+
# Compatible with TCP
|
54
|
+
def close_connection(*args)
|
55
|
+
@transport.close
|
56
|
+
end
|
57
|
+
|
58
|
+
def on_read(data, *args) # user to define
|
59
|
+
end
|
60
|
+
|
61
|
+
def post_init(*args)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
class TcpConnection < Connection
|
66
|
+
def write(data)
|
67
|
+
@transport.write(data)
|
68
|
+
end
|
69
|
+
|
70
|
+
def close_connection(after_writing = false)
|
71
|
+
if after_writing
|
72
|
+
@transport.shutdown
|
73
|
+
else
|
74
|
+
@transport.close
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def stream_file(filename)
|
79
|
+
end
|
80
|
+
|
81
|
+
def on_connect(transport) # user to define
|
82
|
+
end
|
83
|
+
|
84
|
+
def on_close # user to define
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class InboundConnection < TcpConnection
|
89
|
+
def initialize(tcp)
|
90
|
+
super()
|
91
|
+
|
92
|
+
@loop = tcp.loop
|
93
|
+
@transport = tcp
|
94
|
+
@transport.finally method(:on_close)
|
95
|
+
@transport.progress method(:on_read)
|
96
|
+
end
|
97
|
+
|
98
|
+
def use_tls(args = {})
|
99
|
+
args[:server] = true
|
100
|
+
|
101
|
+
if @transport.connected
|
102
|
+
@transport.start_tls(args)
|
103
|
+
else
|
104
|
+
@using_tls = args
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class OutboundConnection < TcpConnection
|
110
|
+
|
111
|
+
def initialize(server, port)
|
112
|
+
super()
|
113
|
+
|
114
|
+
@loop = Libuv::Loop.current
|
115
|
+
@server = server
|
116
|
+
@port = port
|
117
|
+
@transport = @loop.tcp
|
118
|
+
|
119
|
+
::UvRays.try_connect(@transport, self, @server, @port)
|
120
|
+
end
|
121
|
+
|
122
|
+
def use_tls(args = {})
|
123
|
+
args.delete(:server)
|
124
|
+
|
125
|
+
if @transport.connected
|
126
|
+
@transport.start_tls(args)
|
127
|
+
else
|
128
|
+
@using_tls = args
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def reconnect(server = nil, port = nil)
|
133
|
+
@loop = Libuv::Loop.current || @loop
|
134
|
+
|
135
|
+
@transport = @loop.tcp
|
136
|
+
@server = server || @server
|
137
|
+
@port = port || @port
|
138
|
+
|
139
|
+
::UvRays.try_connect(@transport, self, @server, @port)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class DatagramConnection < Connection
|
144
|
+
def initialize(server = nil, port = nil)
|
145
|
+
super()
|
146
|
+
|
147
|
+
@loop = Libuv::Loop.current
|
148
|
+
@transport = @loop.udp
|
149
|
+
@transport.progress method(:on_read)
|
150
|
+
|
151
|
+
if not server.nil?
|
152
|
+
server = '127.0.0.1' if server == 'localhost'
|
153
|
+
if IPAddress.valid? server
|
154
|
+
@transport.bind(server, port)
|
155
|
+
else
|
156
|
+
raise ArgumentError, "Invalid server address #{server}"
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
@transport.start_read
|
161
|
+
end
|
162
|
+
|
163
|
+
def send_datagram(data, recipient_address, recipient_port)
|
164
|
+
if IPAddress.valid? recipient_address
|
165
|
+
@transport.send recipient_address, recipient_port, data
|
166
|
+
else
|
167
|
+
# Async DNS resolution
|
168
|
+
# Note:: send here will chain the promise
|
169
|
+
tcp.loop.lookup(server).then do |result|
|
170
|
+
@transport.send result[0][0], recipient_port, data
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module UvRays
|
2
|
+
module Http
|
3
|
+
module Encoding
|
4
|
+
HTTP_REQUEST_HEADER="%s %s HTTP/1.1\r\n"
|
5
|
+
FIELD_ENCODING = "%s: %s\r\n"
|
6
|
+
|
7
|
+
def escape(s)
|
8
|
+
if defined?(EscapeUtils)
|
9
|
+
EscapeUtils.escape_url(s.to_s)
|
10
|
+
else
|
11
|
+
s.to_s.gsub(/([^a-zA-Z0-9_.-]+)/) {
|
12
|
+
'%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def unescape(s)
|
18
|
+
if defined?(EscapeUtils)
|
19
|
+
EscapeUtils.unescape_url(s.to_s)
|
20
|
+
else
|
21
|
+
s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/) {
|
22
|
+
[$1.delete('%')].pack('H*')
|
23
|
+
}
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Map all header keys to a downcased string version
|
28
|
+
def munge_header_keys(head)
|
29
|
+
head.inject({}) { |h, (k, v)| h[k.to_s.downcase] = v; h }
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def encode_request(method, uri, query)
|
34
|
+
query = encode_query(uri, query)
|
35
|
+
|
36
|
+
HTTP_REQUEST_HEADER % [method.to_s.upcase, query]
|
37
|
+
end
|
38
|
+
|
39
|
+
def encode_query(uri, query)
|
40
|
+
encoded_query = if query.kind_of?(Hash)
|
41
|
+
query.map { |k, v| encode_param(k, v) }.join('&')
|
42
|
+
else
|
43
|
+
query.to_s
|
44
|
+
end
|
45
|
+
encoded_query.to_s.empty? ? uri : "#{uri}?#{encoded_query}"
|
46
|
+
end
|
47
|
+
|
48
|
+
# URL encodes query parameters:
|
49
|
+
# single k=v, or a URL encoded array, if v is an array of values
|
50
|
+
def encode_param(k, v)
|
51
|
+
if v.is_a?(Array)
|
52
|
+
v.map { |e| escape(k) + "[]=" + escape(e) }.join("&")
|
53
|
+
else
|
54
|
+
escape(k) + "=" + escape(v)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def form_encode_body(obj)
|
59
|
+
pairs = []
|
60
|
+
recursive = Proc.new do |h, prefix|
|
61
|
+
h.each do |k,v|
|
62
|
+
key = prefix == '' ? escape(k) : "#{prefix}[#{escape(k)}]"
|
63
|
+
|
64
|
+
if v.is_a? Array
|
65
|
+
nh = Hash.new
|
66
|
+
v.size.times { |t| nh[t] = v[t] }
|
67
|
+
recursive.call(nh, key)
|
68
|
+
|
69
|
+
elsif v.is_a? Hash
|
70
|
+
recursive.call(v, key)
|
71
|
+
else
|
72
|
+
pairs << "#{key}=#{escape(v)}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
recursive.call(obj, '')
|
78
|
+
return pairs.join('&')
|
79
|
+
end
|
80
|
+
|
81
|
+
# Encode a field in an HTTP header
|
82
|
+
def encode_field(k, v)
|
83
|
+
FIELD_ENCODING % [k, v]
|
84
|
+
end
|
85
|
+
|
86
|
+
# Encode basic auth in an HTTP header
|
87
|
+
# In: Array ([user, pass]) - for basic auth
|
88
|
+
# String - custom auth string (OAuth, etc)
|
89
|
+
def encode_auth(k,v)
|
90
|
+
if v.is_a? Array
|
91
|
+
FIELD_ENCODING % [k, ["Basic", Base64.encode64(v.join(":")).split.join].join(" ")]
|
92
|
+
else
|
93
|
+
encode_field(k,v)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def encode_headers(head)
|
98
|
+
head.inject('') do |result, (key, value)|
|
99
|
+
# Munge keys from foo-bar-baz to Foo-Bar-Baz
|
100
|
+
key = key.split('-').map { |k| k.to_s.capitalize }.join('-')
|
101
|
+
result << case key
|
102
|
+
when 'Authorization', 'Proxy-Authorization'
|
103
|
+
encode_auth(key, value)
|
104
|
+
else
|
105
|
+
encode_field(key, value)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def encode_cookie(cookie)
|
111
|
+
if cookie.is_a? Hash
|
112
|
+
cookie.inject('') { |result, (k, v)| result << encode_param(k, v) + ";" }
|
113
|
+
else
|
114
|
+
cookie
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|