├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ ├── docker.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── build.xml ├── build ├── binary-phar-autoload.php.in ├── library-phar-autoload.php.in ├── phar-manifest.php └── phar-version.php ├── composer.json ├── composer.lock ├── docs └── man1 │ └── phpdraft.1 ├── phpdraft ├── phpstan.neon.dist ├── sonar-project.properties ├── src └── PHPDraft │ ├── Core │ └── Autoloader.php │ ├── In │ ├── ApibFileParser.php │ └── Tests │ │ └── ApibFileParserTest.php │ ├── Model │ ├── Category.php │ ├── Comparable.php │ ├── Elements │ │ ├── ArrayStructureElement.php │ │ ├── BasicStructureElement.php │ │ ├── ElementStructureElement.php │ │ ├── EnumStructureElement.php │ │ ├── ObjectStructureElement.php │ │ ├── RequestBodyElement.php │ │ ├── StructureElement.php │ │ └── Tests │ │ │ ├── ArrayStructureElementTest.php │ │ │ ├── BasicStructureElementTest.php │ │ │ ├── ElementStructureElementTest.php │ │ │ ├── EnumStructureElementTest.php │ │ │ ├── ObjectStructureElementTest.php │ │ │ └── RequestBodyElementTest.php │ ├── HTTPRequest.php │ ├── HTTPResponse.php │ ├── HierarchyElement.php │ ├── Resource.php │ ├── Tests │ │ ├── CategoryTest.php │ │ ├── HTTPRequestTest.php │ │ ├── HTTPResponseTest.php │ │ ├── HierarchyElementChildTestBase.php │ │ ├── HierarchyElementTest.php │ │ ├── ObjectElementTest.php │ │ ├── ResourceTest.php │ │ └── TransitionTest.php │ └── Transition.php │ ├── Out │ ├── BaseTemplateRenderer.php │ ├── HTML │ │ ├── default │ │ │ ├── category.twig │ │ │ ├── main.css │ │ │ ├── main.js │ │ │ ├── main.twig │ │ │ ├── nav.twig │ │ │ ├── resource.twig │ │ │ ├── structure.twig │ │ │ ├── transition.twig │ │ │ └── value.twig │ │ └── material │ │ │ ├── main.css │ │ │ ├── main.js │ │ │ ├── main.twig │ │ │ ├── nav.twig │ │ │ ├── structure.twig │ │ │ └── transition.twig │ ├── HtmlTemplateRenderer.php │ ├── OpenAPI │ │ ├── OpenApiRenderer.php │ │ └── Tests │ │ │ └── OpenApiRendererTest.php │ ├── Sorting.php │ ├── Tests │ │ ├── HtmlTemplateRendererTest.php │ │ ├── SortingTest.php │ │ ├── TwigFactoryTest.php │ │ └── VersionTest.php │ ├── TwigFactory.php │ └── Version.php │ └── Parse │ ├── BaseHtmlGenerator.php │ ├── BaseParser.php │ ├── Drafter.php │ ├── DrafterAPI.php │ ├── ExecutionException.php │ ├── HtmlGenerator.php │ ├── ParserFactory.php │ ├── ResourceException.php │ └── Tests │ ├── BaseParserTest.php │ ├── DrafterAPITest.php │ ├── DrafterTest.php │ ├── HtmlGeneratorTest.php │ └── ParserFactoryTest.php └── tests ├── phpcs.xml ├── phpunit.xml ├── statics ├── basic_html_template ├── drafter │ ├── apib │ │ ├── errors.apib │ │ ├── include.apib │ │ ├── include.md │ │ ├── including.apib │ │ ├── index.apib │ │ └── inheritance.apib │ ├── help.txt │ ├── html │ │ ├── basic.html │ │ ├── basic_old.html │ │ ├── index.html │ │ ├── inheritance.html │ │ ├── material.html │ │ └── material_old.html │ └── json │ │ ├── error.json │ │ ├── index.json │ │ └── inheritance.json ├── empty_html_template ├── full_html_template ├── full_test.apib ├── include_folders │ ├── hello │ │ └── hello.txt │ └── templates │ │ ├── test.txt │ │ └── text │ │ └── text.txt ├── include_single │ └── hello.txt └── openapi │ └── empty.json └── test.bootstrap.inc.php /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.html] 2 | indent_size = 4 3 | indent_style = space 4 | 5 | [*.php] 6 | indent_size = 4 7 | indent_style = space 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: smillerdev 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | # Check for updates to GitHub Actions every week 12 | interval: "weekly" 13 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish a Docker image 2 | on: 3 | push: 4 | branches: ['main'] 5 | tags: 6 | - ".*" 7 | pull_request: 8 | paths: 9 | - Dockerfile 10 | - .github/workflows/docker.yml 11 | env: 12 | REGISTRY: ghcr.io 13 | IMAGE_NAME: ${{ github.repository }} 14 | jobs: 15 | build-and-push-image: 16 | runs-on: ubuntu-latest 17 | permissions: 18 | contents: read 19 | packages: write 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v4 23 | with: 24 | fetch-tags: true 25 | fetch-depth: 0 26 | 27 | - name: Log in to the Container registry 28 | uses: docker/login-action@master 29 | with: 30 | registry: ${{ env.REGISTRY }} 31 | username: ${{ github.actor }} 32 | password: ${{ secrets.GITHUB_TOKEN }} 33 | 34 | - name: Extract metadata (tags, labels) for Drafter 35 | id: meta-drafter 36 | uses: docker/metadata-action@master 37 | with: 38 | images: ${{ env.REGISTRY }}/${{ github.repository }}/drafter 39 | 40 | - name: Build and push drafter Docker image 41 | uses: docker/build-push-action@master 42 | with: 43 | context: . 44 | push: true 45 | tags: ${{ steps.meta-drafter.outputs.tags }} 46 | labels: ${{ steps.meta-drafter.outputs.labels }} 47 | target: drafter 48 | no-cache-filters: drafter-build,drafter 49 | cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ github.repository }}/drafter:latest 50 | 51 | - name: Extract metadata (tags, labels) for PHPDraft 52 | id: meta 53 | uses: docker/metadata-action@master 54 | with: 55 | images: ${{ env.REGISTRY }}/${{ github.repository }} 56 | 57 | - name: Last tag 58 | id: tag-info 59 | run: | 60 | echo "latest=$(git describe --tags --always --abbrev=0)" >> "$GITHUB_OUTPUT" 61 | 62 | - name: Build and push PHPDraft Docker image 63 | uses: docker/build-push-action@master 64 | with: 65 | push: true 66 | tags: ${{ steps.meta.outputs.tags }} 67 | labels: ${{ steps.meta.outputs.labels }} 68 | target: phpdraft 69 | no-cache-filters: composer,phpdraft-build,phpdraft 70 | build-args: | 71 | BUILDKIT_CONTEXT_KEEP_GIT_DIR=true 72 | PHPDRAFT_RELEASE_ID=${{ steps.tag-info.outputs.latest }} 73 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: PHPDrafter release 2 | 3 | on: 4 | release: 5 | types: [created, edited] 6 | 7 | jobs: 8 | run: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Setup PHP 15 | uses: shivammathur/setup-php@v2 16 | with: 17 | php-version: 8.1 18 | ini-values: assert.exception=1, phar.readonly=0, zend.assertions=1 19 | extensions: curl, json, phar, mbstring, gzip, bzip2, openssl 20 | tools: pecl, phing 21 | coverage: none 22 | 23 | - name: Get Composer Cache Directory 24 | id: composer-cache 25 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 26 | 27 | - name: Cache dependencies 28 | uses: actions/cache@v4 29 | with: 30 | path: ${{ steps.composer-cache.outputs.dir }} 31 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 32 | restore-keys: ${{ runner.os }}-composer- 33 | 34 | - name: Validate composer.json and composer.lock 35 | run: composer validate 36 | 37 | - name: Install dependencies 38 | run: composer install --prefer-dist --no-progress --ignore-platform-reqs 39 | 40 | - name: Compile phar 41 | run: phing phar 42 | 43 | - name: Shasum builds 44 | run: sha256sum build/out/* 45 | 46 | - name: Upload binary to release 47 | uses: svenstaro/upload-release-action@2.9.0 48 | with: 49 | repo_token: ${{ secrets.GITHUB_TOKEN }} 50 | file: build/out/phpdraft-${{ github.event.release.tag_name }}.phar 51 | asset_name: phpdraft-${{ github.event.release.tag_name }}.phar 52 | tag: ${{ github.event.release.tag_name }} 53 | overwrite: false 54 | 55 | - name: Upload library to release 56 | uses: svenstaro/upload-release-action@2.9.0 57 | with: 58 | repo_token: ${{ secrets.GITHUB_TOKEN }} 59 | file: build/out/phpdraft-library-${{ github.event.release.tag_name }}.phar 60 | asset_name: phpdraft-library-${{ github.event.release.tag_name }}.phar 61 | tag: ${{ github.event.release.tag_name }} 62 | overwrite: false 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /.phpintel/ 3 | /tests/.phpunit.cache/** 4 | /tests/.phpunit.result.cache 5 | 6 | # IntelliJ 7 | /build/coverage 8 | /build/logs 9 | /build/phar 10 | /build/tmp 11 | /build/out 12 | /build/*.phar 13 | /tests/statics/index.* 14 | src/.gitignore 15 | vendor/** 16 | 17 | # JIRA plugin 18 | atlassian-ide-plugin.xml 19 | 20 | *.pem 21 | 22 | /index.html 23 | /openapi.json 24 | 25 | /coverage.xml 26 | /event.json 27 | !/src/PHPDraft/Out/ 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye-slim AS drafter-build 2 | RUN apt-get update && \ 3 | apt-get install --yes curl ca-certificates 4 | 5 | RUN curl -L --fail -o drafter.tar.gz https://github.com/apiaryio/drafter/releases/download/v5.1.0/drafter-v5.1.0.tar.gz 6 | RUN install -d /usr/src/drafter 7 | RUN tar -xvf drafter.tar.gz --strip-components=1 --directory /usr/src/drafter 8 | 9 | WORKDIR /usr/src/drafter 10 | 11 | RUN apt-get install --yes cmake g++ 12 | 13 | RUN cmake -S . -B build -DCMAKE_BUILD_TYPE=Release 14 | RUN cmake --build build 15 | RUN cmake --install build 16 | 17 | FROM debian:bullseye-slim AS drafter 18 | COPY --from=drafter-build /usr/local/bin/drafter /usr/local/bin/drafter 19 | 20 | CMD drafter 21 | 22 | FROM composer:latest AS composer 23 | 24 | WORKDIR /usr/src/phpdraft 25 | COPY . /usr/src/phpdraft/ 26 | RUN composer install --ignore-platform-req=ext-uopz 27 | 28 | FROM php:8.3-cli-bullseye AS phpdraft-build 29 | 30 | ARG PHPDRAFT_RELEASE_ID=0.0.0 31 | 32 | RUN echo $PHPDRAFT_RELEASE_ID 33 | 34 | COPY --from=composer /usr/src/phpdraft /usr/src/phpdraft 35 | WORKDIR /usr/src/phpdraft 36 | 37 | RUN echo "phar.readonly=0" >> /usr/local/etc/php/conf.d/phar.ini 38 | 39 | RUN php ./vendor/bin/phing phar-nightly 40 | RUN cp /usr/src/phpdraft/build/out/phpdraft-nightly.phar /usr/local/bin/phpdraft 41 | 42 | FROM php:8.3-cli-bullseye AS phpdraft 43 | 44 | LABEL maintainer="Sean Molenaar sean@seanmolenaar.eu" 45 | 46 | COPY --from=drafter-build /usr/local/bin/drafter /usr/local/bin/drafter 47 | COPY --from=phpdraft-build /usr/local/bin/phpdraft /usr/local/bin/phpdraft 48 | 49 | CMD phpdraft 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHPDraft [![Packagist Version](https://img.shields.io/packagist/v/smillerdev/phpdraft.svg)](https://github.com/SMillerDev/phpdraft/releases/latest) [![Sonar Quality Gate](https://img.shields.io/sonar/https/sonarcloud.io/SMillerDev_phpdraft/alert_status.svg)](https://sonarcloud.io/dashboard?id=SMillerDev_phpdraft) [![codecov](https://codecov.io/gh/SMillerDev/phpdraft/branch/master/graph/badge.svg?token=2IPSlcCwXM)](https://codecov.io/gh/SMillerDev/phpdraft) [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FSMillerDev%2Fphpdraft.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2FSMillerDev%2Fphpdraft?ref=badge_shield) 2 | 3 | This is a parser for API Blueprint files in PHP.[1](#dependencies) 4 | 5 | ## Dependencies 6 | PHPDraft requires [drafter](https://github.com/apiaryio/drafter) to be installed. Refer to the drafter page for the installation details. If you don't want to install drafter, you can pass `-o` to the command to make it use [https://api.apiblueprint.org/parser](https://api.apiblueprint.org/parser) 7 | 8 | ## Usage 9 | Requires PHP 8.1+ to run. Unittests require runkit or uopz 10 | For direct usage you can run: 11 | ```bash 12 | $ ./phpdraft.phar -f blueprint-file.apib > blueprint-webpage.html 13 | ``` 14 | You can also install it first: 15 | ```bash 16 | $ cp phpdraft.phar /usr/bin/phpdraft 17 | $ chmod +x /usr/bin/phpdraft 18 | $ phpdraft -f blueprint-file.apib > blueprint-webpage.html 19 | ``` 20 | 21 | ## Extra features 22 | We got some fun stuff, check the [wiki](https://github.com/SMillerDev/phpdraft/wiki) for more. 23 | 24 | ## Writing API documentation 25 | 26 | For writing API documentation using [API Blueprint](http://apiblueprint.org/) syntax. You can read about its [specification](https://github.com/apiaryio/api-blueprint/blob/master/API%20Blueprint%20Specification.md). 27 | 28 | Here's the example: 29 | 30 | ```markdown 31 | FORMAT: 1A 32 | HOST: https://api.example.com/v1 33 | 34 | # Hello API 35 | 36 | A simple API demo 37 | 38 | # Group People 39 | 40 | This section describes about the People 41 | 42 | ## Person [/people/{id}] 43 | 44 | Represent particular Person 45 | 46 | + Parameters 47 | 48 | + id (required, string, `123`) ... The id of the Person. 49 | 50 | + Model (application/json) 51 | 52 | ``` 53 | {"name":"Gesang","birthdate":"01-09-1917"} 54 | ``` 55 | 56 | ### Retrieve Person [GET] 57 | 58 | Return the information for the Person 59 | 60 | + Request (application/json) 61 | 62 | + Headers 63 | 64 | ``` 65 | Authorization: Basic AbcdeFg= 66 | ``` 67 | 68 | + Response 200 (application/json) 69 | 70 | [Person][] 71 | 72 | ``` 73 | 74 | ## Building an executable 75 | Install the binary dependencies with composer (`composer install`). 76 | Run `phing phar` or `phing phar-nightly` 77 | 78 | ## Libraries 79 | This app usage the following libraries: 80 | * https://github.com/michelf/php-markdown.git 81 | 82 | 83 | ## License 84 | [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FSMillerDev%2Fphpdraft.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FSMillerDev%2Fphpdraft?ref=badge_large) 85 | -------------------------------------------------------------------------------- /build/binary-phar-autoload.php.in: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ')) { 4 | fwrite( 5 | STDERR, 6 | 'This version of PHPDraft requires PHP ___PHPMINVER___; using the latest version of PHP is highly recommended.' . PHP_EOL 7 | ); 8 | 9 | die(1); 10 | } 11 | 12 | if (__FILE__ == realpath($GLOBALS['_SERVER']['SCRIPT_NAME'])) { 13 | $execute = true; 14 | } else { 15 | $execute = false; 16 | } 17 | 18 | define('__PHPDRAFT_PHAR__', str_replace(DIRECTORY_SEPARATOR, '/', __FILE__)); 19 | define('__PHPDRAFT_PHAR_ROOT__', 'phar://___PHAR___'); 20 | 21 | Phar::mapPhar('___PHAR___'); 22 | 23 | ___FILELIST___ 24 | 25 | if ($execute) { 26 | if (isset($_SERVER['argv'][1]) && $_SERVER['argv'][1] == '--manifest') { 27 | print file_get_contents(__PHPDRAFT_PHAR_ROOT__ . '/manifest.txt'); 28 | exit; 29 | } 30 | 31 | require_once __PHPDRAFT_PHAR_ROOT__.DIRECTORY_SEPARATOR.'phpdraft'.DIRECTORY_SEPARATOR.'phpdraft'; 32 | } 33 | 34 | __HALT_COMPILER(); 35 | -------------------------------------------------------------------------------- /build/library-phar-autoload.php.in: -------------------------------------------------------------------------------- 1 | &1'); 6 | 7 | if (strpos($tag, '-') === false && strpos($tag, 'No names found') === false) { 8 | print $tag; 9 | } else { 10 | $branch = @exec('git rev-parse --abbrev-ref HEAD'); 11 | $hash = @exec('git log -1 --format="%H"'); 12 | print $branch . '@' . $hash; 13 | } 14 | 15 | print "\n"; 16 | -------------------------------------------------------------------------------- /build/phar-version.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | ] 7 | .PP 8 | Parse API Blueprint files. 9 | .PP 10 | OPTIONS 11 | .TP 12 | \fB\-\-css\fR, \fB\-c\fR 13 | Specifies a CSS file to include (value is put in a link 14 | element without checking). 15 | .TP 16 | \fB\-\-debug\-json\fR 17 | Input a rendered JSON text for debugging. 18 | .TP 19 | \fB\-\-debug\-json\-file\fR 20 | Input a rendered JSON file for debugging. 21 | .TP 22 | \fB\-\-file\fR, \fB\-f\fR 23 | Specifies the file to parse. 24 | .TP 25 | \fB\-\-header_image\fR, \fB\-i\fR 26 | Specifies an image to display in the header. 27 | .TP 28 | \fB\-\-help\fR, \-? 29 | Display this help. 30 | .TP 31 | \fB\-\-javascript\fR, \fB\-j\fR 32 | Specifies a JS file to include (value is put in a script 33 | element without checking). 34 | .TP 35 | \fB\-\-online\fR, \fB\-o\fR 36 | Always use the online mode. 37 | .TP 38 | \fB\-\-sort\fR, \fB\-s\fR 39 | Sort displayed values [All|None|Structures|Webservices] 40 | (defaults to the way the objects are in the file). 41 | .TP 42 | \fB\-\-template\fR, \fB\-t\fR 43 | Specifies the template to use. (defaults to 'default'). 44 | .TP 45 | \fB\-\-version\fR, \fB\-v\fR 46 | Print the version for PHPDraft. 47 | .TP 48 | \fB\-\-yes\fR, \fB\-y\fR 49 | Always accept using the online mode. 50 | .SH "SEE ALSO" 51 | The full documentation for 52 | .B PHPDraft: 53 | is maintained as a Texinfo manual. If the 54 | .B info 55 | and 56 | .B PHPDraft: 57 | programs are properly installed at your site, the command 58 | .IP 59 | .B info PHPDraft: 60 | .PP 61 | should give you access to the complete manual. 62 | -------------------------------------------------------------------------------- /phpdraft: -------------------------------------------------------------------------------- 1 | description('Parse API Blueprint files.') 28 | ->opt('help:h', 'This help text', false) 29 | ->opt('version:v', 'Print the version for PHPDraft.', false) 30 | ->opt('file:f', 'Specifies the file to parse.', false) 31 | ->opt('openapi:a', 'Output location for an OpenAPI file.', false) 32 | ->opt('yes:y', 'Always accept using the online mode.', false, 'bool') 33 | ->opt('online:o', 'Always use the online mode.', false, 'bool') 34 | ->opt('template:t', 'Specifies the template to use. (defaults to \'default\').', false) 35 | ->opt('sort:s', 'Sort displayed values [All|None|Structures|Webservices] (defaults to the way the objects are in the file).', false) 36 | ->opt('header_image:i', 'Specifies an image to display in the header.', false) 37 | ->opt('css:c', 'Specifies a CSS file to include (value is put in a link element without checking).', false) 38 | ->opt('javascript:j', 'Specifies a JS file to include (value is put in a script element without checking).', false) 39 | ->opt('debug-json-file', 'Input a rendered JSON file for debugging.', false) 40 | ->opt('debug-json', 'Input a rendered JSON text for debugging.', false); 41 | 42 | // Parse and return cli args. 43 | $args = $cli->parse($argv, FALSE); 44 | if (isset($args['help']) || empty($args->getOpts())) { 45 | $cli->writeHelp(); 46 | throw new ExecutionException('', 0); 47 | } 48 | if (isset($args['version'])) { 49 | Version::version(); 50 | throw new ExecutionException('', 0); 51 | } 52 | 53 | stream_set_blocking(STDIN, false); 54 | $stdin = stream_get_contents(STDIN); 55 | $file = $args->getOpt('file'); 56 | if (!empty($stdin) && $file !== NULL) { 57 | throw new ExecutionException('ERROR: Passed data in both file and stdin', 2); 58 | } elseif (!empty($stdin) && $file === NULL) { 59 | $file = tempnam(sys_get_temp_dir(), 'phpdraft'); 60 | file_put_contents($file, $stdin); 61 | } 62 | if ($file === NULL || $file === '') 63 | { 64 | throw new ExecutionException('ERROR: File does not exist', 200); 65 | } 66 | 67 | if (!($file !== NULL || isset($args['debug-json-file']) || isset($args['debug-json']))) { 68 | throw new ExecutionException('Missing required option: file', 1); 69 | } 70 | 71 | define('THIRD_PARTY_ALLOWED', getenv('PHPDRAFT_THIRD_PARTY') !== '0'); 72 | if ((isset($args['y']) || isset($args['o'])) && THIRD_PARTY_ALLOWED) { 73 | define('DRAFTER_ONLINE_MODE', 1); 74 | } 75 | 76 | if (!isset($args['debug-json-file']) && !isset($args['debug-json'])) { 77 | $apib_parser = new ApibFileParser($file); 78 | $apib = $apib_parser->parse(); 79 | $offline = FALSE; 80 | $online = FALSE; 81 | 82 | try { 83 | $parser = ParserFactory::getDrafter(); 84 | $parser = $parser->init($apib); 85 | $data = $parser->parseToJson(); 86 | } catch (ResourceException $exception) { 87 | throw new ExecutionException('No drafter available', 255); 88 | } 89 | } else { 90 | $json_string = $args['debug-json'] ?? file_get_contents($args['debug-json-file']); 91 | $data = json_decode($json_string); 92 | } 93 | 94 | if (isset($args['openapi'])) { 95 | $openapi = ParserFactory::getOpenAPI()->init($data); 96 | $openapi->write($args['openapi']); 97 | } 98 | 99 | $html = ParserFactory::getJson()->init($data); 100 | $name = 'PHPD_SORT_' . strtoupper($args->getOpt('sort', '')); 101 | $html->sorting = Sorting::${$name} ?? Sorting::PHPD_SORT_NONE->value; 102 | 103 | $color1 = getenv('COLOR_PRIMARY') === FALSE ? NULL : getenv('COLOR_PRIMARY'); 104 | $color2 = getenv('COLOR_SECONDARY') === FALSE ? NULL : getenv('COLOR_SECONDARY'); 105 | $colors = (is_null($color1) || is_null($color2)) ? '' : '__' . $color1 . '__' . $color2; 106 | $html->build_html( 107 | $args->getOpt('template', 'default') . $colors, 108 | $args['header_image'], 109 | $args['css'], 110 | $args['javascript'] 111 | ); 112 | 113 | echo $html; 114 | } 115 | catch (ExecutionException|Exception $exception) 116 | { 117 | file_put_contents('php://stderr', $exception->getMessage() . PHP_EOL); 118 | exit($exception->getCode()); 119 | } 120 | 121 | function phpdraft_var_dump(...$vars) 122 | { 123 | if (defined('__PHPDRAFT_PHAR__')) 124 | { 125 | return; 126 | } 127 | echo '
';
128 |     foreach ($vars as $var)
129 |     {
130 |         var_dump($var);
131 |     }
132 |     echo '
'; 133 | } -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | phpVersion: 80100 3 | bootstrapFiles: 4 | - tests/test.bootstrap.inc.php 5 | excludePaths: 6 | - src/PHPDraft/Out/Version.php 7 | - src/PHPDraft/Parse/Tests/BaseParserTest.php 8 | - src/PHPDraft/Parse/Tests/DrafterTest.php 9 | - src/PHPDraft/Parse/Tests/DrafterAPITest.php 10 | - src/PHPDraft/**/Tests/* 11 | ignoreErrors: 12 | - '#Access to an undefined property object::\$[a-zA-Z0-9\\_]+#' 13 | - '#Access to an undefined property PHPDraft\\Model\\HierarchyElement::\$[a-zA-Z0-9_]+#' -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=SMillerDev_phpdraft 2 | sonar.organization=smillerdev 3 | sonar.projectName=PHPDraft 4 | sonar.sources=src/PHPDraft 5 | sonar.php.coverage.reportPaths=coverage.xml 6 | sonar.exclusions=src/PHPDraft/**/Tests/**, tests/** 7 | sonar.coverage.exclusions=src/PHPDraft/Out/HTML/** 8 | -------------------------------------------------------------------------------- /src/PHPDraft/Core/Autoloader.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | /** 14 | * Autoload classes according to PSR-1. 15 | */ 16 | spl_autoload_register( 17 | function (string $classname): void { 18 | $classname = ltrim($classname, '\\'); 19 | preg_match('/^(.+)?([^\\\\]+)$/U', $classname, $match); 20 | $classname = str_replace('\\', '/', $match[1]) . str_replace(['\\', '_'], '/', $match[2]) . '.php'; 21 | if (in_array($classname, ['PHPDraft', 'Mitchelf', 'QL'], true) !== false) { 22 | include_once $classname; 23 | } 24 | } 25 | ); 26 | -------------------------------------------------------------------------------- /src/PHPDraft/In/ApibFileParser.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | 11 | namespace PHPDraft\In; 12 | 13 | use PHPDraft\Parse\ExecutionException; 14 | use Stringable; 15 | 16 | /** 17 | * Class ApibFileParser. 18 | */ 19 | class ApibFileParser implements Stringable 20 | { 21 | /** 22 | * Complete API Blueprint. 23 | * 24 | * @var string 25 | */ 26 | protected string $full_apib; 27 | 28 | /** 29 | * Location of the API Blueprint to parse. 30 | * 31 | * @var string 32 | */ 33 | protected string $location; 34 | 35 | /** 36 | * Filename to parse. 37 | * 38 | * @var string 39 | */ 40 | private string $filename; 41 | 42 | /** 43 | * FileParser constructor. 44 | * 45 | * @param string $filename File to parse 46 | */ 47 | public function __construct(string $filename = 'index.apib') 48 | { 49 | $this->filename = $filename; 50 | $this->location = pathinfo($this->filename, PATHINFO_DIRNAME) . '/'; 51 | 52 | set_include_path(get_include_path() . ':' . $this->location); 53 | } 54 | 55 | /** 56 | * Get parse the apib file. 57 | * 58 | * @throws ExecutionException 59 | * 60 | * @return self self reference. 61 | */ 62 | public function parse(): self 63 | { 64 | $this->full_apib = $this->get_apib($this->filename, $this->location); 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Set apib content. 71 | * 72 | * @param string $content The content 73 | */ 74 | public function set_apib_content(string $content): void 75 | { 76 | $this->full_apib = $content; 77 | } 78 | 79 | /** 80 | * Parse a given API Blueprint file 81 | * This changes all `include(file)` tags to the contents of the file. 82 | * 83 | * @param string $filename File to parse. 84 | * @param string|null $rel_path File location to look. 85 | * 86 | * @throws ExecutionException when the file could not be found. 87 | * 88 | * @return string The full API blueprint file. 89 | */ 90 | private function get_apib(string $filename, ?string $rel_path = null): string 91 | { 92 | $path = $this->file_path($filename, $rel_path); 93 | $file = file_get_contents($path); 94 | $matches = []; 95 | preg_match_all('', $file, $matches); 96 | for ($i = 0; $i < count($matches[1]); $i++) { 97 | $file = str_replace( 98 | '', 99 | $this->get_apib($matches[1][$i] . $matches[2][$i], dirname($path)), 100 | $file 101 | ); 102 | } 103 | 104 | preg_match_all('', $file, $matches); 105 | foreach ($matches[1] as $value) { 106 | $file = str_replace('', $this->get_schema($value), $file); 107 | } 108 | 109 | return $file; 110 | } 111 | 112 | /** 113 | * Check if an APIB file exists. 114 | * 115 | * @param string $filename File to check 116 | * @param string|null $rel_path File location to look. 117 | * 118 | * @throws ExecutionException when the file could not be found. 119 | * 120 | * @return string 121 | */ 122 | private function file_path(string $filename, ?string $rel_path = null): string 123 | { 124 | // Absolute path 125 | if (file_exists($filename)) { 126 | return $filename; 127 | } 128 | 129 | // Path relative to the top file 130 | if ($rel_path !== null && file_exists($rel_path . $filename)) { 131 | return $rel_path . $filename; 132 | } 133 | 134 | // Path relative to the top file 135 | if (file_exists($this->location . $filename)) { 136 | return $this->location . $filename; 137 | } 138 | 139 | $included_path = stream_resolve_include_path($filename); 140 | if ($included_path !== false) { 141 | return $included_path; 142 | } 143 | 144 | throw new ExecutionException("API File not found: $filename", 1); 145 | } 146 | 147 | /** 148 | * Get an external Schema by URL. 149 | * 150 | * @param string $url URL to fetch the schema from 151 | * 152 | * @return string The schema as a string 153 | */ 154 | private function get_schema(string $url): string 155 | { 156 | $ch = curl_init(); 157 | curl_setopt($ch, CURLOPT_URL, $url); 158 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 159 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); 160 | $result = curl_exec($ch); 161 | curl_close($ch); 162 | 163 | return $result; 164 | } 165 | 166 | /** 167 | * Return the value of the file. 168 | * 169 | * @return string 170 | */ 171 | public function content(): string 172 | { 173 | return $this->full_apib; 174 | } 175 | 176 | /** 177 | * Return the value of the class. 178 | * 179 | * @return string 180 | */ 181 | public function __toString(): string 182 | { 183 | return $this->content(); 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/PHPDraft/In/Tests/ApibFileParserTest.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace PHPDraft\In\Tests; 11 | 12 | use Lunr\Halo\LunrBaseTest; 13 | use PHPDraft\In\ApibFileParser; 14 | use ReflectionClass; 15 | 16 | /** 17 | * Class ApibFileParserTest 18 | * @covers \PHPDraft\In\ApibFileParser 19 | */ 20 | class ApibFileParserTest extends LunrBaseTest 21 | { 22 | 23 | private ApibFileParser $class; 24 | 25 | /** 26 | * Set up tests. 27 | * 28 | * @return void Test is now set up. 29 | */ 30 | public function setUp(): void 31 | { 32 | $this->class = new ApibFileParser(__DIR__ . '/ApibFileParserTest.php'); 33 | $this->baseSetUp($this->class); 34 | } 35 | 36 | /** 37 | * Test if setup is successful 38 | * @return void 39 | */ 40 | public function testLocationSetup(): void 41 | { 42 | $this->assertPropertyEquals('location', __DIR__ . '/'); 43 | } 44 | 45 | /** 46 | * Test if setup is successful 47 | * @return void 48 | */ 49 | public function testFilenameSetup(): void 50 | { 51 | $this->assertPropertySame('filename', __DIR__ . '/ApibFileParserTest.php'); 52 | } 53 | 54 | /** 55 | * Test if exception when the file doesn't exist 56 | * 57 | * @return void 58 | */ 59 | public function testFilenameSetupWrong(): void 60 | { 61 | $this->expectException('\PHPDraft\Parse\ExecutionException'); 62 | $this->expectExceptionMessageMatches('/API File not found: .*\/drafter\/non_existing_including_apib/'); 63 | $this->expectExceptionCode(1); 64 | 65 | $this->set_reflection_property_value('filename', TEST_STATICS . '/drafter/non_existing_including_apib'); 66 | $this->class->parse(); 67 | } 68 | 69 | /** 70 | * Test if setup is successful 71 | * @return void 72 | */ 73 | public function testParseBasic(): void 74 | { 75 | $this->set_reflection_property_value('filename', TEST_STATICS . '/drafter/apib/including.apib'); 76 | $this->set_reflection_property_value('location', TEST_STATICS . '/drafter/apib/'); 77 | 78 | 79 | $this->mock_function('curl_exec', fn() => 'hello'); 80 | 81 | $this->class->parse(); 82 | 83 | $this->unmock_function('curl_exec'); 84 | 85 | $text = "FORMAT: 1A\nHOST: https://owner-api.teslamotors.com\n"; 86 | $text .= "EXTRA_HOSTS: https://test.owner-api.teslamotors.com\nSOMETHING: INFO\n\n"; 87 | $text .= "# Tesla Model S JSON API\nThis is unofficial documentation of the"; 88 | $text .= " Tesla Model S JSON API used by the iOS and Android apps. It features"; 89 | $text .= " functionality to monitor and control the Model S remotely.\n\nTEST"; 90 | $text .= "\n\n# Hello\nThis is a test.\nhello"; 91 | 92 | $this->assertPropertyEquals('full_apib', $text); 93 | $this->assertSame($text, $this->class->__toString()); 94 | } 95 | 96 | /** 97 | * Test setting content 98 | * 99 | * @covers \PHPDraft\In\ApibFileParser::set_apib_content 100 | */ 101 | public function testSetContent(): void 102 | { 103 | $this->class->set_apib_content('content'); 104 | $this->assertEquals('content', $this->get_reflection_property_value('full_apib')); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Category.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Model; 14 | 15 | use PHPDraft\Model\Elements\BasicStructureElement; 16 | use PHPDraft\Model\Elements\ObjectStructureElement; 17 | 18 | /** 19 | * Class Category. 20 | */ 21 | class Category extends HierarchyElement 22 | { 23 | /** 24 | * API Structure element. 25 | * 26 | * @var BasicStructureElement[] 27 | */ 28 | public array $structures = []; 29 | 30 | /** 31 | * Fill class values based on JSON object. 32 | * 33 | * @param object $object JSON object 34 | * 35 | * @return self self-reference 36 | */ 37 | public function parse(object $object): self 38 | { 39 | parent::parse($object); 40 | 41 | foreach ($object->content as $item) { 42 | switch ($item->element) { 43 | case 'resource': 44 | $resource = new Resource($this); 45 | $this->children[] = $resource->parse($item); 46 | break; 47 | case 'dataStructure': 48 | $deps = []; 49 | $struct = (new ObjectStructureElement())->get_class($item->content->element); 50 | $struct->deps = $deps; 51 | $struct->parse($item->content, $deps); 52 | 53 | if (isset($item->content->content) && is_array($item->content->content) && isset($item->content->content[0]->meta->id)) { 54 | $this->structures[$item->content->content[0]->meta->id] = $struct; 55 | } elseif (isset($item->content->meta->id->content)) { 56 | $this->structures[$item->content->meta->id->content] = $struct; 57 | } else { 58 | $this->structures[] = $struct; 59 | } 60 | 61 | break; 62 | default: 63 | continue 2; 64 | } 65 | } 66 | 67 | return $this; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Comparable.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Model\Elements; 14 | 15 | use Michelf\MarkdownExtra; 16 | 17 | /** 18 | * Class ArrayStructureElement. 19 | */ 20 | class ArrayStructureElement extends BasicStructureElement 21 | { 22 | /** 23 | * Parse an array object. 24 | * 25 | * @param object|null $object APIB Item to parse 26 | * @param string[] $dependencies List of dependencies build 27 | * 28 | * @return self Self reference 29 | */ 30 | public function parse(?object $object, array &$dependencies): self 31 | { 32 | $this->element = $object->element ?? 'array'; 33 | 34 | $this->parse_common($object, $dependencies); 35 | 36 | if (!isset($object->content)) { 37 | $this->value = []; 38 | 39 | return $this; 40 | } 41 | 42 | foreach ($object->content as $sub_item) { 43 | $element = new ElementStructureElement(); 44 | $element->parse($sub_item, $dependencies); 45 | $this->value[] = $element; 46 | } 47 | 48 | $this->deps = $dependencies; 49 | 50 | return $this; 51 | } 52 | 53 | /** 54 | * Provide HTML representation. 55 | * 56 | * @return string 57 | */ 58 | public function __toString(): string 59 | { 60 | if (is_string($this->value)) { 61 | $type = $this->get_element_as_html($this->element); 62 | $desc = ''; 63 | if ($this->description !== null) { 64 | $desc = MarkdownExtra::defaultTransform($this->description); 65 | } 66 | 67 | return "$this->key$type$desc"; 68 | } 69 | 70 | $return = ''; 71 | foreach ($this->value as $item) { 72 | $return .= $item->__toString(); 73 | } 74 | 75 | return ''; 76 | } 77 | 78 | /** 79 | * Get a new instance of a class. 80 | * 81 | * @return self 82 | */ 83 | protected function new_instance(): self 84 | { 85 | return new self(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Elements/BasicStructureElement.php: -------------------------------------------------------------------------------- 1 | key = null; 105 | if (isset($object->content->key)) { 106 | $key = new ElementStructureElement(); 107 | $key->parse($object->content->key, $dependencies); 108 | $this->key = $key; 109 | } 110 | 111 | $this->type = $object->content->value->element 112 | ?? $object->meta->title->content 113 | ?? $object->meta->id->content 114 | ?? null; 115 | $this->description = null; 116 | if (isset($object->meta->description->content)) { 117 | $this->description = htmlentities($object->meta->description->content); 118 | } elseif (isset($object->meta->description)) { 119 | $this->description = htmlentities($object->meta->description); 120 | } 121 | if ($this->description !== null) { 122 | $encoded = htmlentities($this->description, ENT_COMPAT, 'ISO-8859-1', false); 123 | $this->description = $encoded; 124 | } 125 | 126 | $this->ref = null; 127 | if ($this->element === 'ref') { 128 | $this->ref = $object->content; 129 | } 130 | 131 | $this->is_variable = $object->attributes->variable->content ?? false; 132 | 133 | if (isset($object->attributes->typeAttributes->content)) { 134 | $data = array_map(function ($item) { 135 | return $item->content; 136 | }, $object->attributes->typeAttributes->content); 137 | $this->status = $data; 138 | } elseif (isset($object->attributes->typeAttributes)) { 139 | $this->status = $object->attributes->typeAttributes; 140 | } 141 | 142 | if (!in_array($this->type, self::DEFAULTS, true) && $this->type !== null) { 143 | $dependencies[] = $this->type; 144 | } 145 | } 146 | 147 | /** 148 | * Represent the element in HTML. 149 | * 150 | * @param string $element Element name 151 | * 152 | * @return string HTML string 153 | */ 154 | protected function get_element_as_html(string $element): string 155 | { 156 | if (in_array($element, self::DEFAULTS, true)) { 157 | return '' . $element . ''; 158 | } 159 | 160 | $link_name = str_replace(' ', '-', strtolower($element)); 161 | return '' . $element . ''; 162 | } 163 | 164 | /** 165 | * Get a string representation of the value. 166 | * 167 | * @param bool $flat get a flat representation of the item. 168 | * 169 | * @return string 170 | */ 171 | public function string_value(bool $flat = false) 172 | { 173 | if (is_array($this->value)) { 174 | $value_key = rand(0, count($this->value)); 175 | if (is_subclass_of($this->value[$value_key], StructureElement::class) && $flat === false) { 176 | return $this->value[$value_key]->string_value($flat); 177 | } 178 | 179 | return $this->value[$value_key]; 180 | } 181 | 182 | if (is_subclass_of($this->value, BasicStructureElement::class) && $flat === true) { 183 | return is_array($this->value->value) ? array_keys($this->value->value)[0] : $this->value->value; 184 | } 185 | 186 | return $this->value; 187 | } 188 | 189 | /** 190 | * Get what element to parse with. 191 | * 192 | * @param string $element The string to parse. 193 | * 194 | * @return BasicStructureElement The element to parse to 195 | */ 196 | public function get_class(string $element): BasicStructureElement 197 | { 198 | return match ($element) { 199 | default => $this->new_instance(), 200 | 'array' => new ArrayStructureElement(), 201 | 'enum' => new EnumStructureElement(), 202 | }; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Elements/ElementStructureElement.php: -------------------------------------------------------------------------------- 1 | element, self::DEFAULTS, true)) { 41 | $dependencies[] = $object->element; 42 | } 43 | 44 | $this->type = $object->element; 45 | $this->value = $object->content ?? null; 46 | $this->description = $object->meta->description->content ?? null; 47 | 48 | return $this; 49 | } 50 | 51 | public function __toString(): string 52 | { 53 | $type = $this->get_element_as_html($this->type); 54 | 55 | $desc = is_null($this->description) ? '' : " - $this->description"; 56 | $value = is_null($this->value) ? '' : " - $this->value"; 57 | return '
  • ' . $type . $desc . $value . '
  • '; 58 | } 59 | 60 | /** 61 | * Get a string representation of the value. 62 | * 63 | * @param bool $flat get a flat representation of the item. 64 | * 65 | * @return string 66 | */ 67 | public function string_value(bool $flat = false) 68 | { 69 | if ($flat === true) { 70 | return $this->value; 71 | } 72 | 73 | return $this->__toString(); 74 | } 75 | 76 | /** 77 | * Represent the element in HTML. 78 | * 79 | * @param string|null $element Element name 80 | * 81 | * @return string HTML string 82 | */ 83 | protected function get_element_as_html(?string $element): string 84 | { 85 | if ($element === null) { 86 | return 'null'; 87 | } 88 | 89 | if (in_array($element, self::DEFAULTS, true)) { 90 | return '' . $element . ''; 91 | } 92 | 93 | $link_name = str_replace(' ', '-', strtolower($element)); 94 | return '' . $element . ''; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Elements/EnumStructureElement.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Model\Elements; 14 | 15 | use Michelf\MarkdownExtra; 16 | 17 | class EnumStructureElement extends BasicStructureElement 18 | { 19 | /** 20 | * Parse an array object. 21 | * 22 | * @param object|null $object APIB Item to parse 23 | * @param string[] $dependencies List of dependencies build 24 | * 25 | * @return self 26 | */ 27 | public function parse(?object $object, array &$dependencies): self 28 | { 29 | $this->element = $object->element; 30 | 31 | $this->parse_common($object, $dependencies); 32 | if (!isset($this->key) && isset($object->content->content)) { 33 | $this->key = new ElementStructureElement(); 34 | $this->key->parse($object->content, $dependencies); 35 | } 36 | $this->type = $this->type ?? $object->content->element ?? null; 37 | 38 | if (!isset($object->content) && !isset($object->attributes)) { 39 | $this->value = $this->key; 40 | 41 | return $this; 42 | } 43 | 44 | if (isset($object->attributes->default)) { 45 | if (!in_array($object->attributes->default->content->element ?? '', self::DEFAULTS, true)) { 46 | $dependencies[] = $object->attributes->default->content->element; 47 | } 48 | $this->value = $object->attributes->default->content->content; 49 | $this->deps = $dependencies; 50 | 51 | return $this; 52 | } 53 | 54 | if (isset($object->content)) { 55 | if (!in_array($object->content->element, self::DEFAULTS, true)) { 56 | $dependencies[] = $object->content->element; 57 | } 58 | $this->value = $object->content->content; 59 | $this->deps = $dependencies; 60 | 61 | return $this; 62 | } 63 | 64 | foreach ($object->attributes->enumerations->content as $sub_item) { 65 | $element = new ElementStructureElement(); 66 | $element->parse($sub_item, $dependencies); 67 | $this->value[] = $element; 68 | } 69 | 70 | $this->deps = $dependencies; 71 | 72 | return $this; 73 | } 74 | 75 | /** 76 | * Provide HTML representation. 77 | * 78 | * @return string 79 | */ 80 | public function __toString(): string 81 | { 82 | if (is_iterable($this->value)) { 83 | $return = ''; 84 | foreach ($this->value as $item) { 85 | $return .= $item->__toString(); 86 | } 87 | 88 | return ''; 89 | } 90 | 91 | $type = $this->get_element_as_html($this->element); 92 | $desc = $this->description === null ? '' : MarkdownExtra::defaultTransform($this->description); 93 | 94 | return "{$this->key->value}$type$desc"; 95 | } 96 | 97 | /** 98 | * Get a new instance of a class. 99 | * 100 | * @return self 101 | */ 102 | protected function new_instance(): self 103 | { 104 | return new self(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Elements/ObjectStructureElement.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Model\Elements; 14 | 15 | use Michelf\MarkdownExtra; 16 | 17 | /** 18 | * Class ObjectStructureElement. 19 | */ 20 | class ObjectStructureElement extends BasicStructureElement 21 | { 22 | /** 23 | * Object representation before parsing 24 | * @var object|null 25 | * @phpstan-ignore-next-line 26 | */ 27 | private ?object $object; 28 | 29 | /** 30 | * Unset object function. 31 | * @internal Only for tests 32 | */ 33 | public function __clearForTest(): void 34 | { 35 | $this->object = null; 36 | } 37 | 38 | /** 39 | * Parse a JSON object to a data structure. 40 | * 41 | * @param object|null $object An object to parse 42 | * @param string[] $dependencies Dependencies of this object 43 | * 44 | * @return self self reference 45 | */ 46 | public function parse(?object $object, array &$dependencies): self 47 | { 48 | $this->object = $object; 49 | if (is_null($object) || !isset($object->element) || !(isset($object->content) || isset($object->meta) )) { 50 | return $this; 51 | } 52 | 53 | $this->element = $object->element; 54 | $this->parse_common($object, $dependencies); 55 | 56 | if (isset($object->content) && is_array($object->content)) { 57 | $this->parse_array_content($object, $dependencies); 58 | return $this; 59 | } 60 | 61 | if (in_array($this->type, ['object', 'array', 'enum'], true) || !in_array($this->type, self::DEFAULTS, true)) { 62 | $this->parse_value_structure($object, $dependencies); 63 | 64 | return $this; 65 | } 66 | 67 | if (isset($object->content->value->content)) { 68 | $this->value = $object->content->value->content; 69 | } elseif (isset($object->content->value->attributes->samples)) { 70 | $this->value = array_reduce($object->content->value->attributes->samples->content, function ($carry, $item) { 71 | if ($carry === null) { 72 | return "$item->content ($item->element)"; 73 | } 74 | return "$carry | $item->content ($item->element)"; 75 | }); 76 | } else { 77 | $this->value = null; 78 | } 79 | 80 | return $this; 81 | } 82 | 83 | /** 84 | * Parse $this->value as a structure based on given content. 85 | * 86 | * @param object $object APIB content 87 | * @param string[] $dependencies Object dependencies 88 | * 89 | * @return void 90 | */ 91 | protected function parse_value_structure(object $object, array &$dependencies) 92 | { 93 | if (isset($object->content->content) || in_array($this->element, ['boolean', 'string', 'number', 'ref'], true)) { 94 | return; 95 | } 96 | 97 | $value = $object->content->value ?? $object; 98 | $type = in_array($this->element, ['member'], true) ? $this->type : $this->element; 99 | $struct = $this->get_class($type); 100 | 101 | $this->value = $struct->parse($value, $dependencies); 102 | 103 | unset($struct); 104 | unset($value); 105 | } 106 | 107 | /** 108 | * Get a new instance of a class. 109 | * 110 | * @return ObjectStructureElement 111 | */ 112 | protected function new_instance(): self 113 | { 114 | return new self(); 115 | } 116 | 117 | /** 118 | * Parse content formed as an array. 119 | * 120 | * @param object $object APIB content 121 | * @param string[] $dependencies Object dependencies 122 | * 123 | * @return void 124 | */ 125 | protected function parse_array_content(object $object, array &$dependencies): void 126 | { 127 | foreach ($object->content as $value) { 128 | $type = $this->element === 'member' ? $this->type : $this->element; 129 | $struct = $this->get_class($type); 130 | 131 | $this->value[] = $struct->parse($value, $dependencies); 132 | unset($struct); 133 | } 134 | 135 | unset($value); 136 | } 137 | 138 | /** 139 | * Print a string representation. 140 | * 141 | * @return string 142 | */ 143 | public function __toString(): string 144 | { 145 | if (is_array($this->value)) { 146 | $return = ''; 147 | foreach ($this->value as $object) { 148 | if (is_string($object) || is_subclass_of(get_class($object), StructureElement::class)) { 149 | $return .= $object; 150 | } 151 | } 152 | 153 | return "$return
    "; 154 | } 155 | 156 | if ($this->value === null && $this->key === null && $this->description !== null) { 157 | return ''; 158 | } 159 | 160 | if ($this->value === null && $this->key === null && $this->description === null) { 161 | return '{ }'; 162 | } 163 | 164 | if (is_null($this->value)) { 165 | return $this->construct_string_return(''); 166 | } 167 | 168 | if (is_object($this->value) && (self::class === get_class($this->value) || RequestBodyElement::class === get_class($this->value))) { 169 | return $this->construct_string_return('
    ' . $this->value . '
    '); 170 | } 171 | 172 | if (is_object($this->value) && (ArrayStructureElement::class === get_class($this->value))) { 173 | return $this->construct_string_return('
    ' . $this->value . '
    '); 174 | } 175 | 176 | if (is_object($this->value) && (EnumStructureElement::class === get_class($this->value))) { 177 | return $this->construct_string_return('
    ' . $this->value . '
    '); 178 | } 179 | 180 | $value = ''; 181 | if (is_bool($this->value)) { 182 | $value .= ($this->value) ? 'true' : 'false'; 183 | } else { 184 | $value .= $this->value; 185 | } 186 | 187 | $value .= ''; 188 | 189 | return $this->construct_string_return($value); 190 | } 191 | 192 | /** 193 | * Create an HTML return. 194 | * 195 | * @param string $value value to display 196 | * 197 | * @return string 198 | */ 199 | protected function construct_string_return(string $value): string 200 | { 201 | if ($this->type === null) { 202 | return $value; 203 | } 204 | 205 | $type = $this->get_element_as_html($this->type); 206 | $variable = ''; 207 | if ($this->is_variable === true) { 208 | $link_name = str_replace(' ', '-', strtolower($this->key->type)); 209 | $tooltip = 'This is a variable key of type "' . $this->key->type . '"'; 210 | $variable = ''; 211 | } 212 | $desc = ''; 213 | if ($this->description !== null) { 214 | $desc = MarkdownExtra::defaultTransform($this->description); 215 | } 216 | 217 | $status_string = join(', ', $this->status); 218 | return "{$this->key->value}{$variable}{$type} {$status_string}{$desc}{$value}"; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Elements/RequestBodyElement.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Model\Elements; 14 | 15 | /** 16 | * Class RequestBodyElement. 17 | */ 18 | class RequestBodyElement extends ObjectStructureElement 19 | { 20 | /** 21 | * Print the request body as a string. 22 | * 23 | * @param string|null $type The type of request 24 | * 25 | * @return string Request body 26 | */ 27 | public function print_request(?string $type = 'application/x-www-form-urlencoded'): string 28 | { 29 | if (is_array($this->value)) { 30 | $return = ''; 31 | $list = []; 32 | foreach ($this->value as $object) { 33 | if (get_class($object) === self::class) { 34 | $list[] = $object->print_request($type); 35 | } 36 | } 37 | 38 | $return .= match ($type) { 39 | 'application/x-www-form-urlencoded' => join('&', $list), 40 | default => join(PHP_EOL, $list), 41 | }; 42 | 43 | $return .= ''; 44 | 45 | return $return; 46 | } 47 | 48 | $value = ($this->value === null || $this->value === '') ? '?' : $this->value; 49 | 50 | switch ($type) { 51 | case 'application/x-www-form-urlencoded': 52 | return "{$this->key->value}=$value"; 53 | default: 54 | $object = []; 55 | $object[$this->key->value] = $value; 56 | 57 | return json_encode($object); 58 | } 59 | } 60 | 61 | /** 62 | * Return a new instance. 63 | * 64 | * @return RequestBodyElement 65 | */ 66 | protected function new_instance(): self 67 | { 68 | return new self(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Elements/StructureElement.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Model\Elements; 14 | 15 | use Stringable; 16 | 17 | interface StructureElement extends Stringable 18 | { 19 | /** 20 | * Default data types. 21 | * 22 | * @var string[] 23 | */ 24 | public const DEFAULTS = ['boolean', 'string', 'number', 'object', 'array', 'enum']; 25 | 26 | /** 27 | * Parse a JSON object to a structure. 28 | * 29 | * @param object|null $object An object to parse 30 | * @param string[] $dependencies Dependencies of this object 31 | * 32 | * @return self self reference 33 | */ 34 | public function parse(?object $object, array &$dependencies): self; 35 | 36 | 37 | /** 38 | * Get a string representation of the value. 39 | * 40 | * @param bool $flat get a flat representation of the item. 41 | * 42 | * @return string 43 | */ 44 | public function string_value(bool $flat = false); 45 | } 46 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Elements/Tests/BasicStructureElementTest.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace PHPDraft\Model\Elements\Tests; 11 | 12 | use Lunr\Halo\LunrBaseTest; 13 | use PHPDraft\Model\Elements\ArrayStructureElement; 14 | use PHPDraft\Model\Elements\BasicStructureElement; 15 | use PHPDraft\Model\Elements\ElementStructureElement; 16 | use PHPDraft\Model\Elements\ObjectStructureElement; 17 | use ReflectionClass; 18 | 19 | /** 20 | * Class BasicStructureElementTest 21 | * @covers \PHPDraft\Model\Elements\BasicStructureElement 22 | */ 23 | class BasicStructureElementTest extends LunrBaseTest 24 | { 25 | private BasicStructureElement $class; 26 | 27 | /** 28 | * Set up tests 29 | * 30 | * @return void 31 | */ 32 | public function setUp(): void 33 | { 34 | $this->class = $this->getMockBuilder('\PHPDraft\Model\Elements\BasicStructureElement') 35 | ->disableOriginalConstructor() 36 | ->getMockForAbstractClass(); 37 | $this->baseSetUp($this->class); 38 | } 39 | 40 | /** 41 | * Test if the value the class is initialized with is correct 42 | */ 43 | public function testSetupCorrectly(): void 44 | { 45 | $this->assertPropertyEquals('element', NULL); 46 | } 47 | 48 | /** 49 | * Test if the value the class is initialized with is correct 50 | * 51 | * @dataProvider stringValueProvider 52 | * 53 | * @param mixed $value Value to set to the class 54 | * @param mixed $string_value Expected string representation 55 | */ 56 | public function testStringValue(mixed $value, mixed $string_value): void 57 | { 58 | $this->set_reflection_property_value('value', $value); 59 | 60 | $this->mock_function('rand', fn() => 0); 61 | $return = $this->class->string_value(); 62 | $this->unmock_function('rand'); 63 | 64 | $this->assertSame($string_value, $return); 65 | } 66 | 67 | /** 68 | * Provide string values 69 | * 70 | * @return array 71 | */ 72 | public static function stringValueProvider(): array 73 | { 74 | $return = []; 75 | 76 | $return[] = ['hello', 'hello']; 77 | $return[] = [1, 1]; 78 | $return[] = [true, true]; 79 | $return[] = [[1], 1]; 80 | 81 | $obj = new ArrayStructureElement(); 82 | $obj->value = 'hello'; 83 | $return[] = [[$obj], 'hello']; 84 | 85 | return $return; 86 | } 87 | 88 | /** 89 | * Test if the value the class is initialized with is correct 90 | */ 91 | public function testParseCommonDeps(): void 92 | { 93 | $dep = []; 94 | 95 | $json = '{"meta":{},"attributes":{},"content":{"key":{"element": "string", "content":"key"}, "value":{"element":"cat"}}}'; 96 | 97 | $answer = new ObjectStructureElement(); 98 | $answer->key = new ElementStructureElement(); 99 | $answer->key->type = 'string'; 100 | $answer->key->value = 'key'; 101 | $answer->type = 'cat'; 102 | $answer->description = null; 103 | 104 | $method = $this->get_reflection_method('parse_common'); 105 | $method->invokeArgs($this->class, [json_decode($json), &$dep]); 106 | 107 | $this->assertEquals($answer->key, $this->class->key); 108 | $this->assertEquals($answer->type, $this->class->type); 109 | $this->assertEquals($answer->description, $this->class->description); 110 | $this->assertEquals($answer->status, $this->class->status); 111 | $this->assertEquals(['cat'], $dep); 112 | } 113 | 114 | /** 115 | * Test if the value the class is initialized with is correct 116 | * 117 | * @dataProvider parseValueProvider 118 | * 119 | * @param mixed $value Value to set to the class 120 | * @param BasicStructureElement $expected_value Expected string representation 121 | */ 122 | public function testParseCommon($value, BasicStructureElement $expected_value): void 123 | { 124 | $dep = []; 125 | $method = $this->get_reflection_method('parse_common'); 126 | $method->invokeArgs($this->class, [$value, &$dep]); 127 | 128 | $this->assertEquals($expected_value->key, $this->class->key); 129 | $this->assertEquals($expected_value->type, $this->class->type); 130 | $this->assertEquals($expected_value->description, $this->class->description); 131 | $this->assertEquals($expected_value->status, $this->class->status); 132 | $this->assertEquals([], $dep); 133 | } 134 | 135 | public static function parseValueProvider(): array 136 | { 137 | $return = []; 138 | 139 | $json = '{"meta":{},"attributes":{},"content":{"key":{"element": "string", "content":"key"}, "value":{"element":"string"}}}'; 140 | $obj = json_decode($json); 141 | 142 | $answer = new ObjectStructureElement(); 143 | $answer->key = new ElementStructureElement(); 144 | $answer->key->type = 'string'; 145 | $answer->key->value = 'key'; 146 | $answer->type = 'string'; 147 | $answer->description = PHP_EOL; 148 | 149 | $return[] = [$obj, $answer]; 150 | 151 | $obj2 = clone $obj; 152 | $obj2->attributes->typeAttributes = [1, 2]; 153 | $answer->status = [1, 2]; 154 | 155 | $return[] = [$obj2, $answer]; 156 | 157 | $obj3 = clone $obj; 158 | $obj3->meta->description = '__hello__'; 159 | $answer->description = '__hello__'; 160 | 161 | $return[] = [$obj3, $answer]; 162 | 163 | return $return; 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Elements/Tests/ElementStructureElementTest.php: -------------------------------------------------------------------------------- 1 | class = new ElementStructureElement(); 23 | $this->baseSetUp($this->class); 24 | } 25 | 26 | /** 27 | * Tear down 28 | */ 29 | public function tearDown(): void 30 | { 31 | unset($this->class); 32 | unset($this->reflection); 33 | } 34 | 35 | /** 36 | * @covers \PHPDraft\Model\Elements\ElementStructureElement::parse 37 | */ 38 | public function testParse(): void 39 | { 40 | $json = '{"element": "Cow", "content": "stuff", "meta": {"description": {"content": "desc"}}}'; 41 | $dep = []; 42 | $this->class->parse(json_decode($json), $dep); 43 | 44 | $this->assertPropertySame('type', 'Cow'); 45 | $this->assertPropertySame('value', 'stuff'); 46 | $this->assertPropertySame('description', 'desc'); 47 | $this->assertSame(['Cow'], $dep); 48 | } 49 | 50 | /** 51 | * @covers \PHPDraft\Model\Elements\ElementStructureElement::string_value 52 | */ 53 | public function testStringValue(): void 54 | { 55 | $this->set_reflection_property_value('type', 'string'); 56 | $this->set_reflection_property_value('description', null); 57 | $this->assertSame('
  • string
  • ', $this->class->string_value()); 58 | } 59 | 60 | /** 61 | * @covers \PHPDraft\Model\Elements\ElementStructureElement::__toString 62 | */ 63 | public function testToString(): void 64 | { 65 | $this->set_reflection_property_value('type', 'string'); 66 | $this->set_reflection_property_value('description', null); 67 | 68 | $this->assertSame('
  • string
  • ', $this->class->__toString()); 69 | } 70 | 71 | /** 72 | * @covers \PHPDraft\Model\Elements\ElementStructureElement::__toString 73 | */ 74 | public function testToStringCustomType(): void 75 | { 76 | $this->set_reflection_property_value('type', 'Cow'); 77 | $this->set_reflection_property_value('description', null); 78 | 79 | $this->assertSame('
  • Cow
  • ', $this->class->__toString()); 80 | } 81 | 82 | /** 83 | * @covers \PHPDraft\Model\Elements\ElementStructureElement::__toString 84 | */ 85 | public function testToStringDescription(): void 86 | { 87 | $this->set_reflection_property_value('type', 'Cow'); 88 | $this->set_reflection_property_value('description', 'Something'); 89 | 90 | $this->assertSame('
  • Cow - Something
  • ', $this->class->__toString()); 91 | } 92 | 93 | /** 94 | * @covers \PHPDraft\Model\Elements\ElementStructureElement::__toString 95 | */ 96 | public function testToStringValue(): void 97 | { 98 | $this->set_reflection_property_value('type', 'Cow'); 99 | $this->set_reflection_property_value('value', 'stuff'); 100 | $this->set_reflection_property_value('description', null); 101 | 102 | $this->assertSame('
  • Cow - stuff
  • ', $this->class->__toString()); 103 | } 104 | 105 | /** 106 | * @covers \PHPDraft\Model\Elements\ElementStructureElement::__toString 107 | */ 108 | public function testToStringDescriptionAndValue(): void 109 | { 110 | $this->set_reflection_property_value('type', 'Cow'); 111 | $this->set_reflection_property_value('value', 'stuff'); 112 | $this->set_reflection_property_value('description', 'Something'); 113 | 114 | $this->assertSame('
  • Cow - Something - stuff
  • ', $this->class->__toString()); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/HTTPRequest.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Model; 14 | 15 | use PHPDraft\Model\Elements\ObjectStructureElement; 16 | use PHPDraft\Model\Elements\RequestBodyElement; 17 | use PHPDraft\Model\Elements\StructureElement; 18 | 19 | class HTTPRequest implements Comparable 20 | { 21 | /** 22 | * HTTP Headers. 23 | * 24 | * @var array 25 | */ 26 | public array $headers = []; 27 | 28 | /** 29 | * The HTTP Method. 30 | * 31 | * @var string 32 | */ 33 | public string $method; 34 | 35 | /** 36 | * Title of the request. 37 | * 38 | * @var string|null 39 | */ 40 | public ?string $title; 41 | 42 | /** 43 | * Description of the request. 44 | * 45 | * @var string|null 46 | */ 47 | public ?string $description = NULL; 48 | 49 | /** 50 | * Parent class. 51 | * 52 | * @var Transition 53 | */ 54 | public Transition $parent; 55 | 56 | /** 57 | * Body of the request. 58 | * 59 | * @var mixed 60 | */ 61 | public mixed $body = null; 62 | 63 | /** 64 | * Schema of the body of the request. 65 | * 66 | * @var string|null 67 | */ 68 | public ?string $body_schema = null; 69 | /** 70 | * Structure of the request. 71 | * 72 | * @var RequestBodyElement[]|RequestBodyElement|ObjectStructureElement 73 | */ 74 | public RequestBodyElement|ObjectStructureElement|array|null $struct = []; 75 | 76 | /** 77 | * Identifier for the request. 78 | * 79 | * @var string 80 | */ 81 | protected string $id; 82 | 83 | /** 84 | * HTTPRequest constructor. 85 | * 86 | * @param Transition $parent Parent entity 87 | */ 88 | public function __construct(Transition &$parent) 89 | { 90 | $this->parent = &$parent; 91 | $this->id = defined('ID_STATIC') ? ID_STATIC : md5(microtime()); 92 | } 93 | 94 | /** 95 | * Fill class values based on JSON object. 96 | * 97 | * @param object $object JSON object 98 | * 99 | * @return self self-reference 100 | */ 101 | public function parse(object $object): self 102 | { 103 | $this->method = $object->attributes->method->content ?? $object->attributes->method; 104 | $this->title = $object->meta->title->content ?? $object->meta->title ?? null; 105 | 106 | if (isset($object->content) && $object->content !== null) { 107 | foreach ($object->content as $value) { 108 | if ($value->element === 'dataStructure') { 109 | $this->parse_structure($value); 110 | continue; 111 | } 112 | 113 | if ($value->element === 'copy') { 114 | $this->description = $value->content; 115 | continue; 116 | } 117 | 118 | if ($value->element !== 'asset') { 119 | continue; 120 | } 121 | if (is_array($value->meta->classes) && in_array('messageBody', $value->meta->classes, true)) { 122 | $this->body[] = (isset($value->content)) ? $value->content : null; 123 | $this->headers['Content-Type'] = (isset($value->attributes->contentType)) ? $value->attributes->contentType : ''; 124 | continue; 125 | } 126 | 127 | if ( 128 | isset($value->meta->classes->content) 129 | && is_array($value->meta->classes->content) 130 | && $value->meta->classes->content[0]->content === 'messageBody' 131 | ) { 132 | $this->body[] = (isset($value->content)) ? $value->content : null; 133 | $this->headers['Content-Type'] = (isset($value->attributes->contentType->content)) ? $value->attributes->contentType->content : ''; 134 | } elseif ( 135 | isset($value->meta->classes->content) 136 | && is_array($value->meta->classes->content) 137 | && $value->meta->classes->content[0]->content === 'messageBodySchema' 138 | ) { 139 | $this->body_schema = (isset($value->content)) ? $value->content : null; 140 | } 141 | } 142 | } 143 | 144 | if (isset($object->attributes->headers)) { 145 | foreach ($object->attributes->headers->content as $value) { 146 | $this->headers[$value->content->key->content] = $value->content->value->content; 147 | } 148 | } 149 | 150 | if ($this->body === null) { 151 | $this->body = &$this->struct; 152 | } 153 | 154 | return $this; 155 | } 156 | 157 | /** 158 | * Parse the objects into a request body. 159 | * 160 | * @param object $objects JSON objects 161 | */ 162 | private function parse_structure(object $objects): void 163 | { 164 | $deps = []; 165 | $structure = new RequestBodyElement(); 166 | $structure->parse($objects->content, $deps); 167 | $structure->deps = $deps; 168 | 169 | $this->struct = $structure; 170 | } 171 | 172 | public function get_id(): string 173 | { 174 | return $this->id; 175 | } 176 | 177 | /** 178 | * Generate a cURL command for the HTTP request. 179 | * 180 | * @param string $base_url URL to the base server 181 | * @param array $additional Extra options to pass to cURL 182 | * 183 | * @return string An executable cURL command 184 | */ 185 | public function get_curl_command(string $base_url, array $additional = []): string 186 | { 187 | $options = []; 188 | 189 | $type = $this->headers['Content-Type'] ?? null; 190 | 191 | $options[] = '-X' . $this->method; 192 | if (is_string($this->body)) { 193 | $options[] = '--data-binary ' . escapeshellarg($this->body); 194 | } elseif (is_array($this->body) && $this->body !== []) { 195 | $options[] = '--data-binary ' . escapeshellarg(join('', $this->body)); 196 | } elseif (is_subclass_of($this->struct, StructureElement::class)) { 197 | foreach ($this->struct->value as $body) { 198 | if (is_null($body) || $body === []) { 199 | continue; 200 | } 201 | $options[] = '--data-binary ' . escapeshellarg(strip_tags($body->print_request($type))); 202 | } 203 | } 204 | foreach ($this->headers as $header => $value) { 205 | $options[] = '-H ' . escapeshellarg($header . ': ' . $value); 206 | } 207 | 208 | $options = array_merge($options, $additional); 209 | $url = escapeshellarg($this->parent->build_url($base_url, true)); 210 | 211 | return htmlspecialchars('curl ' . join(' ', $options) . ' ' . $url, ENT_NOQUOTES | ENT_SUBSTITUTE); 212 | } 213 | 214 | /** 215 | * Check if item is the same as other item. 216 | * 217 | * @param object $b Object to compare to 218 | * 219 | * @return bool 220 | */ 221 | public function is_equal_to(object $b): bool 222 | { 223 | if (!($b instanceof self)) { 224 | return false; 225 | } 226 | return ($this->method === $b->method) 227 | && ($this->body == $b->body) 228 | && ($this->headers == $b->headers) 229 | && ($this->title === $b->title); 230 | } 231 | 232 | /** 233 | * Convert class to string identifier 234 | */ 235 | public function __toString(): string 236 | { 237 | $headers = json_encode($this->headers); 238 | $body = json_encode($this->body); 239 | return sprintf("%s_%s_%s", $this->method, $body, $headers); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/HTTPResponse.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Model; 14 | 15 | use PHPDraft\Model\Elements\ObjectStructureElement; 16 | 17 | class HTTPResponse implements Comparable 18 | { 19 | /** 20 | * HTTP Status code. 21 | * 22 | * @var int 23 | */ 24 | public int $statuscode; 25 | 26 | /** 27 | * Description of the object. 28 | * 29 | * @var string|null 30 | */ 31 | public ?string $description = null; 32 | 33 | /** 34 | * Identifier for the request. 35 | * 36 | * @var string 37 | */ 38 | protected string $id; 39 | 40 | /** 41 | * Response headers. 42 | * 43 | * @var array 44 | */ 45 | public array $headers = []; 46 | 47 | /** 48 | * Response bodies. 49 | * 50 | * @var array 51 | */ 52 | public array $content = []; 53 | 54 | /** 55 | * Response structure. 56 | * 57 | * @var ObjectStructureElement[] 58 | */ 59 | public array $structure = []; 60 | 61 | /** 62 | * Parent entity. 63 | * 64 | * @var Transition 65 | */ 66 | protected Transition $parent; 67 | 68 | public function __construct(Transition $parent) 69 | { 70 | $this->parent = &$parent; 71 | $this->id = defined('ID_STATIC') ? ID_STATIC : md5(microtime()); 72 | } 73 | 74 | /** 75 | * Fill class values based on JSON object. 76 | * 77 | * @param object $object JSON object 78 | * 79 | * @return self self-reference 80 | */ 81 | public function parse(object $object): self 82 | { 83 | if (isset($object->attributes->statusCode->content)) { 84 | $this->statuscode = intval($object->attributes->statusCode->content); 85 | } elseif (isset($object->attributes->statusCode)) { 86 | $this->statuscode = intval($object->attributes->statusCode); 87 | } 88 | if (isset($object->attributes->headers)) { 89 | $this->parse_headers($object->attributes->headers); 90 | } 91 | 92 | foreach ($object->content as $value) { 93 | $this->parse_content($value); 94 | } 95 | 96 | return $this; 97 | } 98 | 99 | public function get_id(): string 100 | { 101 | return $this->id; 102 | } 103 | 104 | /** 105 | * Parse request headers. 106 | * 107 | * @param object $object An object to parse for headers 108 | * 109 | * @return void 110 | */ 111 | protected function parse_headers(object $object): void 112 | { 113 | foreach ($object->content as $value) { 114 | if (isset($value->content)) { 115 | $this->headers[$value->content->key->content] = $value->content->value->content; 116 | } 117 | } 118 | } 119 | 120 | /** 121 | * Parse request content. 122 | * 123 | * @param object $value An object to parse for content 124 | * 125 | * @return void 126 | */ 127 | protected function parse_content(object $value): void 128 | { 129 | if ($value->element === 'copy') { 130 | $this->description = $value->content; 131 | return; 132 | } 133 | 134 | if ($value->element === 'asset') { 135 | if (isset($value->attributes->contentType->content)) { 136 | $this->content[$value->attributes->contentType->content] = $value->content; 137 | } elseif (isset($value->attributes->contentType)) { 138 | $this->content[$value->attributes->contentType] = $value->content; 139 | } 140 | return; 141 | } 142 | 143 | if ($value->element === 'dataStructure') { 144 | foreach ($value->content->content as $object) { 145 | $this->parse_structure($object); 146 | } 147 | } 148 | } 149 | 150 | /** 151 | * Parse structure of the content. 152 | * 153 | * @param object $object Objects containing the structure 154 | * 155 | * @return void 156 | */ 157 | protected function parse_structure(object $object): void 158 | { 159 | $deps = []; 160 | $struct = new ObjectStructureElement(); 161 | $struct->parse($object, $deps); 162 | $struct->deps = $deps; 163 | foreach ($this->structure as $prev) { 164 | if ($struct->__toString() === $prev->__toString()) { 165 | return; 166 | } 167 | } 168 | 169 | $this->structure[] = $struct; 170 | } 171 | 172 | /** 173 | * Check if item is the same as other item. 174 | * 175 | * @param object $b Object to compare to 176 | * 177 | * @return bool 178 | */ 179 | public function is_equal_to(object $b): bool 180 | { 181 | if (!($b instanceof self)) { 182 | return false; 183 | } 184 | return ($this->statuscode === $b->statuscode) 185 | && ($this->description === $b->description); 186 | } 187 | 188 | /** 189 | * Convert class to string identifier 190 | */ 191 | public function __toString(): string 192 | { 193 | return "{$this->statuscode}_{$this->description}"; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/HierarchyElement.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Model; 14 | 15 | /** 16 | * Class HierarchyElement. 17 | */ 18 | abstract class HierarchyElement 19 | { 20 | /** 21 | * Title of the element. 22 | * 23 | * @var string 24 | */ 25 | public string $title; 26 | 27 | /** 28 | * Description of the element. 29 | * 30 | * @var string|null 31 | */ 32 | public ?string $description = NULL; 33 | 34 | /** 35 | * Child elements. 36 | * 37 | * @var HierarchyElement[] 38 | */ 39 | public array $children = []; 40 | 41 | /** 42 | * Parent Element. 43 | * 44 | * @var HierarchyElement|null 45 | */ 46 | protected ?HierarchyElement $parent = null; 47 | 48 | /** 49 | * Parse a JSON object to an element. 50 | * 51 | * @param object $object an object to parse 52 | * 53 | * @return void 54 | */ 55 | public function parse(object $object) 56 | { 57 | if (isset($object->meta) && isset($object->meta->title)) { 58 | $this->title = $object->meta->title->content ?? $object->meta->title; 59 | } 60 | 61 | if (!isset($object->content) || !is_array($object->content)) { 62 | return; 63 | } 64 | 65 | foreach ($object->content as $key => $item) { 66 | if ($item->element === 'copy') { 67 | $this->description = $item->content; 68 | unset($object->content[$key]); 69 | } 70 | } 71 | 72 | if ($object->content !== null && $object->content !== []) { 73 | $object->content = array_slice($object->content, 0); 74 | } 75 | } 76 | 77 | /** 78 | * Get a linkable HREF. 79 | * 80 | * @return string 81 | */ 82 | public function get_href(): string 83 | { 84 | $separator = '-'; 85 | $prep = ($this->parent !== null) ? $this->parent->get_href() . $separator : ''; 86 | 87 | return $prep . str_replace(' ', '-', strtolower($this->title)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Resource.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Model; 14 | 15 | use PHPDraft\Model\Elements\ObjectStructureElement; 16 | 17 | class Resource extends HierarchyElement 18 | { 19 | /** 20 | * Location relative to the base URL. 21 | * 22 | * @var string|null 23 | */ 24 | public ?string $href = null; 25 | 26 | /** 27 | * URL variables. 28 | * 29 | * @var ObjectStructureElement[] 30 | */ 31 | public array $url_variables = []; 32 | 33 | /** 34 | * Resource constructor. 35 | * 36 | * @param Category $parent A reference to the parent object 37 | */ 38 | public function __construct(Category &$parent) 39 | { 40 | $this->parent = $parent; 41 | } 42 | 43 | /** 44 | * Fill class values based on JSON object. 45 | * 46 | * @param object $object JSON object 47 | * 48 | * @return self self-reference 49 | */ 50 | public function parse(object $object): self 51 | { 52 | parent::parse($object); 53 | 54 | if (isset($object->attributes)) { 55 | $this->href = $object->attributes->href->content ?? $object->attributes->href; 56 | } 57 | 58 | if (isset($object->attributes->hrefVariables)) { 59 | $deps = []; 60 | foreach ($object->attributes->hrefVariables->content as $variable) { 61 | $struct = new ObjectStructureElement(); 62 | $this->url_variables[] = $struct->parse($variable, $deps); 63 | } 64 | } 65 | 66 | foreach ($object->content as $item) { 67 | $transition = new Transition($this); 68 | $this->children[] = $transition->parse($item); 69 | } 70 | 71 | return $this; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Tests/CategoryTest.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace PHPDraft\Model\Tests; 11 | 12 | use PHPDraft\Model\Category; 13 | use ReflectionClass; 14 | 15 | /** 16 | * Class CategoryTest 17 | * @covers \PHPDraft\Model\Category 18 | */ 19 | class CategoryTest extends HierarchyElementChildTestBase 20 | { 21 | private Category $class; 22 | 23 | /** 24 | * Set up 25 | */ 26 | public function setUp(): void 27 | { 28 | parent::setUp(); 29 | $this->class = new Category(); 30 | $this->baseSetUp($this->class); 31 | } 32 | 33 | /** 34 | * Tear down 35 | */ 36 | public function tearDown(): void 37 | { 38 | unset($this->parent); 39 | parent::tearDown(); 40 | } 41 | 42 | /** 43 | * Test if the value the class is initialized with is correct 44 | * @covers \PHPDraft\Model\HierarchyElement 45 | */ 46 | public function testChildrenSetup(): void 47 | { 48 | $this->assertSame([], $this->class->children); 49 | } 50 | 51 | /** 52 | * Test if the value the class is initialized with is correct 53 | */ 54 | public function testSetupCorrectly(): void 55 | { 56 | $this->assertPropertySame('parent', null); 57 | } 58 | 59 | /** 60 | * Test if the value the class is initialized with is correct 61 | */ 62 | public function testStructuresSetup(): void 63 | { 64 | $this->assertSame([], $this->class->structures); 65 | } 66 | 67 | /** 68 | * Test basic parse functions 69 | */ 70 | public function testParseIsCalled(): void 71 | { 72 | $this->set_reflection_property_value('parent', $this->parent); 73 | 74 | $obj = (object) []; 75 | $obj->content = []; 76 | 77 | $this->class->parse($obj); 78 | 79 | $this->assertSame($this->parent, $this->get_reflection_property_value('parent')); 80 | } 81 | 82 | /** 83 | * Test basic parse functions where 'element=resource' 84 | */ 85 | public function testParseIsCalledResource(): void 86 | { 87 | $this->set_reflection_property_value('parent', $this->parent); 88 | 89 | $json = '{"content":[{"element":"resource", "content":[{"element":"copy", "content":""}]}]}'; 90 | 91 | $this->class->parse(json_decode($json)); 92 | 93 | $this->assertSame($this->parent, $this->get_reflection_property_value('parent')); 94 | 95 | $this->assertNotEmpty($this->get_reflection_property_value('children')); 96 | } 97 | 98 | /** 99 | * Test basic parse functions where 'element=dataStructure' 100 | */ 101 | public function testParseIsCalledObject(): void 102 | { 103 | $this->set_reflection_property_value('parent', $this->parent); 104 | 105 | $json = '{"content":[{"element":"dataStructure", "content":{"element": "object", "key":{"content":"none"}, "value":{"element":"none"}}}]}'; 106 | 107 | $this->class->parse(json_decode($json)); 108 | 109 | $this->assertSame($this->parent, $this->get_reflection_property_value('parent')); 110 | $this->assertNotEmpty($this->get_reflection_property_value('structures')); 111 | } 112 | 113 | /** 114 | * Test basic parse functions where 'element=dataStructure' 115 | */ 116 | public function testParseIsCalledObjectMetaID(): void 117 | { 118 | $this->set_reflection_property_value('parent', $this->parent); 119 | 120 | $json = '{ 121 | "element": "category", 122 | "meta": { 123 | "classes": { 124 | "element": "array", 125 | "content": [ 126 | { 127 | "element": "string", 128 | "content": "dataStructures" 129 | } 130 | ] 131 | } 132 | }, 133 | "content": [ 134 | { 135 | "element": "dataStructure", 136 | "content": { 137 | "element": "object", 138 | "meta": { 139 | "id": { 140 | "element": "string", 141 | "content": "Org" 142 | }, 143 | "description": { 144 | "element": "string", 145 | "content": "An organization" 146 | } 147 | }, 148 | "content": [ 149 | { 150 | "element": "member", 151 | "content": { 152 | "key": { 153 | "element": "string", 154 | "content": "name" 155 | }, 156 | "value": { 157 | "element": "string", 158 | "content": "Apiary" 159 | } 160 | } 161 | } 162 | ] 163 | } 164 | } 165 | ] 166 | }'; 167 | 168 | $this->class->parse(json_decode($json)); 169 | 170 | $this->assertSame($this->parent, $this->get_reflection_property_value('parent')); 171 | $this->assertNotEmpty($this->get_reflection_property_value('structures')); 172 | } 173 | 174 | /** 175 | * Test basic parse functions where 'element=henk' 176 | */ 177 | public function testParseIsCalledDef(): void 178 | { 179 | $this->set_reflection_property_value('parent', $this->parent); 180 | 181 | $json = '{"content":[{"element":"henk", "content":[{"element":"copy", "content":""}]}]}'; 182 | 183 | $this->class->parse(json_decode($json)); 184 | 185 | $this->assertSame($this->parent, $this->get_reflection_property_value('parent')); 186 | $this->assertEmpty($this->get_reflection_property_value('children')); 187 | $this->assertEmpty($this->get_reflection_property_value('structures')); 188 | } 189 | 190 | /** 191 | * Test basic get_href 192 | */ 193 | public function testGetHrefIsCalledWithParent(): void 194 | { 195 | $this->set_reflection_property_value('parent', $this->parent); 196 | $this->set_reflection_property_value('title', 'title'); 197 | 198 | $this->parent->expects($this->once()) 199 | ->method('get_href') 200 | ->willReturn('hello'); 201 | 202 | $result = $this->class->get_href(); 203 | 204 | $this->assertSame($result, 'hello-title'); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Tests/HTTPResponseTest.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace PHPDraft\Model\Tests; 11 | 12 | use Lunr\Halo\LunrBaseTest; 13 | use PHPDraft\Model\HierarchyElement; 14 | use PHPDraft\Model\HTTPResponse; 15 | use PHPDraft\Model\Transition; 16 | use PHPUnit\Framework\MockObject\MockObject; 17 | use ReflectionClass; 18 | 19 | /** 20 | * Class HTTPResponseTest 21 | * @covers \PHPDraft\Model\HTTPResponse 22 | */ 23 | class HTTPResponseTest extends LunrBaseTest 24 | { 25 | 26 | private HTTPResponse $class; 27 | 28 | /** 29 | * Mock of the parent class 30 | * 31 | * @var HierarchyElement|MockObject 32 | */ 33 | protected mixed $parent; 34 | 35 | /** 36 | * Mock of the parent class 37 | * 38 | * @var Transition|MockObject 39 | */ 40 | protected mixed $parent_transition; 41 | 42 | /** 43 | * Set up 44 | */ 45 | public function setUp(): void 46 | { 47 | $this->parent_transition = $this->getMockBuilder('\PHPDraft\Model\Transition') 48 | ->disableOriginalConstructor() 49 | ->getMock(); 50 | $this->parent = $this->getMockBuilder('\PHPDraft\Model\Transition') 51 | ->disableOriginalConstructor() 52 | ->getMock(); 53 | $this->mock_function('microtime', fn() => '1000'); 54 | $this->class = new HTTPResponse($this->parent_transition); 55 | $this->unmock_function('microtime'); 56 | $this->baseSetUp($this->class); 57 | } 58 | 59 | /** 60 | * Tear down 61 | */ 62 | public function tearDown(): void 63 | { 64 | parent::tearDown(); 65 | } 66 | 67 | /** 68 | * Test if the value the class is initialized with is correct 69 | */ 70 | public function testSetupCorrectly(): void 71 | { 72 | $this->assertSame($this->parent_transition, $this->get_reflection_property_value('parent')); 73 | $this->assertSame('a9b7ba70783b617e9998dc4dd82eb3c5', $this->get_reflection_property_value('id')); 74 | } 75 | 76 | /** 77 | * Tests if get_id returns the correct ID. 78 | */ 79 | public function testGetId(): void 80 | { 81 | $this->assertSame('a9b7ba70783b617e9998dc4dd82eb3c5', $this->class->get_id()); 82 | } 83 | 84 | /** 85 | * Test basic parse functions 86 | */ 87 | public function testParseIsCalled(): void 88 | { 89 | $this->set_reflection_property_value('parent', $this->parent); 90 | 91 | $obj = '{"attributes":{"statusCode":1000, "headers":{"content":[]}}, "content":[]}'; 92 | 93 | $this->class->parse(json_decode($obj)); 94 | 95 | $this->assertSame($this->parent, $this->get_reflection_property_value('parent')); 96 | $this->assertSame(1000, $this->get_reflection_property_value('statuscode')); 97 | } 98 | 99 | /** 100 | * Test basic parse functions 101 | */ 102 | public function testParseIsCalledExtraHeaders(): void 103 | { 104 | $this->set_reflection_property_value('parent', $this->parent); 105 | 106 | $obj = '{"attributes":{"statusCode":1000, "headers":{"content":[{"content":{"key":{"content":"contentKEY"}, "value":{"content":"contentVALUE"}}}]}}, "content":[]}'; 107 | 108 | $this->class->parse(json_decode($obj)); 109 | 110 | $this->assertSame($this->parent, $this->get_reflection_property_value('parent')); 111 | $this->assertSame(['contentKEY' => 'contentVALUE'], $this->get_reflection_property_value('headers')); 112 | } 113 | 114 | /** 115 | * Test basic parse functions 116 | */ 117 | public function testParseIsCalledWOAttributes(): void 118 | { 119 | $this->set_reflection_property_value('parent', $this->parent); 120 | $this->set_reflection_property_value('statuscode', 200); 121 | 122 | $obj = '{"content":[]}'; 123 | 124 | $this->class->parse(json_decode($obj)); 125 | 126 | $this->assertSame($this->parent, $this->get_reflection_property_value('parent')); 127 | $this->assertSame($this->get_reflection_property_value('statuscode'), 200); 128 | } 129 | 130 | /** 131 | * Test basic parse functions 132 | */ 133 | public function testParseIsCalledCopyContent(): void 134 | { 135 | $this->set_reflection_property_value('parent', $this->parent); 136 | 137 | $obj = '{"content":[{"element":"copy", "content":""}]}'; 138 | 139 | $this->class->parse(json_decode($obj)); 140 | 141 | $this->assertSame($this->parent, $this->get_reflection_property_value('parent')); 142 | $this->assertSame('', $this->get_reflection_property_value('description')); 143 | } 144 | 145 | /** 146 | * Test basic parse functions 147 | */ 148 | public function testParseIsCalledStructContentEmpty(): void 149 | { 150 | $this->set_reflection_property_value('parent', $this->parent); 151 | 152 | $obj = '{"content":[{"element":"dataStructure", "content":{"content": {}}}]}'; 153 | 154 | $this->class->parse(json_decode($obj)); 155 | 156 | $this->assertSame($this->parent, $this->get_reflection_property_value('parent')); 157 | $this->assertEmpty($this->get_reflection_property_value('structure')); 158 | } 159 | 160 | /** 161 | * Test basic parse functions 162 | */ 163 | public function testParseIsCalledStructContent(): void 164 | { 165 | $this->set_reflection_property_value('parent', $this->parent); 166 | 167 | $obj = '{"content":[{"element":"dataStructure", "content":{"content": [{}]}}]}'; 168 | 169 | $this->class->parse(json_decode($obj)); 170 | 171 | $this->assertSame($this->parent, $this->get_reflection_property_value('parent')); 172 | $this->assertNotEmpty($this->get_reflection_property_value('structure')); 173 | } 174 | 175 | /** 176 | * Test basic parse functions 177 | */ 178 | public function testParseIsCalledStructContentHasAttr(): void 179 | { 180 | $this->set_reflection_property_value('parent', $this->parent); 181 | 182 | $obj = '{"content":[{"content":"hello", "attributes":{"contentType":"content"}, "element":"asset"}]}'; 183 | 184 | $this->class->parse(json_decode($obj)); 185 | $prop = $this->get_reflection_property_value('content'); 186 | $this->assertArrayHasKey('content', $prop); 187 | $this->assertSame('hello', $prop['content']); 188 | } 189 | 190 | /** 191 | * Test basic is_equal_to functions 192 | */ 193 | public function testEqualOnStatusCode(): void 194 | { 195 | $this->set_reflection_property_value('statuscode', 200); 196 | 197 | $obj = '{"statuscode":200, "description":"hello"}'; 198 | 199 | $return = $this->class->is_equal_to(json_decode($obj)); 200 | 201 | $this->assertFalse($return); 202 | } 203 | 204 | /** 205 | * Test basic is_equal_to functions 206 | */ 207 | public function testEqualOnDesc(): void 208 | { 209 | $this->set_reflection_property_value('description', 'hello'); 210 | 211 | $obj = '{"statuscode":300, "description":"hello"}'; 212 | 213 | $return = $this->class->is_equal_to(json_decode($obj)); 214 | 215 | $this->assertFalse($return); 216 | } 217 | 218 | /** 219 | * Test basic is_equal_to functions 220 | */ 221 | public function testEqualOnBoth(): void 222 | { 223 | $this->set_reflection_property_value('statuscode', 200); 224 | $this->set_reflection_property_value('description', 'hello'); 225 | 226 | $obj = '{"attributes":{"statusCode":200}, "content":[{"element":"copy", "content": "hello"}]}'; 227 | $b = new HTTPResponse($this->parent_transition); 228 | $b->parse(json_decode($obj)); 229 | 230 | $return = $this->class->is_equal_to($b); 231 | 232 | $this->assertTrue($return); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Tests/HierarchyElementChildTestBase.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace PHPDraft\Model\Tests; 11 | 12 | use Lunr\Halo\LunrBaseTest; 13 | use PHPDraft\Model\HierarchyElement; 14 | use PHPUnit\Framework\MockObject\MockObject; 15 | 16 | /** 17 | * Class HierarchyElementChildTest 18 | * @package PHPDraft\Model\Tests 19 | */ 20 | abstract class HierarchyElementChildTestBase extends LunrBaseTest 21 | { 22 | /** 23 | * Mock of the parent class 24 | * 25 | * @var HierarchyElement|MockObject 26 | */ 27 | protected HierarchyElement|MockObject $parent; 28 | 29 | public function setUp(): void 30 | { 31 | $this->parent = $this->getMockBuilder('\PHPDraft\Model\HierarchyElement') 32 | ->getMock(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Tests/HierarchyElementTest.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace PHPDraft\Model\Tests; 11 | 12 | use Lunr\Halo\LunrBaseTest; 13 | use PHPDraft\Model\HierarchyElement; 14 | use PHPUnit\Framework\MockObject\MockObject; 15 | use ReflectionClass; 16 | 17 | /** 18 | * Class HierarchyElementTest 19 | * @covers \PHPDraft\Model\HierarchyElement 20 | */ 21 | class HierarchyElementTest extends LunrBaseTest 22 | { 23 | private HierarchyElement $class; 24 | 25 | /** 26 | * Mock of the parent class 27 | * 28 | * @var HierarchyElement|MockObject 29 | */ 30 | protected mixed $parent; 31 | 32 | /** 33 | * Set up 34 | */ 35 | public function setUp(): void 36 | { 37 | 38 | $this->parent = $this->getMockBuilder('\PHPDraft\Model\Transition') 39 | ->disableOriginalConstructor() 40 | ->getMock(); 41 | $this->class = $this->getMockForAbstractClass('PHPDraft\Model\HierarchyElement'); 42 | $this->baseSetUp($this->class); 43 | } 44 | 45 | /** 46 | * Tear down 47 | */ 48 | public function tearDown(): void 49 | { 50 | parent::tearDown(); 51 | } 52 | 53 | /** 54 | * Test if the value the class is initialized with is correct 55 | */ 56 | public function testSetupCorrectly(): void 57 | { 58 | $this->assertPropertyEquals('parent', NULL); 59 | } 60 | 61 | /** 62 | * Test basic parse functions 63 | */ 64 | public function testParseIsCalled(): void 65 | { 66 | $this->set_reflection_property_value('parent', $this->parent); 67 | 68 | $obj = '{"meta":{"title":"TEST"}, "content":""}'; 69 | 70 | $this->class->parse(json_decode($obj)); 71 | 72 | $this->assertPropertySame('parent', $this->parent); 73 | $this->assertPropertySame('title', 'TEST'); 74 | } 75 | 76 | /** 77 | * Test basic parse functions 78 | */ 79 | public function testParseIsCalledLoop(): void 80 | { 81 | $this->set_reflection_property_value('parent', $this->parent); 82 | 83 | $obj = '{"meta":{"title":"TEST"}, "content":[{"element":"copy", "content":"hello"}]}'; 84 | 85 | $this->class->parse(json_decode($obj)); 86 | 87 | $this->assertPropertySame('parent', $this->parent); 88 | $this->assertPropertySame('title', 'TEST'); 89 | $this->assertPropertySame('description', 'hello'); 90 | } 91 | 92 | /** 93 | * Test basic parse functions 94 | */ 95 | public function testParseIsCalledSlice(): void 96 | { 97 | $this->set_reflection_property_value('parent', $this->parent); 98 | 99 | $obj = '{"meta":{"title":"TEST"}, "content":[{"element":"copy", "content":"hello"}, {"element":"test", "content":"hello"}]}'; 100 | 101 | $this->class->parse(json_decode($obj)); 102 | 103 | $this->assertPropertySame('parent', $this->parent); 104 | $this->assertPropertySame('title', 'TEST'); 105 | $this->assertPropertySame('description', 'hello'); 106 | } 107 | 108 | 109 | /** 110 | * Test basic get_href 111 | */ 112 | public function testGetHrefIsCalledWithParent(): void 113 | { 114 | $this->set_reflection_property_value('parent', $this->parent); 115 | $this->set_reflection_property_value('title', 'title'); 116 | 117 | $this->parent->expects($this->once()) 118 | ->method('get_href') 119 | ->willReturn('hello'); 120 | 121 | $result = $this->class->get_href(); 122 | 123 | $this->assertSame($result, 'hello-title'); 124 | } 125 | 126 | /** 127 | * Test basic get_href 128 | */ 129 | public function testGetHrefIsCalledWithoutParent(): void 130 | { 131 | $this->set_reflection_property_value('title', 'title'); 132 | $result = $this->class->get_href(); 133 | 134 | $this->assertSame($result, 'title'); 135 | } 136 | 137 | /** 138 | * Test basic get_href 139 | */ 140 | public function testGetHrefIsCalledWithTitleWithSpaces(): void 141 | { 142 | $this->set_reflection_property_value('title', 'some title'); 143 | $this->set_reflection_property_value('parent', $this->parent); 144 | 145 | $this->parent->expects($this->once()) 146 | ->method('get_href') 147 | ->willReturn('hello'); 148 | 149 | $result = $this->class->get_href(); 150 | 151 | $this->assertSame($result, 'hello-some-title'); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Tests/ObjectElementTest.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace PHPDraft\Model\Tests; 11 | 12 | use Lunr\Halo\LunrBaseTest; 13 | use PHPDraft\Model\Elements\ObjectStructureElement; 14 | 15 | /** 16 | * Class ObjectElementTest 17 | */ 18 | class ObjectElementTest extends LunrBaseTest 19 | { 20 | 21 | private ObjectStructureElement $class; 22 | 23 | /** 24 | * Set up 25 | */ 26 | public function setUp(): void 27 | { 28 | $this->class = new ObjectStructureElement(); 29 | $this->baseSetUp($this->class); 30 | } 31 | 32 | /** 33 | * Tear down 34 | */ 35 | public function tearDown(): void 36 | { 37 | unset($this->parent); 38 | parent::tearDown(); 39 | } 40 | 41 | /** 42 | * @covers \PHPDraft\Model\Elements\ObjectStructureElement 43 | */ 44 | public function testKeySetup(): void 45 | { 46 | $this->assertSame(null, $this->class->key); 47 | } 48 | 49 | /** 50 | * @covers \PHPDraft\Model\Elements\ObjectStructureElement 51 | */ 52 | public function testTypeSetup(): void 53 | { 54 | $this->assertSame(null, $this->class->type); 55 | } 56 | 57 | /** 58 | * @covers \PHPDraft\Model\Elements\ObjectStructureElement 59 | */ 60 | public function testDescriptionSetup(): void 61 | { 62 | $this->assertSame(null, $this->class->description); 63 | } 64 | 65 | /** 66 | * @covers \PHPDraft\Model\Elements\ObjectStructureElement 67 | */ 68 | public function testElementSetup(): void 69 | { 70 | $this->assertSame(null, $this->class->element); 71 | } 72 | 73 | /** 74 | * @covers \PHPDraft\Model\Elements\ObjectStructureElement 75 | */ 76 | public function testValueSetup(): void 77 | { 78 | $this->assertSame(null, $this->class->value); 79 | } 80 | 81 | /** 82 | * @covers \PHPDraft\Model\Elements\ObjectStructureElement 83 | */ 84 | public function testStatusSetup(): void 85 | { 86 | $this->assertSame([], $this->class->status); 87 | } 88 | 89 | /** 90 | * @covers \PHPDraft\Model\Elements\ObjectStructureElement 91 | */ 92 | public function testDepsSetup(): void 93 | { 94 | $this->assertSame(null, $this->class->deps); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/PHPDraft/Model/Tests/ResourceTest.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace PHPDraft\Model\Tests; 11 | 12 | use Lunr\Halo\LunrBaseTest; 13 | use PHPDraft\Model\HierarchyElement; 14 | use PHPDraft\Model\Resource; 15 | use PHPUnit\Framework\MockObject\MockObject; 16 | use ReflectionClass; 17 | 18 | /** 19 | * Class ResourceTest 20 | * @covers \PHPDraft\Model\Resource 21 | */ 22 | class ResourceTest extends LunrBaseTest 23 | { 24 | private Resource $class; 25 | 26 | /** 27 | * Mock of the parent class 28 | * 29 | * @var HierarchyElement|MockObject 30 | */ 31 | protected mixed $parent; 32 | 33 | /** 34 | * Set up 35 | */ 36 | public function setUp(): void 37 | { 38 | $this->parent = $this->getMockBuilder('\PHPDraft\Model\Category') 39 | ->getMock(); 40 | 41 | $this->parent->href = null; 42 | $this->class = new Resource($this->parent); 43 | $this->baseSetUp($this->class); 44 | } 45 | 46 | /** 47 | * Tear down 48 | */ 49 | public function tearDown(): void 50 | { 51 | parent::tearDown(); 52 | } 53 | 54 | /** 55 | * Test if the value the class is initialized with is correct 56 | */ 57 | public function testSetupCorrectly(): void 58 | { 59 | $this->assertSame($this->parent, $this->get_reflection_property_value('parent')); 60 | } 61 | 62 | /** 63 | * Test basic parse functions 64 | */ 65 | public function testParseIsCalled(): void 66 | { 67 | $this->set_reflection_property_value('parent', $this->parent); 68 | 69 | $obj = '{"attributes":{"href": "something", "hrefVariables":{"content": [{}]}}, "content":[]}'; 70 | 71 | $this->class->parse(json_decode($obj)); 72 | 73 | $this->assertPropertyEquals('href', 'something'); 74 | } 75 | 76 | /** 77 | * Test basic parse functions 78 | * 79 | * @covers \PHPDraft\Model\Resource::parse 80 | */ 81 | public function testParseIsCalledNoHREF(): void 82 | { 83 | $this->set_reflection_property_value('parent', $this->parent); 84 | $this->set_reflection_property_value('href', null); 85 | 86 | $obj = '{"content":[]}'; 87 | 88 | $this->class->parse(json_decode($obj)); 89 | 90 | $this->assertNull($this->get_reflection_property_value('href')); 91 | } 92 | 93 | /** 94 | * Test basic parse functions 95 | */ 96 | public function testParseIsCalledIsCopy(): void 97 | { 98 | $this->set_reflection_property_value('parent', $this->parent); 99 | $this->set_reflection_property_value('href', null); 100 | 101 | $obj = '{"content":[{"element":"copy", "content":""},{"element":"hello", "content":""}, {"element":"hello", "content":""}]}'; 102 | 103 | $this->class->parse(json_decode($obj)); 104 | 105 | $this->assertNull($this->get_reflection_property_value('href')); 106 | } 107 | 108 | /** 109 | * Test basic parse functions 110 | */ 111 | public function testParseIsCalledIsNotCopy(): void 112 | { 113 | $this->set_reflection_property_value('parent', $this->parent); 114 | $this->set_reflection_property_value('href', null); 115 | $this->assertEmpty($this->get_reflection_property_value('children')); 116 | 117 | $obj = '{"content":[{"element":"hello", "content":""}]}'; 118 | 119 | $this->class->parse(json_decode($obj)); 120 | 121 | $this->assertSame($this->parent, $this->get_reflection_property_value('parent')); 122 | $this->assertNotEmpty($this->get_reflection_property_value('children')); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/PHPDraft/Out/BaseTemplateRenderer.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Out; 14 | 15 | use Lukasoppermann\Httpstatus\Httpstatus; 16 | use PHPDraft\Model\Category; 17 | use PHPDraft\Model\Elements\BasicStructureElement; 18 | 19 | abstract class BaseTemplateRenderer 20 | { 21 | /** 22 | * Type of sorting to do on objects. 23 | * 24 | * @var int 25 | */ 26 | public int $sorting; 27 | 28 | /** 29 | * JSON representation of an API Blueprint. 30 | * 31 | * @var object 32 | */ 33 | protected object $object; 34 | 35 | /** 36 | * The base data of the API. 37 | * 38 | * @var array 39 | */ 40 | protected array $base_data = []; 41 | 42 | /** 43 | * JSON object of the API blueprint. 44 | * 45 | * @var Category[] 46 | */ 47 | protected array $categories = []; 48 | /** 49 | * Structures used in all data. 50 | * 51 | * @var BasicStructureElement[] 52 | */ 53 | 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 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/PHPDraft/Out/HTML/default/category.twig: -------------------------------------------------------------------------------- 1 |
    2 | {% if category.title %} 3 |

    {{ category.title }}

    4 | {% endif %} 5 | {% if category.description %} 6 |

    {{ category.description|markdown_to_html }}

    7 | {% endif %} 8 | {% for resource in category.children %} 9 | {% include 'resource.twig' %} 10 | {% endfor %} 11 |
    -------------------------------------------------------------------------------- /src/PHPDraft/Out/HTML/default/main.css: -------------------------------------------------------------------------------- 1 | var.url-param { 2 | font-weight: bold; 3 | } 4 | 5 | var.url-value { 6 | color: #c90c00; 7 | padding: 2px; 8 | } 9 | 10 | div.main-url { 11 | text-align: justify; 12 | line-break: loose; 13 | word-break: break-all; 14 | } 15 | 16 | button.extra-info.btn { 17 | position: fixed; 18 | bottom: 10px; 19 | right: 10px; 20 | border-radius: 30px; 21 | z-index: 999; 22 | } 23 | 24 | span.variable-info { 25 | margin-left: 10px; 26 | background: rgba(0,0,0,.5); 27 | padding: 5px 10px; 28 | border-radius: 25px; 29 | font-size: 0.7em; 30 | } 31 | 32 | button.extra-info.btn:focus { 33 | border-width: 0px; 34 | box-shadow: none; 35 | outline: 0; 36 | } 37 | 38 | .request-card > code, .popover-content { 39 | word-break: break-all; 40 | } 41 | 42 | body .media h1.media-heading { 43 | margin-top: 20px; 44 | margin-bottom: 10px; 45 | } 46 | 47 | body .media h1.media-heading .form-control { 48 | display: flex; 49 | width: auto; 50 | float: right; 51 | } 52 | 53 | div.card { 54 | margin: 10px auto; 55 | } 56 | 57 | .card-title var, h4.response var { 58 | padding: 6px 12px; 59 | margin-right: 12px; 60 | border-radius: 3px; 61 | display: inline-block; 62 | background: rgba(0, 0, 0, 0.1); 63 | } 64 | 65 | h4.response.warning var { 66 | background: rgba(255, 103, 8, 0.2); 67 | } 68 | 69 | h4.response.error var { 70 | background: rgba(201, 12, 0, 0.1); 71 | } 72 | 73 | .card-title code { 74 | color: #000; 75 | background-color: rgba(255, 255, 255, 0.7); 76 | padding: 1px 4px; 77 | margin-top: 2px; 78 | border: 1px solid transparent; 79 | border-radius: 3px 80 | } 81 | 82 | .card-title a.transition-title { 83 | padding: 6px 0; 84 | } 85 | 86 | body .col-md-10 h2:first-child, 87 | body .col-md-10 h3:first-child { 88 | margin-top: 0; 89 | } 90 | 91 | @media (min-width: 768px) { 92 | .method-nav { 93 | max-height: 100vh; 94 | overflow-x: visible; 95 | overflow-y: scroll; 96 | top: 0; 97 | box-shadow: 0px -5px 5px -5px rgba(0, 0, 0, 0.2) inset, 0px 0px 1px rgba(102, 102, 102, 0.4); 98 | } 99 | } 100 | .method-nav { 101 | padding: 0px 10px; 102 | } 103 | .method-nav nav { 104 | width: 100%; 105 | } 106 | .method-nav nav.category, 107 | .method-nav nav.structures, 108 | .method-nav nav.structures a.nav-link, 109 | .method-nav nav.resource a.nav-link { 110 | padding-left: 0px; 111 | } 112 | 113 | .method-nav nav.resource .transition a.nav-link { 114 | padding-left: .5rem; 115 | } 116 | 117 | 118 | .method-nav a.nav-link { 119 | line-height: 26px; 120 | } 121 | 122 | .method-nav a.nav-link i { 123 | color: #fff; 124 | padding: 0 5px; 125 | box-sizing: content-box; 126 | border-radius: 15px; 127 | line-height: 22px; 128 | } 129 | 130 | .main-content { 131 | position: relative; 132 | } 133 | 134 | .example-value { 135 | color: rgba(0, 0, 0, 0.4); 136 | text-align: right; 137 | } 138 | 139 | a.code { 140 | padding: 2px 4px; 141 | font-size: 90%; 142 | background-color: #f9f2f4; 143 | border-radius: 4px; 144 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 145 | } 146 | 147 | .popover { 148 | width: auto; 149 | max-width: 50%; 150 | } 151 | 152 | .popover-content { 153 | width: auto; 154 | max-width: 100%; 155 | line-break: normal; 156 | white-space: pre-wrap; 157 | } 158 | 159 | .card-body { 160 | position: relative; 161 | } 162 | 163 | .curl.btn { 164 | z-index: 999; 165 | position: absolute; 166 | top: 15px; 167 | } 168 | 169 | .curl.btn { 170 | right: 15px; 171 | border-top-left-radius: 0px; 172 | border-bottom-left-radius: 0px 173 | } 174 | 175 | a.variable-key { 176 | color: inherit; 177 | } 178 | 179 | body { 180 | --put-color: rgb(248, 148, 6); 181 | --post-color: rgb(98, 196, 98); 182 | --get-color: rgb(91, 192, 222); 183 | --delete-color: rgb(238, 95, 91); 184 | --head-color: rgb(222, 121, 91); 185 | --patch-color: rgb(196, 98, 196); 186 | } 187 | 188 | .PUT:not(.structure) > .card-header{ 189 | background: var(--put-color); 190 | } 191 | span.PUT { 192 | color: var(--put-color); 193 | } 194 | 195 | .POST:not(.structure) > .card-header{ 196 | background: var(--post-color); 197 | } 198 | span.POST { 199 | color: var(--post-color); 200 | } 201 | 202 | .GET:not(.structure) > .card-header{ 203 | background: var(--get-color); 204 | } 205 | span.GET { 206 | color: var(--get-color); 207 | } 208 | 209 | .DELETE:not(.structure) > .card-header{ 210 | background: var(--delete-color); 211 | } 212 | span.DELETE { 213 | color: var(--delete-color); 214 | } 215 | 216 | .HEAD:not(.structure) > .card-header{ 217 | background: var(--head-color); 218 | } 219 | span.HEAD { 220 | color: var(--head-color); 221 | } 222 | 223 | .PATCH:not(.structure) > .card-header{ 224 | background: var(--patch-color); 225 | } 226 | span.PATCH { 227 | color: var(--patch-color); 228 | } 229 | 230 | h1.media-heading { 231 | max-width: 80%; 232 | word-break: break-word; 233 | } 234 | 235 | .host-information { 236 | position: absolute; 237 | top: 0; 238 | right: 0; 239 | } 240 | 241 | .host-information label.host-dropdown { 242 | } 243 | 244 | .host-information label.host-dropdown select { 245 | border: 0 none rgba(0,0,0,0); 246 | display: inherit; 247 | width: auto; 248 | } -------------------------------------------------------------------------------- /src/PHPDraft/Out/HTML/default/main.js: -------------------------------------------------------------------------------- 1 | function getParameters() { 2 | let result = {}; 3 | let tmp = []; 4 | 5 | if (location.search === '') { return result; } 6 | 7 | location.search 8 | .substr(1) 9 | .split("&") 10 | .forEach(function (item) {tmp = item.split("="); result[tmp[0]] = decodeURIComponent(tmp[1]); }); 11 | return result; 12 | }; 13 | 14 | function trigger_popover() { 15 | $('[data-toggle="popover"]').popover({ 16 | html: true, 17 | sanitize: false, 18 | }); 19 | } 20 | 21 | function escapeRegExp(str) { return str.replace(/[-\[\]/{}()*+?.\\^$|]/g, "\\$&"); }; 22 | 23 | $(function () { 24 | $('[data-toggle="tooltip"]').tooltip(); 25 | $('body').on('click', function (e) { 26 | $('[data-toggle="popover"]').each(function () { 27 | if (!$(this).is(e.target) && $(this).has(e.target).length === 0 && $('.popover').has(e.target).length === 0) { 28 | $(this).popover('hide'); 29 | } 30 | }); 31 | }); 32 | let contentDom = $('body>div>div.row'); 33 | 34 | let formControlDom = $('h1.media-heading select.form-control'); 35 | let selectedhost = formControlDom.val(); 36 | formControlDom.on('change', function () { 37 | let html = contentDom.html(); 38 | let re = new RegExp(escapeRegExp(selectedhost), 'g'); 39 | let new_html = html.replace(re, formControlDom.val()); 40 | selectedhost = formControlDom.val(); 41 | contentDom.html(new_html); 42 | trigger_popover(); 43 | }); 44 | 45 | $('table:not(.table)').each(function () { 46 | $(this).addClass('table'); 47 | }); 48 | 49 | let parameters = getParameters(); 50 | Object.keys(parameters).forEach(function(key) { 51 | let html = contentDom.html(); 52 | 53 | const regex = `${key}: [a-zA-Z0-9\ \\\-\/]*`; 54 | let list_re = new RegExp(regex, 'g'); 55 | 56 | const curl_regex = `-H '${key}: [a-zA-Z0-9\ \\\-\/]*'`; 57 | let curl_re = new RegExp(curl_regex, 'g'); 58 | 59 | let new_html = html.replace(list_re, `${key}: ${parameters[key]}`) 60 | .replace(curl_re, `-H '${key}: ${parameters[key]}'`); 61 | contentDom.html(new_html); 62 | }); 63 | trigger_popover(); 64 | }); 65 | 66 | $('.collapse.request-card').on('shown.bs.collapse', function () { 67 | $(this).parent().find('h6.request .fas.indicator').removeClass('fa-angle-up').addClass('fa-angle-down'); 68 | }).on('hidden.bs.collapse', function () { 69 | $(this).parent().find('h6.request .fas.indicator').removeClass('fa-angle-down').addClass('fa-angle-up'); 70 | }); 71 | 72 | $('.collapse.response-card').on('shown.bs.collapse', function () { 73 | $(this).parent().find('h6.response .fas.indicator').removeClass('fa-angle-up').addClass("fa-angle-down"); 74 | }).on('hidden.bs.collapse', function () { 75 | $(this).parent().find('h6.response .fas.indicator').removeClass('fa-angle-down').addClass("fa-angle-up"); 76 | }); 77 | 78 | $('pre.collapse.response-body').on('shown.bs.collapse', function () { 79 | $(this).parent().find('h6.response-body .fas.indicator').removeClass('fa-angle-up').addClass('fa-angle-down'); 80 | }).on('hidden.bs.collapse', function () { 81 | $(this).parent().find('h6.response-body .fas.indicator').removeClass('fa-angle-down').addClass('fa-angle-up'); 82 | }); 83 | 84 | anchors.options = { 85 | placement: 'left', 86 | visible: 'touch', 87 | }; 88 | anchors.add('.main-content h1, .main-content h2, .main-content h3, .main-content .card-header a'); -------------------------------------------------------------------------------- /src/PHPDraft/Out/HTML/default/main.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ data.TITLE }} 5 | 6 | 7 | 8 | 9 | {% for style in css %} 10 | 11 | {% endfor %} 12 | 13 | 14 | 15 |
    16 |
    17 |
    18 |

    {{ data.TITLE|raw }}

    19 |
    20 | {% if data.ALT_HOST %} 21 | 30 | {%- else %} 31 | 32 | {% endif %} 33 |
    34 | {% if data.DESC %} 35 | {{ data.DESC|markdown_to_html }} 36 | {% endif %} 37 |
    38 | {% if image %} 39 |
    40 | 41 | Image 42 | 43 |
    44 | {% endif %} 45 |
    46 |
    47 | {% include 'nav.twig' %} 48 |
    49 | {% for category in categories %} 50 | {% include 'category.twig' %} 51 | {% endfor %} 52 | {% if structures|length > 0 %} 53 |

    Data structures

    54 | {% for name,structure in structures %} 55 | {% include 'structure.twig' %} 56 | {% endfor %} 57 | {% endif %} 58 |
    59 |
    60 |
    61 | {% if extra_data|length > 1 %} 62 | 72 | {% endif %} 73 | {% for script in js %} 74 | 75 | {% endfor %} 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /src/PHPDraft/Out/HTML/default/nav.twig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/PHPDraft/Out/HTML/default/resource.twig: -------------------------------------------------------------------------------- 1 |
    2 |

    3 | 6 | {{ resource.title }} 7 | 8 | {{ resource.href }} 9 |

    10 | {% if resource.description %} 11 | {{ resource.description|markdown_to_html }} 12 | {% endif %} 13 | {% if resource.url_variables %} 14 |
    URI Parameters
    15 |
    16 | 17 | 18 | 19 | {% for url_variable in resource.url_variables %}{{ url_variable|raw }}{% endfor %} 20 | 21 |
    keytypestatusdescriptionvalue
    22 |
    23 | {% endif %} 24 | {% for transition in resource.children %} 25 | {% include 'transition.twig' %} 26 | {% endfor %} 27 |
    -------------------------------------------------------------------------------- /src/PHPDraft/Out/HTML/default/structure.twig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/PHPDraft/Out/HTML/default/value.twig: -------------------------------------------------------------------------------- 1 | {% if value is bool %} 2 | {% if value %}true{% else %}false{% endif %} 3 | {% elseif value is array_type %} 4 |
    5 | {% if value.value is iterable %} 6 |
      7 | {% for value in value.value %} 8 | {% include 'value.twig' %} 9 | {% endfor %} 10 |
    11 | {% elseif value.value is string %} 12 | {{ value.key }}{{ value.get_element_as_html(value.type) }}{{ value.description }} 13 | {% endif %} 14 |
    15 | {% elseif value is enum_type %} 16 |
    17 | {% if value.value is iterable %} 18 |
      19 | {% for value in value.value %} 20 | {% include 'value.twig' %} 21 | {% endfor %} 22 |
    23 | {% elseif value.value is string %} 24 | {{ value.key.value }}{{ value.get_element_as_html(value.type) }}{{ value.description }} 25 | {% endif %} 26 |
    27 | {% elseif value is string %} 28 | {{ value }} 29 | {% else %} 30 | {{ value|raw }} 31 | {% endif %} -------------------------------------------------------------------------------- /src/PHPDraft/Out/HTML/material/main.css: -------------------------------------------------------------------------------- 1 | div#navbar { 2 | height: 100vh; 3 | position: sticky; 4 | top:0; 5 | } 6 | 7 | var.url-param { 8 | font-weight: bold; 9 | } 10 | 11 | var.url-value { 12 | color: #c90c00; 13 | padding: 2px; 14 | } 15 | 16 | div.main-url { 17 | text-align: justify; 18 | line-break: loose; 19 | word-break: break-all; 20 | } 21 | 22 | a.extra-info.btn { 23 | position: fixed; 24 | bottom: 10px; 25 | right: 10px; 26 | border-radius: 30px; 27 | z-index: 999; 28 | } 29 | 30 | span.variable-info { 31 | margin-left: 10px; 32 | background: rgba(0,0,0,.5); 33 | padding: 5px 10px; 34 | border-radius: 25px; 35 | font-size: 0.7em; 36 | } 37 | 38 | button.extra-info.btn:focus { 39 | border-width: 0px; 40 | box-shadow: none; 41 | outline: 0; 42 | } 43 | 44 | .request-card > code, .popover-content { 45 | word-break: break-all; 46 | } 47 | 48 | body .media h1.media-heading { 49 | margin-top: 20px; 50 | margin-bottom: 10px; 51 | } 52 | 53 | body .media h1.media-heading .form-control { 54 | display: flex; 55 | width: auto; 56 | float: right; 57 | } 58 | 59 | div.card { 60 | margin: 10px auto; 61 | } 62 | 63 | .card-title a { 64 | color: #000; 65 | } 66 | .card-title var, h4.response var { 67 | padding: 6px 12px; 68 | margin-right: 12px; 69 | border-radius: 3px; 70 | display: inline-block; 71 | background: rgba(0, 0, 0, 0.1); 72 | } 73 | 74 | h4.response.warning var { 75 | background: rgba(255, 103, 8, 0.2); 76 | } 77 | 78 | h4.response.error var { 79 | background: rgba(201, 12, 0, 0.1); 80 | } 81 | 82 | .card-title code { 83 | color: #000; 84 | background-color: rgba(255, 255, 255, 0.7); 85 | padding: 1px 4px; 86 | margin-top: 2px; 87 | border: 1px solid transparent; 88 | border-radius: 3px 89 | } 90 | 91 | .card-title a.transition-title { 92 | padding: 6px 0; 93 | } 94 | 95 | body .col-md-10 h2:first-child, 96 | body .col-md-10 h3:first-child { 97 | margin-top: 0; 98 | } 99 | 100 | @media (min-width: 768px) { 101 | .method-nav { 102 | max-height: 100vh; 103 | overflow-x: visible; 104 | overflow-y: scroll; 105 | top: 0; 106 | box-shadow: 0px -5px 5px -5px rgba(0, 0, 0, 0.2) inset, 0px 0px 1px rgba(102, 102, 102, 0.4); 107 | } 108 | } 109 | .method-nav { 110 | padding: 0px 10px; 111 | } 112 | .method-nav nav { 113 | width: 100%; 114 | } 115 | .method-nav nav.category, 116 | .method-nav nav.structures, 117 | .method-nav nav.structures a.nav-link, 118 | .method-nav nav.resource a.nav-link { 119 | padding-left: 0px; 120 | } 121 | 122 | .method-nav nav.resource .transition a.nav-link { 123 | padding-left: .5rem; 124 | } 125 | 126 | .method-nav a.nav-link { 127 | line-height: 26px; 128 | width: 100%; 129 | display: block; 130 | } 131 | 132 | .method-nav a.nav-link span { 133 | float: right; 134 | } 135 | 136 | .method-nav a.nav-link i { 137 | color: #fff; 138 | padding: 0 5px; 139 | border-radius: 15px; 140 | line-height: 22px; 141 | } 142 | 143 | .main-content { 144 | position: relative; 145 | } 146 | 147 | .example-value { 148 | color: rgba(0, 0, 0, 0.4); 149 | text-align: right; 150 | } 151 | 152 | a.code { 153 | padding: 2px 4px; 154 | font-size: 90%; 155 | background-color: #f9f2f4; 156 | border-radius: 4px; 157 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 158 | } 159 | 160 | .popover { 161 | width: auto; 162 | max-width: 50%; 163 | } 164 | 165 | .popover-content { 166 | width: auto; 167 | max-width: 100%; 168 | line-break: normal; 169 | white-space: pre-wrap; 170 | } 171 | 172 | ul.collapsible li div.collapsible-body { 173 | position: relative; 174 | } 175 | 176 | .modal textarea { 177 | height: 18rem; 178 | } 179 | 180 | .curl.btn { 181 | z-index: 999; 182 | position: absolute; 183 | top: 15px; 184 | right: 15px; 185 | border-top-left-radius: 0px; 186 | border-bottom-left-radius: 0px 187 | } 188 | 189 | a.variable-key { 190 | color: inherit; 191 | } 192 | 193 | body { 194 | --put-color: rgb(248, 148, 6); 195 | --post-color: rgb(98, 196, 98); 196 | --get-color: rgb(91, 192, 222); 197 | --delete-color: rgb(238, 95, 91); 198 | --head-color: rgb(222, 121, 91); 199 | --patch-color: rgb(196, 98, 196); 200 | } 201 | 202 | .PUT:not(.structure) > .card-header{ 203 | background: var(--put-color); 204 | } 205 | span.PUT { 206 | color: var(--put-color); 207 | } 208 | 209 | .POST:not(.structure) > .card-header{ 210 | background: var(--post-color); 211 | } 212 | span.POST { 213 | color: var(--post-color); 214 | } 215 | 216 | .GET:not(.structure) > .card-header{ 217 | background: var(--get-color); 218 | } 219 | span.GET { 220 | color: var(--get-color); 221 | } 222 | 223 | .DELETE:not(.structure) > .card-header{ 224 | background: var(--delete-color); 225 | } 226 | span.DELETE { 227 | color: var(--delete-color); 228 | } 229 | 230 | .HEAD:not(.structure) > .card-header{ 231 | background: var(--head-color); 232 | } 233 | span.HEAD { 234 | color: var(--head-color); 235 | } 236 | 237 | .PATCH:not(.structure) > .card-header{ 238 | background: var(--patch-color); 239 | } 240 | span.PATCH { 241 | color: var(--patch-color); 242 | } -------------------------------------------------------------------------------- /src/PHPDraft/Out/HTML/material/main.js: -------------------------------------------------------------------------------- 1 | function getParameters() { 2 | let result = {}; 3 | let tmp = []; 4 | 5 | if (location.search === '') { return result; } 6 | 7 | location.search 8 | .substr(1) 9 | .split("&") 10 | .forEach(function (item) {tmp = item.split("="); result[tmp[0]] = decodeURIComponent(tmp[1]); }); 11 | return result; 12 | } 13 | 14 | function escapeRegExp(str) { return str.replace(/[-\[\]/{}()*+?.\\^$|]/g, "\\$&"); }; 15 | 16 | $(document).ready(function(){ 17 | $('[data-toggle="tooltip"]').tooltip(); 18 | $('.collapsible').collapsible(); 19 | $('.modal').modal(); 20 | if (!localStorage.getItem('visited')) { 21 | $('.tap-target').tapTarget().tapTarget('open'); 22 | } 23 | localStorage.setItem('visited', true); 24 | 25 | let contentDom = $('body>div>div.row'); 26 | 27 | let formControlDom = $('h1.media-heading select.form-control'); 28 | let selectedhost = formControlDom.val(); 29 | formControlDom.on('change', function () { 30 | let html = contentDom.html(); 31 | let re = new RegExp(escapeRegExp(selectedhost), 'g'); 32 | let new_html = html.replace(re, formControlDom.val()); 33 | selectedhost = formControlDom.val(); 34 | contentDom.html(new_html); 35 | trigger_popover(); 36 | }); 37 | 38 | let parameters = getParameters(); 39 | Object.keys(parameters).forEach(function(key) { 40 | let html = contentDom.html(); 41 | 42 | const regex = `${key}: [a-zA-Z0-9\ \\\-\/]*`; 43 | let list_re = new RegExp(regex, 'g'); 44 | 45 | const curl_regex = `-H '${key}: [a-zA-Z0-9\ \\\-\/]*'`; 46 | let curl_re = new RegExp(curl_regex, 'g'); 47 | 48 | let new_html = html.replace(list_re, `${key}: ${parameters[key]}`) 49 | .replace(curl_re, `-H '${key}: ${parameters[key]}'`); 50 | contentDom.html(new_html); 51 | }); 52 | }); 53 | 54 | anchors.options = { 55 | placement: 'left', 56 | visible: 'touch', 57 | }; 58 | anchors.add('.main-content h1, .main-content h2, .main-content h3, .main-content .card-header a'); -------------------------------------------------------------------------------- /src/PHPDraft/Out/HTML/material/main.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ data.TITLE }} 5 | 6 | 7 | 8 | 9 | 10 | {% for style in css %} 11 | 12 | {% endfor %} 13 | 14 | 15 | 16 |
    17 |
    18 |
    19 |

    {{ data.TITLE|raw }} 20 | {% if data.ALT_HOST %} 21 | 30 | {%- else %} 31 | {{ data.HOST }} 32 | {% endif %} 33 |

    34 | {% if data.DESC %} 35 | {{ data.DESC|markdown_to_html }} 36 | {% endif %} 37 |
    38 | {% if image %} 39 |
    40 | 41 | Image 42 | 43 |
    44 | {% endif %} 45 |
    46 |
    47 | {% include 'nav.twig' %} 48 |
    49 | {% for category in categories %} 50 | {% include 'category.twig' %} 51 | {% endfor %} 52 | {% if structures|length > 0 %} 53 |

    Data structures

    54 | {% for name,structure in structures %} 55 | {% include 'structure.twig' %} 56 | {% endfor %} 57 | {% endif %} 58 |
    59 |
    60 |
    61 | {% if extra_data|length > 1 %} 62 | info_outline 63 | 64 |
    65 |
    66 |
    Metadata
    67 |

    This button shows the metadata for the API

    68 |
    69 |
    70 | 79 | {% endif %} 80 | {% for script in js %} 81 | 82 | {% endfor %} 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /src/PHPDraft/Out/HTML/material/nav.twig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/PHPDraft/Out/HTML/material/structure.twig: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/PHPDraft/Out/OpenAPI/Tests/OpenApiRendererTest.php: -------------------------------------------------------------------------------- 1 | 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)['schemas' => []],$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([],$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 | } -------------------------------------------------------------------------------- /src/PHPDraft/Out/Sorting.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Out; 14 | 15 | /** 16 | * Sorting constants. 17 | */ 18 | enum Sorting: int 19 | { 20 | /** 21 | * Sets sorting to all parts. 22 | */ 23 | case PHPD_SORT_ALL = 3; 24 | 25 | /** 26 | * Sets sorting to all webservices. 27 | */ 28 | case PHPD_SORT_WEBSERVICES = 2; 29 | 30 | /** 31 | * Sets sorting to all data structures. 32 | */ 33 | case PHPD_SORT_STRUCTURES = 1; 34 | 35 | /** 36 | * Sets sorting to no data structures. 37 | */ 38 | case PHPD_SORT_NONE = -1; 39 | 40 | /** 41 | * Check if structures should be sorted. 42 | * 43 | * @param int $sort The sorting level. 44 | * 45 | * @return bool 46 | */ 47 | public static function sortStructures(int $sort): bool 48 | { 49 | return $sort === Sorting::PHPD_SORT_ALL->value || $sort === Sorting::PHPD_SORT_STRUCTURES->value; 50 | } 51 | 52 | /** 53 | * Check if services should be sorted. 54 | * 55 | * @param int $sort The sorting level. 56 | * 57 | * @return bool 58 | */ 59 | public static function sortServices(int $sort): bool 60 | { 61 | return $sort === Sorting::PHPD_SORT_ALL->value || $sort === Sorting::PHPD_SORT_WEBSERVICES->value; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/PHPDraft/Out/Tests/SortingTest.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace PHPDraft\Out\Tests; 11 | 12 | use Lunr\Halo\LunrBaseTest; 13 | use PHPDraft\Out\Sorting; 14 | use PHPDraft\Out\Version; 15 | use ReflectionClass; 16 | 17 | /** 18 | * Class SortingTest 19 | * 20 | * @covers \PHPDraft\Out\Sorting 21 | */ 22 | class SortingTest extends LunrBaseTest 23 | { 24 | /** 25 | * Test if service sorting is determined correctly. 26 | * 27 | * @covers \PHPDraft\Out\Sorting::sortServices 28 | */ 29 | public function testSortsServicesIfNeeded(): void 30 | { 31 | $this->assertTrue(Sorting::sortServices(3)); 32 | $this->assertTrue(Sorting::sortServices(2)); 33 | $this->assertFalse(Sorting::sortServices(-1)); 34 | $this->assertFalse(Sorting::sortServices(1)); 35 | $this->assertFalse(Sorting::sortServices(0)); 36 | } 37 | 38 | /** 39 | * Test if structure sorting is determined correctly. 40 | * 41 | * @covers \PHPDraft\Out\Sorting::sortStructures 42 | */ 43 | public function testSortsStructureIfNeeded(): void 44 | { 45 | $this->assertTrue(Sorting::sortStructures(3)); 46 | $this->assertTrue(Sorting::sortStructures(1)); 47 | $this->assertFalse(Sorting::sortStructures(-1)); 48 | $this->assertFalse(Sorting::sortStructures(2)); 49 | $this->assertFalse(Sorting::sortStructures(0)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/PHPDraft/Out/Tests/TwigFactoryTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Environment::class, TwigFactory::get($loader)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/PHPDraft/Out/Tests/VersionTest.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace PHPDraft\Out\Tests; 11 | 12 | use Lunr\Halo\LunrBaseTest; 13 | use PHPDraft\Out\Version; 14 | use ReflectionClass; 15 | 16 | /** 17 | * Class VersionTest 18 | * 19 | * @covers \PHPDraft\Out\Version 20 | */ 21 | class VersionTest extends LunrBaseTest 22 | { 23 | /** 24 | * Set up tests 25 | * 26 | * @return void 27 | */ 28 | public function setUp(): void 29 | { 30 | $this->class = new Version(); 31 | $this->reflection = new ReflectionClass('PHPDraft\Out\Version'); 32 | } 33 | 34 | /** 35 | * Test if the value the class is initialized with is correct 36 | */ 37 | public function testReleaseIDIsNull(): void 38 | { 39 | $this->constant_redefine('VERSION', '0'); 40 | $this->mock_function('exec', fn() => '12'); 41 | $return = $this->class->release_id(); 42 | $this->assertSame('12', $return); 43 | $this->unmock_function('exec'); 44 | } 45 | 46 | /** 47 | * Test if the value the class is initialized with is correct 48 | */ 49 | public function testReleaseIDIsNotNull(): void 50 | { 51 | $this->constant_redefine('VERSION', '1.2.3'); 52 | $return = $this->class->release_id(); 53 | $this->assertSame('1.2.3', $return); 54 | } 55 | 56 | /** 57 | * Test if the value the class is initialized with is correct 58 | */ 59 | public function testVersion(): void 60 | { 61 | $this->constant_redefine('VERSION', '1.2.4'); 62 | $this->class->version(); 63 | $this->expectOutputString('PHPDraft: 1.2.4'); 64 | } 65 | 66 | /** 67 | * Test if the value the class is initialized with is correct 68 | */ 69 | public function testSeries(): void 70 | { 71 | $this->constant_redefine('VERSION', '1.2.4'); 72 | $return = $this->class->series(); 73 | $this->assertSame('1.2', $return); 74 | } 75 | 76 | /** 77 | * Test if the value the class is initialized with is correct 78 | */ 79 | public function testReleaseChannel(): void 80 | { 81 | $this->constant_redefine('VERSION', '1.2.4-beta'); 82 | $return = $this->class->getReleaseChannel(); 83 | $this->assertSame('-nightly', $return); 84 | } 85 | 86 | /** 87 | * Test if the value the class is initialized with is correct 88 | */ 89 | public function testReleaseChannelNormal(): void 90 | { 91 | $this->constant_redefine('VERSION', '1.2.4'); 92 | $return = $this->class->getReleaseChannel(); 93 | $this->assertSame('', $return); 94 | } 95 | 96 | /** 97 | * Test if the value the class is initialized with is correct 98 | */ 99 | public function testSeriesNightly(): void 100 | { 101 | $this->constant_redefine('VERSION', '1.2.4-beta'); 102 | $return = $this->class->series(); 103 | $this->assertSame('1.2', $return); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/PHPDraft/Out/TwigFactory.php: -------------------------------------------------------------------------------- 1 | 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))); 31 | $twig->addFilter(new TwigFilter('status_reason', fn(int $code) => (new Httpstatus())->getReasonPhrase($code))); 32 | $twig->addFilter(new TwigFilter('minify_css', function (string $string) { 33 | $minify = new Css(); 34 | $minify->add($string); 35 | return $minify->minify(); 36 | })); 37 | $twig->addFilter(new TwigFilter('minify_js', function (string $string) { 38 | $minify = new JS(); 39 | $minify->add($string); 40 | return $minify->minify(); 41 | })); 42 | 43 | $twig->addTest(new TwigTest('enum_type', fn(object $object) => $object instanceof EnumStructureElement)); 44 | $twig->addTest(new TwigTest('object_type', fn(object $object) => $object instanceof ObjectStructureElement)); 45 | $twig->addTest(new TwigTest('array_type', fn(object $object) => $object instanceof ArrayStructureElement)); 46 | $twig->addTest(new TwigTest('bool', fn($object) => is_bool($object))); 47 | $twig->addTest(new TwigTest('string', fn($object) => is_string($object))); 48 | $twig->addTest(new TwigTest('variable_type', fn(BasicStructureElement $object) => $object->is_variable)); 49 | $twig->addTest(new TwigTest('inheriting', function (BasicStructureElement $object): bool { 50 | $options = array_merge(StructureElement::DEFAULTS, ['member', 'select', 'option', 'ref', 'T', 'hrefVariables']); 51 | return !(is_null($object->element) || in_array($object->element, $options, true)); 52 | })); 53 | 54 | $twig->addRuntimeLoader(new class implements RuntimeLoaderInterface { 55 | public function load(string $class): ?object 56 | { 57 | if (MarkdownRuntime::class === $class) { 58 | return new MarkdownRuntime(new DefaultMarkdown()); 59 | } 60 | return null; 61 | } 62 | }); 63 | $twig->addExtension(new MarkdownExtension()); 64 | 65 | return $twig; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/PHPDraft/Out/Version.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Out; 14 | 15 | /** 16 | * Class Version. 17 | */ 18 | class Version 19 | { 20 | /** 21 | * Return the version. 22 | * 23 | * @return void 24 | */ 25 | public static function version(): void 26 | { 27 | $version = self::release_id(); 28 | echo 'PHPDraft: ' . $version; 29 | } 30 | 31 | /** 32 | * Get the version number. 33 | * 34 | * @return string 35 | */ 36 | public static function release_id(): string 37 | { 38 | $env_id = getenv('PHPDRAFT_RELEASE_ID'); 39 | if ($env_id !== FALSE) { 40 | return $env_id; 41 | } 42 | 43 | return VERSION !== '0' ? VERSION : @exec('git describe --tags 2>&1'); 44 | } 45 | 46 | /** 47 | * Print the series of the update. 48 | * 49 | * @return string Series 50 | */ 51 | public function series(): string 52 | { 53 | if (strpos(self::release_id(), '-')) { 54 | $version = explode('-', self::release_id())[0]; 55 | } else { 56 | $version = self::release_id(); 57 | } 58 | 59 | return implode('.', array_slice(explode('.', $version), 0, 2)); 60 | } 61 | 62 | /** 63 | * Get the manner of releasing. 64 | * 65 | * @return string 66 | */ 67 | public function getReleaseChannel(): string 68 | { 69 | if (str_contains(self::release_id(), '-')) { 70 | return '-nightly'; 71 | } 72 | 73 | return ''; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/PHPDraft/Parse/BaseHtmlGenerator.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Parse; 14 | 15 | use Stringable; 16 | 17 | abstract class BaseHtmlGenerator implements Stringable 18 | { 19 | /** 20 | * Type of sorting to do. 21 | * 22 | * @var int 23 | */ 24 | public int $sorting; 25 | 26 | /** 27 | * JSON representation of an API Blueprint. 28 | * 29 | * @var object 30 | */ 31 | protected object $object; 32 | 33 | /** 34 | * Rendered HTML 35 | * 36 | * @var string 37 | */ 38 | protected string $html; 39 | 40 | /** 41 | * Constructor. 42 | * 43 | * @param object $json Representation of an API Blueprint 44 | * 45 | * @return self 46 | */ 47 | public function init(object $json): self 48 | { 49 | $this->object = $json; 50 | 51 | return $this; 52 | } 53 | 54 | /** 55 | * Build the HTML representation of the object. 56 | * 57 | * @param string $template Type of template to display. 58 | * @param string|null $image Image to use as a logo 59 | * @param string|null $css CSS to load 60 | * @param string|null $js JS to load 61 | * 62 | * @return void 63 | * 64 | * @throws ExecutionException As a runtime exception 65 | */ 66 | abstract public function build_html(string $template = 'default', ?string $image = null, ?string $css = null, ?string $js = null): void; 67 | 68 | 69 | /** 70 | * Get the HTML representation of the object. 71 | * 72 | * @return string 73 | */ 74 | abstract public function __toString(): string; 75 | } 76 | -------------------------------------------------------------------------------- /src/PHPDraft/Parse/BaseParser.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Parse; 14 | 15 | use PHPDraft\In\ApibFileParser; 16 | 17 | /** 18 | * Class BaseParser. 19 | * 20 | * @package PHPDraft\Parse 21 | */ 22 | abstract class BaseParser 23 | { 24 | /** 25 | * The API Blueprint output (JSON). 26 | * 27 | * @var object|null 28 | */ 29 | public ?object $json; 30 | 31 | /** 32 | * Temp directory. 33 | * 34 | * @var string 35 | */ 36 | protected string $tmp_dir; 37 | 38 | /** 39 | * The API Blueprint input. 40 | * 41 | * @var ApibFileParser 42 | */ 43 | protected ApibFileParser $apib; 44 | 45 | /** 46 | * BaseParser constructor. 47 | * 48 | * @param ApibFileParser $apib API Blueprint text 49 | * 50 | * @return self 51 | */ 52 | public function init(ApibFileParser $apib): self 53 | { 54 | $this->apib = $apib; 55 | $this->tmp_dir = sys_get_temp_dir() . '/drafter'; 56 | 57 | return $this; 58 | } 59 | 60 | /** 61 | * BaseParser destructor. 62 | */ 63 | public function __destruct() 64 | { 65 | unset($this->apib); 66 | unset($this->json); 67 | unset($this->tmp_dir); 68 | } 69 | 70 | /** 71 | * Parse the API Blueprint text to JSON. 72 | * 73 | * @throws ExecutionException When the JSON is invalid or warnings are thrown in parsing 74 | * 75 | * @return object JSON output. 76 | */ 77 | public function parseToJson(): object 78 | { 79 | if (!file_exists($this->tmp_dir)) { 80 | mkdir($this->tmp_dir, 0777, true); 81 | } 82 | 83 | file_put_contents($this->tmp_dir . '/index.apib', $this->apib->content()); 84 | 85 | $this->parse(); 86 | 87 | if (json_last_error() !== JSON_ERROR_NONE) { 88 | file_put_contents('php://stderr', 'ERROR: invalid json in ' . $this->tmp_dir . '/index.json'); 89 | 90 | throw new ExecutionException('Drafter generated invalid JSON (' . json_last_error_msg() . ')', 2); 91 | } 92 | 93 | $warnings = false; 94 | foreach ($this->json->content as $item) { 95 | if ($item->element === 'annotation') { 96 | $warnings = true; 97 | $line = $item->attributes->sourceMap->content[0]->content[0]->content[0]->attributes->line->content ?? 'UNKNOWN'; 98 | $prefix = (is_array($item->meta->classes)) ? strtoupper($item->meta->classes[0]) : strtoupper($item->meta->classes->content[0]->content); 99 | $error = $item->content; 100 | file_put_contents('php://stderr', "$prefix: $error (line $line)\n"); 101 | file_put_contents('php://stdout', "
    $prefix: $error (line $line)
    \n"); 102 | } 103 | } 104 | 105 | if ($warnings) { 106 | throw new ExecutionException('Parsing encountered errors and stopped', 2); 107 | } 108 | 109 | return $this->json; 110 | } 111 | 112 | /** 113 | * Parses the apib for the selected method. 114 | * 115 | * @return void 116 | */ 117 | abstract protected function parse(): void; 118 | 119 | /** 120 | * Check if a given parser is available. 121 | * 122 | * @return bool 123 | */ 124 | abstract public static function available(): bool; 125 | } 126 | -------------------------------------------------------------------------------- /src/PHPDraft/Parse/Drafter.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Parse; 14 | 15 | use PHPDraft\In\ApibFileParser; 16 | use RuntimeException; 17 | use UnexpectedValueException; 18 | 19 | class Drafter extends BaseParser 20 | { 21 | /** 22 | * The location of the drafter executable. 23 | * 24 | * @var string 25 | */ 26 | protected string $drafter; 27 | 28 | /** 29 | * ApibToJson constructor. 30 | * 31 | * @param ApibFileParser $apib API Blueprint text 32 | * 33 | * @return BaseParser 34 | */ 35 | public function init(ApibFileParser $apib): BaseParser 36 | { 37 | parent::init($apib); 38 | $loc = self::location(); 39 | if ($loc === false) { 40 | throw new UnexpectedValueException("Could not find drafter location!"); 41 | } 42 | $this->drafter = $loc; 43 | 44 | return $this; 45 | } 46 | 47 | /** 48 | * Return drafter location if found. 49 | * 50 | * @return false|string 51 | */ 52 | public static function location(): false|string 53 | { 54 | $returnVal = shell_exec('which drafter 2> /dev/null'); 55 | if ($returnVal === NULL) 56 | { 57 | return false; 58 | } 59 | 60 | $returnVal = preg_replace('/^\s+|\n|\r|\s+$/m', '', $returnVal); 61 | 62 | return $returnVal === null || $returnVal === '' ? false : $returnVal; 63 | } 64 | 65 | /** 66 | * Parses the apib for the selected method. 67 | * 68 | * @return void 69 | */ 70 | protected function parse(): void 71 | { 72 | shell_exec("$this->drafter $this->tmp_dir/index.apib -f json -o $this->tmp_dir/index.json 2> /dev/null"); 73 | $content = file_get_contents($this->tmp_dir . '/index.json'); 74 | if (!is_string($content)) { 75 | throw new RuntimeException('Could not read intermediary APIB file!'); 76 | } 77 | 78 | $this->json = json_decode($content); 79 | } 80 | 81 | /** 82 | * Check if a given parser is available. 83 | * 84 | * @return bool 85 | */ 86 | public static function available(): bool 87 | { 88 | $path = self::location(); 89 | 90 | $version = shell_exec('drafter -v 2> /dev/null'); 91 | if ($version === NULL) 92 | { 93 | return false; 94 | } 95 | 96 | $version = preg_match('/^v([45])/', $version); 97 | 98 | return $path && $version === 1; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/PHPDraft/Parse/DrafterAPI.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Parse; 14 | 15 | use CurlHandle; 16 | use PHPDraft\In\ApibFileParser; 17 | 18 | class DrafterAPI extends BaseParser 19 | { 20 | /** 21 | * ApibToJson constructor. 22 | * 23 | * @param ApibFileParser $apib API Blueprint text 24 | * 25 | * @return BaseParser 26 | */ 27 | public function init(ApibFileParser $apib): BaseParser 28 | { 29 | parent::init($apib); 30 | 31 | return $this; 32 | } 33 | 34 | /** 35 | * Parses the apib for the selected method. 36 | * 37 | * @return void 38 | */ 39 | protected function parse(): void 40 | { 41 | $ch = self::curl_init_drafter($this->apib->content()); 42 | 43 | $response = curl_exec($ch); 44 | 45 | if (curl_errno($ch) !== 0) { 46 | throw new ResourceException('Drafter webservice failed to parse input', 1); 47 | } 48 | 49 | $this->json = json_decode($response); 50 | } 51 | 52 | /** 53 | * Init curl for drafter webservice. 54 | * 55 | * @param string $message API blueprint to parse 56 | * 57 | * @return CurlHandle 58 | */ 59 | public static function curl_init_drafter(string $message): CurlHandle 60 | { 61 | $ch = curl_init(); 62 | 63 | curl_setopt($ch, CURLOPT_URL, 'https://api.apiblueprint.org/parser'); 64 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 65 | curl_setopt($ch, CURLOPT_HEADER, false); 66 | 67 | curl_setopt($ch, CURLOPT_POST, true); 68 | 69 | curl_setopt($ch, CURLOPT_POSTFIELDS, $message); 70 | 71 | curl_setopt($ch, CURLOPT_HTTPHEADER, [ 72 | 'Content-Type: text/vnd.apiblueprint', 73 | 'Accept: application/vnd.refract.parse-result+json', 74 | ]); 75 | 76 | return $ch; 77 | } 78 | 79 | /** 80 | * Check if a given parser is available. 81 | * 82 | * @return bool 83 | */ 84 | public static function available(): bool 85 | { 86 | if (!defined('DRAFTER_ONLINE_MODE') || DRAFTER_ONLINE_MODE !== 1) { 87 | return false; 88 | } 89 | 90 | $ch = self::curl_init_drafter('# Hello API 91 | ## /message 92 | ### GET 93 | + Response 200 (text/plain) 94 | 95 | Hello World!'); 96 | 97 | curl_exec($ch); 98 | 99 | if (curl_errno($ch) !== CURLE_OK) { 100 | return false; 101 | } 102 | curl_close($ch); 103 | 104 | return true; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/PHPDraft/Parse/ExecutionException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Parse; 14 | 15 | use Exception; 16 | 17 | /** 18 | * Class ExecutionException. 19 | * 20 | * @package Parse 21 | */ 22 | class ExecutionException extends Exception 23 | { 24 | } 25 | -------------------------------------------------------------------------------- /src/PHPDraft/Parse/HtmlGenerator.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Parse; 14 | 15 | use Twig\Error\LoaderError; 16 | use Twig\Error\RuntimeError; 17 | use Twig\Error\SyntaxError; 18 | use PHPDraft\Out\HtmlTemplateRenderer; 19 | 20 | /** 21 | * Class HtmlGenerator. 22 | */ 23 | class HtmlGenerator extends BaseHtmlGenerator 24 | { 25 | /** 26 | * Get the HTML representation of the JSON object. 27 | * 28 | * @param string $template Type of template to display. 29 | * @param string|null $image Image to use as a logo 30 | * @param string|null $css CSS to load 31 | * @param string|null $js JS to load 32 | * 33 | * @return void HTML template to display 34 | * 35 | * @throws ExecutionException As a runtime exception 36 | * @throws LoaderError 37 | * @throws RuntimeError 38 | * @throws SyntaxError 39 | */ 40 | public function build_html(string $template = 'default', ?string $image = null, ?string $css = null, ?string $js = null): void 41 | { 42 | $gen = new HtmlTemplateRenderer($template, $image); 43 | 44 | if (!is_null($css)) { 45 | $gen->css = explode(',', $css); 46 | } 47 | 48 | if (!is_null($js)) { 49 | $gen->js = explode(',', $js); 50 | } 51 | 52 | $gen->sorting = $this->sorting; 53 | 54 | $this->html = $gen->get($this->object); 55 | } 56 | 57 | /** 58 | * Returns the generated HTML. 59 | * 60 | * @return string 61 | */ 62 | public function __toString(): string 63 | { 64 | return $this->html; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/PHPDraft/Parse/ParserFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | namespace PHPDraft\Parse; 14 | 15 | use RuntimeException; 16 | 17 | /** 18 | * Class ResourceException. 19 | * 20 | * @package Parse 21 | */ 22 | class ResourceException extends RuntimeException 23 | { 24 | } 25 | -------------------------------------------------------------------------------- /src/PHPDraft/Parse/Tests/BaseParserTest.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace PHPDraft\Parse\Tests; 11 | 12 | use Lunr\Halo\LunrBaseTest; 13 | use PHPDraft\In\ApibFileParser; 14 | use PHPUnit\Framework\MockObject\MockObject; 15 | use ReflectionClass; 16 | 17 | /** 18 | * Class BaseParserTest 19 | * @covers \PHPDraft\Parse\BaseParser 20 | */ 21 | class BaseParserTest extends LunrBaseTest 22 | { 23 | /** 24 | * Shared instance of the file parser. 25 | * 26 | * @var ApibFileParser&MockObject 27 | */ 28 | private $parser; 29 | 30 | /** 31 | * Set up 32 | */ 33 | public function setUp(): void 34 | { 35 | $this->mock_function('sys_get_temp_dir', fn() => TEST_STATICS); 36 | $this->mock_function('shell_exec', fn() => "/some/dir/drafter\n"); 37 | 38 | $this->parser = $this->getMockBuilder('\PHPDraft\In\ApibFileParser') 39 | ->disableOriginalConstructor() 40 | ->getMock(); 41 | $this->class = $this->getMockBuilder('\PHPDraft\Parse\BaseParser') 42 | ->getMockForAbstractClass(); 43 | 44 | $this->parser->set_apib_content(file_get_contents(TEST_STATICS . '/drafter/apib/index.apib')); 45 | 46 | $this->class->init($this->parser); 47 | $this->baseSetUp($this->class); 48 | 49 | $this->unmock_function('shell_exec'); 50 | $this->unmock_function('sys_get_temp_dir'); 51 | } 52 | 53 | /** 54 | * Tear down 55 | */ 56 | public function tearDown(): void 57 | { 58 | unset($this->class); 59 | unset($this->reflection); 60 | unset($this->parser); 61 | } 62 | 63 | /** 64 | * Test if the value the class is initialized with is correct 65 | * 66 | * @covers \PHPDraft\Parse\BaseParser::parseToJson() 67 | */ 68 | public function testSetupCorrectly(): void 69 | { 70 | $this->assertInstanceOf('\PHPDraft\In\ApibFileParser', $this->get_reflection_property_value('apib')); 71 | } 72 | 73 | /** 74 | * Check if parsing the APIB to JSON gives the expected result 75 | * 76 | * @covers \PHPDraft\Parse\BaseParser::parseToJson() 77 | */ 78 | public function testParseToJSON(): void 79 | { 80 | $this->class->expects($this->once()) 81 | ->method('parse'); 82 | 83 | $this->class->json = json_decode(file_get_contents(TEST_STATICS . '/drafter/json/index.json')); 84 | $this->class->parseToJson(); 85 | $this->assertEquals(json_decode(file_get_contents(TEST_STATICS . '/drafter/json/index.json')), $this->class->json); 86 | } 87 | 88 | /** 89 | * Check if parsing the APIB to JSON gives the expected result 90 | * 91 | * @covers \PHPDraft\Parse\BaseParser::parseToJson() 92 | */ 93 | public function testParseToJSONMkDir(): void 94 | { 95 | $this->class->expects($this->once()) 96 | ->method('parse'); 97 | 98 | $this->class->json = json_decode(file_get_contents(TEST_STATICS . '/drafter/json/index.json')); 99 | $this->class->parseToJson(); 100 | $this->assertEquals(json_decode(file_get_contents(TEST_STATICS . '/drafter/json/index.json')), $this->class->json); 101 | } 102 | 103 | /** 104 | * Check if parsing the APIB to JSON gives the expected result 105 | * 106 | * @covers \PHPDraft\Parse\BaseParser::parseToJson() 107 | */ 108 | public function testParseToJSONMkTmp(): void 109 | { 110 | $tmp_dir = dirname(TEST_STATICS, 2) . '/build/tmp'; 111 | if (file_exists($tmp_dir . DIRECTORY_SEPARATOR . 'index.apib')) { 112 | unlink($tmp_dir . DIRECTORY_SEPARATOR . 'index.apib'); 113 | } 114 | if (file_exists($tmp_dir)) { 115 | rmdir($tmp_dir); 116 | } 117 | 118 | $this->set_reflection_property_value('tmp_dir', $tmp_dir); 119 | 120 | $this->class->expects($this->once()) 121 | ->method('parse'); 122 | 123 | $this->class->json = json_decode(file_get_contents(TEST_STATICS . '/drafter/json/index.json')); 124 | $this->class->parseToJson(); 125 | $this->assertDirectoryExists($tmp_dir); 126 | $this->assertEquals(json_decode(file_get_contents(TEST_STATICS . '/drafter/json/index.json')), $this->class->json); 127 | } 128 | 129 | /** 130 | * Check if parsing the fails when invalid JSON 131 | * 132 | * @covers \PHPDraft\Parse\BaseParser::parseToJson() 133 | */ 134 | public function testParseToJSONWithInvalidJSON(): void 135 | { 136 | $this->class->expects($this->once()) 137 | ->method('parse'); 138 | 139 | $this->expectException('\PHPDraft\Parse\ExecutionException'); 140 | $this->expectExceptionMessage('Drafter generated invalid JSON (ERROR)'); 141 | $this->expectExceptionCode(2); 142 | 143 | $this->mock_function('json_last_error', fn() => JSON_ERROR_DEPTH); 144 | $this->mock_function('json_last_error_msg', fn() => "ERROR"); 145 | $this->class->parseToJson(); 146 | $this->expectOutputString('ERROR: invalid json in /tmp/drafter/index.json'); 147 | $this->unmock_function('json_last_error_msg'); 148 | $this->unmock_function('json_last_error'); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/PHPDraft/Parse/Tests/DrafterAPITest.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace PHPDraft\Parse\Tests; 11 | 12 | use Lunr\Halo\LunrBaseTest; 13 | use PHPDraft\In\ApibFileParser; 14 | use PHPDraft\Parse\DrafterAPI; 15 | use PHPUnit\Framework\MockObject\MockObject; 16 | use ReflectionClass; 17 | 18 | /** 19 | * Class DrafterAPITest 20 | * @covers \PHPDraft\Parse\DrafterAPI 21 | */ 22 | class DrafterAPITest extends LunrBaseTest 23 | { 24 | /** 25 | * Shared instance of the file parser. 26 | * 27 | * @var ApibFileParser|MockObject 28 | */ 29 | private mixed $parser; 30 | 31 | private DrafterAPI $class; 32 | 33 | /** 34 | * Basic setup 35 | */ 36 | public function setUp(): void 37 | { 38 | $this->mock_function('sys_get_temp_dir', fn() => TEST_STATICS); 39 | 40 | $this->parser = $this->getMockBuilder('\PHPDraft\In\ApibFileParser') 41 | ->disableOriginalConstructor() 42 | ->getMock(); 43 | 44 | $this->parser->set_apib_content(file_get_contents(TEST_STATICS . '/drafter/apib/index.apib')); 45 | 46 | $this->class = new DrafterAPI(); 47 | $this->baseSetUp($this->class); 48 | 49 | $this->class->init($this->parser); 50 | 51 | $this->unmock_function('sys_get_temp_dir'); 52 | } 53 | 54 | /** 55 | * Tear down 56 | */ 57 | public function tearDown(): void 58 | { 59 | parent::tearDown(); 60 | unset($this->parser); 61 | } 62 | 63 | /** 64 | * Test if the value the class is initialized with is correct 65 | * 66 | * @covers \PHPDraft\Parse\DrafterAPI::parseToJson() 67 | */ 68 | public function testSetupCorrectly(): void 69 | { 70 | $this->assertInstanceOf('\PHPDraft\In\ApibFileParser', $this->get_reflection_property_value('apib')); 71 | } 72 | 73 | /** 74 | * Test if the drafter api can be used 75 | * 76 | * @covers \PHPDraft\Parse\DrafterAPI::parseToJson() 77 | */ 78 | public function testAvailableFails(): void 79 | { 80 | $this->mock_function('curl_exec', fn() => "/some/dir/drafter\n"); 81 | $this->mock_function('curl_errno', fn() => 1); 82 | 83 | $this->assertFalse(DrafterAPI::available()); 84 | 85 | $this->unmock_function('curl_errno'); 86 | $this->unmock_function('curl_exec'); 87 | } 88 | 89 | /** 90 | * Test if the drafter api can be used 91 | * 92 | * @covers \PHPDraft\Parse\DrafterAPI::parseToJson() 93 | */ 94 | public function testAvailableSuccess(): void 95 | { 96 | $this->mock_function('curl_exec', fn() => "/some/dir/drafter\n"); 97 | $this->mock_function('curl_errno', fn() => 0); 98 | 99 | $this->assertFalse(DrafterAPI::available()); 100 | 101 | $this->unmock_function('curl_errno'); 102 | $this->unmock_function('curl_exec'); 103 | } 104 | 105 | /** 106 | * Check if parsing the fails without drafter 107 | * 108 | * @covers \PHPDraft\Parse\DrafterAPI::parseToJson() 109 | */ 110 | public function testParseWithFailingWebservice(): void 111 | { 112 | $this->expectException('\PHPDraft\Parse\ResourceException'); 113 | $this->expectExceptionMessage('Drafter webservice failed to parse input'); 114 | $this->expectExceptionCode(1); 115 | 116 | $this->mock_function('curl_errno', fn() => 1); 117 | $this->class->parseToJson(); 118 | $this->unmock_function('curl_errno'); 119 | } 120 | 121 | /** 122 | * Check if parsing the succeeds 123 | * 124 | * @covers \PHPDraft\Parse\DrafterAPI::parseToJson() 125 | */ 126 | public function testParseSuccess(): void 127 | { 128 | $this->mock_function('json_last_error', fn() => 0); 129 | $this->mock_function('curl_errno', fn() => 0); 130 | $this->mock_function('curl_exec', fn() => '{"content":[{"element":"world"}]}'); 131 | 132 | $this->class->parseToJson(); 133 | 134 | $this->unmock_function('curl_exec'); 135 | $this->unmock_function('curl_errno'); 136 | $this->unmock_function('json_last_error'); 137 | 138 | $obj = (object)[]; 139 | $obj2 = (object)[]; 140 | $obj2->element = 'world'; 141 | $obj->content = [ $obj2 ]; 142 | $this->assertEquals($obj, $this->class->json); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/PHPDraft/Parse/Tests/DrafterTest.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace PHPDraft\Parse\Tests; 11 | 12 | use Lunr\Halo\LunrBaseTest; 13 | use PHPDraft\In\ApibFileParser; 14 | use PHPDraft\Parse\Drafter; 15 | use PHPDraft\Parse\ExecutionException; 16 | use PHPUnit\Framework\MockObject\MockObject; 17 | use ReflectionClass; 18 | 19 | /** 20 | * Class DrafterTest 21 | * 22 | * @covers \PHPDraft\Parse\Drafter 23 | */ 24 | class DrafterTest extends LunrBaseTest 25 | { 26 | /** 27 | * Shared instance of the file parser. 28 | * 29 | * @var ApibFileParser|MockObject 30 | */ 31 | private mixed $parser; 32 | 33 | /** 34 | * Set up 35 | */ 36 | public function setUp(): void 37 | { 38 | $this->mock_function('sys_get_temp_dir', fn() => TEST_STATICS); 39 | $this->mock_function('shell_exec', fn() => "/some/dir/drafter\n"); 40 | 41 | $this->parser = $this->getMockBuilder('\PHPDraft\In\ApibFileParser') 42 | ->disableOriginalConstructor() 43 | ->getMock(); 44 | 45 | $this->parser->set_apib_content(file_get_contents(TEST_STATICS . '/drafter/apib/index.apib')); 46 | 47 | $this->class = new Drafter(); 48 | $this->baseSetUp($this->class); 49 | 50 | $this->class->init($this->parser); 51 | 52 | $this->unmock_function('shell_exec'); 53 | $this->unmock_function('sys_get_temp_dir'); 54 | } 55 | 56 | /** 57 | * Tear down 58 | */ 59 | public function tearDown(): void 60 | { 61 | if (file_exists(TEST_STATICS . '/drafter/index.json')) { 62 | unlink(TEST_STATICS . '/drafter/index.json'); 63 | } 64 | if (file_exists(TEST_STATICS . '/drafter/index.apib')) { 65 | unlink(TEST_STATICS . '/drafter/index.apib'); 66 | } 67 | unset($this->class); 68 | unset($this->reflection); 69 | unset($this->parser); 70 | } 71 | 72 | /** 73 | * Test if the value the class is initialized with is correct 74 | * 75 | * @covers \PHPDraft\Parse\Drafter::parseToJson() 76 | */ 77 | public function testSetupCorrectly(): void 78 | { 79 | $this->assertInstanceOf('\PHPDraft\In\ApibFileParser', $this->get_reflection_property_value('apib')); 80 | } 81 | 82 | /** 83 | * Check if parsing the APIB to JSON gives the expected result 84 | */ 85 | public function testParseToJSON(): void 86 | { 87 | $this->mock_function('json_last_error', fn() => JSON_ERROR_NONE); 88 | $this->mock_function('shell_exec', fn() => ""); 89 | file_put_contents( 90 | TEST_STATICS . '/drafter/index.json', 91 | file_get_contents(TEST_STATICS . '/drafter/json/index.json') 92 | ); 93 | $this->class->parseToJson(); 94 | $this->assertEquals( 95 | json_decode(file_get_contents(TEST_STATICS . '/drafter/json/index.json')), 96 | $this->class->json 97 | ); 98 | $this->unmock_function('shell_exec'); 99 | $this->unmock_function('json_last_error'); 100 | } 101 | 102 | /** 103 | * Check if parsing the APIB to JSON gives the expected result with inheritance 104 | * 105 | * @covers \PHPDraft\Parse\Drafter::parseToJson() 106 | */ 107 | public function testParseToJSONInheritance(): void 108 | { 109 | $this->mock_function('json_last_error', fn() => JSON_ERROR_NONE); 110 | $this->mock_function('shell_exec', fn() => ''); 111 | file_put_contents( 112 | TEST_STATICS . '/drafter/index.json', 113 | file_get_contents(TEST_STATICS . '/drafter/json/inheritance.json') 114 | ); 115 | $this->class->parseToJson(); 116 | $this->assertEquals( 117 | json_decode(file_get_contents(TEST_STATICS . '/drafter/json/inheritance.json')), 118 | $this->class->json 119 | ); 120 | $this->unmock_function('shell_exec'); 121 | $this->unmock_function('json_last_error'); 122 | } 123 | 124 | /** 125 | * Check if parsing the APIB to JSON gives the expected result 126 | * 127 | * @covers \PHPDraft\Parse\Drafter::parseToJson() 128 | */ 129 | public function testParseToJSONWithErrors(): void 130 | { 131 | $this->expectException('\PHPDraft\Parse\ExecutionException'); 132 | $this->expectExceptionMessage('Parsing encountered errors and stopped'); 133 | $this->expectExceptionCode(2); 134 | 135 | $this->mock_function('shell_exec', fn() => ''); 136 | file_put_contents( 137 | TEST_STATICS . '/drafter/index.json', 138 | file_get_contents(TEST_STATICS . '/drafter/json/error.json') 139 | ); 140 | $this->class->parseToJson(); 141 | $this->expectOutputString("WARNING: ignoring unrecognized block\nWARNING: no headers specified\nWARNING: ignoring unrecognized block\nWARNING: empty request message-body"); 142 | $this->unmock_function('shell_exec'); 143 | } 144 | 145 | /** 146 | * Check if parsing the fails without drafter 147 | * 148 | * @covers \PHPDraft\Parse\Drafter::available() 149 | */ 150 | public function testSetupWithoutDrafter(): void 151 | { 152 | $this->mock_function('shell_exec', fn() => ''); 153 | $this->assertFalse(Drafter::available()); 154 | $this->unmock_function('shell_exec'); 155 | } 156 | 157 | /** 158 | * Check if parsing the fails when invalid JSON 159 | * 160 | * @covers \PHPDraft\Parse\Drafter::parseToJson() 161 | */ 162 | public function testParseToJSONWithInvalidJSON(): void 163 | { 164 | $this->expectException('\PHPDraft\Parse\ExecutionException'); 165 | $this->expectExceptionMessage('Drafter generated invalid JSON (ERROR)'); 166 | $this->expectExceptionCode(2); 167 | 168 | $this->mock_function('json_last_error', fn() => JSON_ERROR_DEPTH); 169 | $this->mock_function('json_last_error_msg', fn() => 'ERROR'); 170 | file_put_contents(TEST_STATICS . '/drafter/index.json', '["hello: \'world}'); 171 | $this->class->parseToJson(); 172 | $this->expectOutputString('ERROR: invalid json in /tmp/drafter/index.json'); 173 | $this->unmock_function('json_last_error_msg'); 174 | $this->unmock_function('json_last_error'); 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/PHPDraft/Parse/Tests/HtmlGeneratorTest.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | namespace PHPDraft\Parse\Tests; 11 | 12 | use Lunr\Halo\LunrBaseTest; 13 | use PHPDraft\Parse\HtmlGenerator; 14 | use ReflectionClass; 15 | 16 | /** 17 | * Class JsonToHTMLTest 18 | * @covers \PHPDraft\Parse\HtmlGenerator 19 | */ 20 | class HtmlGeneratorTest extends LunrBaseTest 21 | { 22 | /** 23 | * Test Class 24 | * @var HtmlGenerator 25 | */ 26 | protected HtmlGenerator $class; 27 | 28 | /** 29 | * Set up 30 | * @requires ext-uopz 31 | */ 32 | public function setUp(): void 33 | { 34 | define('ID_STATIC', 'SOME_ID'); 35 | $data = json_decode(file_get_contents(TEST_STATICS . '/drafter/json/index.json')); 36 | $this->class = new HtmlGenerator(); 37 | 38 | $this->baseSetUp($this->class); 39 | $this->class->init($data); 40 | 41 | $this->class->sorting = -1; 42 | } 43 | 44 | /** 45 | * Tear down 46 | */ 47 | public function tearDown(): void 48 | { 49 | $this->constant_undefine('ID_STATIC'); 50 | unset($this->class); 51 | unset($this->reflection); 52 | } 53 | 54 | /** 55 | * Tests if the constructor sets the property correctly 56 | * 57 | * @requires ext-uopz 58 | */ 59 | public function testSetupCorrectly(): void 60 | { 61 | $json = json_decode(file_get_contents(TEST_STATICS . '/drafter/json/index.json')); 62 | $this->assertEquals($json, $this->get_reflection_property_value('object')); 63 | } 64 | 65 | /** 66 | * Tests if the constructor sets the property correctly 67 | * @requires ext-uopz 68 | */ 69 | public function testGetHTML(): void 70 | { 71 | $this->class->build_html(); 72 | if (version_compare(PHP_VERSION, '8.1.0', '>=')) { 73 | $this->assertStringEqualsFile(TEST_STATICS . '/drafter/html/basic.html', $this->class->__toString()); 74 | } else { 75 | $this->assertStringEqualsFile(TEST_STATICS . '/drafter/html/basic_old.html', $this->class->__toString()); 76 | } 77 | } 78 | 79 | /** 80 | * Tests if the constructor sets the property correctly 81 | * @requires ext-uopz 82 | */ 83 | public function testGetHTMLMaterial(): void 84 | { 85 | $this->class->build_html('material'); 86 | if (version_compare(PHP_VERSION, '8.1.0', '>=')) { 87 | $this->assertStringEqualsFile(TEST_STATICS . '/drafter/html/material.html', $this->class->__toString()); 88 | } else { 89 | $this->assertStringEqualsFile(TEST_STATICS . '/drafter/html/material_old.html', $this->class->__toString()); 90 | } 91 | } 92 | 93 | /** 94 | * Tests if the constructor sets the property correctly 95 | * @requires ext-uopz 96 | */ 97 | public function testGetHTMLAdvanced(): void 98 | { 99 | $this->class->build_html('material', 'img.jpg', 'test.css,index.css', 'index.js,test.js'); 100 | 101 | $this->assertMatchesRegularExpression('//', $this->class->__toString()); 102 | $this->assertMatchesRegularExpression('/