evil-client 0.2.1
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/.codeclimate.yml +7 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.rubocop.yml +98 -0
- data/.travis.yml +17 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +21 -0
- data/README.md +144 -0
- data/Rakefile +6 -0
- data/docs/base_url.md +38 -0
- data/docs/documentation.md +9 -0
- data/docs/headers.md +59 -0
- data/docs/http_method.md +31 -0
- data/docs/index.md +127 -0
- data/docs/license.md +19 -0
- data/docs/model.md +173 -0
- data/docs/operation.md +0 -0
- data/docs/overview.md +0 -0
- data/docs/path.md +48 -0
- data/docs/query.md +99 -0
- data/docs/responses.md +66 -0
- data/docs/security.md +102 -0
- data/docs/settings.md +32 -0
- data/evil-client.gemspec +25 -0
- data/lib/evil/client.rb +97 -0
- data/lib/evil/client/connection.rb +35 -0
- data/lib/evil/client/connection/net_http.rb +57 -0
- data/lib/evil/client/dsl.rb +110 -0
- data/lib/evil/client/dsl/files.rb +37 -0
- data/lib/evil/client/dsl/operation.rb +102 -0
- data/lib/evil/client/dsl/operations.rb +41 -0
- data/lib/evil/client/dsl/scope.rb +34 -0
- data/lib/evil/client/dsl/security.rb +57 -0
- data/lib/evil/client/middleware.rb +81 -0
- data/lib/evil/client/middleware/base.rb +15 -0
- data/lib/evil/client/middleware/merge_security.rb +16 -0
- data/lib/evil/client/middleware/normalize_headers.rb +13 -0
- data/lib/evil/client/middleware/stringify_form.rb +36 -0
- data/lib/evil/client/middleware/stringify_json.rb +15 -0
- data/lib/evil/client/middleware/stringify_multipart.rb +32 -0
- data/lib/evil/client/middleware/stringify_multipart/part.rb +36 -0
- data/lib/evil/client/middleware/stringify_query.rb +31 -0
- data/lib/evil/client/model.rb +65 -0
- data/lib/evil/client/operation.rb +34 -0
- data/lib/evil/client/operation/request.rb +42 -0
- data/lib/evil/client/operation/response.rb +40 -0
- data/lib/evil/client/operation/response_error.rb +12 -0
- data/lib/evil/client/operation/unexpected_response_error.rb +16 -0
- data/mkdocs.yml +21 -0
- data/spec/features/instantiation_spec.rb +68 -0
- data/spec/features/middleware_spec.rb +75 -0
- data/spec/features/operation_with_documentation_spec.rb +41 -0
- data/spec/features/operation_with_files_spec.rb +40 -0
- data/spec/features/operation_with_form_body_spec.rb +158 -0
- data/spec/features/operation_with_headers_spec.rb +99 -0
- data/spec/features/operation_with_http_method_spec.rb +45 -0
- data/spec/features/operation_with_json_body_spec.rb +156 -0
- data/spec/features/operation_with_path_spec.rb +47 -0
- data/spec/features/operation_with_query_spec.rb +84 -0
- data/spec/features/operation_with_response_spec.rb +109 -0
- data/spec/features/operation_with_security_spec.rb +228 -0
- data/spec/features/scoping_spec.rb +48 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/test_client.rb +15 -0
- data/spec/unit/evil/client/connection/net_http_spec.rb +38 -0
- data/spec/unit/evil/client/dsl/files_spec.rb +37 -0
- data/spec/unit/evil/client/dsl/operation_spec.rb +233 -0
- data/spec/unit/evil/client/dsl/operations_spec.rb +27 -0
- data/spec/unit/evil/client/dsl/scope_spec.rb +30 -0
- data/spec/unit/evil/client/dsl/security_spec.rb +135 -0
- data/spec/unit/evil/client/dsl_spec.rb +57 -0
- data/spec/unit/evil/client/middleware/merge_security_spec.rb +32 -0
- data/spec/unit/evil/client/middleware/normalize_headers_spec.rb +17 -0
- data/spec/unit/evil/client/middleware/stringify_form_spec.rb +63 -0
- data/spec/unit/evil/client/middleware/stringify_json_spec.rb +61 -0
- data/spec/unit/evil/client/middleware/stringify_multipart/part_spec.rb +59 -0
- data/spec/unit/evil/client/middleware/stringify_multipart_spec.rb +62 -0
- data/spec/unit/evil/client/middleware/stringify_query_spec.rb +40 -0
- data/spec/unit/evil/client/middleware_spec.rb +46 -0
- data/spec/unit/evil/client/model_spec.rb +100 -0
- data/spec/unit/evil/client/operation/request_spec.rb +49 -0
- data/spec/unit/evil/client/operation/response_spec.rb +61 -0
- metadata +271 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
RSpec.describe "scoping" do
|
2
|
+
# see Test::Client definition in `/spec/support/test_client.rb`
|
3
|
+
before do
|
4
|
+
class Test::Client < Evil::Client
|
5
|
+
operation :update_user do
|
6
|
+
http_method :put
|
7
|
+
path { |id:, **| "users/#{id}" }
|
8
|
+
|
9
|
+
query do
|
10
|
+
attribute :token
|
11
|
+
end
|
12
|
+
|
13
|
+
body format: :form do
|
14
|
+
attribute :name
|
15
|
+
end
|
16
|
+
|
17
|
+
response 200
|
18
|
+
end
|
19
|
+
|
20
|
+
scope do
|
21
|
+
param :token
|
22
|
+
|
23
|
+
scope :users do
|
24
|
+
scope do
|
25
|
+
param :id
|
26
|
+
|
27
|
+
def update(name:)
|
28
|
+
operations[:update_user].call(id: id, token: token, name: name)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
stub_request(:put, //)
|
36
|
+
end
|
37
|
+
|
38
|
+
let(:path) { "https://foo.example.com/api/v3/users/7?token=qux" }
|
39
|
+
let(:client) do
|
40
|
+
Test::Client.new "foo", version: 3, user: "bar"
|
41
|
+
end
|
42
|
+
|
43
|
+
it "provides access to params over nested scopes" do
|
44
|
+
client["qux"].users[7].update name: "baz"
|
45
|
+
|
46
|
+
expect(a_request(:put, path).with body: "name=baz").to have_been_made
|
47
|
+
end
|
48
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
begin
|
2
|
+
require "pry"
|
3
|
+
rescue LoadError
|
4
|
+
nil
|
5
|
+
end
|
6
|
+
|
7
|
+
require "evil/client"
|
8
|
+
require "dry-types"
|
9
|
+
require "webmock/rspec"
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.order = :random
|
13
|
+
config.filter_run focus: true
|
14
|
+
config.run_all_when_everything_filtered = true
|
15
|
+
|
16
|
+
# Prepare the Test namespace for constants defined in specs
|
17
|
+
config.around(:each) do |example|
|
18
|
+
stub_request(:any, //)
|
19
|
+
load File.expand_path("../support/test_client.rb", __FILE__)
|
20
|
+
example.run
|
21
|
+
Object.send :remove_const, :Test
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Test
|
2
|
+
class Client < Evil::Client
|
3
|
+
settings do
|
4
|
+
param :subdomain, type: Dry::Types["strict.string"]
|
5
|
+
option :version, type: Dry::Types["coercible.int"], default: proc { 1 }
|
6
|
+
option :user, type: Dry::Types["strict.string"]
|
7
|
+
option :password, type: Dry::Types["coercible.string"], optional: true
|
8
|
+
option :token, type: Dry::Types["coercible.string"], optional: true
|
9
|
+
end
|
10
|
+
|
11
|
+
base_url do |settings|
|
12
|
+
"https://#{settings.subdomain}.example.com/api/v#{settings.version}/"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "evil/client/connection/net_http"
|
2
|
+
|
3
|
+
describe Evil::Client::Connection::NetHTTP do
|
4
|
+
let(:uri) { URI("https://example.com/foo/") }
|
5
|
+
let(:connection) { described_class.new(uri) }
|
6
|
+
let(:env) do
|
7
|
+
{
|
8
|
+
http_method: "post",
|
9
|
+
path: "bar/baz",
|
10
|
+
headers: { "Content-Type": "text/plain", "Accept": "text/plain" },
|
11
|
+
body_string: "Check!",
|
12
|
+
query_string: "status=new"
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
before do
|
17
|
+
stub_request(:post, "https://example.com/foo/bar/baz?status=new")
|
18
|
+
.to_return status: 201,
|
19
|
+
body: "Success!",
|
20
|
+
headers: { "Content-Type" => "text/plain; charset: utf-8" }
|
21
|
+
end
|
22
|
+
|
23
|
+
subject { connection.call env }
|
24
|
+
|
25
|
+
it "sends a request" do
|
26
|
+
subject
|
27
|
+
expect(a_request(:post, "https://example.com/foo/bar/baz?status=new"))
|
28
|
+
.to have_been_made
|
29
|
+
end
|
30
|
+
|
31
|
+
it "returns rack-compatible response" do
|
32
|
+
expect(subject).to eq [
|
33
|
+
201,
|
34
|
+
{ "content-type" => ["text/plain; charset: utf-8"] },
|
35
|
+
["Success!"]
|
36
|
+
]
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
RSpec.describe Evil::Client::DSL::Files do
|
2
|
+
subject do
|
3
|
+
described_class.new(&block).call file: "Hi!",
|
4
|
+
charset: "utf-16",
|
5
|
+
type: "application/json",
|
6
|
+
filename: "greetings.json",
|
7
|
+
text: "Hoorah!"
|
8
|
+
end
|
9
|
+
|
10
|
+
context "from block without definitions:" do
|
11
|
+
let(:block) { proc {} }
|
12
|
+
|
13
|
+
it "provides an empty schema" do
|
14
|
+
expect(subject).to eq([])
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context "from block with definitions:" do
|
19
|
+
let(:block) do
|
20
|
+
proc do |file:, **opts|
|
21
|
+
add file, **opts
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "provides a schema" do
|
26
|
+
expect(subject).to be_a Array
|
27
|
+
expect(subject.count).to eq 1
|
28
|
+
|
29
|
+
item = subject.first
|
30
|
+
expect(item[:file]).to be_a StringIO
|
31
|
+
expect(item[:file].read).to eq "Hi!"
|
32
|
+
expect(item[:type]).to eq MIME::Types["application/json"].first
|
33
|
+
expect(item[:charset]).to eq "utf-16"
|
34
|
+
expect(item[:filename]).to eq "greetings.json"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,233 @@
|
|
1
|
+
RSpec.describe Evil::Client::DSL::Operation do
|
2
|
+
let(:operation) { described_class.new(:some_operation, block) }
|
3
|
+
let(:settings) { double version: 3, user: "foo", password: "bar" }
|
4
|
+
|
5
|
+
subject { operation.finalize(settings) }
|
6
|
+
|
7
|
+
context "without definitions" do
|
8
|
+
let(:block) { proc {} }
|
9
|
+
it "returns a hash" do
|
10
|
+
expect(subject).to eq key: :some_operation, responses: {}
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context "with #documentation" do
|
15
|
+
let(:block) do
|
16
|
+
proc { |settings| documentation "https://foo.bar/v#{settings.version}" }
|
17
|
+
end
|
18
|
+
|
19
|
+
it "defines link to :doc" do
|
20
|
+
expect(subject[:doc]).to eq "https://foo.bar/v3"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "with #http_method" do
|
25
|
+
let(:block) do
|
26
|
+
proc { |settings| http_method settings.version > 2 ? "post" : "get" }
|
27
|
+
end
|
28
|
+
|
29
|
+
it "defines :method" do
|
30
|
+
expect(subject[:method]).to eq "post"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "with #path" do
|
35
|
+
let(:block) do
|
36
|
+
proc do |settings|
|
37
|
+
path { |id:, **| "/v#{settings.version}/users/#{id}/" }
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it "defines :path without trailing slashes" do
|
42
|
+
path = subject[:path]
|
43
|
+
expect(path).to be_kind_of Proc
|
44
|
+
expect(path.call(id: 55)).to eq "v3/users/55"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context "with #files" do
|
49
|
+
let(:block) do
|
50
|
+
proc do
|
51
|
+
files do |file:, **options|
|
52
|
+
add file, **options
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
it "sets format to file" do
|
58
|
+
expect(subject[:format]).to eq "multipart"
|
59
|
+
end
|
60
|
+
|
61
|
+
it "defines schema for a file" do
|
62
|
+
file = Tempfile.new
|
63
|
+
|
64
|
+
schema = subject[:files].call file: file,
|
65
|
+
type: "text/html",
|
66
|
+
charset: "utf-16"
|
67
|
+
|
68
|
+
expect(schema).to contain_exactly \
|
69
|
+
file: file,
|
70
|
+
type: MIME::Types["text/html"].first,
|
71
|
+
charset: "utf-16",
|
72
|
+
filename: nil
|
73
|
+
end
|
74
|
+
|
75
|
+
it "wraps string to StringIO" do
|
76
|
+
schema = subject[:files].call file: "Hello!"
|
77
|
+
file = schema.first[:file]
|
78
|
+
|
79
|
+
expect(file).to be_kind_of StringIO
|
80
|
+
expect(file.read).to eq "Hello!"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
context "with #security" do
|
85
|
+
let(:block) do
|
86
|
+
proc do |settings|
|
87
|
+
security do
|
88
|
+
basic_auth settings.user, settings.password
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
it "defines :security schema" do
|
94
|
+
expect(subject[:security].call)
|
95
|
+
.to eq headers: { "authorization" => "Basic Zm9vOmJhcg==" }
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
context "with #body" do
|
100
|
+
context "with block without :model" do
|
101
|
+
let(:block) do
|
102
|
+
proc do |_|
|
103
|
+
body do
|
104
|
+
attribute :foo
|
105
|
+
attribute :bar
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
it "sets format to json" do
|
111
|
+
expect(subject[:format]).to eq "json"
|
112
|
+
end
|
113
|
+
|
114
|
+
it "defines :block as model filter" do
|
115
|
+
model = subject[:body][foo: :FOO, bar: :BAR, baz: :BAZ]
|
116
|
+
expect(model).to eq foo: :FOO, bar: :BAR
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
context "with :model without block" do
|
121
|
+
before do
|
122
|
+
class Test::Foo < Evil::Client::Model
|
123
|
+
attribute :qux
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
let(:block) do
|
128
|
+
proc do |_|
|
129
|
+
body model: Test::Foo
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
it "defines :block as model filter" do
|
134
|
+
model = subject[:body][foo: :FOO, bar: :BAR, qux: :QUX, baz: :BAZ]
|
135
|
+
expect(model).to eq qux: :QUX
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
context "with :model and block" do
|
140
|
+
before do
|
141
|
+
class Test::Foo < Evil::Client::Model
|
142
|
+
attribute :qux
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
let(:block) do
|
147
|
+
proc do |_|
|
148
|
+
body model: Test::Foo do
|
149
|
+
attribute :foo
|
150
|
+
attribute :bar
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
it "defines :block as model filter" do
|
156
|
+
model = subject[:body][foo: :FOO, bar: :BAR, qux: :QUX, baz: :BAZ]
|
157
|
+
expect(model).to eq foo: :FOO, bar: :BAR, qux: :QUX
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
context "with #query" do
|
163
|
+
before do
|
164
|
+
class Test::Foo < Evil::Client::Model
|
165
|
+
attribute :qux
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
let(:block) do
|
170
|
+
proc do |settings|
|
171
|
+
query model: Test::Foo do
|
172
|
+
attribute settings.user
|
173
|
+
attribute :bar
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
it "defines :block as model filter" do
|
179
|
+
model = subject[:query][foo: :FOO, bar: :BAR, qux: :QUX, baz: :BAZ]
|
180
|
+
expect(model).to eq foo: :FOO, bar: :BAR, qux: :QUX
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
context "with #headers" do
|
185
|
+
before do
|
186
|
+
class Test::Foo < Evil::Client::Model
|
187
|
+
attribute :qux
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
let(:block) do
|
192
|
+
proc do |settings|
|
193
|
+
headers model: Test::Foo do
|
194
|
+
attribute settings.user
|
195
|
+
attribute :bar
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
it "defines :block as model filter" do
|
201
|
+
model = subject[:headers][foo: :FOO, bar: :BAR, qux: :QUX, baz: :BAZ]
|
202
|
+
expect(model).to eq foo: :FOO, bar: :BAR, qux: :QUX
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
context "with #response" do
|
207
|
+
let(:block) do
|
208
|
+
proc do |_|
|
209
|
+
response 200 do |value|
|
210
|
+
value.to_sym
|
211
|
+
end
|
212
|
+
|
213
|
+
response 404, raise: true do |value|
|
214
|
+
value.to_s
|
215
|
+
end
|
216
|
+
|
217
|
+
response 500, raise: true
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
it "defines :responses" do
|
222
|
+
responses = subject[:responses]
|
223
|
+
|
224
|
+
expect(responses[200]).to include raise: false
|
225
|
+
expect(responses[404]).to include raise: true
|
226
|
+
expect(responses[500]).to include raise: true
|
227
|
+
|
228
|
+
expect(responses[200][:coercer]["foo"]).to eq :foo
|
229
|
+
expect(responses[404][:coercer][:foo]).to eq "foo"
|
230
|
+
expect(responses[500][:coercer][response: :foo]).to eq :foo
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
describe Evil::Client::DSL::Operations do
|
2
|
+
let(:operations) { described_class.new }
|
3
|
+
let(:settings) { double(:settings, version: 1, user: "foo", password: "bar") }
|
4
|
+
|
5
|
+
before do
|
6
|
+
operations.register(nil) do |settings|
|
7
|
+
http_method :post
|
8
|
+
security { basic_auth settings.user, settings.password }
|
9
|
+
end
|
10
|
+
|
11
|
+
operations.register(:find_user) do |_|
|
12
|
+
http_method :get
|
13
|
+
path { |id:, **| "/users/#{id}" }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
subject { operations.finalize(settings) }
|
18
|
+
|
19
|
+
it "builds a proper schema" do
|
20
|
+
find_user = subject[:find_user]
|
21
|
+
|
22
|
+
expect(find_user[:method]).to eq "get"
|
23
|
+
expect(find_user[:path].call(id: 7)).to eq "users/7"
|
24
|
+
expect(find_user[:security].call)
|
25
|
+
.to eq headers: { "authorization" => "Basic Zm9vOmJhcg==" }
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
RSpec.describe Evil::Client::DSL, ".scope" do
|
2
|
+
before do
|
3
|
+
class Test::Foo
|
4
|
+
extend Evil::Client::DSL
|
5
|
+
|
6
|
+
scope :foo do
|
7
|
+
param :bar
|
8
|
+
|
9
|
+
scope do
|
10
|
+
param :baz
|
11
|
+
|
12
|
+
def find
|
13
|
+
qux(bar: bar, baz: baz)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def qux(bar:, baz:)
|
19
|
+
"#{bar}/#{baz}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:client) { Test::Foo.new }
|
25
|
+
subject { client.foo("users")[54].find }
|
26
|
+
|
27
|
+
it "provides access to params and methods via nested scopes" do
|
28
|
+
expect(subject).to eq "users/54"
|
29
|
+
end
|
30
|
+
end
|