rspec-rest 0.1.0 → 0.2.0
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 +4 -4
- data/.gitignore +3 -0
- data/CHANGELOG.md +22 -0
- data/Gemfile.lock +1 -1
- data/README.md +33 -3
- data/lib/rspec/rest/dsl.rb +26 -2
- data/lib/rspec/rest/expectations.rb +19 -28
- data/lib/rspec/rest/json_item_expectations.rb +52 -0
- data/lib/rspec/rest/path_composer.rb +15 -0
- data/lib/rspec/rest/session.rb +6 -3
- data/lib/rspec/rest/version.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6d00a1ef08b51bc62ee1b737217eb5f441ee625ca3cbd848d0db1cbacff619f2
|
|
4
|
+
data.tar.gz: 6507ec718a8f8a0ecf5de375e2efba9f3d686cc577258baf89a05d9ccc47d64d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 742d99b9ceecc699b920038a11476f304a657b1023599d819fc69fd17f19154387745ceffeabda7cc98b36ccf065ba6c164f2eb3c618c34822e68ecd9e4f50b6
|
|
7
|
+
data.tar.gz: 6c80fa3a4b6f088172aa9b795ff8fc48e82cc7f64052bdf3b0028e3741983d6c73088bb72a29be44f9575da1ecc57c70b97d4a8f428b96f4385a5aded4805e81
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,28 @@ Semantic Versioning.
|
|
|
9
9
|
|
|
10
10
|
No changes yet.
|
|
11
11
|
|
|
12
|
+
## [0.2.0] - 2026-03-08
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- JSON array item expectation helpers:
|
|
16
|
+
- `expect_json_first(expected = nil, &block)`
|
|
17
|
+
- `expect_json_item(index, expected = nil, &block)`
|
|
18
|
+
- `expect_json_last(expected = nil, &block)`
|
|
19
|
+
- Optional behavior descriptions on verb DSL calls:
|
|
20
|
+
- `get(path, description = nil) { ... }` (and same for other verbs)
|
|
21
|
+
- Full-route example naming support in DSL output (composed from `base_path` + resource path + endpoint path).
|
|
22
|
+
- Shared path composition utility used by both request execution and example naming.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- README examples and expectation helper docs updated for:
|
|
26
|
+
- Ruby-style JSON item helpers
|
|
27
|
+
- Optional verb descriptions and full-route example names
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- `expect_json_item` now validates index type and reports non-integer indexes via actionable expectation failures.
|
|
31
|
+
- JSON value assertion semantics are centralized across `expect_json`, `expect_json_at`, and JSON item helpers to reduce drift.
|
|
32
|
+
- Unknown/invalid JSON item and contract expectation failures continue to include enriched request/response/curl diagnostics.
|
|
33
|
+
|
|
12
34
|
## [0.1.0] - 2026-03-07
|
|
13
35
|
|
|
14
36
|
### Added
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -123,13 +123,13 @@ RSpec.describe "Posts API" do
|
|
|
123
123
|
with_auth auth_token
|
|
124
124
|
with_query per_page: 10
|
|
125
125
|
|
|
126
|
-
get "/" do
|
|
126
|
+
get "/", "returns first page of posts for authenticated user" do
|
|
127
127
|
query page: 1
|
|
128
128
|
|
|
129
129
|
expect_status 200
|
|
130
130
|
expect_json array_of(expect_json_contract(:post_summary))
|
|
131
|
-
|
|
132
|
-
|
|
131
|
+
expect_json_first hash_including("id" => posts.first.id)
|
|
132
|
+
expect_json_item(0) { |item| expect(item["author"]["id"]).to eq(posts.first.author.id) }
|
|
133
133
|
expect_page_size 10
|
|
134
134
|
expect_max_page_size 20
|
|
135
135
|
end
|
|
@@ -188,6 +188,7 @@ Supported config:
|
|
|
188
188
|
|
|
189
189
|
- `resource "/users" do ... end`
|
|
190
190
|
- `get`, `post`, `put`, `patch`, `delete`
|
|
191
|
+
- optional form: `get(path, description = nil) { ... }`
|
|
191
192
|
|
|
192
193
|
Resource paths are composable and support placeholders:
|
|
193
194
|
|
|
@@ -202,6 +203,20 @@ resource "/users" do
|
|
|
202
203
|
end
|
|
203
204
|
```
|
|
204
205
|
|
|
206
|
+
Example with an explicit behavior name:
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
resource "/users" do
|
|
210
|
+
get "/", "returns public users for authenticated client" do
|
|
211
|
+
expect_status 200
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
RSpec example output uses the composed full route (including `base_path`) and appends the optional description, for example:
|
|
217
|
+
- `GET /v1/users - returns public users for authenticated client`
|
|
218
|
+
|
|
219
|
+
Note: Example names (including `base_path`) are composed when the verb macro is evaluated. To ensure the example names include `base_path`, declare your `api` (and its `base_path`) before defining `resource` blocks and their verbs. If you configure `api`/`base_path` afterward, requests will use the configured `base_path`, but the previously defined example names will not reflect it.
|
|
205
220
|
## Shared Request Presets
|
|
206
221
|
|
|
207
222
|
Define shared request defaults at group/resource scope:
|
|
@@ -288,6 +303,9 @@ Available expectation helpers:
|
|
|
288
303
|
- `expect_json(expected = nil, &block)`
|
|
289
304
|
- `expect_json_contract(name)`
|
|
290
305
|
- `expect_json_at(selector, expected = nil, &block)`
|
|
306
|
+
- `expect_json_first(expected = nil, &block)`
|
|
307
|
+
- `expect_json_item(index, expected = nil, &block)`
|
|
308
|
+
- `expect_json_last(expected = nil, &block)`
|
|
291
309
|
- `expect_error(status:, message: nil, includes: nil, field: nil, key: "error")`
|
|
292
310
|
- `expect_page_size(size, selector: "$")`
|
|
293
311
|
- `expect_max_page_size(max, selector: "$")`
|
|
@@ -311,6 +329,18 @@ Available expectation helpers:
|
|
|
311
329
|
- block mode:
|
|
312
330
|
- `expect_json_at "$.items[0]" { |item| expect(item["id"]).to integer }`
|
|
313
331
|
|
|
332
|
+
For common array-item checks, use Ruby-style helpers instead of selector strings:
|
|
333
|
+
|
|
334
|
+
- `expect_json_first(...)`
|
|
335
|
+
- `expect_json_item(index, ...)`
|
|
336
|
+
- `expect_json_last(...)`
|
|
337
|
+
|
|
338
|
+
```ruby
|
|
339
|
+
expect_json_first hash_including("id" => integer)
|
|
340
|
+
expect_json_item 2, hash_including("name" => "Third")
|
|
341
|
+
expect_json_last { |item| expect(item["id"]).to integer }
|
|
342
|
+
```
|
|
343
|
+
|
|
314
344
|
`expect_error` is a convenience helper for common API error payload assertions:
|
|
315
345
|
|
|
316
346
|
```ruby
|
data/lib/rspec/rest/dsl.rb
CHANGED
|
@@ -7,6 +7,7 @@ require_relative "class_level_presets"
|
|
|
7
7
|
require_relative "errors"
|
|
8
8
|
require_relative "expectations"
|
|
9
9
|
require_relative "json_selector"
|
|
10
|
+
require_relative "path_composer"
|
|
10
11
|
require_relative "request_builders"
|
|
11
12
|
require_relative "session"
|
|
12
13
|
module RSpec
|
|
@@ -77,10 +78,16 @@ module RSpec
|
|
|
77
78
|
end
|
|
78
79
|
|
|
79
80
|
HTTP_METHODS.each do |method|
|
|
80
|
-
define_method(method) do |path, &block|
|
|
81
|
+
define_method(method) do |path, description = nil, &block|
|
|
81
82
|
resource_path = current_resource_path
|
|
82
83
|
request_presets = deep_dup_presets(current_request_presets)
|
|
83
|
-
|
|
84
|
+
example_name = build_example_name(
|
|
85
|
+
method: method,
|
|
86
|
+
path: path,
|
|
87
|
+
resource_path: resource_path,
|
|
88
|
+
description: description
|
|
89
|
+
)
|
|
90
|
+
it(example_name) do
|
|
84
91
|
start_rest_request(
|
|
85
92
|
method: method,
|
|
86
93
|
path: path,
|
|
@@ -109,6 +116,23 @@ module RSpec
|
|
|
109
116
|
|
|
110
117
|
private
|
|
111
118
|
|
|
119
|
+
def build_example_name(method:, path:, resource_path:, description:)
|
|
120
|
+
route = compose_route_for_example(resource_path: resource_path, endpoint_path: path)
|
|
121
|
+
base = "#{method.to_s.upcase} #{route}"
|
|
122
|
+
normalized_description = description.to_s.strip
|
|
123
|
+
return base if normalized_description.empty?
|
|
124
|
+
|
|
125
|
+
"#{base} - #{normalized_description}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def compose_route_for_example(resource_path:, endpoint_path:)
|
|
129
|
+
PathComposer.compose(
|
|
130
|
+
base_path: rest_config.base_path,
|
|
131
|
+
resource_path: resource_path,
|
|
132
|
+
endpoint_path: endpoint_path
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
112
136
|
def current_resource_path
|
|
113
137
|
stack = @rest_resource_stack || []
|
|
114
138
|
return nil if stack.empty?
|
|
@@ -5,6 +5,7 @@ require_relative "formatters/request_recorder"
|
|
|
5
5
|
require_relative "error_expectations"
|
|
6
6
|
require_relative "contract_expectations"
|
|
7
7
|
require_relative "header_expectations"
|
|
8
|
+
require_relative "json_item_expectations"
|
|
8
9
|
require_relative "json_selector"
|
|
9
10
|
require_relative "json_type_helpers"
|
|
10
11
|
require_relative "pagination_expectations"
|
|
@@ -15,6 +16,7 @@ module RSpec
|
|
|
15
16
|
include ContractExpectations
|
|
16
17
|
include ErrorExpectations
|
|
17
18
|
include HeaderExpectations
|
|
19
|
+
include JsonItemExpectations
|
|
18
20
|
include JsonTypeHelpers
|
|
19
21
|
include PaginationExpectations
|
|
20
22
|
|
|
@@ -27,46 +29,35 @@ module RSpec
|
|
|
27
29
|
def expect_json(expected = nil, &block)
|
|
28
30
|
with_request_dump_on_failure do
|
|
29
31
|
parsed = rest_response.json
|
|
30
|
-
|
|
31
|
-
if block
|
|
32
|
-
instance_exec(parsed, &block)
|
|
33
|
-
next parsed
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
next parsed if expected.nil?
|
|
37
|
-
|
|
38
|
-
if expected.respond_to?(:matches?)
|
|
39
|
-
expect(parsed).to expected
|
|
40
|
-
else
|
|
41
|
-
expect(parsed).to eq(expected)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
parsed
|
|
32
|
+
evaluate_json_value(parsed, expected, &block)
|
|
45
33
|
end
|
|
46
34
|
end
|
|
47
35
|
|
|
48
36
|
def expect_json_at(selector, expected = nil, &block)
|
|
49
37
|
with_request_dump_on_failure do
|
|
50
38
|
selected = JsonSelector.extract(rest_response.json, selector)
|
|
39
|
+
evaluate_json_value(selected, expected, &block)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
51
42
|
|
|
52
|
-
|
|
53
|
-
instance_exec(selected, &block)
|
|
54
|
-
next selected
|
|
55
|
-
end
|
|
43
|
+
private
|
|
56
44
|
|
|
57
|
-
|
|
45
|
+
def evaluate_json_value(value, expected = nil, &block)
|
|
46
|
+
if block
|
|
47
|
+
instance_exec(value, &block)
|
|
48
|
+
return value
|
|
49
|
+
end
|
|
58
50
|
|
|
59
|
-
|
|
60
|
-
expect(selected).to expected
|
|
61
|
-
else
|
|
62
|
-
expect(selected).to eq(expected)
|
|
63
|
-
end
|
|
51
|
+
return value if expected.nil?
|
|
64
52
|
|
|
65
|
-
|
|
53
|
+
if expected.respond_to?(:matches?)
|
|
54
|
+
expect(value).to expected
|
|
55
|
+
else
|
|
56
|
+
expect(value).to eq(expected)
|
|
66
57
|
end
|
|
67
|
-
end
|
|
68
58
|
|
|
69
|
-
|
|
59
|
+
value
|
|
60
|
+
end
|
|
70
61
|
|
|
71
62
|
def with_request_dump_on_failure
|
|
72
63
|
yield
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpec
|
|
4
|
+
module Rest
|
|
5
|
+
module JsonItemExpectations
|
|
6
|
+
def expect_json_item(index, expected = nil, &block)
|
|
7
|
+
with_request_dump_on_failure do
|
|
8
|
+
payload = json_array_payload
|
|
9
|
+
normalized_index = normalize_json_item_index(index)
|
|
10
|
+
item = payload.fetch(normalized_index)
|
|
11
|
+
evaluate_json_value(item, expected, &block)
|
|
12
|
+
rescue IndexError
|
|
13
|
+
raise ::RSpec::Expectations::ExpectationNotMetError,
|
|
14
|
+
"Index #{index.inspect} is out of bounds for JSON array of size #{payload.size}."
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def expect_json_first(expected = nil, &)
|
|
19
|
+
expect_json_item(0, expected, &)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def expect_json_last(expected = nil, &block)
|
|
23
|
+
with_request_dump_on_failure do
|
|
24
|
+
payload = json_array_payload
|
|
25
|
+
if payload.empty?
|
|
26
|
+
raise ::RSpec::Expectations::ExpectationNotMetError,
|
|
27
|
+
"Cannot select last item from an empty JSON array."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
evaluate_json_value(payload.last, expected, &block)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def json_array_payload
|
|
37
|
+
payload = rest_response.json
|
|
38
|
+
return payload if payload.is_a?(Array)
|
|
39
|
+
|
|
40
|
+
raise ::RSpec::Expectations::ExpectationNotMetError,
|
|
41
|
+
"Expected JSON payload to be an Array, got #{payload.class}."
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def normalize_json_item_index(index)
|
|
45
|
+
return index if index.is_a?(Integer)
|
|
46
|
+
|
|
47
|
+
raise ::RSpec::Expectations::ExpectationNotMetError,
|
|
48
|
+
"Expected JSON item index to be an Integer, got #{index.inspect} (#{index.class})."
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RSpec
|
|
4
|
+
module Rest
|
|
5
|
+
module PathComposer
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def compose(base_path:, resource_path:, endpoint_path:)
|
|
9
|
+
segments = [base_path, resource_path, endpoint_path].compact.map(&:to_s)
|
|
10
|
+
normalized = segments.map { |segment| segment.gsub(%r{\A/+|/+\z}, "") }.reject(&:empty?)
|
|
11
|
+
"/#{normalized.join('/')}"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/rspec/rest/session.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "json"
|
|
|
4
4
|
require "rack/test"
|
|
5
5
|
require "rack/utils"
|
|
6
6
|
require_relative "errors"
|
|
7
|
+
require_relative "path_composer"
|
|
7
8
|
require_relative "response"
|
|
8
9
|
|
|
9
10
|
module RSpec
|
|
@@ -123,9 +124,11 @@ module RSpec
|
|
|
123
124
|
end
|
|
124
125
|
|
|
125
126
|
def build_path(base_path, resource_path, endpoint_path)
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
PathComposer.compose(
|
|
128
|
+
base_path: base_path,
|
|
129
|
+
resource_path: resource_path,
|
|
130
|
+
endpoint_path: endpoint_path
|
|
131
|
+
)
|
|
129
132
|
end
|
|
130
133
|
|
|
131
134
|
def build_url(base_url, path)
|
data/lib/rspec/rest/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rspec-rest
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carl
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-08 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack-test
|
|
@@ -119,9 +119,11 @@ files:
|
|
|
119
119
|
- lib/rspec/rest/formatters/request_dump.rb
|
|
120
120
|
- lib/rspec/rest/formatters/request_recorder.rb
|
|
121
121
|
- lib/rspec/rest/header_expectations.rb
|
|
122
|
+
- lib/rspec/rest/json_item_expectations.rb
|
|
122
123
|
- lib/rspec/rest/json_selector.rb
|
|
123
124
|
- lib/rspec/rest/json_type_helpers.rb
|
|
124
125
|
- lib/rspec/rest/pagination_expectations.rb
|
|
126
|
+
- lib/rspec/rest/path_composer.rb
|
|
125
127
|
- lib/rspec/rest/request_builders.rb
|
|
126
128
|
- lib/rspec/rest/response.rb
|
|
127
129
|
- lib/rspec/rest/session.rb
|