swaggerforge 0.3.1__tar.gz → 0.4.0__tar.gz
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.
- {swaggerforge-0.3.1/src/swaggerforge.egg-info → swaggerforge-0.4.0}/PKG-INFO +91 -5
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/README.md +90 -4
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/pyproject.toml +1 -1
- swaggerforge-0.4.0/src/swaggerforge/dependencies.py +129 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge/generator.py +255 -24
- swaggerforge-0.4.0/src/swaggerforge/models.py +85 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge/parser.py +79 -5
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge/template.py +171 -4
- swaggerforge-0.4.0/src/swaggerforge/templates/test_file.py.j2 +96 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0/src/swaggerforge.egg-info}/PKG-INFO +91 -5
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/SOURCES.txt +3 -0
- swaggerforge-0.4.0/tests/test_dependencies.py +218 -0
- swaggerforge-0.4.0/tests/test_e2e_stateful.py +207 -0
- swaggerforge-0.4.0/tests/test_generator.py +816 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/tests/test_parser.py +112 -0
- swaggerforge-0.4.0/tests/test_template.py +548 -0
- swaggerforge-0.3.1/src/swaggerforge/models.py +0 -47
- swaggerforge-0.3.1/src/swaggerforge/templates/test_file.py.j2 +0 -40
- swaggerforge-0.3.1/tests/test_generator.py +0 -332
- swaggerforge-0.3.1/tests/test_template.py +0 -290
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/LICENSE +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/setup.cfg +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge/__init__.py +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge/__main__.py +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge/auth.py +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge/cli.py +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge/config.py +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge/output.py +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge/validator.py +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/dependency_links.txt +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/entry_points.txt +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/requires.txt +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/top_level.txt +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/tests/test_auth.py +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/tests/test_config.py +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/tests/test_e2e_auth.py +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/tests/test_output.py +0 -0
- {swaggerforge-0.3.1 → swaggerforge-0.4.0}/tests/test_validator.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: swaggerforge
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Automatic pytest test generation from OpenAPI (Swagger) specifications
|
|
5
5
|
Author: Viktor Pylypenko
|
|
6
6
|
License-Expression: MIT
|
|
@@ -43,12 +43,18 @@ established test-design techniques.
|
|
|
43
43
|
response schema
|
|
44
44
|
- **Missing required field** >>> omits a required field, expects 400
|
|
45
45
|
- **Wrong data type** >>> sends a mistyped field, expects 400/422
|
|
46
|
-
- **Nonexistent resource** >>> requests
|
|
46
|
+
- **Nonexistent resource** >>> requests a resource that was created and then
|
|
47
|
+
deleted, so its absence is guaranteed (or, when the API offers no delete,
|
|
48
|
+
an unlikely identifier); expects 404
|
|
47
49
|
- **Boundary values** >>> tests values at and just beyond declared
|
|
48
50
|
numeric/length limits (Boundary Value Analysis)
|
|
49
51
|
- **Boolean coverage** >>> exercises both `true` and `false` for boolean fields
|
|
52
|
+
- Stateful tests: operations on an existing resource run against one the test
|
|
53
|
+
creates itself and removes afterwards, instead of a guessed identifier
|
|
54
|
+
- Valid request data built from the specification's own examples, defaults,
|
|
55
|
+
enums and constraints
|
|
50
56
|
- Deterministic output: the same specification always produces identical tests
|
|
51
|
-
- Generated files use
|
|
57
|
+
- Generated files use pytest fixtures and run with no manual edits
|
|
52
58
|
- Optional `swaggerforge.toml` config file for project-level defaults
|
|
53
59
|
- Comprehensive authentication: bearer, basic, API key (header/query/cookie),
|
|
54
60
|
multiple-scheme AND/OR requirements, and OAuth2 / OpenID Connect (optional
|
|
@@ -192,12 +198,79 @@ an error. If a specification requires a scheme whose credentials are not
|
|
|
192
198
|
configured, SwaggerForge prints a warning naming the scheme and generates the
|
|
193
199
|
tests without that authentication rather than failing.
|
|
194
200
|
|
|
201
|
+
## Stateful tests
|
|
202
|
+
|
|
203
|
+
Endpoints that act on an existing resource - `GET`, `PUT`, `PATCH` or
|
|
204
|
+
`DELETE` on `/pets/{petId}` - need a resource that really exists. SwaggerForge
|
|
205
|
+
detects the operation that creates it and generates fixtures that create the
|
|
206
|
+
resource before a test and remove it afterwards.
|
|
207
|
+
|
|
208
|
+
A create operation is recognised by REST convention: a `POST` on a collection
|
|
209
|
+
path (`/pets`) produces resources addressed by that path plus one parameter
|
|
210
|
+
(`/pets/{petId}`). The new resource's identifier is taken from the response
|
|
211
|
+
field named like the path parameter, or else from `id`. When the
|
|
212
|
+
specification declares explicit OpenAPI `links`, they take priority.
|
|
213
|
+
|
|
214
|
+
For each such resource, generated files contain two fixtures:
|
|
215
|
+
|
|
216
|
+
- `created_<resource>` creates the resource, yields its identifier (read from
|
|
217
|
+
the response body, or from the `Location` header) and deletes it after the
|
|
218
|
+
test
|
|
219
|
+
- `deleted_<resource>` creates the resource and deletes it immediately, giving
|
|
220
|
+
the nonexistent-resource test an identifier that is guaranteed not to exist
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
@pytest.fixture
|
|
224
|
+
def created_pet(base_url, api_session):
|
|
225
|
+
response = api_session.post(f"{base_url}/pet", json={'id': uuid.uuid4().int >> 80, 'name': 'doggie', 'photoUrls': ['string']})
|
|
226
|
+
assert response.status_code in [200], (
|
|
227
|
+
f"Creating pet failed: "
|
|
228
|
+
f"{response.status_code} {response.text}"
|
|
229
|
+
)
|
|
230
|
+
resource_id = _extract_id(response, 'id')
|
|
231
|
+
yield resource_id
|
|
232
|
+
try:
|
|
233
|
+
api_session.delete(f"{base_url}/pet/{resource_id}")
|
|
234
|
+
except requests.RequestException:
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_get_pet_by_id_success(base_url, api_session, created_pet):
|
|
239
|
+
"""GET /pet/{petId} - positive scenario"""
|
|
240
|
+
response = api_session.get(f"{base_url}/pet/{created_pet}")
|
|
241
|
+
assert response.status_code in [200]
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
*(Abridged from a file generated for the Swagger Petstore specification.)*
|
|
245
|
+
|
|
246
|
+
Every scenario on such an endpoint uses the created resource - negative
|
|
247
|
+
scenarios included, so a 404 for a missing resource cannot hide the 400 a
|
|
248
|
+
validation test expects. A top-level `id` in the create request is sent as a
|
|
249
|
+
fresh unique value on every run, so tests never share or collide on one
|
|
250
|
+
resource; whether the server keeps that value or assigns its own, the fixture
|
|
251
|
+
reads the actual identifier from the response. The generated files themselves
|
|
252
|
+
stay identical for the same specification - only the runtime data varies.
|
|
253
|
+
|
|
254
|
+
Endpoints without a recognisable create operation are generated exactly as
|
|
255
|
+
before, with example identifiers.
|
|
256
|
+
|
|
257
|
+
## Request data
|
|
258
|
+
|
|
259
|
+
Request bodies and parameters are built deterministically from the
|
|
260
|
+
specification, without random data. For each value, the first available
|
|
261
|
+
source is used: `const`, then `example` (or the first of `examples`), then
|
|
262
|
+
`default`, then the first `enum` member, and finally a placeholder matching
|
|
263
|
+
the declared type and format (for example `user@example.com` for an `email`).
|
|
264
|
+
Placeholder strings are fitted to `minLength` and `maxLength`. Properties
|
|
265
|
+
marked `readOnly` are left out of requests, as OpenAPI prescribes.
|
|
266
|
+
|
|
195
267
|
## How it works
|
|
196
268
|
|
|
197
269
|
SwaggerForge runs a six-stage pipeline: the specification is validated, parsed
|
|
198
270
|
into an internal model (with `$ref`s resolved), turned into test scenarios
|
|
199
|
-
based on test-design techniques
|
|
200
|
-
|
|
271
|
+
based on test-design techniques - with dependencies between operations
|
|
272
|
+
inferred, so stateful scenarios can share created resources - rendered into
|
|
273
|
+
pytest code via templates, and written to per-resource files.
|
|
201
274
|
|
|
202
275
|
## Limitations
|
|
203
276
|
|
|
@@ -207,6 +280,19 @@ written to per-resource files.
|
|
|
207
280
|
(authorization code) generate a placeholder for a manually obtained token,
|
|
208
281
|
as they require a browser-based login that cannot be automated
|
|
209
282
|
- Boundary tests require the specification to declare numeric/length constraints
|
|
283
|
+
- Stateful tests cover single-level resources (a collection and its items);
|
|
284
|
+
nested chains such as `/owners/{ownerId}/pets/{petId}` keep example
|
|
285
|
+
identifiers, and only `links` passing a top-level response field by
|
|
286
|
+
`operationId` are used
|
|
287
|
+
- APIs that soft-delete (a deleted resource still answers 200) fail the
|
|
288
|
+
nonexistent-resource test - a reflection of the API's semantics
|
|
289
|
+
- Resource fixtures use the file's session authentication; a create operation
|
|
290
|
+
that needs different credentials than the file's default is not overridden
|
|
291
|
+
- Only a top-level field named `id` is treated as a server-side surrogate key;
|
|
292
|
+
a differently named one is sent with its example value
|
|
293
|
+
- An identifier read from a `Location` header arrives as a string
|
|
294
|
+
- The create operation's own positive test does not remove the resource it
|
|
295
|
+
creates
|
|
210
296
|
|
|
211
297
|
## License
|
|
212
298
|
|
|
@@ -17,12 +17,18 @@ established test-design techniques.
|
|
|
17
17
|
response schema
|
|
18
18
|
- **Missing required field** >>> omits a required field, expects 400
|
|
19
19
|
- **Wrong data type** >>> sends a mistyped field, expects 400/422
|
|
20
|
-
- **Nonexistent resource** >>> requests
|
|
20
|
+
- **Nonexistent resource** >>> requests a resource that was created and then
|
|
21
|
+
deleted, so its absence is guaranteed (or, when the API offers no delete,
|
|
22
|
+
an unlikely identifier); expects 404
|
|
21
23
|
- **Boundary values** >>> tests values at and just beyond declared
|
|
22
24
|
numeric/length limits (Boundary Value Analysis)
|
|
23
25
|
- **Boolean coverage** >>> exercises both `true` and `false` for boolean fields
|
|
26
|
+
- Stateful tests: operations on an existing resource run against one the test
|
|
27
|
+
creates itself and removes afterwards, instead of a guessed identifier
|
|
28
|
+
- Valid request data built from the specification's own examples, defaults,
|
|
29
|
+
enums and constraints
|
|
24
30
|
- Deterministic output: the same specification always produces identical tests
|
|
25
|
-
- Generated files use
|
|
31
|
+
- Generated files use pytest fixtures and run with no manual edits
|
|
26
32
|
- Optional `swaggerforge.toml` config file for project-level defaults
|
|
27
33
|
- Comprehensive authentication: bearer, basic, API key (header/query/cookie),
|
|
28
34
|
multiple-scheme AND/OR requirements, and OAuth2 / OpenID Connect (optional
|
|
@@ -166,12 +172,79 @@ an error. If a specification requires a scheme whose credentials are not
|
|
|
166
172
|
configured, SwaggerForge prints a warning naming the scheme and generates the
|
|
167
173
|
tests without that authentication rather than failing.
|
|
168
174
|
|
|
175
|
+
## Stateful tests
|
|
176
|
+
|
|
177
|
+
Endpoints that act on an existing resource - `GET`, `PUT`, `PATCH` or
|
|
178
|
+
`DELETE` on `/pets/{petId}` - need a resource that really exists. SwaggerForge
|
|
179
|
+
detects the operation that creates it and generates fixtures that create the
|
|
180
|
+
resource before a test and remove it afterwards.
|
|
181
|
+
|
|
182
|
+
A create operation is recognised by REST convention: a `POST` on a collection
|
|
183
|
+
path (`/pets`) produces resources addressed by that path plus one parameter
|
|
184
|
+
(`/pets/{petId}`). The new resource's identifier is taken from the response
|
|
185
|
+
field named like the path parameter, or else from `id`. When the
|
|
186
|
+
specification declares explicit OpenAPI `links`, they take priority.
|
|
187
|
+
|
|
188
|
+
For each such resource, generated files contain two fixtures:
|
|
189
|
+
|
|
190
|
+
- `created_<resource>` creates the resource, yields its identifier (read from
|
|
191
|
+
the response body, or from the `Location` header) and deletes it after the
|
|
192
|
+
test
|
|
193
|
+
- `deleted_<resource>` creates the resource and deletes it immediately, giving
|
|
194
|
+
the nonexistent-resource test an identifier that is guaranteed not to exist
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
@pytest.fixture
|
|
198
|
+
def created_pet(base_url, api_session):
|
|
199
|
+
response = api_session.post(f"{base_url}/pet", json={'id': uuid.uuid4().int >> 80, 'name': 'doggie', 'photoUrls': ['string']})
|
|
200
|
+
assert response.status_code in [200], (
|
|
201
|
+
f"Creating pet failed: "
|
|
202
|
+
f"{response.status_code} {response.text}"
|
|
203
|
+
)
|
|
204
|
+
resource_id = _extract_id(response, 'id')
|
|
205
|
+
yield resource_id
|
|
206
|
+
try:
|
|
207
|
+
api_session.delete(f"{base_url}/pet/{resource_id}")
|
|
208
|
+
except requests.RequestException:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_get_pet_by_id_success(base_url, api_session, created_pet):
|
|
213
|
+
"""GET /pet/{petId} - positive scenario"""
|
|
214
|
+
response = api_session.get(f"{base_url}/pet/{created_pet}")
|
|
215
|
+
assert response.status_code in [200]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
*(Abridged from a file generated for the Swagger Petstore specification.)*
|
|
219
|
+
|
|
220
|
+
Every scenario on such an endpoint uses the created resource - negative
|
|
221
|
+
scenarios included, so a 404 for a missing resource cannot hide the 400 a
|
|
222
|
+
validation test expects. A top-level `id` in the create request is sent as a
|
|
223
|
+
fresh unique value on every run, so tests never share or collide on one
|
|
224
|
+
resource; whether the server keeps that value or assigns its own, the fixture
|
|
225
|
+
reads the actual identifier from the response. The generated files themselves
|
|
226
|
+
stay identical for the same specification - only the runtime data varies.
|
|
227
|
+
|
|
228
|
+
Endpoints without a recognisable create operation are generated exactly as
|
|
229
|
+
before, with example identifiers.
|
|
230
|
+
|
|
231
|
+
## Request data
|
|
232
|
+
|
|
233
|
+
Request bodies and parameters are built deterministically from the
|
|
234
|
+
specification, without random data. For each value, the first available
|
|
235
|
+
source is used: `const`, then `example` (or the first of `examples`), then
|
|
236
|
+
`default`, then the first `enum` member, and finally a placeholder matching
|
|
237
|
+
the declared type and format (for example `user@example.com` for an `email`).
|
|
238
|
+
Placeholder strings are fitted to `minLength` and `maxLength`. Properties
|
|
239
|
+
marked `readOnly` are left out of requests, as OpenAPI prescribes.
|
|
240
|
+
|
|
169
241
|
## How it works
|
|
170
242
|
|
|
171
243
|
SwaggerForge runs a six-stage pipeline: the specification is validated, parsed
|
|
172
244
|
into an internal model (with `$ref`s resolved), turned into test scenarios
|
|
173
|
-
based on test-design techniques
|
|
174
|
-
|
|
245
|
+
based on test-design techniques - with dependencies between operations
|
|
246
|
+
inferred, so stateful scenarios can share created resources - rendered into
|
|
247
|
+
pytest code via templates, and written to per-resource files.
|
|
175
248
|
|
|
176
249
|
## Limitations
|
|
177
250
|
|
|
@@ -181,6 +254,19 @@ written to per-resource files.
|
|
|
181
254
|
(authorization code) generate a placeholder for a manually obtained token,
|
|
182
255
|
as they require a browser-based login that cannot be automated
|
|
183
256
|
- Boundary tests require the specification to declare numeric/length constraints
|
|
257
|
+
- Stateful tests cover single-level resources (a collection and its items);
|
|
258
|
+
nested chains such as `/owners/{ownerId}/pets/{petId}` keep example
|
|
259
|
+
identifiers, and only `links` passing a top-level response field by
|
|
260
|
+
`operationId` are used
|
|
261
|
+
- APIs that soft-delete (a deleted resource still answers 200) fail the
|
|
262
|
+
nonexistent-resource test - a reflection of the API's semantics
|
|
263
|
+
- Resource fixtures use the file's session authentication; a create operation
|
|
264
|
+
that needs different credentials than the file's default is not overridden
|
|
265
|
+
- Only a top-level field named `id` is treated as a server-side surrogate key;
|
|
266
|
+
a differently named one is sent with its example value
|
|
267
|
+
- An identifier read from a `Location` header arrives as a string
|
|
268
|
+
- The create operation's own positive test does not remove the resource it
|
|
269
|
+
creates
|
|
184
270
|
|
|
185
271
|
## License
|
|
186
272
|
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""Inference of resource dependencies between OpenAPI operations.
|
|
2
|
+
|
|
3
|
+
Identifies create operations whose responses yield identifiers consumed by
|
|
4
|
+
operations on the corresponding item path. Explicit OpenAPI links on the
|
|
5
|
+
create's response take priority; otherwise inference follows the REST convention
|
|
6
|
+
that a POST on a collection path creates a resource addressed by the
|
|
7
|
+
collection path plus one path parameter. Only single-level relationships
|
|
8
|
+
are inferred; a POST on a path that already contains a parameter is not
|
|
9
|
+
treated as a producer.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
from swaggerforge.models import ResourceProducer
|
|
15
|
+
|
|
16
|
+
# matches a path segment consisting of exactly one parameter, e.g. "{petId}"
|
|
17
|
+
_PARAM_SEGMENT = re.compile(r"^\{([^{}/]+)\}$")
|
|
18
|
+
|
|
19
|
+
# finds every path parameter in a path, e.g. "/items/{itemId}" -> ["itemId"]
|
|
20
|
+
_PATH_PARAMS = re.compile(r"\{([^{}/]+)\}")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def find_producers(endpoints):
|
|
24
|
+
"""Infer resource producers from a list of endpoints.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
endpoints: A list of Endpoint objects from the parser.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A list of ResourceProducer objects, in specification order.
|
|
31
|
+
"""
|
|
32
|
+
by_path = {}
|
|
33
|
+
by_operation = {}
|
|
34
|
+
for endpoint in endpoints:
|
|
35
|
+
by_path.setdefault(endpoint.path, {})[endpoint.method] = endpoint
|
|
36
|
+
if endpoint.operation_id:
|
|
37
|
+
by_operation[endpoint.operation_id] = endpoint
|
|
38
|
+
|
|
39
|
+
producers = []
|
|
40
|
+
for path, methods in by_path.items():
|
|
41
|
+
create = methods.get("post")
|
|
42
|
+
if create is None or "{" in path:
|
|
43
|
+
continue
|
|
44
|
+
producer = (
|
|
45
|
+
_producer_from_links(path, create, by_path, by_operation)
|
|
46
|
+
or _producer_for(path, create, by_path)
|
|
47
|
+
)
|
|
48
|
+
if producer is not None:
|
|
49
|
+
producers.append(producer)
|
|
50
|
+
return producers
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _producer_from_links(collection_path, create, by_path, by_operation):
|
|
54
|
+
"""Build a producer from the create's explicit OpenAPI links, or None.
|
|
55
|
+
|
|
56
|
+
A link is usable when its target operation exists and its path has
|
|
57
|
+
exactly one path parameter, which the link supplies from the response
|
|
58
|
+
body. Explicit links are the specification's own statement of the
|
|
59
|
+
relationship, so they take priority over path inference.
|
|
60
|
+
"""
|
|
61
|
+
for target_id, parameters in create.links.items():
|
|
62
|
+
target = by_operation.get(target_id)
|
|
63
|
+
if target is None:
|
|
64
|
+
continue
|
|
65
|
+
params = _PATH_PARAMS.findall(target.path)
|
|
66
|
+
if len(params) != 1 or params[0] not in parameters:
|
|
67
|
+
continue
|
|
68
|
+
param = params[0]
|
|
69
|
+
return ResourceProducer(
|
|
70
|
+
resource=_resource_name(collection_path),
|
|
71
|
+
create=create,
|
|
72
|
+
item_path=target.path,
|
|
73
|
+
path_param=param,
|
|
74
|
+
id_field=parameters[param],
|
|
75
|
+
delete=by_path.get(target.path, {}).get("delete"),
|
|
76
|
+
)
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _producer_for(collection_path, create, by_path):
|
|
81
|
+
"""Build the producer for a collection POST, or None if none applies."""
|
|
82
|
+
for path, methods in by_path.items():
|
|
83
|
+
param = _single_param_child(collection_path, path)
|
|
84
|
+
if param is None:
|
|
85
|
+
continue
|
|
86
|
+
id_field = _id_field(create.response_schema, param)
|
|
87
|
+
if id_field is None:
|
|
88
|
+
continue
|
|
89
|
+
return ResourceProducer(
|
|
90
|
+
resource=_resource_name(collection_path),
|
|
91
|
+
create=create,
|
|
92
|
+
item_path=path,
|
|
93
|
+
path_param=param,
|
|
94
|
+
id_field=id_field,
|
|
95
|
+
delete=methods.get("delete"),
|
|
96
|
+
)
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _single_param_child(parent, path):
|
|
101
|
+
"""Return the parameter name if path is parent plus one parameter."""
|
|
102
|
+
prefix = parent.rstrip("/") + "/"
|
|
103
|
+
if not path.startswith(prefix):
|
|
104
|
+
return None
|
|
105
|
+
match = _PARAM_SEGMENT.match(path[len(prefix):])
|
|
106
|
+
return match.group(1) if match else None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _id_field(response_schema, param):
|
|
110
|
+
"""Pick the response field holding the identifier.
|
|
111
|
+
|
|
112
|
+
A field named exactly like the path parameter wins; otherwise a field
|
|
113
|
+
named 'id' is used. Returns None when neither exists.
|
|
114
|
+
"""
|
|
115
|
+
if not isinstance(response_schema, dict):
|
|
116
|
+
return None
|
|
117
|
+
properties = response_schema.get("properties", {})
|
|
118
|
+
if param in properties:
|
|
119
|
+
return param
|
|
120
|
+
if "id" in properties:
|
|
121
|
+
return "id"
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _resource_name(collection_path):
|
|
126
|
+
"""Derive a fixture-safe resource name from the last path segment."""
|
|
127
|
+
segment = collection_path.rstrip("/").rsplit("/", 1)[-1]
|
|
128
|
+
name = re.sub(r"[^a-zA-Z0-9_]", "_", segment).lower()
|
|
129
|
+
return name or "resource"
|