fire-and-forget 0.1.0
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.
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +80 -0
- data/Rakefile +1 -0
- data/fire-and-forget.gemspec +25 -0
- data/lib/fire/forget.rb +34 -0
- data/lib/fire/request.rb +96 -0
- data/lib/fire/version.rb +3 -0
- data/spec/request_spec.rb +89 -0
- data/spec/spec_helper.rb +2 -0
- metadata +133 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Matt Aimonetti
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
# Fire and Forget HTTP client
|
2
|
+
|
3
|
+
Using this gem is probably a very bad idea in at least 98% of the cases
|
4
|
+
I can think of. Think of a it as a HTTP client that doesn't care about
|
5
|
+
the response and doesn't care about basically anything besides sending
|
6
|
+
its payload to a server. An alternative name for this client is
|
7
|
+
"scumbag-HTTP-client".
|
8
|
+
|
9
|
+
So, unless you are really sure you want to use this lib, you're probably
|
10
|
+
better off looking at the hundreds other Ruby HTTP libraries.
|
11
|
+
|
12
|
+
## Installation
|
13
|
+
|
14
|
+
Add this line to your application's Gemfile:
|
15
|
+
|
16
|
+
gem 'fire-and-forget', :require => 'fire/forget'
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install fire-and-forget
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
FAF.post "http://example.com", {:foo => 'bar'}, {"X-Custom" => true}
|
30
|
+
```
|
31
|
+
In the example above, the request will be sent as a JSON request and the
|
32
|
+
body (a Ruby hash) will be converted to json if `to_json` is available on
|
33
|
+
the object (otherwise `to_s` will be used).
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
FAF.post "http://example.com", "{\"language\": {\"created_at\": \"2010/11/23 19:47:05 +0000\",\"updated_at\": \"2010/11/23 19:47:05 +0000\",\"active\": true, \"code\": \"en\", \"id\":37}}"
|
37
|
+
```
|
38
|
+
|
39
|
+
Would send the request passing the JSON string as the body, strings
|
40
|
+
aren't converted when passed as the request body.
|
41
|
+
|
42
|
+
Sometimes, you might want to debug the response or potentially slow down
|
43
|
+
the application code. You can pass a block to do that:
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
FAF.post "http://example.com", {:foo => 'bar'} do |socket|
|
47
|
+
sleep(0.02)
|
48
|
+
end
|
49
|
+
```
|
50
|
+
|
51
|
+
or
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
FAF.post "http://example.com", {:foo => 'bar'} do |socket|
|
55
|
+
while output = socket.gets
|
56
|
+
print output
|
57
|
+
end
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
This will not "forget" about the response, but instead wait for data to
|
62
|
+
come down the socket so we can print.
|
63
|
+
|
64
|
+
Currently, FAF only supports basic options, no
|
65
|
+
authentication unless you pass all the details via the headers.
|
66
|
+
|
67
|
+
Once the request is sent, the socket is closed which might or might not
|
68
|
+
please the server receiving the request (you might want to use a
|
69
|
+
block/sleep
|
70
|
+
to slow down the closing of the socket if your proxy/cache doesn't like
|
71
|
+
that FAF closes the connection right away.
|
72
|
+
|
73
|
+
|
74
|
+
## Contributing
|
75
|
+
|
76
|
+
1. Fork it
|
77
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
78
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
79
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
80
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'fire/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "fire-and-forget"
|
8
|
+
spec.version = FireAndForget::VERSION
|
9
|
+
spec.authors = ["Matt Aimonetti"]
|
10
|
+
spec.email = ["mattaimonetti@gmail.com"]
|
11
|
+
spec.description = %q{Allows developers to fire HTTP requests and not worry about the response (only the initial connection is verified).}
|
12
|
+
spec.summary = %q{For whenever you need to push data to an endpoint but don't care about the response (or its status).}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", ">= 1.2"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
spec.add_runtime_dependency "json"
|
25
|
+
end
|
data/lib/fire/forget.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require "fire/version"
|
2
|
+
require 'fire/request'
|
3
|
+
|
4
|
+
module FireAndForget
|
5
|
+
def self.get(url, headers={}, &block)
|
6
|
+
Request.new(:method => :get, :url => url, :headers => headers, :callback => block).execute
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.post(url, payload, headers={}, &block)
|
10
|
+
Request.new(:method => :post, :url => url, :payload => payload, :headers => headers, :callback => block).execute
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.patch(url, payload, headers={}, &block)
|
14
|
+
Request.new(:method => :patch, :url => url, :payload => payload, :headers => headers, :callback => block).execute
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.put(url, payload, headers={}, &block)
|
18
|
+
Request.new(:method => :put, :url => url, :payload => payload, :headers => headers, :callback => block).execute
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.delete(url, headers={}, &block)
|
22
|
+
Request.new(:method => :delete, :url => url, :headers => headers, :callback => block).execute
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.head(url, headers={}, &block)
|
26
|
+
Request.new(:method => :head, :url => url, :headers => headers, :callback => block).execute
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.options(url, headers={}, &block)
|
30
|
+
Request.new(:method => :options, :url => url, :headers => headers, :callback => block).execute
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
FAF = FireAndForget
|
data/lib/fire/request.rb
ADDED
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'socket'
|
3
|
+
require 'uri'
|
4
|
+
begin
|
5
|
+
require 'json'
|
6
|
+
rescue LoadError
|
7
|
+
puts "The json lib wasn't available, payload objects won't be automatically converted to json."
|
8
|
+
end
|
9
|
+
|
10
|
+
module FireAndForget
|
11
|
+
class Request
|
12
|
+
attr_reader :method, :url, :content_type, :payload, :headers, :callback
|
13
|
+
|
14
|
+
def initialize(args)
|
15
|
+
@method = args[:method] || raise(ArgumentError.new("must pass :method"))
|
16
|
+
@headers = args[:headers] || {}
|
17
|
+
raise ArgumentError, ":headers should be nil or a Hash" unless (@headers.respond_to?(:keys) && @headers.respond_to?(:each))
|
18
|
+
@content_type = args[:content_type] || headers['Content-Type'] || "application/json"
|
19
|
+
@headers = {'Content-Type' => @content_type}.merge(headers)
|
20
|
+
if args[:url]
|
21
|
+
@url = args[:url]
|
22
|
+
else
|
23
|
+
raise ArgumentError, "must pass :url"
|
24
|
+
end
|
25
|
+
@callback = args[:callback]
|
26
|
+
@payload = args[:payload]
|
27
|
+
@args = args
|
28
|
+
end
|
29
|
+
|
30
|
+
def execute
|
31
|
+
uri = URI.parse(url)
|
32
|
+
req = []
|
33
|
+
req << "#{method.respond_to?(:upcase) ? method.upcase : method.to_s.upcase} #{uri.request_uri} HTTP/1.0"
|
34
|
+
if uri.port != 80
|
35
|
+
req << "Host: #{uri.host}:#{uri.port}"
|
36
|
+
else
|
37
|
+
req << "Host: #{uri.host}"
|
38
|
+
end
|
39
|
+
req << "User-Agent: FAF #{FireAndForget::VERSION}"
|
40
|
+
req << "Accept: */*"
|
41
|
+
processed_headers.each do |part|
|
42
|
+
req << part
|
43
|
+
end
|
44
|
+
req << "Content-Length: #{body_length}"
|
45
|
+
|
46
|
+
socket = TCPSocket.open(uri.host, uri.port)
|
47
|
+
req.each do |req_part|
|
48
|
+
socket.puts(req_part + "\r\n")
|
49
|
+
end
|
50
|
+
#puts (req << body).inspect
|
51
|
+
socket.puts "\r\n"
|
52
|
+
socket.puts body
|
53
|
+
# For debugging purposes you can pass a block to read the socket or sleep for a bit.
|
54
|
+
# You can also set a callback when creating the instance and it will be called passing the socket.
|
55
|
+
if callback
|
56
|
+
callback.call(socket)
|
57
|
+
elsif block_given?
|
58
|
+
yield(socket)
|
59
|
+
end
|
60
|
+
ensure
|
61
|
+
socket.close if socket
|
62
|
+
end
|
63
|
+
|
64
|
+
def body
|
65
|
+
if @body
|
66
|
+
@body
|
67
|
+
else
|
68
|
+
if payload.nil?
|
69
|
+
@body = ""
|
70
|
+
elsif payload.is_a?(String)
|
71
|
+
@body = payload
|
72
|
+
elsif payload.respond_to?(:to_json)
|
73
|
+
@body = payload.to_json
|
74
|
+
else
|
75
|
+
@body = ""
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def body_length
|
81
|
+
body ? body.bytesize : 0
|
82
|
+
end
|
83
|
+
|
84
|
+
def processed_headers
|
85
|
+
chunks = []
|
86
|
+
headers.each do |key, value|
|
87
|
+
if key.is_a? Symbol
|
88
|
+
key = key.to_s.split(/_/).map { |w| w.capitalize }.join('-')
|
89
|
+
end
|
90
|
+
chunks << "#{key}: #{value.to_s}"
|
91
|
+
end
|
92
|
+
chunks
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
end
|
data/lib/fire/version.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FireAndForget::Request do
|
4
|
+
describe "#initialize" do
|
5
|
+
|
6
|
+
def new_req(args)
|
7
|
+
FireAndForget::Request.new(args)
|
8
|
+
end
|
9
|
+
|
10
|
+
def min_valid_args
|
11
|
+
{:method => :get, :url => "http://matt.aimonetti.net/"}
|
12
|
+
end
|
13
|
+
|
14
|
+
it "requires a method argument" do
|
15
|
+
lambda{ new_req({}) }.should raise_error(ArgumentError, "must pass :method")
|
16
|
+
end
|
17
|
+
|
18
|
+
it "requires a url argument" do
|
19
|
+
lambda{ new_req(:method => :get) }.should raise_error(ArgumentError, "must pass :url")
|
20
|
+
end
|
21
|
+
|
22
|
+
it "requires both a method and url" do
|
23
|
+
lambda{ new_req(:method => :get, :url => "http://google.com") }.should_not raise_error
|
24
|
+
end
|
25
|
+
|
26
|
+
it "takes custom headers" do
|
27
|
+
req = new_req(min_valid_args.merge({:headers => {"X-Request-Id" => 42}}))
|
28
|
+
req.headers["X-Request-Id"].should eq(42)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "has default headers" do
|
32
|
+
req = new_req(min_valid_args)
|
33
|
+
req.headers.should_not be_nil
|
34
|
+
req.headers.should_not be_empty
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "content type" do
|
38
|
+
|
39
|
+
it "defaults to json" do
|
40
|
+
req = new_req(min_valid_args)
|
41
|
+
req.headers['Content-Type'].should eq("application/json")
|
42
|
+
end
|
43
|
+
|
44
|
+
it "can be set via argument" do
|
45
|
+
req = new_req(min_valid_args.merge(:content_type => 'text/xml'))
|
46
|
+
req.content_type.should eq('text/xml')
|
47
|
+
req.headers['Content-Type'].should eq('text/xml')
|
48
|
+
end
|
49
|
+
|
50
|
+
it "can be set via headers" do
|
51
|
+
req = new_req(min_valid_args.merge(:headers => {'Content-Type' => 'text/xml'}))
|
52
|
+
req.content_type.should eq('text/xml')
|
53
|
+
req.headers['Content-Type'].should eq('text/xml')
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "body" do
|
59
|
+
|
60
|
+
it "is extracted from the payload as-is, if passed as a string" do
|
61
|
+
text = "this is a test"
|
62
|
+
req = new_req(min_valid_args.merge(:payload => text))
|
63
|
+
req.body.should eq(text)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "becomes an empty string if passed as nil" do
|
67
|
+
req = new_req(min_valid_args)
|
68
|
+
req.body.should eq("")
|
69
|
+
end
|
70
|
+
|
71
|
+
it "is converted to json if it's not a string or nil" do
|
72
|
+
payload = {:foo => :bar}
|
73
|
+
req = new_req(min_valid_args.merge(:payload => payload))
|
74
|
+
req.body.should eq(payload.to_json)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe "converted headers" do
|
79
|
+
it "are represented as an array of strings" do
|
80
|
+
req = new_req(min_valid_args)
|
81
|
+
req.processed_headers.each do |header_line|
|
82
|
+
header_line.is_a?(String).should be_true
|
83
|
+
header_line.should match(/.*?:\s.+/)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fire-and-forget
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Matt Aimonetti
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-02-26 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.2'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.2'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: json
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
description: Allows developers to fire HTTP requests and not worry about the response
|
79
|
+
(only the initial connection is verified).
|
80
|
+
email:
|
81
|
+
- mattaimonetti@gmail.com
|
82
|
+
executables: []
|
83
|
+
extensions: []
|
84
|
+
extra_rdoc_files: []
|
85
|
+
files:
|
86
|
+
- .gitignore
|
87
|
+
- .rspec
|
88
|
+
- CHANGELOG.md
|
89
|
+
- Gemfile
|
90
|
+
- LICENSE.txt
|
91
|
+
- README.md
|
92
|
+
- Rakefile
|
93
|
+
- fire-and-forget.gemspec
|
94
|
+
- lib/fire/forget.rb
|
95
|
+
- lib/fire/request.rb
|
96
|
+
- lib/fire/version.rb
|
97
|
+
- spec/request_spec.rb
|
98
|
+
- spec/spec_helper.rb
|
99
|
+
homepage: ''
|
100
|
+
licenses:
|
101
|
+
- MIT
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
none: false
|
108
|
+
requirements:
|
109
|
+
- - ! '>='
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
segments:
|
113
|
+
- 0
|
114
|
+
hash: 3818509771490622433
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
+
none: false
|
117
|
+
requirements:
|
118
|
+
- - ! '>='
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
version: '0'
|
121
|
+
segments:
|
122
|
+
- 0
|
123
|
+
hash: 3818509771490622433
|
124
|
+
requirements: []
|
125
|
+
rubyforge_project:
|
126
|
+
rubygems_version: 1.8.24
|
127
|
+
signing_key:
|
128
|
+
specification_version: 3
|
129
|
+
summary: For whenever you need to push data to an endpoint but don't care about the
|
130
|
+
response (or its status).
|
131
|
+
test_files:
|
132
|
+
- spec/request_spec.rb
|
133
|
+
- spec/spec_helper.rb
|