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