Skip to content

Commit e308434

Browse files
committed
refactor: move OpenApiSpec attribute to core namespace for framework-agnostic use
Extract OpenApiSpec attribute from Laravel namespace to Studio\OpenApiContractTesting and introduce OpenApiSpecResolver trait with an openApiSpecFallback() hook. The Laravel trait now composes OpenApiSpecResolver and overrides the fallback for config-based defaults.
1 parent 0274a7a commit e308434

7 files changed

Lines changed: 142 additions & 26 deletions

README.md

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ class GetPetsTest extends TestCase
109109
To use a different spec for a specific test class, add the `#[OpenApiSpec]` attribute:
110110

111111
```php
112-
use Studio\OpenApiContractTesting\Laravel\OpenApiSpec;
112+
use Studio\OpenApiContractTesting\OpenApiSpec;
113113
use Studio\OpenApiContractTesting\Laravel\ValidatesOpenApiSchema;
114114

115115
#[OpenApiSpec('admin')]
@@ -153,6 +153,38 @@ Resolution priority (highest to lowest):
153153
154154
#### Framework-agnostic
155155

156+
You can use the `#[OpenApiSpec]` attribute with the `OpenApiSpecResolver` trait in any PHPUnit test:
157+
158+
```php
159+
use Studio\OpenApiContractTesting\OpenApiSpec;
160+
use Studio\OpenApiContractTesting\OpenApiSpecResolver;
161+
use Studio\OpenApiContractTesting\OpenApiResponseValidator;
162+
163+
#[OpenApiSpec('front')]
164+
class GetPetsTest extends TestCase
165+
{
166+
use OpenApiSpecResolver;
167+
168+
public function test_list_pets(): void
169+
{
170+
$specName = $this->resolveOpenApiSpec(); // 'front'
171+
$validator = new OpenApiResponseValidator();
172+
$result = $validator->validate(
173+
specName: $specName,
174+
method: 'GET',
175+
requestPath: '/api/v1/pets',
176+
statusCode: 200,
177+
responseBody: $decodedJsonBody,
178+
responseContentType: 'application/json',
179+
);
180+
181+
$this->assertTrue($result->isValid(), $result->errorMessage());
182+
}
183+
}
184+
```
185+
186+
Or without the attribute, pass the spec name directly:
187+
156188
```php
157189
use Studio\OpenApiContractTesting\OpenApiResponseValidator;
158190
use Studio\OpenApiContractTesting\OpenApiSpecLoader;

src/Laravel/ValidatesOpenApiSchema.php

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,10 @@
66

77
use Illuminate\Testing\TestResponse;
88
use JsonException;
9-
use ReflectionClass;
10-
use ReflectionMethod;
119
use Studio\OpenApiContractTesting\HttpMethod;
1210
use Studio\OpenApiContractTesting\OpenApiCoverageTracker;
1311
use Studio\OpenApiContractTesting\OpenApiResponseValidator;
12+
use Studio\OpenApiContractTesting\OpenApiSpecResolver;
1413

1514
use function is_numeric;
1615
use function is_string;
@@ -19,6 +18,8 @@
1918

2019
trait ValidatesOpenApiSchema
2120
{
21+
use OpenApiSpecResolver;
22+
2223
protected function openApiSpec(): string
2324
{
2425
$spec = config('openapi-contract-testing.default_spec');
@@ -30,6 +31,11 @@ protected function openApiSpec(): string
3031
return $spec;
3132
}
3233

34+
protected function openApiSpecFallback(): string
35+
{
36+
return $this->openApiSpec();
37+
}
38+
3339
protected function assertResponseMatchesOpenApiSchema(
3440
TestResponse $response,
3541
?HttpMethod $method = null,
@@ -86,27 +92,6 @@ protected function assertResponseMatchesOpenApiSchema(
8692
);
8793
}
8894

89-
private function resolveOpenApiSpec(): string
90-
{
91-
// 1. Method-level #[OpenApiSpec] attribute
92-
$methodName = $this->name(); // @phpstan-ignore method.notFound
93-
$refMethod = new ReflectionMethod($this, $methodName);
94-
$methodAttrs = $refMethod->getAttributes(OpenApiSpec::class);
95-
if ($methodAttrs !== []) {
96-
return $methodAttrs[0]->newInstance()->name;
97-
}
98-
99-
// 2. Class-level #[OpenApiSpec] attribute
100-
$refClass = new ReflectionClass($this);
101-
$classAttrs = $refClass->getAttributes(OpenApiSpec::class);
102-
if ($classAttrs !== []) {
103-
return $classAttrs[0]->newInstance()->name;
104-
}
105-
106-
// 3. openApiSpec() method override / config default
107-
return $this->openApiSpec();
108-
}
109-
11095
/** @return null|array<string, mixed> */
11196
private function extractJsonBody(TestResponse $response, string $content, string $contentType): ?array
11297
{
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
declare(strict_types=1);
44

5-
namespace Studio\OpenApiContractTesting\Laravel;
5+
namespace Studio\OpenApiContractTesting;
66

77
use Attribute;
88

src/OpenApiSpecResolver.php

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Studio\OpenApiContractTesting;
6+
7+
use ReflectionClass;
8+
use ReflectionMethod;
9+
10+
trait OpenApiSpecResolver
11+
{
12+
protected function openApiSpecFallback(): string
13+
{
14+
return '';
15+
}
16+
17+
private function resolveOpenApiSpec(): string
18+
{
19+
// 1. Method-level #[OpenApiSpec] attribute
20+
$methodName = $this->name(); // @phpstan-ignore method.notFound
21+
$refMethod = new ReflectionMethod($this, $methodName);
22+
$methodAttrs = $refMethod->getAttributes(OpenApiSpec::class);
23+
if ($methodAttrs !== []) {
24+
return $methodAttrs[0]->newInstance()->name;
25+
}
26+
27+
// 2. Class-level #[OpenApiSpec] attribute
28+
$refClass = new ReflectionClass($this);
29+
$classAttrs = $refClass->getAttributes(OpenApiSpec::class);
30+
if ($classAttrs !== []) {
31+
return $classAttrs[0]->newInstance()->name;
32+
}
33+
34+
// 3. Subclass hook (e.g. openApiSpec() in Laravel trait)
35+
return $this->openApiSpecFallback();
36+
}
37+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Studio\OpenApiContractTesting\Tests\Unit;
6+
7+
use PHPUnit\Framework\Attributes\Test;
8+
use PHPUnit\Framework\TestCase;
9+
use Studio\OpenApiContractTesting\OpenApiSpecResolver;
10+
11+
class OpenApiSpecResolverFallbackTest extends TestCase
12+
{
13+
use OpenApiSpecResolver;
14+
15+
#[Test]
16+
public function fallback_is_used_when_no_attribute_present(): void
17+
{
18+
$this->assertSame('from-fallback', $this->resolveOpenApiSpec());
19+
}
20+
21+
protected function openApiSpecFallback(): string
22+
{
23+
return 'from-fallback';
24+
}
25+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Studio\OpenApiContractTesting\Tests\Unit;
6+
7+
use PHPUnit\Framework\Attributes\Test;
8+
use PHPUnit\Framework\TestCase;
9+
use Studio\OpenApiContractTesting\OpenApiSpec;
10+
use Studio\OpenApiContractTesting\OpenApiSpecResolver;
11+
12+
#[OpenApiSpec('petstore-3.0')]
13+
class OpenApiSpecResolverTest extends TestCase
14+
{
15+
use OpenApiSpecResolver;
16+
17+
#[Test]
18+
public function class_level_attribute_is_resolved(): void
19+
{
20+
$this->assertSame('petstore-3.0', $this->resolveOpenApiSpec());
21+
}
22+
23+
#[Test]
24+
#[OpenApiSpec('petstore-3.1')]
25+
public function method_level_attribute_overrides_class_level(): void
26+
{
27+
$this->assertSame('petstore-3.1', $this->resolveOpenApiSpec());
28+
}
29+
30+
#[Test]
31+
public function fallback_returns_empty_string_when_no_attribute(): void
32+
{
33+
// This test class has a class-level attribute, so we verify
34+
// the fallback method itself returns empty by default.
35+
$this->assertSame('', $this->openApiSpecFallback());
36+
}
37+
}

tests/Unit/ValidatesOpenApiSchemaAttributeTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
use PHPUnit\Framework\Attributes\Test;
1010
use PHPUnit\Framework\TestCase;
1111
use Studio\OpenApiContractTesting\HttpMethod;
12-
use Studio\OpenApiContractTesting\Laravel\OpenApiSpec;
1312
use Studio\OpenApiContractTesting\Laravel\ValidatesOpenApiSchema;
1413
use Studio\OpenApiContractTesting\OpenApiCoverageTracker;
14+
use Studio\OpenApiContractTesting\OpenApiSpec;
1515
use Studio\OpenApiContractTesting\OpenApiSpecLoader;
1616
use Studio\OpenApiContractTesting\Tests\Helpers\CreatesTestResponse;
1717

0 commit comments

Comments
 (0)