dialers 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.
- 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
|