wrappi 0.2.2 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +2 -0
- data/Gemfile +12 -2
- data/README.md +65 -2
- data/article.md +163 -0
- data/bin/console +18 -2
- data/lib/wrappi/client.rb +1 -0
- data/lib/wrappi/endpoint.rb +24 -2
- data/lib/wrappi/path_gen.rb +22 -0
- data/lib/wrappi/testing.rb +45 -0
- data/lib/wrappi/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 13686ad4035cddda1cbf1ef0c76cd1d67d221900dd2bcf929e41daff2f81b1dd
|
4
|
+
data.tar.gz: 4d313c68d7d3c401ce78d0dbd5a8c088f6818767d7e6807a8620f3e0f2acf751
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7a1a0fc2bbff8c668f246b15562bff9c33d098b22f2cd4b12a5342425f8242bf6fb5a2aeb2578e1a1c283b05340a83aab942fd5b9fea2b208d3572c24f03da9e
|
7
|
+
data.tar.gz: e4f5f5490059cb6fa8469c44583271186445273d46ca9cc77d139f90b38feefade82cc2617b6eb8a1d7ee17b15b3605ad3c7db472e472cdc759c7b1389f4b2de
|
data/.coveralls.yml
ADDED
data/Gemfile
CHANGED
@@ -2,5 +2,15 @@ source 'https://rubygems.org'
|
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in wrappi.gemspec
|
4
4
|
gemspec
|
5
|
-
|
6
|
-
|
5
|
+
|
6
|
+
group :development, :test do
|
7
|
+
gem 'cache_mock'
|
8
|
+
gem 'pry'
|
9
|
+
# gem 'rubocop', '~> 0.37.2', require: false unless RUBY_VERSION =~ /^1.8/
|
10
|
+
gem 'coveralls', require: false
|
11
|
+
|
12
|
+
platforms :mri, :mingw do
|
13
|
+
gem 'pry', require: false
|
14
|
+
gem 'pry-coolline', require: false
|
15
|
+
end
|
16
|
+
end
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
[![Build Status](https://travis-ci.org/arturictus/wrappi.svg?branch=master)](https://travis-ci.org/arturictus/wrappi)
|
2
2
|
[![Maintainability](https://api.codeclimate.com/v1/badges/8751a0b6523a52b5e23e/maintainability)](https://codeclimate.com/github/arturictus/wrappi/maintainability)
|
3
|
-
[![
|
3
|
+
[![Coverage Status](https://coveralls.io/repos/github/arturictus/wrappi/badge.svg?branch=master)](https://coveralls.io/github/arturictus/wrappi?branch=master)
|
4
4
|
|
5
5
|
# Wrappi
|
6
6
|
Making APIs fun again!
|
@@ -251,6 +251,7 @@ end
|
|
251
251
|
| timeout | Hash | { write: 9, connect: 9, read: 9 } | |
|
252
252
|
| use_ssl_context | Boolean | false | |
|
253
253
|
| ssl_context | OpenSSL::SSL::SSLContext | | |
|
254
|
+
| basic_auth | Hash (keys: user, pass) | | |
|
254
255
|
|
255
256
|
#### Endpoint
|
256
257
|
|
@@ -261,7 +262,7 @@ end
|
|
261
262
|
| verb | Symbol | :get | * |
|
262
263
|
| default_params | Hash `or` block -> Hash | {} | |
|
263
264
|
| headers | Hash `or` block -> Hash | proc { client.headers } | |
|
264
|
-
| basic_auth | Hash (keys: user, pass) `or` block -> Hash |
|
265
|
+
| basic_auth | Hash (keys: user, pass) `or` block -> Hash | proc { client.basic_auth } | |
|
265
266
|
| follow_redirects | Boolean `or` block -> Boolean | true | |
|
266
267
|
| body_type | Symbol, one of: :json,:form,:body | :json | |
|
267
268
|
| cache | Boolean `or` block -> Boolean | proc { options[:cache] }| |
|
@@ -527,6 +528,68 @@ It holds the common configuration for all the endpoints (`Wrappi::Endpoint`).
|
|
527
528
|
end
|
528
529
|
```
|
529
530
|
|
531
|
+
## Code Organization
|
532
|
+
### Build a gem
|
533
|
+
|
534
|
+
Wrappi is designed to be able to build HTTP client gems with it.
|
535
|
+
|
536
|
+
```ruby
|
537
|
+
module GithubCLI
|
538
|
+
class Client < Wrappi::Client
|
539
|
+
setup do |config|
|
540
|
+
config.domain = 'https://api.github.com'
|
541
|
+
config.headers = {
|
542
|
+
'Content-Type' => 'application/json',
|
543
|
+
'Accept' => 'application/vnd.github.v3+json',
|
544
|
+
}
|
545
|
+
end
|
546
|
+
|
547
|
+
class << self
|
548
|
+
attr_accessor :my_custom_config
|
549
|
+
end
|
550
|
+
end
|
551
|
+
|
552
|
+
def self.setup
|
553
|
+
yield(Client)
|
554
|
+
end
|
555
|
+
|
556
|
+
class Endpoint < Wrappi::Endpoint
|
557
|
+
client Client
|
558
|
+
end
|
559
|
+
|
560
|
+
class User < Endpoint
|
561
|
+
verb :get
|
562
|
+
path "users/:username"
|
563
|
+
end
|
564
|
+
|
565
|
+
def self.user(params, opts = {})
|
566
|
+
User.new(params, opts)
|
567
|
+
end
|
568
|
+
end
|
569
|
+
|
570
|
+
user = GithubCLI.user(username: 'arturictus')
|
571
|
+
user.success?
|
572
|
+
```
|
573
|
+
|
574
|
+
### In your project
|
575
|
+
|
576
|
+
## The HTTP clients war
|
577
|
+
|
578
|
+
In ruby there are many ruby clients an everyone has an opinion of which one is the
|
579
|
+
best.
|
580
|
+
Every new API client that you install in your project will install a different HTTP client
|
581
|
+
adding redundant and unnecessary dependencies in your project.
|
582
|
+
That's why __Wrappi is designed to be HTTP client agnostic__.
|
583
|
+
Right now is implemented with [HTTP gem](https://github.com/http/http) (my favorite) but all the logic is decoupled from
|
584
|
+
the HTTP client.
|
585
|
+
|
586
|
+
All the configuration, metadata and logic to build the request is hold by an instance of Endpoint. Allowing to create adapters that translates this processed metadata to the target HTTP client.
|
587
|
+
|
588
|
+
__Tests are HTTP client agnostic__. To help the development of these adapters and probe the reliability of the gem most of the test are run against a Rails application. __All the tests that probe an HTTP call are running this HTTP call against a local server__ making all test End To End and again, HTTP client agnostic.
|
589
|
+
|
590
|
+
Right now is not designed the system to change HTTP clients via configuration but if you are interested to implement one let me know
|
591
|
+
and we will figure out the way.
|
592
|
+
|
530
593
|
## Development
|
531
594
|
|
532
595
|
After checking out the repo, run `bin/setup` to install dependencies.
|
data/article.md
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
# Wrappi an HTTP framework
|
2
|
+
|
3
|
+
I would like to introduce you the gem I recently created to build HTTP clients.
|
4
|
+
|
5
|
+
My motivations:
|
6
|
+
|
7
|
+
- I've been using a lot of API clients in my projects an every time I have to use
|
8
|
+
one I have to memorize the behavior. Or some of them where not easy to use or with
|
9
|
+
estrange interface.
|
10
|
+
|
11
|
+
- When building myself API clients I find myself repeating the same process of finding how to
|
12
|
+
implement the best practices and flexibility enough for use cases, same abstractions to handle request errors, codes, etc.
|
13
|
+
Basically redoing something that I think could be abstracted in a framework.
|
14
|
+
|
15
|
+
That's why I builded Wrappi.
|
16
|
+
|
17
|
+
Let me convince you that your life will be much easier if you use Wrappi.
|
18
|
+
|
19
|
+
Let's do a Github Client together:
|
20
|
+
|
21
|
+
__The client:__
|
22
|
+
|
23
|
+
Client is where the shared configurations for you calls are stored. Domain, api keys, headers.
|
24
|
+
|
25
|
+
Here it is an example:
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
module GithubCLI
|
29
|
+
class Client < Wrappi::Client
|
30
|
+
setup do |config|
|
31
|
+
config.domain = 'https://api.github.com'
|
32
|
+
config.headers = {
|
33
|
+
'Content-Type' => 'application/json',
|
34
|
+
'Accept' => 'application/vnd.github.v3+json',
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
```
|
40
|
+
How you see we created a namespace `GithubCLI`.
|
41
|
+
I like my clients to be appended with `CLI` or `API` because when you use the gem in you project you will need
|
42
|
+
to create a wrapper around it and if the gem is called like the service I makes you have to be creative about the name and
|
43
|
+
your application code is less descriptive.
|
44
|
+
|
45
|
+
see by yourself:
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
module Github
|
49
|
+
def self.update_repos(user)
|
50
|
+
g = GithubCLI.users(username: user.github_username)
|
51
|
+
if g.success?
|
52
|
+
g.body['repos'].each do |r|
|
53
|
+
# what ever
|
54
|
+
end
|
55
|
+
else
|
56
|
+
false
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
Github.update_repos(user)
|
62
|
+
```
|
63
|
+
In this case the service is integrated to our application. We just call `Github` in
|
64
|
+
our application.
|
65
|
+
|
66
|
+
Next we created a Client holding all the global settings to make a call to any API. That means the domain, headers and params that will be shared
|
67
|
+
between all the endpoints in which we use this client.
|
68
|
+
|
69
|
+
Now let's create our first endpoint:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
module GithubCLI
|
73
|
+
class client < Wrappi:Client
|
74
|
+
# ...
|
75
|
+
end
|
76
|
+
|
77
|
+
|
78
|
+
class User < Wrappi::Endpoint
|
79
|
+
client Client
|
80
|
+
verb :get
|
81
|
+
path "users/:username"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
```
|
85
|
+
|
86
|
+
Here we just defined a class iheriting from `Wrappi::Endpoint`. In this class we just set few configs:
|
87
|
+
client, verb and path. This is all we need to make our first request.
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
req = GithubCLI::User.new(username: 'arturictus')
|
91
|
+
user.error? # => false
|
92
|
+
user.status_code # => 200
|
93
|
+
user.body # => {"login"=>"arturictus", "id"=>1930175, ...}
|
94
|
+
```
|
95
|
+
|
96
|
+
Yep!, done.
|
97
|
+
|
98
|
+
Right now is when you say: "I can do the same in a single line of code" and you are right. But now is when things get interesting.
|
99
|
+
|
100
|
+
Imagine that you have have an API key. It's not a hard thing to fix right?, maybe we create a class that all our endpoints inherit from?..
|
101
|
+
|
102
|
+
In wrappi you can just do it with a line of code:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
module GithubCLI
|
106
|
+
class Client < Wrappi::Client
|
107
|
+
setup do |config|
|
108
|
+
config.domain = 'https://api.github.com'
|
109
|
+
config.headers = {
|
110
|
+
'Content-Type' => 'application/json',
|
111
|
+
'Accept' => 'application/vnd.github.v3+json',
|
112
|
+
}
|
113
|
+
config.params = { api_token: 'very_secret' } # Look here
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
Now all the endpoints using this client will send with the request the `api_token` param.
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
req = GithubCLI::User.new(username: 'arturictus')
|
123
|
+
req.conssumated_params # => { api_token: ...}
|
124
|
+
```
|
125
|
+
|
126
|
+
Ok, not impressive but we are starting to safe lines of code.
|
127
|
+
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
module GithubCLI
|
131
|
+
class Client < Wrappi::Client
|
132
|
+
setup do |config|
|
133
|
+
config.domain = 'https://api.github.com'
|
134
|
+
config.headers = {
|
135
|
+
'Content-Type' => 'application/json',
|
136
|
+
'Accept' => 'application/vnd.github.v3+json',
|
137
|
+
}
|
138
|
+
end
|
139
|
+
|
140
|
+
class << self
|
141
|
+
attr_accessor :my_custom_config
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def self.setup
|
146
|
+
yield(Client)
|
147
|
+
end
|
148
|
+
|
149
|
+
class Endpoint < Wrappi::Endpoint
|
150
|
+
client Client
|
151
|
+
end
|
152
|
+
|
153
|
+
class User < Endpoint
|
154
|
+
verb :get
|
155
|
+
path "users/:username"
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.user(params, opts = {})
|
159
|
+
User.new(params, opts)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
```
|
data/bin/console
CHANGED
@@ -5,7 +5,7 @@ require "wrappi"
|
|
5
5
|
|
6
6
|
# Github example:
|
7
7
|
|
8
|
-
module
|
8
|
+
module GithubCLI
|
9
9
|
class Client < Wrappi::Client
|
10
10
|
setup do |config|
|
11
11
|
config.domain = 'https://api.github.com'
|
@@ -14,12 +14,28 @@ module Github
|
|
14
14
|
'Accept' => 'application/vnd.github.v3+json',
|
15
15
|
}
|
16
16
|
end
|
17
|
+
|
18
|
+
class << self
|
19
|
+
attr_accessor :my_custom_config
|
20
|
+
end
|
17
21
|
end
|
18
|
-
|
22
|
+
|
23
|
+
def self.setup
|
24
|
+
yield(Client)
|
25
|
+
end
|
26
|
+
|
27
|
+
class Endpoint < Wrappi::Endpoint
|
19
28
|
client Client
|
29
|
+
end
|
30
|
+
|
31
|
+
class User < Endpoint
|
20
32
|
verb :get
|
21
33
|
path "users/:username"
|
22
34
|
end
|
35
|
+
|
36
|
+
def self.user(params, opts = {})
|
37
|
+
User.new(params, opts)
|
38
|
+
end
|
23
39
|
end
|
24
40
|
|
25
41
|
|
data/lib/wrappi/client.rb
CHANGED
data/lib/wrappi/endpoint.rb
CHANGED
@@ -15,7 +15,8 @@ module Wrappi
|
|
15
15
|
body_type: :json,
|
16
16
|
cache: proc { options[:cache] },
|
17
17
|
cache_options: {},
|
18
|
-
async_callback: proc {}
|
18
|
+
async_callback: proc {},
|
19
|
+
basic_auth: proc { client.basic_auth }
|
19
20
|
}
|
20
21
|
)
|
21
22
|
attr_reader :input_params, :options
|
@@ -111,8 +112,29 @@ module Wrappi
|
|
111
112
|
"?#{d}"
|
112
113
|
end
|
113
114
|
|
115
|
+
# URI behaviour
|
116
|
+
# example:
|
117
|
+
#
|
118
|
+
# URI.join('https://hello.com/foo/bar', '/bin').to_s
|
119
|
+
# => "https://hello.com/bin"
|
120
|
+
#
|
121
|
+
# URI.join('https://hello.com/foo/bar', 'bin').to_s
|
122
|
+
# => "https://hello.com/foo/bin"
|
123
|
+
#
|
124
|
+
#
|
125
|
+
# URI.join('https://hello.com/foo/bar/', '/bin').to_s
|
126
|
+
# => "https://hello.com/bin"
|
127
|
+
#
|
128
|
+
# We want this behaviour:
|
129
|
+
# URI.join('https://hello.com/foo/bar/', 'bin').to_s
|
130
|
+
# => "https://hello.com/foo/bar/bin"
|
114
131
|
def _url
|
115
|
-
URI.join(
|
132
|
+
URI.join(domain, path_gen.for_uri)
|
133
|
+
end
|
134
|
+
|
135
|
+
def domain
|
136
|
+
return client.domain if client.domain =~ /\/$/
|
137
|
+
"#{client.domain}/"
|
116
138
|
end
|
117
139
|
|
118
140
|
def params
|
data/lib/wrappi/path_gen.rb
CHANGED
@@ -15,6 +15,28 @@ module Wrappi
|
|
15
15
|
end
|
16
16
|
alias_method :path, :compiled_path
|
17
17
|
|
18
|
+
# removes first character if path starts with `/`
|
19
|
+
# this is because URI will remove all the paths in between
|
20
|
+
# example:
|
21
|
+
#
|
22
|
+
# URI.join('https://hello.com/foo/bar', '/bin').to_s
|
23
|
+
# => "https://hello.com/bin"
|
24
|
+
#
|
25
|
+
# URI.join('https://hello.com/foo/bar', 'bin').to_s
|
26
|
+
# => "https://hello.com/foo/bin"
|
27
|
+
#
|
28
|
+
#
|
29
|
+
# URI.join('https://hello.com/foo/bar/', '/bin').to_s
|
30
|
+
# => "https://hello.com/bin"
|
31
|
+
#
|
32
|
+
# We want this behaviour:
|
33
|
+
# URI.join('https://hello.com/foo/bar/', 'bin').to_s
|
34
|
+
# => "https://hello.com/foo/bar/bin"
|
35
|
+
def for_uri
|
36
|
+
return compiled_path unless compiled_path =~ /^\//
|
37
|
+
compiled_path.dup.tap { |s| s[0] = '' }
|
38
|
+
end
|
39
|
+
|
18
40
|
def processed_params
|
19
41
|
return input_params unless interpolable?
|
20
42
|
@processed_params ||= input_params.reject{ |k, v| keys_in_params.include?(k.to_sym) }
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Wrappi
|
2
|
+
class Endpoint
|
3
|
+
|
4
|
+
def fixture_name
|
5
|
+
"#{self.class}#{fixture_params_key}.json"
|
6
|
+
end
|
7
|
+
|
8
|
+
def fixture_params_key
|
9
|
+
return if processed_params.empty?
|
10
|
+
d = Digest::MD5.hexdigest processed_params.to_json
|
11
|
+
"-#{d}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def fixture_content
|
15
|
+
return {} unless success?
|
16
|
+
{
|
17
|
+
request: {
|
18
|
+
method: verb.to_s,
|
19
|
+
url: url,
|
20
|
+
domain: domain,
|
21
|
+
headers: headers,
|
22
|
+
params: consummated_params,
|
23
|
+
path: path_gen.path
|
24
|
+
},
|
25
|
+
response: {
|
26
|
+
status: status,
|
27
|
+
body: body
|
28
|
+
}
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module Testing
|
34
|
+
def store_response(path, &block)
|
35
|
+
endpoint = block.call
|
36
|
+
raise "Not succesful call to #{endpoint.class}" unless endpoint.success?
|
37
|
+
file_fullname = File.join(path, endpoint.fixture_name)
|
38
|
+
return endpoint if File.exists?(file_fullname)
|
39
|
+
File.open(file_fullname, "w") do |f|
|
40
|
+
f.write(JSON.pretty_generate(endpoint.fixture_content))
|
41
|
+
end
|
42
|
+
endpoint
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/wrappi/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: wrappi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Artur Pañach
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-04-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -135,6 +135,7 @@ executables: []
|
|
135
135
|
extensions: []
|
136
136
|
extra_rdoc_files: []
|
137
137
|
files:
|
138
|
+
- ".coveralls.yml"
|
138
139
|
- ".gitignore"
|
139
140
|
- ".rspec"
|
140
141
|
- ".travis.yml"
|
@@ -143,6 +144,7 @@ files:
|
|
143
144
|
- LICENSE.txt
|
144
145
|
- README.md
|
145
146
|
- Rakefile
|
147
|
+
- article.md
|
146
148
|
- bin/console
|
147
149
|
- bin/dev_server
|
148
150
|
- bin/setup
|
@@ -161,6 +163,7 @@ files:
|
|
161
163
|
- lib/wrappi/request/template.rb
|
162
164
|
- lib/wrappi/request/with_body.rb
|
163
165
|
- lib/wrappi/response.rb
|
166
|
+
- lib/wrappi/testing.rb
|
164
167
|
- lib/wrappi/uncalled_request.rb
|
165
168
|
- lib/wrappi/version.rb
|
166
169
|
- wrappi.gemspec
|