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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 93914e31c974ea40630915bca0148c886b24608cca438d627f58b7ae83a3c4b5
4
- data.tar.gz: d31e2d01339e37c1d432dc8b36333ba8e707d2e2ebff5637c7ebbce94093ba9b
3
+ metadata.gz: 6d00a1ef08b51bc62ee1b737217eb5f441ee625ca3cbd848d0db1cbacff619f2
4
+ data.tar.gz: 6507ec718a8f8a0ecf5de375e2efba9f3d686cc577258baf89a05d9ccc47d64d
5
5
  SHA512:
6
- metadata.gz: 908e78cda54bbd8dc33be4001429e6d9ca36e0111fcd8da1aa54bded587a27225828663bcbf19c2a130c4c1d634af3f68d8d7eab270dbb2d5769459861b7d5cc
7
- data.tar.gz: 8aed0d1c0d77ce8381169b1ed4babb90ed9f02b9b9749fe12f0f7f7724d6bc73cd9fb4fbb2ad46d75b2eeb2254c29fd7b1ea72fc3642b8ac92d2f9bd0269bdc3
6
+ metadata.gz: 742d99b9ceecc699b920038a11476f304a657b1023599d819fc69fd17f19154387745ceffeabda7cc98b36ccf065ba6c164f2eb3c618c34822e68ecd9e4f50b6
7
+ data.tar.gz: 6c80fa3a4b6f088172aa9b795ff8fc48e82cc7f64052bdf3b0028e3741983d6c73088bb72a29be44f9575da1ecc57c70b97d4a8f428b96f4385a5aded4805e81
data/.gitignore CHANGED
@@ -67,3 +67,6 @@ yarn-debug.log*
67
67
  /storage/*
68
68
  !/storage/.keep
69
69
  /public/uploads
70
+
71
+ # Local planning notes
72
+ /docs/plans/
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rspec-rest (0.1.0)
4
+ rspec-rest (0.2.0)
5
5
  rack-test (~> 2.1)
6
6
 
7
7
  GEM
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
- expect_json_at "$[0].id", posts.first.id
132
- expect_json_at "$[0].author.id", posts.first.author.id
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
@@ -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
- it("#{method.to_s.upcase} #{path}") do
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
- if block
53
- instance_exec(selected, &block)
54
- next selected
55
- end
43
+ private
56
44
 
57
- next selected if expected.nil?
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
- if expected.respond_to?(:matches?)
60
- expect(selected).to expected
61
- else
62
- expect(selected).to eq(expected)
63
- end
51
+ return value if expected.nil?
64
52
 
65
- selected
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
- private
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
@@ -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
- segments = [base_path, resource_path, endpoint_path].compact.map(&:to_s)
127
- normalized = segments.map { |segment| segment.gsub(%r{\A/+|/+\z}, "") }.reject(&:empty?)
128
- "/#{normalized.join('/')}"
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)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RSpec
4
4
  module Rest
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
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.1.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-07 00:00:00.000000000 Z
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