dialers 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rbenv-vars-example +4 -0
- data/.rspec +5 -0
- data/.rubocop.yml +89 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/Guardfile +16 -0
- data/LICENSE.txt +21 -0
- data/NOTES.md +60 -0
- data/README.md +143 -0
- data/Rakefile +1 -0
- data/bin/_guard-core +16 -0
- data/bin/console +7 -0
- data/bin/guard +16 -0
- data/bin/rspec +16 -0
- data/bin/setup +7 -0
- data/dialers.gemspec +37 -0
- data/examples/github/api.rb +19 -0
- data/examples/github/api_caller.rb +27 -0
- data/examples/github/usage.rb +12 -0
- data/examples/twitter/api.rb +27 -0
- data/examples/twitter/api_caller.rb +43 -0
- data/examples/twitter/usage.rb +16 -0
- data/lib/dialers.rb +13 -0
- data/lib/dialers/assign_attributes.rb +20 -0
- data/lib/dialers/caller.rb +128 -0
- data/lib/dialers/errors.rb +77 -0
- data/lib/dialers/request_options.rb +5 -0
- data/lib/dialers/short_circuit.rb +18 -0
- data/lib/dialers/short_circuits_collection.rb +27 -0
- data/lib/dialers/status.rb +82 -0
- data/lib/dialers/transformable.rb +84 -0
- data/lib/dialers/version.rb +3 -0
- data/lib/dialers/wrapper.rb +34 -0
- metadata +260 -0
data/dialers.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "dialers/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "dialers"
|
8
|
+
spec.version = Dialers::VERSION
|
9
|
+
spec.authors = ["juliogarciag"]
|
10
|
+
spec.email = ["julioggonz@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = "Api Wrappers for Ruby"
|
13
|
+
spec.description = "Api Wrappers for Ruby"
|
14
|
+
spec.homepage = "https://github.com/platanus/dialers"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_dependency "faraday", "~> 0.9"
|
25
|
+
spec.add_dependency "faraday_middleware", "~> 0.9"
|
26
|
+
spec.add_dependency "faraday-conductivity", "~> 0.3"
|
27
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
28
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
29
|
+
spec.add_development_dependency "rspec", "~> 3.3"
|
30
|
+
spec.add_development_dependency "pry", "~> 0.10"
|
31
|
+
spec.add_development_dependency "guard", "~> 2.13"
|
32
|
+
spec.add_development_dependency "guard-rspec", "~> 4.6"
|
33
|
+
spec.add_development_dependency "rspec-nc", "~> 0.2"
|
34
|
+
spec.add_development_dependency "rspec-legacy_formatters", "~> 1.0"
|
35
|
+
spec.add_development_dependency "simple_oauth", "~> 0.3"
|
36
|
+
spec.add_development_dependency "patron", "~> 0.4"
|
37
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative "./api_caller"
|
2
|
+
|
3
|
+
module Github
|
4
|
+
class Repository
|
5
|
+
attr_accessor :id, :name, :description, :language
|
6
|
+
|
7
|
+
def to_s
|
8
|
+
"#{id} : #{name} : #{description} : #{language}"
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class Api < Dialers::Wrapper
|
13
|
+
api_caller { ApiCaller.new }
|
14
|
+
|
15
|
+
def user_repos(username)
|
16
|
+
api_caller.get("users/#{username}/repos").transform_to_many(Repository)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "dialers"
|
2
|
+
|
3
|
+
module Github
|
4
|
+
class ApiCaller < Dialers::Caller
|
5
|
+
TIMEOUT_IN_SECONDS = 5
|
6
|
+
GITHUB_API_URL = "https://api.github.com"
|
7
|
+
|
8
|
+
setup_api(url: GITHUB_API_URL) do |faraday|
|
9
|
+
faraday.request :json
|
10
|
+
faraday.request :request_headers, accept: "application/vnd.github.v3+json"
|
11
|
+
faraday.response :json
|
12
|
+
faraday.adapter :net_http
|
13
|
+
faraday.options.timeout = TIMEOUT_IN_SECONDS
|
14
|
+
faraday.options.open_timeout = TIMEOUT_IN_SECONDS
|
15
|
+
end
|
16
|
+
|
17
|
+
short_circuits.add(
|
18
|
+
if: -> (response) { Dialers::Status.new(response.status).server_error? },
|
19
|
+
do: -> (response) { fail Dialers::ServerError.new(response) }
|
20
|
+
)
|
21
|
+
|
22
|
+
short_circuits.add(
|
23
|
+
if: -> (response) { Dialers::Status.new(response.status).is?(404) },
|
24
|
+
do: -> (response) { fail Dialers::NotFoundError.new(response) }
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require_relative "./api_caller"
|
2
|
+
|
3
|
+
module Twitter
|
4
|
+
class Tweet
|
5
|
+
attr_accessor :created_at, :text
|
6
|
+
end
|
7
|
+
|
8
|
+
class User
|
9
|
+
attr_accessor :screen_name, :profile_image_url
|
10
|
+
end
|
11
|
+
|
12
|
+
class Api < Dialers::Wrapper
|
13
|
+
api_caller { ApiCaller.new }
|
14
|
+
|
15
|
+
def get_user_timeline
|
16
|
+
api_caller.get("statuses/user_timeline.json").transform_to_many(Tweet)
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_user
|
20
|
+
api_caller.get("account/verify_credentials.json").transform_to_one(User)
|
21
|
+
end
|
22
|
+
|
23
|
+
def search(query)
|
24
|
+
api_caller.get("search/tweets.json", q: query).transform_to_many(Tweet, root: "statuses")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require "dialers"
|
2
|
+
|
3
|
+
module Twitter
|
4
|
+
class ApiCaller < Dialers::Caller
|
5
|
+
BASE_URL = "https://api.twitter.com/1.1/"
|
6
|
+
CONSUMER_KEY = ENV["TWITTER_CONSUMER_KEY"]
|
7
|
+
CONSUMER_SECRET = ENV["TWITTER_CONSUMER_SECRET"]
|
8
|
+
TOKEN = ENV["TWITTER_TOKEN"]
|
9
|
+
TOKEN_SECRET = ENV["TWITTER_TOKEN_SECRET"]
|
10
|
+
|
11
|
+
setup_api(url: BASE_URL, ssl: { verify: true }) do |faraday|
|
12
|
+
faraday.request :json
|
13
|
+
faraday.request :oauth,
|
14
|
+
consumer_key: CONSUMER_KEY,
|
15
|
+
consumer_secret: CONSUMER_SECRET,
|
16
|
+
token: TOKEN,
|
17
|
+
token_secret: TOKEN_SECRET
|
18
|
+
faraday.request :request_headers, accept: "*/*"
|
19
|
+
faraday.response :json
|
20
|
+
faraday.adapter :patron
|
21
|
+
end
|
22
|
+
|
23
|
+
short_circuits.add(
|
24
|
+
if: -> (response) { Dialers::Status.new(response.status).server_error? },
|
25
|
+
do: -> (response) { fail Dialers::ServerError.new(response) }
|
26
|
+
)
|
27
|
+
|
28
|
+
short_circuits.add(
|
29
|
+
if: -> (response) { Dialers::Status.new(response.status).is?(400) },
|
30
|
+
do: -> (response) { fail Dialers::ResponseError.new(response) }
|
31
|
+
)
|
32
|
+
|
33
|
+
short_circuits.add(
|
34
|
+
if: -> (response) { Dialers::Status.new(response.status).is?(401) },
|
35
|
+
do: -> (response) { fail Dialers::UnauthorizedError.new(response) }
|
36
|
+
)
|
37
|
+
|
38
|
+
short_circuits.add(
|
39
|
+
if: -> (response) { Dialers::Status.new(response.status).is?(404) },
|
40
|
+
do: -> (response) { fail Dialers::NotFoundError.new(response) }
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require_relative "./api"
|
2
|
+
require "simple_oauth"
|
3
|
+
|
4
|
+
api = Twitter::Api.new
|
5
|
+
keyword = ARGV.first
|
6
|
+
|
7
|
+
ruby_tweet = api.get_user_timeline.find do |tweet|
|
8
|
+
tweet.text.include?(keyword)
|
9
|
+
end || api.search(keyword).first
|
10
|
+
|
11
|
+
if ruby_tweet
|
12
|
+
puts "Gotcha!"
|
13
|
+
puts ruby_tweet.text
|
14
|
+
else
|
15
|
+
puts "We searched in your timeline and in the world for #{keyword} and we didn't find nothing :("
|
16
|
+
end
|
data/lib/dialers.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "dialers/version"
|
2
|
+
require "faraday"
|
3
|
+
require "faraday_middleware"
|
4
|
+
require "faraday/conductivity"
|
5
|
+
require "dialers/assign_attributes"
|
6
|
+
require "dialers/errors"
|
7
|
+
require "dialers/request_options"
|
8
|
+
require "dialers/transformable"
|
9
|
+
require "dialers/short_circuit"
|
10
|
+
require "dialers/short_circuits_collection"
|
11
|
+
require "dialers/status"
|
12
|
+
require "dialers/caller"
|
13
|
+
require "dialers/wrapper"
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Dialers
|
2
|
+
module AssignAttributes
|
3
|
+
# Assign the attributes hash into the object calling attribute writers of the object
|
4
|
+
# if the object can respond to them.
|
5
|
+
#
|
6
|
+
# @param object [Object] any object with some or no attribute writers.
|
7
|
+
# @param attributes [Hash<Symbol, Object>] the attributes using symbols and objects.
|
8
|
+
#
|
9
|
+
# @return [Object] the same object passed as parameter.
|
10
|
+
def self.call(object, attributes)
|
11
|
+
attributes.each do |key, value|
|
12
|
+
writer = "#{key}=".to_sym
|
13
|
+
if object.respond_to?(writer)
|
14
|
+
object.public_send(writer, value)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
object
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
module Dialers
|
2
|
+
class Caller
|
3
|
+
IDEMPOTENT_AND_SAFE_METHODS = [:get, :head, :options]
|
4
|
+
MAX_RETRIES = 5
|
5
|
+
|
6
|
+
class << self
|
7
|
+
# Setups a connection using {https://github.com/lostisland/faraday Faraday}.
|
8
|
+
#
|
9
|
+
# @param [Array] Arguments to pass to the faraday connection.
|
10
|
+
# @yield A block to pass to the faraday connection
|
11
|
+
#
|
12
|
+
# @return [Faraday::Connection] a connection
|
13
|
+
def setup_api(*args, &block)
|
14
|
+
api = Faraday.new(*args) { |faraday| block.call(faraday) }
|
15
|
+
const_set "API", api
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [ShortCircuitsCollection] a collection of short circuits that can stop the process.
|
19
|
+
def short_circuits
|
20
|
+
@short_circuits ||= Dialers::ShortCircuitsCollection.new
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# @!macro [attach] query_holder_request_method
|
26
|
+
# @method $1
|
27
|
+
# Make a $1 request.
|
28
|
+
#
|
29
|
+
# @param url [String] The path for the request.
|
30
|
+
# @param params [Hash] The query params to attach to the url.
|
31
|
+
# @param headers [Hash] The headers.
|
32
|
+
#
|
33
|
+
# @return [Transformable] a transformable object
|
34
|
+
def query_holder_request_method(http_method)
|
35
|
+
define_method(http_method) do |url, params = {}, headers = {}|
|
36
|
+
options = RequestOptions.new
|
37
|
+
options.url = url
|
38
|
+
options.http_method = http_method
|
39
|
+
options.query_params = params
|
40
|
+
options.headers = headers
|
41
|
+
|
42
|
+
transform(http_call(options))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# @!macro [attach] body_holder_request_method
|
47
|
+
# @method $1
|
48
|
+
# Make a $1 request.
|
49
|
+
#
|
50
|
+
# @param url [String] The path for the request.
|
51
|
+
# @param payload [Hash] The request body.
|
52
|
+
# @param headers [Hash] The headers.
|
53
|
+
#
|
54
|
+
# @return [Transformable] a transformable object
|
55
|
+
def body_holder_request_method(http_method)
|
56
|
+
define_method(http_method) do |url, payload = {}, headers = {}|
|
57
|
+
options = RequestOptions.new
|
58
|
+
options.url = url
|
59
|
+
options.http_method = http_method
|
60
|
+
options.payload = payload
|
61
|
+
options.headers = headers
|
62
|
+
|
63
|
+
transform(http_call(options))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
public
|
69
|
+
|
70
|
+
query_holder_request_method :get
|
71
|
+
query_holder_request_method :head
|
72
|
+
query_holder_request_method :delete
|
73
|
+
query_holder_request_method :options
|
74
|
+
body_holder_request_method :post
|
75
|
+
body_holder_request_method :put
|
76
|
+
body_holder_request_method :patch
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
def transform(response)
|
81
|
+
self.class.short_circuits.search_for_stops(response)
|
82
|
+
Dialers::Transformable.new(response)
|
83
|
+
end
|
84
|
+
|
85
|
+
def http_call(request_options, current_retries = 0)
|
86
|
+
call_api(request_options)
|
87
|
+
rescue Faraday::ParsingError => _exception
|
88
|
+
raise Dialers::ParsingError.new(exception)
|
89
|
+
rescue Faraday::ConnectionFailed => exception
|
90
|
+
raise Dialers::UnreachableError.new(exception)
|
91
|
+
rescue Faraday::TimeoutError => exception
|
92
|
+
retry_call(request_options, exception, current_retries)
|
93
|
+
end
|
94
|
+
|
95
|
+
def retry_call(request_options, exception, current_retries)
|
96
|
+
if idempotent_and_safe_method?(request_options.http_method) && current_retries <= MAX_RETRIES
|
97
|
+
http_call(request_options, current_retries + 1)
|
98
|
+
else
|
99
|
+
fail Dialers::UnreachableError.new(exception)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def call_api(request_options)
|
104
|
+
api.public_send(
|
105
|
+
request_options.http_method, request_options.url, request_options.query_params || {}
|
106
|
+
) do |request|
|
107
|
+
request.body = request_options.payload
|
108
|
+
(request_options.headers || {}).each do |key, value|
|
109
|
+
request.headers[key] = value
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def api
|
115
|
+
@api ||= get_api
|
116
|
+
end
|
117
|
+
|
118
|
+
def get_api
|
119
|
+
self.class::API
|
120
|
+
rescue NameError
|
121
|
+
raise Dialers::InexistentApiError.new(self.class)
|
122
|
+
end
|
123
|
+
|
124
|
+
def idempotent_and_safe_method?(http_method)
|
125
|
+
IDEMPOTENT_AND_SAFE_METHODS.include?(http_method)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Dialers
|
2
|
+
class ErrorAsErrorProxy < StandardError
|
3
|
+
def initialize(error = nil)
|
4
|
+
self.error = error
|
5
|
+
end
|
6
|
+
|
7
|
+
def message
|
8
|
+
error ? error.message : super
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
attr_accessor :error
|
14
|
+
end
|
15
|
+
|
16
|
+
class ErrorWithResponse < StandardError
|
17
|
+
def initialize(response = nil)
|
18
|
+
self.response = response
|
19
|
+
end
|
20
|
+
|
21
|
+
def message
|
22
|
+
if response.nil?
|
23
|
+
super
|
24
|
+
else
|
25
|
+
"\n
|
26
|
+
STATUS: #{response.status}
|
27
|
+
URL: #{response.env.url}
|
28
|
+
REQUEST HEADERS: #{response.env.request_headers}
|
29
|
+
HEADERS: #{response.env.response_headers}
|
30
|
+
BODY: #{response.body}\n\n"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
attr_accessor :response
|
37
|
+
end
|
38
|
+
|
39
|
+
class UnreachableError < ErrorAsErrorProxy
|
40
|
+
end
|
41
|
+
|
42
|
+
class ParsingError < ErrorAsErrorProxy
|
43
|
+
end
|
44
|
+
|
45
|
+
class ResponseError < ErrorWithResponse
|
46
|
+
end
|
47
|
+
|
48
|
+
class InexistentApiError < StandardError
|
49
|
+
def initialize(searched_class)
|
50
|
+
self.searched_class = searched_class
|
51
|
+
end
|
52
|
+
|
53
|
+
def message
|
54
|
+
"\n\nSEARCHED CLASS: #{searched_class}\n\n"
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
attr_accessor :searched_class
|
60
|
+
end
|
61
|
+
|
62
|
+
class ServerError < ErrorWithResponse
|
63
|
+
end
|
64
|
+
|
65
|
+
class NotFoundError < ErrorWithResponse
|
66
|
+
end
|
67
|
+
|
68
|
+
class UnauthorizedError < ErrorWithResponse
|
69
|
+
end
|
70
|
+
|
71
|
+
class ImpossibleTranformationError < ErrorWithResponse
|
72
|
+
end
|
73
|
+
|
74
|
+
ERRORS = [
|
75
|
+
UnreachableError, ParsingError, ResponseError, InexistentApiError
|
76
|
+
]
|
77
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Dialers
|
2
|
+
class ShortCircuit
|
3
|
+
def initialize(condition, action)
|
4
|
+
self.condition = condition
|
5
|
+
self.action = action
|
6
|
+
end
|
7
|
+
|
8
|
+
def can_stop?(response)
|
9
|
+
condition.call(response)
|
10
|
+
end
|
11
|
+
|
12
|
+
def stop(response)
|
13
|
+
action.call(response)
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :condition, :action
|
17
|
+
end
|
18
|
+
end
|