evil-client 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +7 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +98 -0
  6. data/.travis.yml +17 -0
  7. data/Gemfile +9 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +144 -0
  10. data/Rakefile +6 -0
  11. data/docs/base_url.md +38 -0
  12. data/docs/documentation.md +9 -0
  13. data/docs/headers.md +59 -0
  14. data/docs/http_method.md +31 -0
  15. data/docs/index.md +127 -0
  16. data/docs/license.md +19 -0
  17. data/docs/model.md +173 -0
  18. data/docs/operation.md +0 -0
  19. data/docs/overview.md +0 -0
  20. data/docs/path.md +48 -0
  21. data/docs/query.md +99 -0
  22. data/docs/responses.md +66 -0
  23. data/docs/security.md +102 -0
  24. data/docs/settings.md +32 -0
  25. data/evil-client.gemspec +25 -0
  26. data/lib/evil/client.rb +97 -0
  27. data/lib/evil/client/connection.rb +35 -0
  28. data/lib/evil/client/connection/net_http.rb +57 -0
  29. data/lib/evil/client/dsl.rb +110 -0
  30. data/lib/evil/client/dsl/files.rb +37 -0
  31. data/lib/evil/client/dsl/operation.rb +102 -0
  32. data/lib/evil/client/dsl/operations.rb +41 -0
  33. data/lib/evil/client/dsl/scope.rb +34 -0
  34. data/lib/evil/client/dsl/security.rb +57 -0
  35. data/lib/evil/client/middleware.rb +81 -0
  36. data/lib/evil/client/middleware/base.rb +15 -0
  37. data/lib/evil/client/middleware/merge_security.rb +16 -0
  38. data/lib/evil/client/middleware/normalize_headers.rb +13 -0
  39. data/lib/evil/client/middleware/stringify_form.rb +36 -0
  40. data/lib/evil/client/middleware/stringify_json.rb +15 -0
  41. data/lib/evil/client/middleware/stringify_multipart.rb +32 -0
  42. data/lib/evil/client/middleware/stringify_multipart/part.rb +36 -0
  43. data/lib/evil/client/middleware/stringify_query.rb +31 -0
  44. data/lib/evil/client/model.rb +65 -0
  45. data/lib/evil/client/operation.rb +34 -0
  46. data/lib/evil/client/operation/request.rb +42 -0
  47. data/lib/evil/client/operation/response.rb +40 -0
  48. data/lib/evil/client/operation/response_error.rb +12 -0
  49. data/lib/evil/client/operation/unexpected_response_error.rb +16 -0
  50. data/mkdocs.yml +21 -0
  51. data/spec/features/instantiation_spec.rb +68 -0
  52. data/spec/features/middleware_spec.rb +75 -0
  53. data/spec/features/operation_with_documentation_spec.rb +41 -0
  54. data/spec/features/operation_with_files_spec.rb +40 -0
  55. data/spec/features/operation_with_form_body_spec.rb +158 -0
  56. data/spec/features/operation_with_headers_spec.rb +99 -0
  57. data/spec/features/operation_with_http_method_spec.rb +45 -0
  58. data/spec/features/operation_with_json_body_spec.rb +156 -0
  59. data/spec/features/operation_with_path_spec.rb +47 -0
  60. data/spec/features/operation_with_query_spec.rb +84 -0
  61. data/spec/features/operation_with_response_spec.rb +109 -0
  62. data/spec/features/operation_with_security_spec.rb +228 -0
  63. data/spec/features/scoping_spec.rb +48 -0
  64. data/spec/spec_helper.rb +23 -0
  65. data/spec/support/test_client.rb +15 -0
  66. data/spec/unit/evil/client/connection/net_http_spec.rb +38 -0
  67. data/spec/unit/evil/client/dsl/files_spec.rb +37 -0
  68. data/spec/unit/evil/client/dsl/operation_spec.rb +233 -0
  69. data/spec/unit/evil/client/dsl/operations_spec.rb +27 -0
  70. data/spec/unit/evil/client/dsl/scope_spec.rb +30 -0
  71. data/spec/unit/evil/client/dsl/security_spec.rb +135 -0
  72. data/spec/unit/evil/client/dsl_spec.rb +57 -0
  73. data/spec/unit/evil/client/middleware/merge_security_spec.rb +32 -0
  74. data/spec/unit/evil/client/middleware/normalize_headers_spec.rb +17 -0
  75. data/spec/unit/evil/client/middleware/stringify_form_spec.rb +63 -0
  76. data/spec/unit/evil/client/middleware/stringify_json_spec.rb +61 -0
  77. data/spec/unit/evil/client/middleware/stringify_multipart/part_spec.rb +59 -0
  78. data/spec/unit/evil/client/middleware/stringify_multipart_spec.rb +62 -0
  79. data/spec/unit/evil/client/middleware/stringify_query_spec.rb +40 -0
  80. data/spec/unit/evil/client/middleware_spec.rb +46 -0
  81. data/spec/unit/evil/client/model_spec.rb +100 -0
  82. data/spec/unit/evil/client/operation/request_spec.rb +49 -0
  83. data/spec/unit/evil/client/operation/response_spec.rb +61 -0
  84. 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
@@ -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