diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e8598c0..c8bb84ea 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -85,6 +85,88 @@ jobs: name: coverage-${{ matrix.php-versions }} path: coverage.xml + openapi: + name: File generation + needs: test + runs-on: ubuntu-latest + env: + extensions: curl,json,mbstring,uopz + key: cache-v1 # can be any string, change to clear the extension cache. + strategy: + matrix: + php-versions: [ '8.3' ] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + + - name: Setup cache environment + id: extcache + uses: shivammathur/cache-extensions@v1 + with: + php-version: ${{ matrix.php-versions }} + extensions: ${{ env.extensions }} + key: ${{ env.key }} + + - name: Cache extensions + uses: actions/cache@v4 + with: + path: ${{ steps.extcache.outputs.dir }} + key: ${{ steps.extcache.outputs.key }} + restore-keys: ${{ steps.extcache.outputs.key }} + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-versions }} + extensions: ${{ env.extensions }} + coverage: pcov + tools: pecl,phpunit + + - name: Get Composer Cache Directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Validate composer.json and composer.lock + run: composer validate + + - name: Install dependencies + run: composer install --prefer-dist --no-progress --ignore-platform-reqs + + - name: Generate OpenAPI definition and HTML + run: php ./phpdraft -f tests/statics/full_test.apib --openapi openapi.json > out.html 2> error.txt || true + + - name: Install check-jsonschema + run: pipx install check-jsonschema + + - name: Validate OpenAPI spec + run: | + if [ -s "error.txt" ]; then + echo "The file 'error.txt' is not empty." + cat error.txt + exit 1 + fi + + if [ ! -s "index.html" ]; then + echo "The file 'index.html' is empty." + exit 1 + fi + + if [ ! -s "openapi.json" ]; then + echo "The file 'openapi.json' is empty." + exit 1 + fi + + check-jsonschema --schemafile https://spec.openapis.org/oas/3.1/schema/latest openapi.json + analytics: name: Analytics needs: test diff --git a/.gitignore b/.gitignore index fb051901..b92cf8c4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ /build/out /build/*.phar /tests/statics/index.* -src/Michelf/* src/.gitignore vendor/** @@ -21,6 +20,8 @@ atlassian-ide-plugin.xml *.pem /index.html +/openapi.json /coverage.xml /event.json +!/src/PHPDraft/Out/ diff --git a/phpdraft b/phpdraft index 90ea0f77..a1468992 100755 --- a/phpdraft +++ b/phpdraft @@ -28,6 +28,7 @@ try ->opt('help:h', 'This help text', false) ->opt('version:v', 'Print the version for PHPDraft.', false) ->opt('file:f', 'Specifies the file to parse.', false) + ->opt('openapi:a', 'Output location for an OpenAPI file.', false) ->opt('yes:y', 'Always accept using the online mode.', false, 'bool') ->opt('online:o', 'Always use the online mode.', false, 'bool') ->opt('template:t', 'Specifies the template to use. (defaults to \'default\').', false) @@ -90,6 +91,11 @@ try $data = json_decode($json_string); } + if (isset($args['openapi'])) { + $openapi = ParserFactory::getOpenAPI()->init($data); + $openapi->write($args['openapi']); + } + $html = ParserFactory::getJson()->init($data); $name = 'PHPD_SORT_' . strtoupper($args->getOpt('sort', '')); $html->sorting = Sorting::${$name} ?? Sorting::PHPD_SORT_NONE->value; diff --git a/src/PHPDraft/Model/HTTPRequest.php b/src/PHPDraft/Model/HTTPRequest.php index c551184f..b19b6745 100644 --- a/src/PHPDraft/Model/HTTPRequest.php +++ b/src/PHPDraft/Model/HTTPRequest.php @@ -12,6 +12,7 @@ namespace PHPDraft\Model; +use PHPDraft\Model\Elements\ObjectStructureElement; use PHPDraft\Model\Elements\RequestBodyElement; use PHPDraft\Model\Elements\StructureElement; @@ -43,7 +44,7 @@ class HTTPRequest implements Comparable * * @var string */ - public string $description; + public string $description = ''; /** * Parent class. @@ -68,9 +69,10 @@ class HTTPRequest implements Comparable /** * Structure of the request. * - * @var RequestBodyElement[]|RequestBodyElement + * @var RequestBodyElement[]|RequestBodyElement|ObjectStructureElement */ - public mixed $struct = []; + public RequestBodyElement|ObjectStructureElement|array|null $struct = []; + /** * Identifier for the request. * diff --git a/src/PHPDraft/Model/Transition.php b/src/PHPDraft/Model/Transition.php index 8158f94e..2d488197 100644 --- a/src/PHPDraft/Model/Transition.php +++ b/src/PHPDraft/Model/Transition.php @@ -23,9 +23,9 @@ class Transition extends HierarchyElement /** * HTTP method used. * - * @var string + * @var string|null */ - public string $method; + public ?string $method = NULL; /** * URI. diff --git a/src/PHPDraft/Out/BaseTemplateRenderer.php b/src/PHPDraft/Out/BaseTemplateRenderer.php index 765e5ec9..b052ab94 100644 --- a/src/PHPDraft/Out/BaseTemplateRenderer.php +++ b/src/PHPDraft/Out/BaseTemplateRenderer.php @@ -24,36 +24,21 @@ abstract class BaseTemplateRenderer * @var int */ public int $sorting; + /** - * CSS Files to load. - * - * @var string[] - */ - public array $css = []; - /** - * JS Files to load. - * - * @var string[] - */ - public array $js = []; - /** - * The image to use as a logo. - * - * @var string|null - */ - protected ?string $image = null; - /** - * The template file to load. + * JSON representation of an API Blueprint. * - * @var string + * @var object */ - protected string $template; + protected object $object; + /** * The base data of the API. * * @var array */ - protected array $base_data; + protected array $base_data = []; + /** * JSON object of the API blueprint. * @@ -66,4 +51,39 @@ abstract class BaseTemplateRenderer * @var BasicStructureElement[] */ protected array $base_structures = []; + + /** + * Parse base data + * + * @param object $object + */ + protected function parse_base_data(object $object): void + { + //Prepare base data + if (!is_array($object->content[0]->content)) { + return; + } + + $this->base_data['TITLE'] = $object->content[0]->meta->title->content ?? ''; + + foreach ($object->content[0]->attributes->metadata->content as $meta) { + $this->base_data[$meta->content->key->content] = $meta->content->value->content; + } + + foreach ($object->content[0]->content as $value) { + if ($value->element === 'copy') { + $this->base_data['DESC'] = $value->content; + continue; + } + + $cat = new Category(); + $cat = $cat->parse($value); + + if (($value->meta->classes->content[0]->content ?? null) === 'dataStructures') { + $this->base_structures = array_merge($this->base_structures, $cat->structures); + } else { + $this->categories[] = $cat; + } + } + } } diff --git a/src/PHPDraft/Out/TemplateRenderer.php b/src/PHPDraft/Out/HtmlTemplateRenderer.php similarity index 85% rename from src/PHPDraft/Out/TemplateRenderer.php rename to src/PHPDraft/Out/HtmlTemplateRenderer.php index c9a503bb..aed964b9 100644 --- a/src/PHPDraft/Out/TemplateRenderer.php +++ b/src/PHPDraft/Out/HtmlTemplateRenderer.php @@ -31,8 +31,36 @@ use Twig\TwigFilter; use Twig\TwigTest; -class TemplateRenderer extends BaseTemplateRenderer +class HtmlTemplateRenderer extends BaseTemplateRenderer { + + + /** + * CSS Files to load. + * + * @var string[] + */ + public array $css = []; + + /** + * JS Files to load. + * + * @var string[] + */ + public array $js = []; + /** + * The image to use as a logo. + * + * @var string|null + */ + protected ?string $image = null; + /** + * The template file to load. + * + * @var string + */ + protected string $template; + /** * TemplateGenerator constructor. * @@ -109,41 +137,6 @@ public function get(object $object): string ]); } - /** - * Parse base data - * - * @param object $object - */ - private function parse_base_data(object $object): void - { - //Prepare base data - if (!is_array($object->content[0]->content)) { - return; - } - - $this->base_data['TITLE'] = $object->content[0]->meta->title->content ?? ''; - - foreach ($object->content[0]->attributes->metadata->content as $meta) { - $this->base_data[$meta->content->key->content] = $meta->content->value->content; - } - - foreach ($object->content[0]->content as $value) { - if ($value->element === 'copy') { - $this->base_data['DESC'] = $value->content; - continue; - } - - $cat = new Category(); - $cat = $cat->parse($value); - - if (($value->meta->classes->content[0]->content ?? null) === 'dataStructures') { - $this->base_structures = array_merge($this->base_structures, $cat->structures); - } else { - $this->categories[] = $cat; - } - } - } - /** * Get the path to a file to include. * diff --git a/src/PHPDraft/Out/OpenAPI/OpenApiRenderer.php b/src/PHPDraft/Out/OpenAPI/OpenApiRenderer.php new file mode 100644 index 00000000..1f10dd19 --- /dev/null +++ b/src/PHPDraft/Out/OpenAPI/OpenApiRenderer.php @@ -0,0 +1,293 @@ +object = $json; + + return $this; + } + + public function write(string $filename): void + { + $output = json_encode($this->toOpenApiObject(), JSON_PRETTY_PRINT); + file_put_contents($filename, $output); + } + + /** + * Get OpenAPI base structure. + * + * @return array|string[]> + */ + private function toOpenApiObject(): array + { + $this->parse_base_data($this->object); + + return [ + 'openapi' => '3.1.0', +// 'jsonSchemaDialect' => '', + 'info' => $this->getApiInfo(), + 'servers' => $this->getServers(), + 'paths' => $this->getPaths(), + 'webhooks' => $this->getWebhooks(), + 'components' => $this->getComponents(), + 'security' => $this->getSecurity(), + 'tags' => $this->getTags(), +// 'externalDocs' => $this->getDocs(), + ]; + } + + /** + * Get generic info for the API + * @return array + */ + private function getApiInfo(): array { + return [ + "title"=> $this->base_data['TITLE'], + "version"=> $this->base_data['VERSION'] ?? '1.0.0', + "summary"=> $this->base_data['TITLE'] . ' generated from API Blueprint', + "description"=> $this->base_data['DESC'], +// "termsOfService"=> "https://example.com/terms/", +// "contact"=> [ +// "name"=> "API Support", +// "url"=> "https://www.example.com/support", +// "email"=> "support@example.com" +// ], +// "license" => [ +// "name"=> "Apache 2.0", +// "url"=> "https://www.apache.org/licenses/LICENSE-2.0.html" +// ], + ]; + } + + /** + * Get information about the servers involved in the API. + * + * @return array> + */ + private function getServers(): array { + $return = []; + $return[] = ['url' => $this->base_data['HOST'], 'description' => 'Main host']; + + foreach (explode(',', $this->base_data['ALT_HOST'] ?? '') as $host) { + $return[] = ['url' => $host]; + } + + return $return; + } + + /** + * Get path information + * + * @return object + */ + private function getPaths(): object { + $return = []; + foreach ($this->categories as $category) { + foreach ($category->children ?? [] as $resource) { + foreach ($resource->children ?? [] as $transition) { + $transition_return = []; + $transition_return['parameters'] = []; + if ($transition->url_variables !== []) { + $transition_return['parameters'] = $this->toParameters($transition->url_variables, $transition->href); + } + + foreach ($transition->requests as $request) { + $request_return = [ + 'operationId' => $request->get_id(), + 'responses' => $this->toResponses($transition->responses), + ]; + if (isset($transition_return['parameters']) && $transition_return['parameters'] !== []) { + $request_return['parameters'] = $transition_return['parameters']; + } + if ($request->body !== NULL) { + $request_return['requestBody'] = $this->toBody($request); + } + + if ($request->title !== NULL) { + $request_return['summary'] = $request->title; + } + if ($request->description !== '') { + $request_return['description'] = $request->description; + } + + $transition_return[strtolower($request->method)] = (object) $request_return; + } + $return[$transition->href] = (object) $transition_return; + } + } + } + + return (object) $return; + } + + /** + * Convert objects into parameters. + * + * @param object[] $objects List of objects to convert + * @param string $href Base URL + * + * @return array> + */ + private function toParameters(array $objects, string $href): array { + $return = []; + + foreach ($objects as $variable) { + $return_tmp = [ + 'name' => $variable->key->value, + 'in' => str_contains($href, '{' . $variable->key->value . '}') ? 'path' : 'query', + 'required' => $variable->status === 'required', + 'schema' => [ + 'type' => $variable->type, + ], + ]; + + if (isset($variable->value)) + { + $return_tmp['example'] = $variable->value; + } + + if (isset($variable->description)) + { + $return_tmp['description'] = $variable->description; + } + $return[] = $return_tmp; + } + + return $return; + } + + /** + * Convert a HTTP Request into an OpenAPI body + * + * @param HTTPRequest $request Request to convert + * + * @return array> OpenAPI style body + */ + private function toBody(HTTPRequest $request): array + { + $return = []; + + if (!is_array($request->struct)) { + $return['description'] = $request->struct->description; + } + + $content_type = $request->headers['Content-Type'] ?? 'text/plain'; + if (isset($request->struct) && $request->struct !== []) + { + $return['content'] = [ + $content_type => [ + 'schema' => [ + 'type' => $request->struct->element, + 'properties' => array_map(fn($value) => [$value->key->value => ['type' => $value->type]], $request->struct->value), + ], + ], + ]; + } else { + $return['content'] = [ + $content_type => [ + 'schema' => [ + 'type' => 'string', + ], + ], + ]; + } + + if ($request->body !== NULL && $request->body !== []) { + $return['content'][$content_type]['example'] = $request->body[0]; + } + + return $return; + } + + /** + * Convert responses to the OpenAPI structure + * + * @param HTTPResponse[] $responses List of responses to parse + * + * @return array> List of status codes with the response + */ + private function toResponses(array $responses): array + { + $return = []; + + foreach ($responses as $response) { + $headers = []; + foreach ($response->headers as $header => $value) { + if ($header === 'Content-Type') { continue; } + $headers[$header] = [ + 'schema' => [ + 'type' => 'string', + 'example' => $value, + ] + ]; + } + + $content = []; + foreach ($response->content as $key => $contents) { + $content[$key] = [ + "schema"=> [ + "type"=> "string", + "example"=> $contents + ] + ]; + } + foreach ($response->structure as $structure) { + if ($structure->key === NULL) { continue; } + $content[$response->headers['Content-Type'] ?? 'text/plain'] = [ + "schema"=> [ + "type"=> "object", + "properties"=> [ + $structure->key->value => [ + "type" => $structure->type, + 'example' => $structure->value, + ] + ] + ] + ]; + } + $return[$response->statuscode] = [ + 'description' => $response->description ?? $response->title ?? '', + 'headers' => (object) $headers, + 'content' => (object) $content, + ]; + } + + return $return; + } + + /** + * Get webhook information for the API. + * @return object + */ + private function getWebhooks(): object { return (object) []; } + + /** + * Get component information for the API. + * @return object + */ + private function getComponents(): object { return (object) []; } + + /** + * Get security information for the API + * @return string[] + */ + private function getSecurity(): array { return []; } + + /** + * Get tags for the API + * @return string[] + */ + private function getTags(): array { return []; } + +// private function getDocs(): object { return new \stdClass(); } + +} \ No newline at end of file diff --git a/src/PHPDraft/Out/OpenAPI/Tests/OpenApiRendererTest.php b/src/PHPDraft/Out/OpenAPI/Tests/OpenApiRendererTest.php new file mode 100644 index 00000000..8d4cdafa --- /dev/null +++ b/src/PHPDraft/Out/OpenAPI/Tests/OpenApiRendererTest.php @@ -0,0 +1,59 @@ +class = new OpenApiRenderer(); + $this->baseSetUp($this->class); + } + + public function tearDown(): void + { + parent::tearDown(); + } + + public function testGetTags(): void + { + $method = $this->get_reflection_method('getTags'); + $result = $method->invokeArgs($this->class, []); + + $this->assertArrayEmpty($result); + } + + public function testGetSecurity(): void + { + $method = $this->get_reflection_method('getSecurity'); + $result = $method->invokeArgs($this->class, []); + + $this->assertArrayEmpty($result); + } + + public function testGetComponents(): void + { + $method = $this->get_reflection_method('getComponents'); + $result = $method->invokeArgs($this->class, []); + + $this->assertEquals((object)[],$result); + } + + public function testGetWebhooks(): void + { + $method = $this->get_reflection_method('getWebhooks'); + $result = $method->invokeArgs($this->class, []); + + $this->assertEquals((object)[],$result); + } +} \ No newline at end of file diff --git a/src/PHPDraft/Out/Tests/HtmlTemplateRendererTest.php b/src/PHPDraft/Out/Tests/HtmlTemplateRendererTest.php new file mode 100644 index 00000000..d4020ee6 --- /dev/null +++ b/src/PHPDraft/Out/Tests/HtmlTemplateRendererTest.php @@ -0,0 +1,384 @@ + + */ + +namespace PHPDraft\Out\Tests; + +use Lunr\Halo\LunrBaseTest; +use PHPDraft\Out\HtmlTemplateRenderer; + +/** + * Class TemplateGeneratorTest + * + * @covers \PHPDraft\Out\HtmlTemplateRenderer + */ +class HtmlTemplateRendererTest extends LunrBaseTest +{ + /** + * @var HtmlTemplateRenderer + */ + protected HtmlTemplateRenderer $class; + + public static function parsableDataProvider(): array + { + $return = []; + $expected_base = [ + 'COLOR_1' => 'green', + 'COLOR_2' => 'light_green', + ]; + + + $return['empty'] = [ ['content' => []], $expected_base ]; + + $return['only title'] = [ + [ + 'content' => [ + [ + 'meta' => [ + 'title' => [ 'content' => 'Title' ], + ], + 'content' => [], + ], + + ], + ], + $expected_base + ['TITLE' => 'Title'], + ]; + + $return['title and metadata'] = [ + [ + 'content' => [ + [ + 'meta' => [ + 'title' => [ 'content' => 'Title' ], + ], + 'attributes' => [ + 'metadata' => [ + 'content' => [ + [ + 'content' => [ + 'key' => [ 'content' => 'Some_key' ], + 'value' => [ 'content' => 'Value' ], + ] + ], + [ + 'content' => [ + 'key' => [ 'content' => 'Some_key2' ], + 'value' => [ 'content' => 'Value2' ], + ] + ] + ] + ] + ], + 'content' => [], + ], + + ], + ], + $expected_base + ['TITLE' => 'Title', 'Some_key' => 'Value', 'Some_key2' => 'Value2'], + ]; + + $return['title and metadata and description'] = [ + [ + 'content' => [ + [ + 'meta' => [ + 'title' => [ 'content' => 'Title' ], + ], + 'attributes' => [ + 'metadata' => [ + 'content' => [ + [ + 'content' => [ + 'key' => [ 'content' => 'Some_key' ], + 'value' => [ 'content' => 'Value' ], + ] + ], + [ + 'content' => [ + 'key' => [ 'content' => 'Some_key2' ], + 'value' => [ 'content' => 'Value2' ], + ] + ] + ] + ] + ], + 'content' => [ + [ + 'element' => 'copy', + 'content' => 'Some description', + ] + ], + ], + + ], + ], + $expected_base + ['TITLE' => 'Title', 'Some_key' => 'Value', 'Some_key2' => 'Value2', 'DESC' => 'Some description'], + ]; + + return $return; + } + + /** + * Provide HTTP status codes + * + * @return array> + */ + public static function responseStatusProvider(): array + { + $return = []; + + $return[] = [ 200, 'text-success' ]; + $return[] = [ 204, 'text-success' ]; + $return[] = [ 304, 'text-warning' ]; + $return[] = [ 404, 'text-error' ]; + $return[] = [ 501, 'text-error' ]; + + return $return; + } + + /** + * Provide HTTP methods + * + * @return array> + */ + public static function requestMethodProvider(): array + { + $return = []; + + $return[] = [ 'POST', 'fas POST fa-plus-square' ]; + $return[] = [ 'post', 'fas POST fa-plus-square' ]; + $return[] = [ 'get', 'fas GET fa-arrow-circle-down' ]; + $return[] = [ 'put', 'fas PUT fa-pen-square' ]; + $return[] = [ 'delete', 'fas DELETE fa-minus-square' ]; + $return[] = [ 'head', 'fas HEAD fa-info' ]; + $return[] = [ 'options', 'fas OPTIONS fa-sliders-h' ]; + $return[] = [ 'PATCH', 'fas PATCH fa-band-aid' ]; + $return[] = [ 'connect', 'fas CONNECT fa-ethernet' ]; + $return[] = [ 'trace', 'fas TRACE fa-route' ]; + $return[] = [ 'cow', 'fas COW' ]; + + return $return; + } + + /** + * Set up tests + * + * @return void + */ + public function setUp(): void + { + $this->class = new HtmlTemplateRenderer('default', 'none'); + $this->baseSetUp($this->class); + } + + /** + * Test if the value the class is initialized with is correct + * + * @covers \PHPDraft\Out\HtmlTemplateRenderer + */ + public function testSetupCorrectly(): void + { + $this->assertSame('default', $this->get_reflection_property_value('template')); + $this->assertEquals('none', $this->get_reflection_property_value('image')); + } + + /** + * Test if the value the class is initialized with is correct + * + * @covers \PHPDraft\Out\HtmlTemplateRenderer::strip_link_spaces + */ + public function testStripSpaces(): void + { + $return = $this->class->strip_link_spaces('hello world'); + $this->assertEquals('hello-world', $return); + } + + /** + * Test if the value the class is initialized with is correct + * + * @dataProvider responseStatusProvider + * + * @param int $code HTTP code + * @param string $text Class to return + * + * @covers \PHPDraft\Out\HtmlTemplateRenderer::get_response_status + */ + public function testResponseStatus(int $code, string $text): void + { + $return = HtmlTemplateRenderer::get_response_status($code); + $this->assertEquals($text, $return); + } + + /** + * Test if the value the class is initialized with is correct + * + * @dataProvider requestMethodProvider + * + * @param string $method HTTP Method + * @param string $text Class to return + * + * @covers \PHPDraft\Out\HtmlTemplateRenderer::get_method_icon + */ + public function testRequestMethod(string $method, string $text): void + { + $return = HtmlTemplateRenderer::get_method_icon($method); + $this->assertEquals($text, $return); + } + + /** + * Test if the value the class is initialized with is correct + * + * @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file + */ + public function testIncludeFileDefault(): void + { + $return = $this->class->find_include_file('default'); + $this->assertEquals('PHPDraft/Out/HTML/default/main.twig', $return); + } + + /** + * Test if the value the class is initialized with is correct + * + * @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file + */ + public function testIncludeFileFallback(): void + { + $return = $this->class->find_include_file('gfsdfdsf'); + $this->assertEquals('PHPDraft/Out/HTML/default/main.twig', $return); + } + + /** + * Test if the value the class is initialized with is correct + * + * @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file + */ + public function testIncludeFileNone(): void + { + $return = $this->class->find_include_file('gfsdfdsf', 'xyz'); + $this->assertEquals(NULL, $return); + } + + /** + * Test if the value the class is initialized with is correct + * + * @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file + */ + public function testIncludeFileSingle(): void + { + set_include_path(TEST_STATICS . '/include_single:' . get_include_path()); + $return = $this->class->find_include_file('hello', 'txt'); + $this->assertEquals('hello.txt', $return); + } + + /** + * Test if the value the class is initialized with is correct + * + * @covers \PHPDraft\Out\HtmlTemplateRenderer::find_include_file + */ + public function testIncludeFileMultiple(): void + { + set_include_path(TEST_STATICS . '/include_folders:' . get_include_path()); + $return = $this->class->find_include_file('hello', 'txt'); + $this->assertEquals('hello/hello.txt', $return); + + $return = $this->class->find_include_file('test', 'txt'); + $this->assertEquals('templates/test.txt', $return); + + $return = $this->class->find_include_file('text', 'txt'); + $this->assertEquals('templates/text/text.txt', $return); + } + + /** + * @covers \PHPDraft\Out\HtmlTemplateRenderer::get + */ + public function testGetTemplateFailsEmpty(): void + { + $this->expectException('PHPDraft\Parse\ExecutionException'); + $this->expectExceptionMessage('Couldn\'t find template \'cow\''); + $this->set_reflection_property_value('template', 'cow'); + $json = '{"content": [{"content": "hello"}]}'; + + $this->assertStringEqualsFile(TEST_STATICS . '/empty_html_template', $this->class->get(json_decode($json))); + } + + /** + * @covers \PHPDraft\Out\HtmlTemplateRenderer::get + * @group twig + */ + public function testGetTemplate(): void + { + $json = '{"content": [{"content": "hello"}]}'; + + $this->assertStringEqualsFile(TEST_STATICS . '/empty_html_template', $this->class->get(json_decode($json))); + } + + /** + * @covers \PHPDraft\Out\HtmlTemplateRenderer::get + * @group twig + */ + public function testGetTemplateSorting(): void + { + $this->set_reflection_property_value('sorting', 3); + $json = '{"content": [{"content": "hello"}]}'; + + $this->assertStringEqualsFile(TEST_STATICS . '/empty_html_template', $this->class->get(json_decode($json))); + } + + /** + * @covers \PHPDraft\Out\HtmlTemplateRenderer::get + * @group twig + */ + public function testGetTemplateMetaData(): void + { + $this->set_reflection_property_value('sorting', 3); + $json = <<<'TAG' +{"content": [{"content": [], "attributes": { +"metadata": {"content": [ +{"content":{"key": {"content": "key"}, "value": {"content": "value"}}} +]}, +"meta": {"title": {"content": "title"}} +}}]} +TAG; + + $this->assertStringEqualsFile(TEST_STATICS . '/basic_html_template', $this->class->get(json_decode($json))); + } + + /** + * @covers \PHPDraft\Out\HtmlTemplateRenderer::get + * @group twig + */ + public function testGetTemplateCategories(): void + { + $this->set_reflection_property_value('sorting', 3); + $json = <<<'TAG' +{"content": [ +{"content": [{"element": "copy", "content": "__desc__"}, {"element": "category", "content": []}], + "attributes": { +"metadata": {"content": [ +{"content":{"key": {"content": "key"}, "value": {"content": "value"}}} +]}, +"meta": {"title": {"content": "title"}} +}}]} +TAG; + + $this->assertStringEqualsFile(TEST_STATICS . '/full_html_template', $this->class->get(json_decode($json))); + } + + /** + * @covers \PHPDraft\Out\BaseTemplateRenderer::parse_base_data + * @dataProvider parsableDataProvider + */ + public function testParseBaseData(array $input, array $expected): void + { + $method = $this->get_reflection_method('parse_base_data'); + $method->invokeArgs($this->class, [ json_decode(json_encode($input)) ]); + + $this->assertPropertyEquals('base_data', $expected); + } +} diff --git a/src/PHPDraft/Out/Tests/TemplateRendererTest.php b/src/PHPDraft/Out/Tests/TemplateRendererTest.php deleted file mode 100644 index ad62eb82..00000000 --- a/src/PHPDraft/Out/Tests/TemplateRendererTest.php +++ /dev/null @@ -1,269 +0,0 @@ - - */ - -namespace PHPDraft\Out\Tests; - -use Lunr\Halo\LunrBaseTest; -use PHPDraft\Out\TemplateRenderer; - -/** - * Class TemplateGeneratorTest - * - * @covers \PHPDraft\Out\TemplateRenderer - */ -class TemplateRendererTest extends LunrBaseTest -{ - /** - * @var TemplateRenderer - */ - protected TemplateRenderer $class; - - /** - * Set up tests - * @return void - */ - public function setUp(): void - { - $this->class = new TemplateRenderer('default', 'none'); - $this->baseSetUp($this->class); - } - - /** - * Test if the value the class is initialized with is correct - * - * @covers \PHPDraft\Out\TemplateRenderer - */ - public function testSetupCorrectly(): void - { - $this->assertSame('default', $this->get_reflection_property_value('template')); - $this->assertEquals('none', $this->get_reflection_property_value('image')); - } - - /** - * Test if the value the class is initialized with is correct - * - * @covers \PHPDraft\Out\TemplateRenderer::strip_link_spaces - */ - public function testStripSpaces(): void - { - $return = $this->class->strip_link_spaces('hello world'); - $this->assertEquals('hello-world', $return); - } - - /** - * Provide HTTP status codes - * @return array> - */ - public static function responseStatusProvider(): array - { - $return = []; - - $return[] = [200, 'text-success']; - $return[] = [204, 'text-success']; - $return[] = [304, 'text-warning']; - $return[] = [404, 'text-error']; - $return[] = [501, 'text-error']; - - return $return; - } - - /** - * Test if the value the class is initialized with is correct - * - * @dataProvider responseStatusProvider - * - * @param int $code HTTP code - * @param string $text Class to return - * - * @covers \PHPDraft\Out\TemplateRenderer::get_response_status - */ - public function testResponseStatus(int $code, string $text): void - { - $return = TemplateRenderer::get_response_status($code); - $this->assertEquals($text, $return); - } - - /** - * Provide HTTP methods - * @return array> - */ - public static function requestMethodProvider(): array - { - $return = []; - - $return[] = ['POST', 'fas POST fa-plus-square']; - $return[] = ['post', 'fas POST fa-plus-square']; - $return[] = ['get', 'fas GET fa-arrow-circle-down']; - $return[] = ['put', 'fas PUT fa-pen-square']; - $return[] = ['delete', 'fas DELETE fa-minus-square']; - $return[] = ['head', 'fas HEAD fa-info']; - $return[] = ['options', 'fas OPTIONS fa-sliders-h']; - $return[] = ['PATCH', 'fas PATCH fa-band-aid']; - $return[] = ['connect', 'fas CONNECT fa-ethernet']; - $return[] = ['trace', 'fas TRACE fa-route']; - $return[] = ['cow', 'fas COW']; - - return $return; - } - - /** - * Test if the value the class is initialized with is correct - * - * @dataProvider requestMethodProvider - * - * @param string $method HTTP Method - * @param string $text Class to return - * - * @covers \PHPDraft\Out\TemplateRenderer::get_method_icon - */ - public function testRequestMethod(string $method, string $text): void - { - $return = TemplateRenderer::get_method_icon($method); - $this->assertEquals($text, $return); - } - - /** - * Test if the value the class is initialized with is correct - * - * @covers \PHPDraft\Out\TemplateRenderer::find_include_file - */ - public function testIncludeFileDefault(): void - { - $return = $this->class->find_include_file('default'); - $this->assertEquals('PHPDraft/Out/HTML/default/main.twig', $return); - } - - /** - * Test if the value the class is initialized with is correct - * - * @covers \PHPDraft\Out\TemplateRenderer::find_include_file - */ - public function testIncludeFileFallback(): void - { - $return = $this->class->find_include_file('gfsdfdsf'); - $this->assertEquals('PHPDraft/Out/HTML/default/main.twig', $return); - } - - /** - * Test if the value the class is initialized with is correct - * - * @covers \PHPDraft\Out\TemplateRenderer::find_include_file - */ - public function testIncludeFileNone(): void - { - $return = $this->class->find_include_file('gfsdfdsf', 'xyz'); - $this->assertEquals(null, $return); - } - - /** - * Test if the value the class is initialized with is correct - * - * @covers \PHPDraft\Out\TemplateRenderer::find_include_file - */ - public function testIncludeFileSingle(): void - { - set_include_path(TEST_STATICS . '/include_single:' . get_include_path()); - $return = $this->class->find_include_file('hello', 'txt'); - $this->assertEquals('hello.txt', $return); - } - - /** - * Test if the value the class is initialized with is correct - * - * @covers \PHPDraft\Out\TemplateRenderer::find_include_file - */ - public function testIncludeFileMultiple(): void - { - set_include_path(TEST_STATICS . '/include_folders:' . get_include_path()); - $return = $this->class->find_include_file('hello', 'txt'); - $this->assertEquals('hello/hello.txt', $return); - - $return = $this->class->find_include_file('test', 'txt'); - $this->assertEquals('templates/test.txt', $return); - - $return = $this->class->find_include_file('text', 'txt'); - $this->assertEquals('templates/text/text.txt', $return); - } - - /** - * @covers \PHPDraft\Out\TemplateRenderer::get - */ - public function testGetTemplateFailsEmpty(): void - { - $this->expectException('PHPDraft\Parse\ExecutionException'); - $this->expectExceptionMessage('Couldn\'t find template \'cow\''); - $this->set_reflection_property_value('template', 'cow'); - $json = '{"content": [{"content": "hello"}]}'; - - $this->assertStringEqualsFile(TEST_STATICS . '/empty_html_template', $this->class->get(json_decode($json))); - } - - /** - * @covers \PHPDraft\Out\TemplateRenderer::get - * @group twig - */ - public function testGetTemplate(): void - { - $json = '{"content": [{"content": "hello"}]}'; - - $this->assertStringEqualsFile(TEST_STATICS . '/empty_html_template', $this->class->get(json_decode($json))); - } - - /** - * @covers \PHPDraft\Out\TemplateRenderer::get - * @group twig - */ - public function testGetTemplateSorting(): void - { - $this->set_reflection_property_value('sorting', 3); - $json = '{"content": [{"content": "hello"}]}'; - - $this->assertStringEqualsFile(TEST_STATICS . '/empty_html_template', $this->class->get(json_decode($json))); - } - - /** - * @covers \PHPDraft\Out\TemplateRenderer::get - * @group twig - */ - public function testGetTemplateMetaData(): void - { - $this->set_reflection_property_value('sorting', 3); - $json = <<<'TAG' -{"content": [{"content": [], "attributes": { -"metadata": {"content": [ -{"content":{"key": {"content": "key"}, "value": {"content": "value"}}} -]}, -"meta": {"title": {"content": "title"}} -}}]} -TAG; - - $this->assertStringEqualsFile(TEST_STATICS . '/basic_html_template', $this->class->get(json_decode($json))); - } - - /** - * @covers \PHPDraft\Out\TemplateRenderer::get - * @group twig - */ - public function testGetTemplateCategories(): void - { - $this->set_reflection_property_value('sorting', 3); - $json = <<<'TAG' -{"content": [ -{"content": [{"element": "copy", "content": "__desc__"}, {"element": "category", "content": []}], - "attributes": { -"metadata": {"content": [ -{"content":{"key": {"content": "key"}, "value": {"content": "value"}}} -]}, -"meta": {"title": {"content": "title"}} -}}]} -TAG; - - $this->assertStringEqualsFile(TEST_STATICS . '/full_html_template', $this->class->get(json_decode($json))); - } -} diff --git a/src/PHPDraft/Out/TwigFactory.php b/src/PHPDraft/Out/TwigFactory.php index 725cbe89..102ed100 100644 --- a/src/PHPDraft/Out/TwigFactory.php +++ b/src/PHPDraft/Out/TwigFactory.php @@ -25,9 +25,9 @@ public static function get(LoaderInterface $loader): Environment { $twig = new Environment($loader); - $twig->addFilter(new TwigFilter('method_icon', fn(string $string) => TemplateRenderer::get_method_icon($string))); - $twig->addFilter(new TwigFilter('strip_link_spaces', fn(string $string) => TemplateRenderer::strip_link_spaces($string))); - $twig->addFilter(new TwigFilter('response_status', fn(string $string) => TemplateRenderer::get_response_status((int) $string))); + $twig->addFilter(new TwigFilter('method_icon', fn(string $string) => HtmlTemplateRenderer::get_method_icon($string))); + $twig->addFilter(new TwigFilter('strip_link_spaces', fn(string $string) => HtmlTemplateRenderer::strip_link_spaces($string))); + $twig->addFilter(new TwigFilter('response_status', fn(string $string) => HtmlTemplateRenderer::get_response_status((int) $string))); $twig->addFilter(new TwigFilter('status_reason', fn(int $code) => (new Httpstatus())->getReasonPhrase($code))); $twig->addFilter(new TwigFilter('minify_css', function (string $string) { $minify = new Css(); diff --git a/src/PHPDraft/Parse/Drafter.php b/src/PHPDraft/Parse/Drafter.php index 1b1b92e3..cdb3d576 100644 --- a/src/PHPDraft/Parse/Drafter.php +++ b/src/PHPDraft/Parse/Drafter.php @@ -52,6 +52,11 @@ public function init(ApibFileParser $apib): BaseParser public static function location(): false|string { $returnVal = shell_exec('which drafter 2> /dev/null'); + if ($returnVal === NULL) + { + return false; + } + $returnVal = preg_replace('/^\s+|\n|\r|\s+$/m', '', $returnVal); return $returnVal === null || $returnVal === '' ? false : $returnVal; diff --git a/src/PHPDraft/Parse/HtmlGenerator.php b/src/PHPDraft/Parse/HtmlGenerator.php index 27a9f15b..aacf4ba5 100644 --- a/src/PHPDraft/Parse/HtmlGenerator.php +++ b/src/PHPDraft/Parse/HtmlGenerator.php @@ -12,10 +12,10 @@ namespace PHPDraft\Parse; -use PHPDraft\Out\TemplateRenderer; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; +use PHPDraft\Out\HtmlTemplateRenderer; /** * Class HtmlGenerator. @@ -39,7 +39,7 @@ class HtmlGenerator extends BaseHtmlGenerator */ public function build_html(string $template = 'default', ?string $image = null, ?string $css = null, ?string $js = null): void { - $gen = new TemplateRenderer($template, $image); + $gen = new HtmlTemplateRenderer($template, $image); if (!is_null($css)) { $gen->css = explode(',', $css); diff --git a/src/PHPDraft/Parse/ParserFactory.php b/src/PHPDraft/Parse/ParserFactory.php index 20271564..6301917b 100644 --- a/src/PHPDraft/Parse/ParserFactory.php +++ b/src/PHPDraft/Parse/ParserFactory.php @@ -4,6 +4,8 @@ namespace PHPDraft\Parse; +use PHPDraft\Out\OpenAPI\OpenApiRenderer; + /** * Class ParserFactory. */ @@ -39,4 +41,13 @@ public static function getJson(): BaseHtmlGenerator throw new ResourceException("Couldn't get a JSON parser", 255); } + + public static function getOpenAPI(): OpenApiRenderer + { + if (Drafter::available() || DrafterAPI::available()) { + return new OpenApiRenderer(); + } + + throw new ResourceException("Couldn't get an OpenAPI renderer", 255); + } } diff --git a/src/PHPDraft/Parse/Tests/ParserFactoryTest.php b/src/PHPDraft/Parse/Tests/ParserFactoryTest.php index 5755cb18..2cf18906 100644 --- a/src/PHPDraft/Parse/Tests/ParserFactoryTest.php +++ b/src/PHPDraft/Parse/Tests/ParserFactoryTest.php @@ -82,4 +82,34 @@ public function testGetJsonFails(): void $this->unmock_method(['\PHPDraft\Parse\Drafter', 'available']); $this->unmock_method(['\PHPDraft\Parse\DrafterAPI', 'available']); } + + /** + * @covers \PHPDraft\Parse\ParserFactory::getOpenAPI + */ + public function testGetOpenAPI(): void + { + $this->mock_method(['\PHPDraft\Parse\Drafter', 'available'], fn() => false); + $this->mock_method(['\PHPDraft\Parse\DrafterAPI', 'available'], fn() => true); + + $this->assertInstanceOf('\PHPDraft\Out\OpenAPI\OpenApiRenderer', ParserFactory::getOpenAPI()); + + $this->unmock_method(['\PHPDraft\Parse\Drafter', 'available']); + $this->unmock_method(['\PHPDraft\Parse\DrafterAPI', 'available']); + } + + /** + * @covers \PHPDraft\Parse\ParserFactory::getOpenAPI + */ + public function testGetOpenAPIFails(): void + { + $this->expectException('\PHPDraft\Parse\ResourceException'); + $this->expectExceptionMessage('Couldn\'t get an OpenAPI renderer'); + $this->mock_method(['\PHPDraft\Parse\Drafter', 'available'], fn() => false); + $this->mock_method(['\PHPDraft\Parse\DrafterAPI', 'available'], fn() => false); + + ParserFactory::getOpenAPI(); + + $this->unmock_method(['\PHPDraft\Parse\Drafter', 'available']); + $this->unmock_method(['\PHPDraft\Parse\DrafterAPI', 'available']); + } } diff --git a/tests/phpunit.xml b/tests/phpunit.xml index 7ac9607c..6fbe54fd 100755 --- a/tests/phpunit.xml +++ b/tests/phpunit.xml @@ -13,6 +13,7 @@ ../src/PHPDraft/Out/Tests/ + ../src/PHPDraft/Out/OpenAPI/Tests/ ../src/PHPDraft/Parse/Tests/