Skip to content
  • Sponsor
  • Notifications You must be signed in to change notification settings
  • Fork 6

Commit d7a10a0

Browse files
authoredSep 29, 2024··
feat: generate OpenAPI JSON from APIB (#602)
1 parent 71bb7ec commit d7a10a0

18 files changed

+1058
-338
lines changed
 

‎.github/workflows/test.yml

+83
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,89 @@ jobs:
8585
name: coverage-${{ matrix.php-versions }}
8686
path: coverage.xml
8787

88+
openapi:
89+
name: File generation
90+
needs: test
91+
runs-on: ubuntu-latest
92+
env:
93+
PHPDRAFT_THIRD_PARTY: 1
94+
extensions: curl,json,mbstring,uopz
95+
key: cache-v1 # can be any string, change to clear the extension cache.
96+
strategy:
97+
matrix:
98+
php-versions: [ '8.3' ]
99+
steps:
100+
- name: Checkout
101+
uses: actions/checkout@v4
102+
with:
103+
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
104+
105+
- name: Setup cache environment
106+
id: extcache
107+
uses: shivammathur/cache-extensions@v1
108+
with:
109+
php-version: ${{ matrix.php-versions }}
110+
extensions: ${{ env.extensions }}
111+
key: ${{ env.key }}
112+
113+
- name: Cache extensions
114+
uses: actions/cache@v4
115+
with:
116+
path: ${{ steps.extcache.outputs.dir }}
117+
key: ${{ steps.extcache.outputs.key }}
118+
restore-keys: ${{ steps.extcache.outputs.key }}
119+
120+
- name: Setup PHP
121+
uses: shivammathur/setup-php@v2
122+
with:
123+
php-version: ${{ matrix.php-versions }}
124+
extensions: ${{ env.extensions }}
125+
coverage: pcov
126+
tools: pecl,phpunit
127+
128+
- name: Get Composer Cache Directory
129+
id: composer-cache
130+
run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT"
131+
132+
- name: Cache dependencies
133+
uses: actions/cache@v4
134+
with:
135+
path: ${{ steps.composer-cache.outputs.dir }}
136+
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
137+
restore-keys: ${{ runner.os }}-composer-
138+
139+
- name: Validate composer.json and composer.lock
140+
run: composer validate
141+
142+
- name: Install dependencies
143+
run: composer install --prefer-dist --no-progress --ignore-platform-reqs
144+
145+
- name: Generate OpenAPI definition and HTML
146+
run: php ./phpdraft --online --file tests/statics/full_test.apib --openapi openapi.json > out.html 2> error.txt || true
147+
148+
- name: Install check-jsonschema
149+
run: pipx install check-jsonschema
150+
151+
- name: Validate OpenAPI spec
152+
run: |
153+
if [ -s "error.txt" ]; then
154+
echo "The file 'error.txt' is not empty."
155+
cat error.txt
156+
exit 1
157+
fi
158+
159+
if [ ! -s "index.html" ]; then
160+
echo "The file 'index.html' is empty."
161+
exit 1
162+
fi
163+
164+
if [ ! -s "openapi.json" ]; then
165+
echo "The file 'openapi.json' is empty."
166+
exit 1
167+
fi
168+
169+
check-jsonschema --schemafile https://spec.openapis.org/oas/3.1/schema/latest openapi.json
170+
88171
analytics:
89172
name: Analytics
90173
needs: test

‎.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
/build/out
1212
/build/*.phar
1313
/tests/statics/index.*
14-
src/Michelf/*
1514
src/.gitignore
1615
vendor/**
1716

@@ -21,6 +20,8 @@ atlassian-ide-plugin.xml
2120
*.pem
2221

2322
/index.html
23+
/openapi.json
2424

2525
/coverage.xml
2626
/event.json
27+
!/src/PHPDraft/Out/

‎phpdraft

+6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ try
2828
->opt('help:h', 'This help text', false)
2929
->opt('version:v', 'Print the version for PHPDraft.', false)
3030
->opt('file:f', 'Specifies the file to parse.', false)
31+
->opt('openapi:a', 'Output location for an OpenAPI file.', false)
3132
->opt('yes:y', 'Always accept using the online mode.', false, 'bool')
3233
->opt('online:o', 'Always use the online mode.', false, 'bool')
3334
->opt('template:t', 'Specifies the template to use. (defaults to \'default\').', false)
@@ -90,6 +91,11 @@ try
9091
$data = json_decode($json_string);
9192
}
9293

94+
if (isset($args['openapi'])) {
95+
$openapi = ParserFactory::getOpenAPI()->init($data);
96+
$openapi->write($args['openapi']);
97+
}
98+
9399
$html = ParserFactory::getJson()->init($data);
94100
$name = 'PHPD_SORT_' . strtoupper($args->getOpt('sort', ''));
95101
$html->sorting = Sorting::${$name} ?? Sorting::PHPD_SORT_NONE->value;

‎src/PHPDraft/Model/HTTPRequest.php

+5-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
namespace PHPDraft\Model;
1414

15+
use PHPDraft\Model\Elements\ObjectStructureElement;
1516
use PHPDraft\Model\Elements\RequestBodyElement;
1617
use PHPDraft\Model\Elements\StructureElement;
1718

@@ -43,7 +44,7 @@ class HTTPRequest implements Comparable
4344
*
4445
* @var string
4546
*/
46-
public string $description;
47+
public string $description = '';
4748

4849
/**
4950
* Parent class.
@@ -68,9 +69,10 @@ class HTTPRequest implements Comparable
6869
/**
6970
* Structure of the request.
7071
*
71-
* @var RequestBodyElement[]|RequestBodyElement
72+
* @var RequestBodyElement[]|RequestBodyElement|ObjectStructureElement
7273
*/
73-
public mixed $struct = [];
74+
public RequestBodyElement|ObjectStructureElement|array|null $struct = [];
75+
7476
/**
7577
* Identifier for the request.
7678
*

‎src/PHPDraft/Model/Transition.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ class Transition extends HierarchyElement
2323
/**
2424
* HTTP method used.
2525
*
26-
* @var string
26+
* @var string|null
2727
*/
28-
public string $method;
28+
public ?string $method = NULL;
2929

3030
/**
3131
* URI.

‎src/PHPDraft/Out/BaseTemplateRenderer.php

+42-22
Original file line numberDiff line numberDiff line change
@@ -24,36 +24,21 @@ abstract class BaseTemplateRenderer
2424
* @var int
2525
*/
2626
public int $sorting;
27+
2728
/**
28-
* CSS Files to load.
29-
*
30-
* @var string[]
31-
*/
32-
public array $css = [];
33-
/**
34-
* JS Files to load.
35-
*
36-
* @var string[]
37-
*/
38-
public array $js = [];
39-
/**
40-
* The image to use as a logo.
41-
*
42-
* @var string|null
43-
*/
44-
protected ?string $image = null;
45-
/**
46-
* The template file to load.
29+
* JSON representation of an API Blueprint.
4730
*
48-
* @var string
31+
* @var object
4932
*/
50-
protected string $template;
33+
protected object $object;
34+
5135
/**
5236
* The base data of the API.
5337
*
5438
* @var array<string, mixed>
5539
*/
56-
protected array $base_data;
40+
protected array $base_data = [];
41+
5742
/**
5843
* JSON object of the API blueprint.
5944
*
@@ -66,4 +51,39 @@ abstract class BaseTemplateRenderer
6651
* @var BasicStructureElement[]
6752
*/
6853
protected array $base_structures = [];
54+
55+
/**
56+
* Parse base data
57+
*
58+
* @param object $object
59+
*/
60+
protected function parse_base_data(object $object): void
61+
{
62+
//Prepare base data
63+
if (!is_array($object->content[0]->content)) {
64+
return;
65+
}
66+
67+
$this->base_data['TITLE'] = $object->content[0]->meta->title->content ?? '';
68+
69+
foreach ($object->content[0]->attributes->metadata->content as $meta) {
70+
$this->base_data[$meta->content->key->content] = $meta->content->value->content;
71+
}
72+
73+
foreach ($object->content[0]->content as $value) {
74+
if ($value->element === 'copy') {
75+
$this->base_data['DESC'] = $value->content;
76+
continue;
77+
}
78+
79+
$cat = new Category();
80+
$cat = $cat->parse($value);
81+
82+
if (($value->meta->classes->content[0]->content ?? null) === 'dataStructures') {
83+
$this->base_structures = array_merge($this->base_structures, $cat->structures);
84+
} else {
85+
$this->categories[] = $cat;
86+
}
87+
}
88+
}
6989
}

