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,41 @@
1
+ RSpec.describe "operation with documentation" do
2
+ # see Test::Client definition in `/spec/support/test_client.rb`
3
+ before do
4
+ class Test::Client < Evil::Client
5
+ operation do |settings|
6
+ documentation "https://docs.example.com/v#{settings.version}/index.html"
7
+ http_method :get
8
+ path { "data" }
9
+ end
10
+
11
+ operation :clear_data
12
+ operation :find_data do |settings|
13
+ documentation "https://docs.example.com/v#{settings.version}/findData"
14
+ end
15
+ end
16
+
17
+ stub_request(:any, //)
18
+ end
19
+
20
+ let(:client) { Test::Client.new("foo", version: 3, user: "bar") }
21
+
22
+ it "displays default documentation in exception messages" do
23
+ begin
24
+ client.operations[:clear_data].call
25
+ rescue => error
26
+ expect(error.message).to include "https://docs.example.com/v3/index.html"
27
+ else
28
+ fail
29
+ end
30
+ end
31
+
32
+ it "reloads default value with operation-specific one" do
33
+ begin
34
+ client.operations[:find_data].call
35
+ rescue => error
36
+ expect(error.message).to include "https://docs.example.com/v3/findData"
37
+ else
38
+ fail
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,40 @@
1
+ RSpec.describe "operation with files" do
2
+ # see Test::Client definition in `/spec/support/test_client.rb`
3
+ before do
4
+ class Test::Client < Evil::Client
5
+ operation :example do
6
+ http_method :get
7
+ path { "users" }
8
+ response 200
9
+
10
+ files do |file:, **|
11
+ add file, type: "text/xml", charset: "utf-16", filename: "foo.xml"
12
+ end
13
+ end
14
+ end
15
+
16
+ stub_request(:get, //)
17
+ end
18
+
19
+ let(:path) { "https://foo.example.com/api/v3/users" }
20
+ let(:client) { Test::Client.new "foo", user: "bar", version: 3, token: "baz" }
21
+
22
+ subject { client.operations[:example].call file: "Hi!" }
23
+
24
+ it "builds a multipart body" do
25
+ request = a_request(:get, path).with do |req|
26
+ expect(req.body).to include "Content-Disposition: form-data;" \
27
+ ' name="AttachedFile1"; filename="foo.xml"'
28
+
29
+ expect(req.body).to include "Content-Type: text/xml; charset=utf-16"
30
+
31
+ expect(req.body).to include "Hi!"
32
+
33
+ expect(req.headers["Content-Type"]).to include "multipart/form-data"
34
+ end
35
+
36
+ subject
37
+
38
+ expect(request).to have_been_made
39
+ end
40
+ end
@@ -0,0 +1,158 @@
1
+ RSpec.describe "operation with form body" do
2
+ # see Test::Client definition in `/spec/support/test_client.rb`
3
+ before do
4
+ class Test::User < Evil::Client::Model
5
+ attribute :name
6
+ end
7
+
8
+ class Test::Client < Evil::Client
9
+ operation do
10
+ http_method :get
11
+ path { "users" }
12
+ response 200
13
+ end
14
+ end
15
+
16
+ stub_request(:get, //)
17
+ end
18
+
19
+ let(:path) { "https://foo.example.com/api/v3/users" }
20
+ let(:client) { Test::Client.new "foo", user: "bar", version: 3, token: "baz" }
21
+ let(:operation) { client.operations[:example] }
22
+
23
+ context "without operation-specific definition" do
24
+ before do
25
+ class Test::Client < Evil::Client
26
+ operation do
27
+ http_method :get
28
+ path { "users" }
29
+ response 200
30
+
31
+ body format: "form" do
32
+ attribute :foo
33
+ end
34
+ end
35
+
36
+ operation :example do
37
+ end
38
+ end
39
+ end
40
+
41
+ it "uses the default one" do
42
+ request = a_request(:get, path).with do |req|
43
+ expect(req.body).to eq "foo[][bar][]=BAZ"
44
+ expect(req.headers)
45
+ .to include "Content-Type" => "application/x-www-form-urlencoded"
46
+ end
47
+
48
+ operation.call foo: [{ bar: [:BAZ] }], baz: :QUX
49
+
50
+ expect(request).to have_been_made
51
+ end
52
+ end
53
+
54
+ context "with operation-specific definition" do
55
+ before do
56
+ class Test::Client < Evil::Client
57
+ operation do
58
+ http_method :get
59
+ path { "users" }
60
+ response 200
61
+
62
+ body format: "form" do
63
+ attribute :foo
64
+ end
65
+ end
66
+
67
+ operation :example do
68
+ body format: "form" do
69
+ attribute :baz
70
+ end
71
+ end
72
+ end
73
+ end
74
+
75
+ it "uses the specific one" do
76
+ request = a_request(:get, path).with body: "baz=QUX"
77
+
78
+ operation.call foo: [{ bar: [:BAZ] }], baz: :QUX
79
+
80
+ expect(request).to have_been_made
81
+ end
82
+ end
83
+
84
+ context "when appended with files" do
85
+ before do
86
+ class Test::Client < Evil::Client
87
+ operation :example do
88
+ body format: "form" do
89
+ attribute :baz
90
+ end
91
+
92
+ files do
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ it "skips body definition" do
99
+ request = a_request(:get, path).with do |req|
100
+ expect(req.body).to be_nil
101
+ end
102
+
103
+ operation.call foo: [{ bar: [:BAZ] }], baz: :QUX
104
+
105
+ expect(request).to have_been_made
106
+ end
107
+ end
108
+
109
+ context "with a model" do
110
+ before do
111
+ class Test::Client < Evil::Client
112
+ operation :example do
113
+ body format: "form", model: Test::User do
114
+ attribute :foo
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ it "extends the model" do
121
+ request = a_request(:get, path).with body: "name=Andy&foo=BAR"
122
+
123
+ operation.call foo: :BAR, name: "Andy"
124
+
125
+ expect(request).to have_been_made
126
+ end
127
+ end
128
+
129
+ context "with a model" do
130
+ before do
131
+ class Test::Client < Evil::Client
132
+ operation :example do
133
+ body format: "form" do
134
+ attribute :foo, type: Dry::Types["strict.string"]
135
+ attribute :bar, default: proc { 1 }
136
+ attribute :baz, optional: true
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ it "requires mandatory arguments" do
143
+ expect { operation.call bar: 2, baz: 3 }.to raise_error(ArgumentError)
144
+ end
145
+
146
+ it "applies type restrictions" do
147
+ expect { operation.call foo: :FOO }.to raise_error(TypeError)
148
+ end
149
+
150
+ it "uses default values and skips undefined optional attributes" do
151
+ request = a_request(:get, path).with body: "foo=BAR&bar=1"
152
+
153
+ operation.call foo: "BAR"
154
+
155
+ expect(request).to have_been_made
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,99 @@
1
+ RSpec.describe "operation with headers" do
2
+ # see Test::Client definition in `/spec/support/test_client.rb`
3
+ before do
4
+ class Test::Client < Evil::Client
5
+ operation do
6
+ http_method :get
7
+ path { "data" }
8
+
9
+ headers do
10
+ attribute :foo
11
+ attribute :bar, type: Dry::Types["strict.string"]
12
+ attribute :baz, optional: true
13
+ end
14
+
15
+ response 200
16
+ end
17
+
18
+ operation :clear_data do
19
+ end
20
+
21
+ operation :get_data do
22
+ headers
23
+ end
24
+
25
+ operation :find_data do |settings|
26
+ headers do
27
+ attribute :foo if settings.version < 2
28
+ attribute :bar, type: Dry::Types["coercible.int"]
29
+ end
30
+ end
31
+ end
32
+
33
+ stub_request(:any, //)
34
+ end
35
+
36
+ let(:path) { "https://foo.example.com/api/v3/data" }
37
+ let(:client) { Test::Client.new("foo", version: 3, user: "bar") }
38
+
39
+ it "uses default value" do
40
+ client.operations[:clear_data].call foo: "FOO", bar: "BAR", baz: "BAZ"
41
+
42
+ request = a_request(:get, path) do |req|
43
+ expect(req.headers).to include "Foo" => "FOO",
44
+ "Bar" => "BAR",
45
+ "Baz" => "BAZ"
46
+ end
47
+
48
+ expect(request).to have_been_made
49
+ end
50
+
51
+ it "ingnores unknown attributes" do
52
+ client.operations[:clear_data]
53
+ .call foo: "FOO", bar: "BAR", baz: "BAZ", qux: "QUX"
54
+
55
+ request = a_request(:get, path).with do |req|
56
+ expect(req.headers.keys).not_to include "Qux"
57
+ end
58
+
59
+ expect(request).to have_been_made
60
+ end
61
+
62
+ it "requires mandatory headers" do
63
+ expect { client.operations[:clear_data].call bar: "BAR", baz: "BAZ" }
64
+ .to raise_error(ArgumentError)
65
+ end
66
+
67
+ it "applies type constraints" do
68
+ expect { client.operations[:clear_data].call foo: "FOO", bar: :BAR }
69
+ .to raise_error(TypeError)
70
+ end
71
+
72
+ it "skips optional headers" do
73
+ client.operations[:clear_data].call foo: "FOO", bar: "BAR"
74
+
75
+ request = a_request(:get, path).with do |req|
76
+ expect(req.headers.keys).not_to include "Baz"
77
+ end
78
+
79
+ expect(request).to have_been_made
80
+ end
81
+
82
+ it "can undefine default headers" do
83
+ client.operations[:get_data].call
84
+
85
+ expect(a_request(:get, path)).to have_been_made
86
+ end
87
+
88
+ it "can reload default headers for a specific operation" do
89
+ client.operations[:find_data].call foo: "FOO", bar: "01", baz: "BAZ"
90
+
91
+ request = a_request(:get, path).with do |req|
92
+ expect(req.headers).to include "Bar" => "1"
93
+ expect(req.headers.keys).not_to include "Foo"
94
+ expect(req.headers.keys).not_to include "Baz"
95
+ end
96
+
97
+ expect(request).to have_been_made
98
+ end
99
+ end
@@ -0,0 +1,45 @@
1
+ RSpec.describe "operation with http_method" do
2
+ # see Test::Client definition in `/spec/support/test_client.rb`
3
+ before do
4
+ class Test::Client < Evil::Client
5
+ operation do |settings|
6
+ http_method settings.version > 1 ? :post : :get
7
+ path { "data" }
8
+ response 200
9
+ end
10
+
11
+ operation :clear_data do
12
+ http_method :delete
13
+ end
14
+
15
+ operation :find_data
16
+
17
+ operation :reset_data do |settings|
18
+ http_method settings.version > 2 ? :patch : :put
19
+ end
20
+ end
21
+
22
+ stub_request(:any, //)
23
+ end
24
+
25
+ let(:path) { "https://foo.example.com/api/v3/data" }
26
+ let(:client) { Test::Client.new("foo", version: 3, user: "bar") }
27
+
28
+ it "uses default value" do
29
+ client.operations[:find_data].call
30
+
31
+ expect(a_request(:post, path)).to have_been_made
32
+ end
33
+
34
+ it "reloads default value with operation-specific one" do
35
+ client.operations[:clear_data].call
36
+
37
+ expect(a_request(:delete, path)).to have_been_made
38
+ end
39
+
40
+ it "is customizeable by settings" do
41
+ client.operations[:reset_data].call
42
+
43
+ expect(a_request(:patch, path)).to have_been_made
44
+ end
45
+ end
@@ -0,0 +1,156 @@
1
+ RSpec.describe "operation with json body" do
2
+ # see Test::Client definition in `/spec/support/test_client.rb`
3
+ before do
4
+ class Test::User < Evil::Client::Model
5
+ attribute :name
6
+ end
7
+
8
+ class Test::Client < Evil::Client
9
+ operation do
10
+ http_method :get
11
+ path { "users" }
12
+ response 200
13
+ end
14
+ end
15
+
16
+ stub_request(:get, //)
17
+ end
18
+
19
+ let(:path) { "https://foo.example.com/api/v3/users" }
20
+ let(:client) { Test::Client.new "foo", user: "bar", version: 3, token: "baz" }
21
+ let(:operation) { client.operations[:example] }
22
+
23
+ context "without operation-specific definition" do
24
+ before do
25
+ class Test::Client < Evil::Client
26
+ operation do
27
+ http_method :get
28
+ path { "users" }
29
+ response 200
30
+
31
+ body do
32
+ attribute :foo
33
+ end
34
+ end
35
+
36
+ operation :example
37
+ end
38
+ end
39
+
40
+ it "uses the default one" do
41
+ request = a_request(:get, path).with do |req|
42
+ expect(req.body).to eq '{"foo":[{"bar":["BAZ"]}]}'
43
+ expect(req.headers).to include "Content-Type" => "application/json"
44
+ end
45
+
46
+ operation.call foo: [{ bar: [:BAZ] }], baz: :QUX
47
+
48
+ expect(request).to have_been_made
49
+ end
50
+ end
51
+
52
+ context "with operation-specific definition" do
53
+ before do
54
+ class Test::Client < Evil::Client
55
+ operation do
56
+ http_method :get
57
+ path { "users" }
58
+ response 200
59
+
60
+ body do
61
+ attribute :foo
62
+ end
63
+ end
64
+
65
+ operation :example do
66
+ body do
67
+ attribute :baz
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ it "uses the specific one" do
74
+ request = a_request(:get, path).with body: '{"baz":"QUX"}'
75
+
76
+ operation.call foo: [{ bar: [:BAZ] }], baz: :QUX
77
+
78
+ expect(request).to have_been_made
79
+ end
80
+ end
81
+
82
+ context "when appended with files" do
83
+ before do
84
+ class Test::Client < Evil::Client
85
+ operation :example do
86
+ body do
87
+ attribute :baz
88
+ end
89
+
90
+ files do
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ it "skips body definition" do
97
+ request = a_request(:get, path).with do |req|
98
+ expect(req.body).to be_nil
99
+ end
100
+
101
+ operation.call foo: [{ bar: [:BAZ] }], baz: :QUX
102
+
103
+ expect(request).to have_been_made
104
+ end
105
+ end
106
+
107
+ context "with a model" do
108
+ before do
109
+ class Test::Client < Evil::Client
110
+ operation :example do
111
+ body model: Test::User do
112
+ attribute :foo
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ it "extends the model" do
119
+ request = a_request(:get, path).with body: '{"name":"Andy","foo":"BAR"}'
120
+
121
+ operation.call foo: :BAR, name: "Andy"
122
+
123
+ expect(request).to have_been_made
124
+ end
125
+ end
126
+
127
+ context "with a model" do
128
+ before do
129
+ class Test::Client < Evil::Client
130
+ operation :example do
131
+ body do
132
+ attribute :foo, type: Dry::Types["strict.string"]
133
+ attribute :bar, default: proc { 1 }
134
+ attribute :baz, optional: true
135
+ end
136
+ end
137
+ end
138
+ end
139
+
140
+ it "requires mandatory arguments" do
141
+ expect { operation.call bar: 2, baz: 3 }.to raise_error(ArgumentError)
142
+ end
143
+
144
+ it "applies type restrictions" do
145
+ expect { operation.call foo: :FOO }.to raise_error(TypeError)
146
+ end
147
+
148
+ it "uses default values and skips undefined optional attributes" do
149
+ request = a_request(:get, path).with body: '{"foo":"BAR","bar":1}'
150
+
151
+ operation.call foo: "BAR"
152
+
153
+ expect(request).to have_been_made
154
+ end
155
+ end
156
+ end