‎src/PHPDraft/Out/TemplateRenderer.php renamed to ‎src/PHPDraft/Out/HtmlTemplateRenderer.php

+29-36
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,36 @@
3131
use Twig\TwigFilter;
3232
use Twig\TwigTest;
3333

34-
class TemplateRenderer extends BaseTemplateRenderer
34+
class HtmlTemplateRenderer extends BaseTemplateRenderer
3535
{
36+
37+
38+
/**
39+
* CSS Files to load.
40+
*
41+
* @var string[]
42+
*/
43+
public array $css = [];
44+
45+
/**
46+
* JS Files to load.
47+
*
48+
* @var string[]
49+
*/
50+
public array $js = [];
51+
/**
52+
* The image to use as a logo.
53+
*
54+
* @var string|null
55+
*/
56+
protected ?string $image = null;
57+
/**
58+
* The template file to load.
59+
*
60+
* @var string
61+
*/
62+
protected string $template;
63+
3664
/**
3765
* TemplateGenerator constructor.
3866
*
@@ -109,41 +137,6 @@ public function get(object $object): string
109137
]);
110138
}
111139

112-
/**
113-
* Parse base data
114-
*
115-
* @param object $object
116-
*/
117-
private function parse_base_data(object $object): void
118-
{
119-
//Prepare base data
120-
if (!is_array($object->content[0]->content)) {
121-
return;
122-
}
123-
124-
$this->base_data['TITLE'] = $object->content[0]->meta->title->content ?? '';
125-
126-
foreach ($object->content[0]->attributes->metadata->content as $meta) {
127-
$this->base_data[$meta->content->key->content] = $meta->content->value->content;
128-
}
129-
130-
foreach ($object->content[0]->content as $value) {
131-
if ($value->element === 'copy') {
132-
$this->base_data['DESC'] = $value->content;
133-
continue;
134-
}
135-
136-
$cat = new Category();
137-
$cat = $cat->parse($value);
138-
139-
if (($value->meta->classes->content[0]->content ?? null) === 'dataStructures') {
140-
$this->base_structures = array_merge($this->base_structures, $cat->structures);
141-
} else {
142-
$this->categories[] = $cat;
143-
}
144-
}
145-
}
146-
147140
/**
148141
* Get the path to a file to include.
149142
*
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
<?php
2+
3+
namespace PHPDraft\Out\OpenAPI;
4+
5+
use PHPDraft\Model\HTTPRequest;
6+
use PHPDraft\Model\HTTPResponse;
7+
use PHPDraft\Out\BaseTemplateRenderer;
8+
use stdClass;
9+
10+
class OpenApiRenderer extends BaseTemplateRenderer {
11+
12+
public function init(object $json): self
13+
{
14+
$this->object = $json;
15+
16+
return $this;
17+
}
18+
19+
public function write(string $filename): void
20+
{
21+
$output = json_encode($this->toOpenApiObject(), JSON_PRETTY_PRINT);
22+
file_put_contents($filename, $output);
23+
}
24+
25+
/**
26+
* Get OpenAPI base structure.
27+
*
28+
* @return array<string,object|array<string,mixed>|string[]>
29+
*/
30+
private function toOpenApiObject(): array
31+
{
32+
$this->parse_base_data($this->object);
33+
34+
return [
35+
'openapi' => '3.1.0',
36+
// 'jsonSchemaDialect' => '',
37+
'info' => $this->getApiInfo(),
38+
'servers' => $this->getServers(),
39+
'paths' => $this->getPaths(),
40+
'webhooks' => $this->getWebhooks(),
41+
'components' => $this->getComponents(),
42+
'security' => $this->getSecurity(),
43+
'tags' => $this->getTags(),
44+
// 'externalDocs' => $this->getDocs(),
45+
];
46+
}
47+
48+
/**
49+
* Get generic info for the API
50+
* @return array<string,string>
51+
*/
52+
private function getApiInfo(): array {
53+
return [
54+
"title"=> $this->base_data['TITLE'],
55+
"version"=> $this->base_data['VERSION'] ?? '1.0.0',
56+
"summary"=> $this->base_data['TITLE'] . ' generated from API Blueprint',
57+
"description"=> $this->base_data['DESC'],
58+
// "termsOfService"=> "https://example.com/terms/",
59+
// "contact"=> [
60+
// "name"=> "API Support",
61+
// "url"=> "https://www.example.com/support",
62+
// "email"=> "support@example.com"
63+
// ],
64+
// "license" => [
65+
// "name"=> "Apache 2.0",
66+
// "url"=> "https://www.apache.org/licenses/LICENSE-2.0.html"
67+
// ],
68+
];
69+
}
70+
71+
/**
72+
* Get information about the servers involved in the API.
73+
*
74+
* @return array<array<string,string>>
75+
*/
76+
private function getServers(): array {
77+
$return = [];
78+
$return[] = ['url' => $this->base_data['HOST'], 'description' => 'Main host'];
79+
80+
foreach (explode(',', $this->base_data['ALT_HOST'] ?? '') as $host) {
81+
$return[] = ['url' => $host];
82+
}
83+
84+
return $return;
85+
}
86+
87+
/**
88+
* Get path information
89+
*
90+
* @return object
91+
*/
92+
private function getPaths(): object {
93+
$return = [];
94+
foreach ($this->categories as $category) {
95+
foreach ($category->children ?? [] as $resource) {
96+
foreach ($resource->children ?? [] as $transition) {
97+
$transition_return = [];
98+
$transition_return['parameters'] = [];
99+
if ($transition->url_variables !== []) {
100+
$transition_return['parameters'] = $this->toParameters($transition->url_variables, $transition->href);
101+
}
102+
103+
foreach ($transition->requests as $request) {
104+
$request_return = [
105+
'operationId' => $request->get_id(),
106+
'responses' => $this->toResponses($transition->responses),
107+
];
108+
if (isset($transition_return['parameters']) && $transition_return['parameters'] !== []) {
109+
$request_return['parameters'] = $transition_return['parameters'];
110+
}
111+
if ($request->body !== NULL) {
112+
$request_return['requestBody'] = $this->toBody($request);
113+
}
114+
115+
if ($request->title !== NULL) {
116+
$request_return['summary'] = $request->title;
117+
}
118+
if ($request->description !== '') {
119+
$request_return['description'] = $request->description;
120+
}
121+
122+
$transition_return[strtolower($request->method)] = (object) $request_return;
123+
}
124+
$return[$transition->href] = (object) $transition_return;
125+
}
126+
}
127+
}
128+
129+
return (object) $return;
130+
}
131+
132+
/**
133+
* Convert objects into parameters.
134+
*
135+
* @param object[] $objects List of objects to convert
136+
* @param string $href Base URL
137+
*
138+
* @return array<array<string,mixed>>
139+
*/
140+
private function toParameters(array $objects, string $href): array {
141+
$return = [];
142+
143+
foreach ($objects as $variable) {
144+
$return_tmp = [
145+
'name' => $variable->key->value,
146+
'in' => str_contains($href, '{' . $variable->key->value . '}') ? 'path' : 'query',
147+
'required' => $variable->status === 'required',
148+
'schema' => [
149+
'type' => $variable->type,
150+
],
151+
];
152+
153+
if (isset($variable->value))
154+
{
155+
$return_tmp['example'] = $variable->value;
156+
}
157+
158+
if (isset($variable->description))
159+
{
160+
$return_tmp['description'] = $variable->description;
161+
}
162+
$return[] = $return_tmp;
163+
}
164+
165+
return $return;
166+
}
167+
168+
/**
169+
* Convert a HTTP Request into an OpenAPI body
170+
*
171+
* @param HTTPRequest $request Request to convert
172+
*
173+
* @return array<string,array<string,mixed>> OpenAPI style body
174+
*/
175+
private function toBody(HTTPRequest $request): array
176+
{
177+
$return = [];
178+
179+
if (!is_array($request->struct)) {
180+
$return['description'] = $request->struct->description;
181+
}
182+
183+
$content_type = $request->headers['Content-Type'] ?? 'text/plain';
184+
if (isset($request->struct) && $request->struct !== [])
185+
{
186+
$return['content'] = [
187+
$content_type => [
188+
'schema' => [
189+
'type' => $request->struct->element,
190+
'properties' => array_map(fn($value) => [$value->key->value => ['type' => $value->type]], $request->struct->value),
191+
],
192+
],
193+
];
194+
} else {
195+
$return['content'] = [
196+
$content_type => [
197+
'schema' => [
198+
'type' => 'string',
199+
],
200+
],
201+
];
202+
}
203+
204+
if ($request->body !== NULL && $request->body !== []) {
205+
$return['content'][$content_type]['example'] = $request->body[0];
206+
}
207+
208+
return $return;
209+
}
210+
211+
/**
212+
* Convert responses to the OpenAPI structure
213+
*
214+
* @param HTTPResponse[] $responses List of responses to parse
215+
*
216+
* @return array<int,array<string,mixed>> List of status codes with the response
217+
*/
218+
private function toResponses(array $responses): array
219+
{
220+
$return = [];
221+
222+
foreach ($responses as $response) {
223+
$headers = [];
224+
foreach ($response->headers as $header => $value) {
225+
if ($header === 'Content-Type') { continue; }
226+
$headers[$header] = [
227+
'schema' => [
228+
'type' => 'string',
229+
'example' => $value,
230+
]
231+
];
232+
}
233+
234+
$content = [];
235+
foreach ($response->content as $key => $contents) {
236+
$content[$key] = [
237+
"schema"=> [
238+
"type"=> "string",
239+
"example"=> $contents
240+
]
241+
];
242+
}
243+
foreach ($response->structure as $structure) {
244+
if ($structure->key === NULL) { continue; }
245+
$content[$response->headers['Content-Type'] ?? 'text/plain'] = [
246+
"schema"=> [
247+
"type"=> "object",
248+
"properties"=> [
249+
$structure->key->value => [
250+
"type" => $structure->type,
251+
'example' => $structure->value,
252+
]
253+
]
254+
]
255+
];
256+
}
257+
$return[$response->statuscode] = [
258+
'description' => $response->description ?? $response->title ?? '',
259+
'headers' => (object) $headers,
260+
'content' => (object) $content,
261+
];
262+
}
263+
264+
return $return;
265+
}
266+
267+
/**
268+
* Get webhook information for the API.
269+
* @return object
270+
*/
271+
private function getWebhooks(): object { return (object) []; }
272+
273+
/**
274+
* Get component information for the API.
275+
* @return object
276+
*/
277+
private function getComponents(): object { return (object) []; }
278+
279+
/**
280+
* Get security information for the API
281+
* @return string[]
282+
*/
283+
private function getSecurity(): array { return []; }
284+
285+
/**
286+
* Get tags for the API
287+
* @return string[]
288+
*/
289+
private function getTags(): array { return []; }
290+
291+
// private function getDocs(): object { return (object) []; }
292+
293+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
namespace PHPDraft\Out\OpenAPI\Tests;
3+
4+
use Lunr\Halo\LunrBaseTest;
5+
use PHPDraft\Model\HTTPRequest;
6+
use PHPDraft\Out\OpenAPI\OpenApiRenderer;
7+
8+
/**
9+
* @covers \PHPDraft\Out\OpenAPI\OpenApiRenderer
10+
*/
11+
class OpenApiRendererTest extends LunrBaseTest
12+
{
13+
private OpenApiRenderer $class;
14+
15+
/**
16+
* Set up tests
17+
*/
18+
public function setUp(): void
19+
{
20+
$this->class = new OpenApiRenderer();
21+
$this->baseSetUp($this->class);
22+
}
23+
24+
public function tearDown(): void
25+
{
26+
parent::tearDown();
27+
}
28+
29+
public function testWrite(): void
30+
{
31+
$this->class->init((object)[]);
32+
33+
$tmpfile = tempnam(sys_get_temp_dir(), 'fdsfds');
34+
$this->class->write($tmpfile);
35+
$this->assertFileEquals(TEST_STATICS . '/openapi/empty.json', $tmpfile);
36+
}
37+
38+
public function testGetTags(): void
39+
{
40+
$method = $this->get_reflection_method('getTags');
41+
$result = $method->invokeArgs($this->class, []);
42+
43+
$this->assertArrayEmpty($result);
44+
}
45+
46+
public function testGetSecurity(): void
47+
{
48+
$method = $this->get_reflection_method('getSecurity');
49+
$result = $method->invokeArgs($this->class, []);
50+
51+
$this->assertArrayEmpty($result);
52+
}
53+
54+
public function testGetComponents(): void
55+
{
56+
$method = $this->get_reflection_method('getComponents');
57+
$result = $method->invokeArgs($this->class, []);
58+
59+
$this->assertEquals((object)[],$result);
60+
}
61+
62+
public function testGetDocs(): void
63+
{
64+
$this->markTestSkipped('Not implemented');
65+
66+
$method = $this->get_reflection_method('getDocs');
67+
$result = $method->invokeArgs($this->class, []);
68+
69+
$this->assertEquals((object)[],$result);
70+
}
71+
72+
public function testGetPaths(): void
73+
{
74+
$method = $this->get_reflection_method('getPaths');
75+
$result = $method->invokeArgs($this->class, []);
76+
77+
$this->assertEquals((object)[],$result);
78+
}
79+
80+
public function testGetServers(): void
81+
{
82+
$method = $this->get_reflection_method('getServers');
83+
$result = $method->invokeArgs($this->class, []);
84+
85+
$this->assertEquals([['url' => null,'description' => 'Main host'], ['url' => '']],$result);
86+
}
87+
88+
public function testGetApiInfo(): void
89+
{
90+
$method = $this->get_reflection_method('getApiInfo');
91+
$result = $method->invokeArgs($this->class, []);
92+
93+
$this->assertEquals([
94+
'title' => null,
95+
'version' => '1.0.0',
96+
'summary' => ' generated from API Blueprint',
97+
'description' => null,
98+
],$result);
99+
}
100+
101+
public function testToResponses(): void
102+
{
103+
$method = $this->get_reflection_method('toResponses');
104+
$result = $method->invokeArgs($this->class, [[]]);
105+
106+
$this->assertEquals([],$result);
107+
}
108+
109+
public function testToBody(): void
110+
{
111+
$mock = $this->getMockBuilder(HttpRequest::class)
112+
->disableOriginalConstructor()
113+
->getMock();
114+
115+
$method = $this->get_reflection_method('toBody');
116+
$result = $method->invokeArgs($this->class, [$mock]);
117+
118+
$this->assertEquals(['content' => ['text/plain' => ['schema' => ['type' => 'string']]]],$result);
119+
}
120+
121+
public function testToParameters(): void
122+
{
123+
$mock = $this->getMockBuilder(HttpRequest::class)
124+
->disableOriginalConstructor()
125+
->getMock();
126+
127+
$method = $this->get_reflection_method('toParameters');
128+
$result = $method->invokeArgs($this->class, [[], 'href']);
129+
130+
$this->assertEquals([],$result);
131+
}
132+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,384 @@
1+
<?php
2+
3+
/**
4+
* This file contains the TemplateGeneratorTest.php
5+
*
6+
* @package PHPDraft\Out
7+
* @author Sean Molenaar<sean@seanmolenaar.eu>
8+
*/
9+
10+
namespace PHPDraft\Out\Tests;
11+
12+
use Lunr\Halo\LunrBaseTest;
13+
use PHPDraft\Out\HtmlTemplateRenderer;
14+
15+
/**
16+
* Class TemplateGeneratorTest
17+
*
18+
* @covers \PHPDraft\Out\HtmlTemplateRenderer
19+
*/
20+
class HtmlTemplateRendererTest extends LunrBaseTest
21+
{
22+
/**
23+
* @var HtmlTemplateRenderer
24+
*/
25+
protected HtmlTemplateRenderer $class;
26+
27+
public static function parsableDataProvider(): array
28+
{
29+
$return = [];
30+
$expected_base = [
31+
'COLOR_1' => 'green',
32+
'COLOR_2' => 'light_green',
33+
];
34+
35+
36+
$return['empty'] = [ ['content' => []], $expected_base ];
37+
38+
$return['only title'] = [
39+
[
40+
'content' => [
41+
[
42+
'meta' => [
43+
'title' => [ 'content' => 'Title' ],
44+
],
45+
'content' => [],
46+
],
47+
48+
],
49+
],
50+
$expected_base + ['TITLE' => 'Title'],
51+
];
52+
53+
$return['title and metadata'] = [
54+
[
55+
'content' => [
56+
[
57+
'meta' => [
58+
'title' => [ 'content' => 'Title' ],
59+
],
60+
'attributes' => [
61+
'metadata' => [
62+
'content' => [
63+
[
64+
'content' => [
65+
'key' => [ 'content' => 'Some_key' ],
66+
'value' => [ 'content' => 'Value' ],
67+
]
68+
],
69+
[
70+
'content' => [
71+
'key' => [ 'content' => 'Some_key2' ],
72+
'value' => [ 'content' => 'Value2' ],
73+
]
74+
]
75+
]
76+
]
77+
],
78+
'content' => [],
79+
],
80+
81+
],
82+
],
83+
$expected_base + ['TITLE' => 'Title', 'Some_key' => 'Value', 'Some_key2' => 'Value2'],
84+
];
85+
86+
$return['title and metadata and description'] = [
87+
[
88+
'content' => [
89+
[
90+
'meta' => [
91+
'title' => [ 'content' => 'Title' ],
92+
],
93+
'attributes' => [
94+
'metadata' => [
95+
'content' => [
96+
[
97+
'content' => [
98+
'key' => [ 'content' => 'Some_key' ],
99+
'value' => [ 'content' => 'Value' ],
100+
]
101+
],
102+
[
103+
'content' => [
104+
'key' => [ 'content' => 'Some_key2' ],
105+
'value' => [ 'content' => 'Value2' ],
106+
]
107+
]
108+
]
109+
]
110+
],
111+
'content' => [
112+
[
113+
'element' => 'copy',
114+
'content' => 'Some description',
115+
]
116+
],
117+
],
118+
119+
],
120+
],
121+
$expected_base + ['TITLE' => 'Title', 'Some_key' => 'Value', 'Some_key2' => 'Value2', 'DESC' => 'Some description'],
122+
];
123+
124+
return $return;
125+
}
126+
127+
/**
128+
* Provide HTTP status codes
129+
*
130+
* @return array<int, array<int, int|string>>
131+
*/
132+
public static function responseStatusProvider(): array
133+
{
134+
$return = [];
135+
136+
$return[] = [ 200, 'text-success' ];
137+
$return[] = [ 204, 'text-success' ];
138+
$return[] = [ 304, 'text-warning' ];
139+
$return[] = [ 404, 'text-error' ];
140+
$return[] = [ 501, 'text-error' ];
141+
142+
return $return;
143+
}
144+
145+
/**
146+
* Provide HTTP methods
147+
*
148+
* @return array<int, array<int, string>>
149+
*/
150+
public static function requestMethodProvider(): array
151+
{
152+
$return = [];
153+
154+
$return[] = [ 'POST', 'fas POST fa-plus-square' ];
155+
$return[] = [ 'post', 'fas POST fa-plus-square' ];
156+
$return[] = [ 'get', 'fas GET fa-arrow-circle-down' ];
157+
$return[] = [ 'put', 'fas PUT fa-pen-square' ];
158+
$return[] = [ 'delete', 'fas DELETE fa-minus-square' ];
159+
$return[] = [ 'head', 'fas HEAD fa-info' ];
160+
$return[] = [ 'options', 'fas OPTIONS fa-sliders-h' ];
161+
$return[] = [ 'PATCH', 'fas PATCH fa-band-aid' ];
162+
$return[] = [ 'connect', 'fas CONNECT fa-ethernet' ];
163+
$return[] = [ 'trace', 'fas TRACE fa-route' ];
164+
$return[] = [ 'cow', 'fas COW' ];
165+
166+
return $return;
167+
}
168+
169+
/**
170+
* Set up tests
171+
*
172+
* @return void
173+
*/
174+
public function setUp(): void
175+
{
176+
$this->class = new HtmlTemplateRenderer('default', 'none');
177+
$this->baseSetUp($this->class);
178+
}
179+
180+
/**
181+
* Test if the value the class is initialized with is correct
182+
*
183+
* @covers \PHPDraft\Out\HtmlTemplateRenderer
184+
*/
185+
public function testSetupCorrectly(): void
186+
{
187+
$this->assertSame('default', $this->get_reflection_property_value('template'));
188+
$this->assertEquals('none', $this->get_reflection_property_value('image'));
189+
}
190+
191+
/**
192+
* Test if the value the class is initialized with is correct
193+
*
194+
* @covers \PHPDraft\Out\HtmlTemplateRenderer::strip_link_spaces
195+
*/
196+
public function testStripSpaces(): void
197+
{
198+
$return = $this->class->strip_link_spaces('hello world');
199+
$this->assertEquals('hello-world', $return);
200+
}
201+
202+
/**
203+
* Test if the value the class is initialized with is correct
204+
*
205+
* @dataProvider responseStatusProvider
206+
*
207+
* @param int $code HTTP code
208+
* @param string $text Class to return
209+
*
210+
* @covers \PHPDraft\Out\HtmlTemplateRenderer::get_response_status
211+
*/
212+
public function testResponseStatus(int $code, string $text): void
213+
{
214+
$return = HtmlTemplateRenderer::get_response_status($code);
215+
$this->assertEquals($text, $return);
216+
}
217+
218+
/**
219+
* Test if the value the class is initialized with is correct
220+
*
221+
* @dataProvider requestMethodProvider
222+
*
223+
* @param string $method HTTP Method
224+
* @param string $text Class to return
225+
*
226+
* @covers \PHPDraft\Out\HtmlTemplateRenderer::get_method_icon
227+
*/
228+
public function testRequestMethod(string $method, string $text): void
229+
{
230+
$return = HtmlTemplateRenderer::get_method_icon($method);
231+
$this->assertEquals($text, $return);
232+
}
233+
234+
/**
235+
* Test if the value the class is initialized with is correct
236+
*
237+
* @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file
238+
*/
239+
public function testIncludeFileDefault(): void
240+
{
241+
$return = $this->class->find_include_file('default');
242+
$this->assertEquals('PHPDraft/Out/HTML/default/main.twig', $return);
243+
}
244+
245+
/**
246+
* Test if the value the class is initialized with is correct
247+
*
248+
* @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file
249+
*/
250+
public function testIncludeFileFallback(): void
251+
{
252+
$return = $this->class->find_include_file('gfsdfdsf');
253+
$this->assertEquals('PHPDraft/Out/HTML/default/main.twig', $return);
254+
}
255+
256+
/**
257+
* Test if the value the class is initialized with is correct
258+
*
259+
* @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file
260+
*/
261+
public function testIncludeFileNone(): void
262+
{
263+
$return = $this->class->find_include_file('gfsdfdsf', 'xyz');
264+
$this->assertEquals(NULL, $return);
265+
}
266+
267+
/**
268+
* Test if the value the class is initialized with is correct
269+
*
270+
* @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file
271+
*/
272+
public function testIncludeFileSingle(): void
273+
{
274+
set_include_path(TEST_STATICS . '/include_single:' . get_include_path());
275+
$return = $this->class->find_include_file('hello', 'txt');
276+
$this->assertEquals('hello.txt', $return);
277+
}
278+
279+
/**
280+
* Test if the value the class is initialized with is correct
281+
*
282+
* @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file
283+
*/
284+
public function testIncludeFileMultiple(): void
285+
{
286+
set_include_path(TEST_STATICS . '/include_folders:' . get_include_path());
287+
$return = $this->class->find_include_file('hello', 'txt');
288+
$this->assertEquals('hello/hello.txt', $return);
289+
290+
$return = $this->class->find_include_file('test', 'txt');
291+
$this->assertEquals('templates/test.txt', $return);
292+
293+
$return = $this->class->find_include_file('text', 'txt');
294+
$this->assertEquals('templates/text/text.txt', $return);
295+
}
296+
297+
/**
298+
* @covers \PHPDraft\Out\HtmlTemplateRenderer::get
299+
*/
300+
public function testGetTemplateFailsEmpty(): void
301+
{
302+
$this->expectException('PHPDraft\Parse\ExecutionException');
303+
$this->expectExceptionMessage('Couldn\'t find template \'cow\'');
304+
$this->set_reflection_property_value('template', 'cow');
305+
$json = '{"content": [{"content": "hello"}]}';
306+
307+
$this->assertStringEqualsFile(TEST_STATICS . '/empty_html_template', $this->class->get(json_decode($json)));
308+
}
309+
310+
/**
311+
* @covers \PHPDraft\Out\HtmlTemplateRenderer::get
312+
* @group twig
313+
*/
314+
public function testGetTemplate(): void
315+
{
316+
$json = '{"content": [{"content": "hello"}]}';
317+
318+
$this->assertStringEqualsFile(TEST_STATICS . '/empty_html_template', $this->class->get(json_decode($json)));
319+
}
320+
321+
/**
322+
* @covers \PHPDraft\Out\HtmlTemplateRenderer::get
323+
* @group twig
324+
*/
325+
public function testGetTemplateSorting(): void
326+
{
327+
$this->set_reflection_property_value('sorting', 3);
328+
$json = '{"content": [{"content": "hello"}]}';
329+
330+
$this->assertStringEqualsFile(TEST_STATICS . '/empty_html_template', $this->class->get(json_decode($json)));
331+
}
332+
333+
/**
334+
* @covers \PHPDraft\Out\HtmlTemplateRenderer::get
335+
* @group twig
336+
*/
337+
public function testGetTemplateMetaData(): void
338+
{
339+
$this->set_reflection_property_value('sorting', 3);
340+
$json = <<<'TAG'
341+
{"content": [{"content": [], "attributes": {
342+
"metadata": {"content": [
343+
{"content":{"key": {"content": "key"}, "value": {"content": "value"}}}
344+
]},
345+
"meta": {"title": {"content": "title"}}
346+
}}]}
347+
TAG;
348+
349+
$this->assertStringEqualsFile(TEST_STATICS . '/basic_html_template', $this->class->get(json_decode($json)));
350+
}
351+
352+
/**
353+
* @covers \PHPDraft\Out\HtmlTemplateRenderer::get
354+
* @group twig
355+
*/
356+
public function testGetTemplateCategories(): void
357+
{
358+
$this->set_reflection_property_value('sorting', 3);
359+
$json = <<<'TAG'
360+
{"content": [
361+
{"content": [{"element": "copy", "content": "__desc__"}, {"element": "category", "content": []}],
362+
"attributes": {
363+
"metadata": {"content": [
364+
{"content":{"key": {"content": "key"}, "value": {"content": "value"}}}
365+
]},
366+
"meta": {"title": {"content": "title"}}
367+
}}]}
368+
TAG;
369+
370+
$this->assertStringEqualsFile(TEST_STATICS . '/full_html_template', $this->class->get(json_decode($json)));
371+
}
372+
373+
/**
374+
* @covers \PHPDraft\Out\BaseTemplateRenderer::parse_base_data
375+
* @dataProvider parsableDataProvider
376+
*/
377+
public function testParseBaseData(array $input, array $expected): void
378+
{
379+
$method = $this->get_reflection_method('parse_base_data');
380+
$method->invokeArgs($this->class, [ json_decode(json_encode($input)) ]);
381+
382+
$this->assertPropertyEquals('base_data', $expected);
383+
}
384+
}

‎src/PHPDraft/Out/Tests/TemplateRendererTest.php

-269
This file was deleted.

‎src/PHPDraft/Out/TwigFactory.php

+3-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,9 @@ public static function get(LoaderInterface $loader): Environment
2525
{
2626
$twig = new Environment($loader);
2727

28-
$twig->addFilter(new TwigFilter('method_icon', fn(string $string) => TemplateRenderer::get_method_icon($string)));
29-
$twig->addFilter(new TwigFilter('strip_link_spaces', fn(string $string) => TemplateRenderer::strip_link_spaces($string)));
30-
$twig->addFilter(new TwigFilter('response_status', fn(string $string) => TemplateRenderer::get_response_status((int) $string)));
28+
$twig->addFilter(new TwigFilter('method_icon', fn(string $string) => HtmlTemplateRenderer::get_method_icon($string)));
29+
$twig->addFilter(new TwigFilter('strip_link_spaces', fn(string $string) => HtmlTemplateRenderer::strip_link_spaces($string)));
30+
$twig->addFilter(new TwigFilter('response_status', fn(string $string) => HtmlTemplateRenderer::get_response_status((int) $string)));
3131
$twig->addFilter(new TwigFilter('status_reason', fn(int $code) => (new Httpstatus())->getReasonPhrase($code)));
3232
$twig->addFilter(new TwigFilter('minify_css', function (string $string) {
3333
$minify = new Css();

‎src/PHPDraft/Parse/Drafter.php

+10
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ public function init(ApibFileParser $apib): BaseParser
5252
public static function location(): false|string
5353
{
5454
$returnVal = shell_exec('which drafter 2> /dev/null');
55+
if ($returnVal === NULL)
56+
{
57+
return false;
58+
}
59+
5560
$returnVal = preg_replace('/^\s+|\n|\r|\s+$/m', '', $returnVal);
5661

5762
return $returnVal === null || $returnVal === '' ? false : $returnVal;
@@ -83,6 +88,11 @@ public static function available(): bool
8388
$path = self::location();
8489

8590
$version = shell_exec('drafter -v 2> /dev/null');
91+
if ($version === NULL)
92+
{
93+
return false;
94+
}
95+
8696
$version = preg_match('/^v([45])/', $version);
8797

8898
return $path && $version === 1;

‎src/PHPDraft/Parse/HtmlGenerator.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212

1313
namespace PHPDraft\Parse;
1414

15-
use PHPDraft\Out\TemplateRenderer;
1615
use Twig\Error\LoaderError;
1716
use Twig\Error\RuntimeError;
1817
use Twig\Error\SyntaxError;
18+
use PHPDraft\Out\HtmlTemplateRenderer;
1919

2020
/**
2121
* Class HtmlGenerator.
@@ -39,7 +39,7 @@ class HtmlGenerator extends BaseHtmlGenerator
3939
*/
4040
public function build_html(string $template = 'default', ?string $image = null, ?string $css = null, ?string $js = null): void
4141
{
42-
$gen = new TemplateRenderer($template, $image);
42+
$gen = new HtmlTemplateRenderer($template, $image);
4343

4444
if (!is_null($css)) {
4545
$gen->css = explode(',', $css);

‎src/PHPDraft/Parse/ParserFactory.php

+11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace PHPDraft\Parse;
66

7+
use PHPDraft\Out\OpenAPI\OpenApiRenderer;
8+
79
/**
810
* Class ParserFactory.
911
*/
@@ -39,4 +41,13 @@ public static function getJson(): BaseHtmlGenerator
3941

4042
throw new ResourceException("Couldn't get a JSON parser", 255);
4143
}
44+
45+
public static function getOpenAPI(): OpenApiRenderer
46+
{
47+
if (Drafter::available() || DrafterAPI::available()) {
48+
return new OpenApiRenderer();
49+
}
50+
51+
throw new ResourceException("Couldn't get an OpenAPI renderer", 255);
52+
}
4253
}

‎src/PHPDraft/Parse/Tests/ParserFactoryTest.php

+30
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,34 @@ public function testGetJsonFails(): void
8282
$this->unmock_method(['\PHPDraft\Parse\Drafter', 'available']);
8383
$this->unmock_method(['\PHPDraft\Parse\DrafterAPI', 'available']);
8484
}
85+
86+
/**
87+
* @covers \PHPDraft\Parse\ParserFactory::getOpenAPI
88+
*/
89+
public function testGetOpenAPI(): void
90+
{
91+
$this->mock_method(['\PHPDraft\Parse\Drafter', 'available'], fn() => false);
92+
$this->mock_method(['\PHPDraft\Parse\DrafterAPI', 'available'], fn() => true);
93+
94+
$this->assertInstanceOf('\PHPDraft\Out\OpenAPI\OpenApiRenderer', ParserFactory::getOpenAPI());
95+
96+
$this->unmock_method(['\PHPDraft\Parse\Drafter', 'available']);
97+
$this->unmock_method(['\PHPDraft\Parse\DrafterAPI', 'available']);
98+
}
99+
100+
/**
101+
* @covers \PHPDraft\Parse\ParserFactory::getOpenAPI
102+
*/
103+
public function testGetOpenAPIFails(): void
104+
{
105+
$this->expectException('\PHPDraft\Parse\ResourceException');
106+
$this->expectExceptionMessage('Couldn\'t get an OpenAPI renderer');
107+
$this->mock_method(['\PHPDraft\Parse\Drafter', 'available'], fn() => false);
108+
$this->mock_method(['\PHPDraft\Parse\DrafterAPI', 'available'], fn() => false);
109+
110+
ParserFactory::getOpenAPI();
111+
112+
$this->unmock_method(['\PHPDraft\Parse\Drafter', 'available']);
113+
$this->unmock_method(['\PHPDraft\Parse\DrafterAPI', 'available']);
114+
}
85115
}

‎tests/phpunit.xml

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
</testsuite>
1414
<testsuite name="Out">
1515
<directory>../src/PHPDraft/Out/Tests/</directory>
16+
<directory>../src/PHPDraft/Out/OpenAPI/Tests/</directory>
1617
</testsuite>
1718
<testsuite name="Parse">
1819
<directory>../src/PHPDraft/Parse/Tests/</directory>

‎tests/statics/openapi/empty.json

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"openapi": "3.1.0",
3+
"info": {
4+
"title": null,
5+
"version": "1.0.0",
6+
"summary": " generated from API Blueprint",
7+
"description": null
8+
},
9+
"servers": [
10+
{
11+
"url": null,
12+
"description": "Main host"
13+
},
14+
{
15+
"url": ""
16+
}
17+
],
18+
"paths": {},
19+
"webhooks": {},
20+
"components": {},
21+
"security": [],
22+
"tags": []
23+
}

0 commit comments

Comments
 (0)
Please sign in to comment.