From 09916d2933c2e2b2f3de65bf692830b814b5ee21 Mon Sep 17 00:00:00 2001 From: Katsuhiko Maeno Date: Tue, 1 Apr 2025 01:36:32 +0900 Subject: [PATCH 1/9] chore: prepare rc/1.2.0 --- .github/workflows/ci.yml | 33 +++- .gitignore | 3 + CHANGELOG.md | 6 + README.md | 136 +++++++++++++- README_JP.md | 121 ++++++++++++- composer.json | 4 +- composer.lock | 331 ++++++++++++++++++++++++++++++++- generate-schema.php | 332 ++++++++++++++++++++++++++++++++++ http_status.php | 14 +- index.php | 13 ++ responses/users/post/400.json | 3 + schema/.gitkeep | 0 start_server.php | 18 +- tests/MockApiTest.php | 100 +++++++++- version.json | 4 +- 15 files changed, 1089 insertions(+), 29 deletions(-) create mode 100644 generate-schema.php create mode 100644 schema/.gitkeep diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index acde684..846a287 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,26 +14,43 @@ jobs: runs-on: ubuntu-latest steps: - - name: リポジトリをチェックアウト + - name: Check out the repository uses: actions/checkout@v4 - - name: PHPのセットアップ + - name: Set up PHP uses: shivammathur/setup-php@v2 with: php-version: '8.3' tools: composer, phpstan, phpcs, phpunit - - name: Composerで依存関係をインストール - run: composer install --no-progress --no-suggest --prefer-dist + - name: Cache Composer dependencies + uses: actions/cache@v3 + with: + path: vendor + key: ${{ runner.os }}-php-${{ hashFiles('composer.lock') }} + restore-keys: | + ${{ runner.os }}-php- + + - name: Install dependencies with Composer + run: composer install --no-progress --prefer-dist - - name: 静的解析(PHPStan) + - name: Static Analysis (PHPStan) run: vendor/bin/phpstan analyse -c phpstan.neon --memory-limit=512M - - name: コードスタイルチェック(PHPCS) + - name: Code Style Check (PHPCS) run: vendor/bin/phpcs --standard=phpcs.xml.dist - - name: APIサーバー起動 + - name: Automatic generation of OpenAPI schema + run: php generate-schema.php yaml + + - name: Upload schema + uses: actions/upload-artifact@v3 + with: + name: openapi-schema + path: schema/openapi.yaml + + - name: Start the API server run: php start_server.php & - - name: 自動テスト実行(PHPUnit) + - name: Automated test execution (PHPUnit) run: vendor/bin/phpunit diff --git a/.gitignore b/.gitignore index 416a33c..7e2cf4e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ responses/* !responses/errors/** !responses/others/** !responses/users/** + +schema/*.json +schema/*.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index aca572b..e194b57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [1.2.0] - 2025-04-01 +### Added +- Added feature of OpenAPI 3.0 schema auto-generation. +- Added unit tests for the OpenAPI 3.0 schema auto-generation feature. + ## [1.1.0] - 2025-03-17 ### Added - Implemented dynamic routing (support for `GET users/:group/:limit` format) @@ -19,5 +24,6 @@ --- #### 🔗 GitHub Releases +[1.2.0]: https://github.com/ka215/MockAPI-PHP/releases/tag/v1.2.0 [1.1.0]: https://github.com/ka215/MockAPI-PHP/releases/tag/v1.1.0 [1.0.0]: https://github.com/ka215/MockAPI-PHP/releases/tag/v1.0.0 diff --git a/README.md b/README.md index 4c07d25..fc00b04 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ It supports dynamic responses, polling functionality, and authentication control
-[To Japanese Readme](./README_JP.md) +[View Japanese version of README](./README_JP.md)
@@ -36,11 +36,16 @@ It supports dynamic responses, polling functionality, and authentication control - **Custom Hooks** - Register custom hooks for each method + endpoint request to override response content. Example: `GET /users` request can be handled by a custom hook `hooks/get_users.php`. +- **OpenAPI Schema Support** + - Equipped with a function to automatically generate OpenAPI 3.0 schema based on JSON responses. + - The `example` items in the schema are also appropriately trimmed and padded to avoid excess response data. + - It uses `opis/json-schema` for validation and automatically checks for schema consistency. - **Logging** - `request.log` stores request details (headers, query, body). - `response.log` stores response details. - `auth.log` stores authentication failures, and `error.log` stores generic errors. - All logs are linked by a request ID for traceability. + - Validation errors during automatic OpenAPI schema generation can be tracked in `validation-error.log`. - **Configuration via Environment Variables** - Uses `vlucas/phpdotenv` to read environment variables. - `.env` can define server settings such as `PORT`, base API path, temporary file storage (`cookies.txt`, etc.), and logging paths. @@ -52,6 +57,7 @@ Below is an example structure of the `responses` directory. You can freely custo ``` mock_api_server/ ├── index.php # Main script for the mock server + ├── generate-schema.php # OpenAPI 3.0 Schema Generation Script ├── http_status.php # HTTP status code definitions ├── start_server.php # Local server startup script ├── .env # Configuration file (.env.sample provides a template) @@ -86,11 +92,13 @@ mock_api_server/ │ └── MockApiTest.php # Initial test cases ├── phpunit.xml # PHPUnit configuration file ├── version.json # Version information file + ├── schema/ # OpenAPI Schema Output Directory └── logs/ # Directory for log storage ├── auth.log # Authentication error logs ├── error.log # General error logs ├── request.log # Request logs - └── response.log # Response logs + ├── response.log # Response logs + └── validation-error.log # OpenAPI Schema Validation Error Logs ``` ## Usage @@ -123,6 +131,10 @@ mock_api_server/ ```bash curl -X POST http://localhost:3030/api/users -H "Content-Type: application/json" -d '{"name": "New User"}' ``` + - **PUT Request (data updating)** + ```bash + curl -X PUT http://localhost:3030/api/users/1 -H "Content-Type: application/json" -d '{"name": "Updated Name"}' + ``` - **DELETE Request** ```bash curl -X DELETE http://localhost:3030/api/users/1 @@ -189,6 +201,96 @@ CREDENTIAL= # Credential (temporary token for individual user authenti Note: The `API_KEY` and `CREDENTIAL` options are implemented as a simple authentication mechanism. If specified, the server will extract the Bearer token from the Authorization header and perform authentication. +## OpenAPI Schema Auto-Generation + +Starting from version 1.2, this project includes a feature to automatically generate OpenAPI 3.0 schemas based on the JSON response files located under `responses/{endpoint_path}/{method}/{status_name}.json`. + +### How to Run +**CLI (Command Line):** +```bash +php generate-schema.php [format] [title] [version] +``` + +- `format` (optional): Output format (`json` or `yaml`). Default: `yaml` +- `title` (optional): Title for the OpenAPI schema +- `version` (optional): Version number for the OpenAPI schema + +Example: +```bash +php generate-schema.php yaml "My Awesome API" "2.0.0" +``` + +**Web (via Browser):** +```http +GET /generate-schema.php?format=json&title=My+Awesome+API&version=2.0.0 +``` + +### Output File +The generated schema will be saved as either `schema/openapi.yaml` or `schema/openapi.json`. + +### Validation +Each JSON response file will be validated against its automatically generated JSON Schema. +If the structure is invalid, the process will be aborted and the error message will be written to `logs/validation-error.log`. + +### Automatic `example` Embedding +Each schema includes an `example` field derived from the original JSON response: + +- If the response is an array, **only the first element** will be included in the example (to avoid oversized outputs). +- This rule is recursively applied to nested arrays and objects as well. + +### Environment Variables (.env) +You can customize default behavior by setting the following environment variables in `.env`: +```env +LOG_DIR=./logs +SCHEMA_DIR=./schema +SCHEMA_FORMAT=yaml +SCHEMA_TITLE=MockAPI-PHP Auto Schema +SCHEMA_VERSION=1.0.0 +``` + +Note: if parameters are passed directly when executing the script, they take precedence over the environment variables. + +### Use Cases for Auto Schema Generation + +- When you want to avoid writing OpenAPI schemas manually. +- For **API-first development** where mocks are created before implementation. +- To integrate with other tools like **Prism** or **SwaggerUI**. +- For ensuring schema consistency through automated testing. + +### Example: Auto-Generating Schema via CI/CD (GitHub Actions) + +`.github/workflows/generate-schema.yml`: + +```yaml +name: Generate OpenAPI Schema + +on: + push: + paths: + - 'responses/**' + - 'generate-schema.php' + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + - run: composer install --no-dev + - run: php generate-schema.php json + - name: Upload schema + uses: actions/upload-artifact@v3 + with: + name: openapi-schema + path: schema/openapi.json +``` + +With this setup, a fresh OpenAPI schema will be automatically generated and saved whenever response files are updated. + + ## Tips ### Custom Responses @@ -374,6 +476,36 @@ Additional test cases can be added in `tests/MockApiTest.php`. php vendor/bin/phpunit ``` +## Comparison with Other Mock API Tools + +The following table compares **MockAPI-PHP** with other major mock API tools such as `json-server`, `MSW`, `WireMock`, and `Prism`. + +| Aspect | **MockAPI-PHP** | **json-server** | **Mock Service Worker (MSW)** | **WireMock** | **Prism (Stoplight)** | +|--------|------------------|------------------|--------------------------------|---------------|------------------------| +| **Language** | PHP | Node.js | JavaScript | Java | Node.js | +| **Ease of Installation** | ★☆☆ (Very easy) | ★☆☆ (Very easy) | ★★☆ (Environment-dependent) | ★★★ (Heavy) | ★★☆ | +| **Response Definition** | PHP logic + JSON files | Static JSON files | Defined in JavaScript | JSON or Java config | OpenAPI-based | +| **Routing Control** | ✅ Flexible (via PHP) | △ Pattern-based | ✅ Defined via `rest.get()` etc. | ✅ URL pattern match | ✅ OpenAPI-driven | +| **Dynamic Response** | ✅ Fully dynamic via PHP | △ Limited | ✅ Supported in JS | ✅ Supported via scripting | △ Difficult | +| **Query/Param Handling** | ✅ Fully controllable | △ Limited | ✅ Fully supported | ✅ Fully supported | △ Schema-based conditions only | +| **Header/Method Handling** | ✅ Fully supported | △ Limited | ✅ Fully supported | ✅ Fully supported | ✅ | +| **Error & Auth Simulation** | ✅ Fully programmable | ✕ Difficult | ✅ Supported via logic | ✅ Fully configurable | △ Limited branching | +| **Delay & Timing Control** | ✅ Flexible via `sleep()` etc. | ✕ | ✅ Via `setTimeout()` etc. | ✅ Via `fixedDelay` etc. | △ Difficult | +| **OpenAPI Integration** | ✅ Built-in schema generation | ✕ | ✕ | △ Can export | ✅ Native | +| **Schema Auto-Generation** | ✅ From JSON responses | ✕ | ✕ | △ Via converter tools | ✅ | +| **Example Embedding** | ✅ Automatic (with trimming) | ✕ | ✕ | ✕ | ✅ | +| **Best Fit For** | PHP-based projects | Node.js projects | Frontend UI development | Java-based projects | API-spec-first organizations | +| **Learning Curve** | ★☆☆ (Low for PHP devs) | ★☆☆ | ★★☆ | ★★★ | ★★☆ | +| **Logging/Tracking** | ✅ Built-in logging (incl. validation) | ✕ | ✅ Via DevTools | ✅ Detailed logs | △ | +| **Flexibility** | ◎ Maximum (code-driven) | ○ Great for simple mocks | △ UI-dev focused | ○ Full-featured | △ Some constraints | + +### Summary of Advantages + +- **MockAPI-PHP allows defining mock responses with dynamic logic using PHP**. +- Especially well-suited for PHP backend projects or frontend-backend decoupled development where the backend is not yet ready. +- Unlike GUI or OpenAPI-based tools, MockAPI-PHP focuses on **code-driven** API mocking. +- Comes with built-in **OpenAPI 3.0 schema generation** from response structures (from v1.2 onwards). + ## License This project is released under the [MIT License](LICENSE). diff --git a/README_JP.md b/README_JP.md index e26bbce..de65b9b 100644 --- a/README_JP.md +++ b/README_JP.md @@ -1,4 +1,4 @@ -# MockAPI-PHP +# MockAPI-PHP ── 日本語ドキュメント このプロジェクトは、PHP製の軽量なモックAPIサーバーです。 開発・テスト環境で実際のAPIを使用せずに、リクエストのシミュレーションが可能です。 @@ -6,7 +6,7 @@
-[To English Readme](./README.md) +[View English version of README](./README.md)
@@ -36,11 +36,16 @@ - **カスタムフック** - メソッド+エンドポイントの任意のリクエスト毎にカスタムフックを登録してレスポンス内容をオーバーライドできる。 例: `GET /users` のリクエストに対して `hooks/get_users.php` のカスタムフックファイルを実行してレスポンスを制御可能。 +- **OpenAPIスキーマへの対応** + - JSONレスポンスを元に OpenAPI 3.0 スキーマを自動生成する機能を搭載。 + - `example` 項目も適切にトリミングされて埋め込まれるため、過剰なレスポンスデータを回避。 + - バリデーションには `opis/json-schema` を使用し、スキーマの整合性チェックも自動化。 - **ロギング** - `request.log` にリクエスト内容(ヘッダー・クエリ・ボディ)を記録。 - `response.log` にレスポンス内容を記録。 - `auth.log` に認証エラー、 `error.log` に汎用エラーの内容を記録。 - 全てのログはリクエストIDにより紐づけられるため、照合可能。 + - OpenAPIスキーマ自動生成時のバリデーションエラーは `validation-error.log` でトラッキング可能。 - **環境変数による設定保存** - 環境変数の読み込みには `vlucas/phpdotenv` を使用。 - `.env` を使い、ポート番号 `PORT` 等の各種環境変数を管理可能。 @@ -53,6 +58,7 @@ ``` mock_api_server/ ├── index.php # モックサーバーのメインスクリプト + ├── generate-schema.php # OpenAPI 3.0 スキーマ生成スクリプト ├── http_status.php # HTTPステータスコードの定義 ├── start_server.php # ローカルサーバー起動スクリプト ├── .env # 設定用( .env.sample を参考に設定) @@ -87,11 +93,13 @@ mock_api_server/ │ └── MockApiTest.php # 初期テストケース ├── phpunit.xml # ユニットテスト設定ファイル ├── version.json # プロジェクトパッケージのバージョン情報 + ├── schema/ # OpenAPI スキーマ出力ディレクトリ └── logs/ # ログ保存ディレクトリ(.envで変更可能) ├── auth.log # 認証エラーのログ ├── error.log # エラーログ ├── request.log # リクエストのログ - └── response.log # レスポンスのログ + ├── response.log # レスポンスのログ + └── validation-error.log # OpenAPI スキーマバリデーションエラーログ ``` ## 使い方 @@ -189,6 +197,96 @@ CREDENCIAL= # 資格情報(ユーザー単位等の単体認証用 ``` ※ API_KEYとCREDENCIALオプションは本プロジェクトでは簡易的な実装となっており、指定時はリクエストのAuthorizationヘッダからBearerトークンを取得して認証処理が行われます。 +## OpenAPI スキーマ自動生成機能 +バージョン1.2以降にて `responses/{エンドポイントパス}/{メソッド}/{ステータス名}.json` に配置されたJSONレスポンスファイル群から OpenAPI 3.0 スキーマを自動生成する機能が追加されました。 + +### 実行方法 +**CLI(コマンドライン)** +```bash +php generate-schema.php [format] [title] [version] +``` + +- `format` (任意):出力フォーマット(`json` または `yaml`)デフォルト:`yaml` +- `title` (任意): OpenAPI のタイトル +- `version` (任意): OpenAPI のバージョン + +例: +```bash +php generate-schema.php yaml "My Awesome API" "2.0.0" +``` + +**Web(ブラウザ経由)** +```http +GET /generate-schema.php?format=json&title=My+Awesome+API&version=2.0.0 +``` + +### 出力ファイル +`schema/openapi.yaml` または `schema/openapi.json` にスキーマが生成されます。 + +### バリデーション +各 JSON レスポンスファイルは、独自に生成されたスキーマに対して JSON Schema バリデーションを実施します。 +不正な構造のレスポンスがあれば `logs/validation-error.log` にエラーメッセージを出力し、処理を中断します。 + +### example 自動登録 +生成される OpenAPI スキーマには、example として元となった JSON の内容が登録されます。 + +- 配列の場合、最初の要素のみ が example に含まれます(肥大化回避のため)。 +- ネストされた配列・オブジェクトにも再帰的に適用されます。 + +### 環境変数(.env) +`.env` に以下の環境変数を定義することで、デフォルト動作をカスタマイズできます。 +```env +LOG_DIR=./logs +SCHEMA_DIR=./schema +SCHEMA_FORMAT=yaml +SCHEMA_TITLE=MockAPI-PHP Auto Schema +SCHEMA_VERSION=1.0.0 +``` + +※ 環境変数で定義された `SCHEMA_FORMAT` `SCHEMA_TITLE` `SCHEMA_VERSION` よりもパラメータで指定した値が優先されます。 + +### スキーマ自動生成の活用シーン + +- 手動で OpenAPI スキーマを書く手間を省きたい場合 +- 実装より先にモックを作る「APIファースト」な開発方針に沿うケース +- 他ツール(Prism, SwaggerUI など)との連携に使いたい場合 +- 自動テストとの連携でスキーマ整合性を確認したいとき + +### CI/CD での自動スキーマ生成例(GitHub Actions) + +`.github/workflows/generate-schema.yml` + +```yaml +name: Generate OpenAPI Schema + +on: + push: + paths: + - 'responses/**' + - 'generate-schema.php' + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + - run: composer install --no-dev + - run: php generate-schema.php json + - name: Upload schema + uses: actions/upload-artifact@v3 + with: + name: openapi-schema + path: schema/openapi.json +``` + +これにより、レスポンス変更時に常に最新のスキーマが生成・保存されるようになります。 + + + ## Tips ### カスタムレスポンス @@ -379,24 +477,27 @@ php vender/bin/phpunit |------|------------------|------------------|------------------------------|---------------|------------------------| | **使用言語** | PHP | Node.js | JavaScript | Java | Node.js | | **インストール難易度** | ★☆☆(簡単) | ★☆☆(簡単) | ★★☆(環境依存) | ★★★(重い) | ★★☆ | -| **レスポンス定義方法** | PHPコード(動的) | JSONファイル(静的) | JavaScriptで定義 | JSON or Java設定 | OpenAPI仕様ベース | -| **ルーティング制御** | 柔軟(PHPで自由) | パスパターン定義 | `rest.get()`等で定義 | URLパターンマッチ | OpenAPI準拠 | -| **動的レスポンス生成** | ✅ PHPロジックで任意対応 | △ 限定的 | ✅ JavaScript可 | ✅ スクリプトで可 | △ 難しい | +| **レスポンス定義方法** | PHPコード+JSONファイル | JSONファイル(静的) | JavaScriptで定義 | JSON or Java設定 | OpenAPI仕様ベース | +| **ルーティング制御** | ✅ 柔軟(PHPで自由) | △ パスパターン定義 | ✅ `rest.get()`等で定義 | ✅ URLパターンマッチ | ✅ OpenAPI準拠 | +| **動的レスポンス生成** | ✅ PHPロジックで自由 | △ 限定的 | ✅ JavaScript可 | ✅ スクリプトで可 | △ 難しい | | **クエリ/パラメータ分岐** | ✅ 任意に処理可 | △ 限定的 | ✅ 完全対応 | ✅ 完全対応 | △ スキーマ駆動 | | **ヘッダー/メソッド制御** | ✅ 完全対応 | △ 限定対応 | ✅ 完全対応 | ✅ 完全対応 | ✅ | | **エラーや認証の再現** | ✅ 自由自在に処理記述 | ✕ 難しい | ✅ ロジックで対応可 | ✅ 詳細制御可 | △ 条件分岐は難 | | **応答遅延・タイミング制御** | ✅ `sleep()`などで柔軟対応 | ✕ | ✅ `setTimeout()`等で対応可 | ✅ `fixedDelay`など | △ 難しい | -| **OpenAPI連携** | ✕(手動対応) | ✕ | ✕ | △ | ✅ 主目的 | -| **開発対象との相性** | ✅ PHPプロジェクトに最適 | Node.js系と相性良 | フロント専用(Vue/Reactなど) | Javaプロジェクト向け | OpenAPI中心の組織向け | +| **OpenAPI連携** | ✅ 自動生成機能あり | ✕ | ✕ | △ エクスポート可能 | ✅ 主目的 | +| **スキーマ自動生成** | ✅ JSONレスポンスから生成 | ✕ | ✕ | △ (変換ツールあり) | ✅ | +| **example 自動埋込** | ✅ 自動(大きな配列は1件のみ) | ✕ | ✕ | ✕ | ✅ | +| **開発対象との相性** | PHPプロジェクトに最適 | Node.js系と相性良 | フロント専用(Vue/Reactなど) | Javaプロジェクト向け | OpenAPI中心の組織向け | | **学習コスト** | ★☆☆(PHP経験者には低い) | ★☆☆ | ★★☆ | ★★★ | ★★☆ | -| **ログ/トラッキング機能** | ✅ 実装可能 | ✕ | ✅(開発ツール) | ✅ 詳細ログあり | △ | -| **用途の柔軟性** | ◎(自由度が高い) | ○(簡単なAPIモックに最適) | △(UI開発特化) | ○(高機能) | △(制約あり) | +| **ログ/トラッキング機能** | ✅ ログ実装可(バリデーション含む) | ✕ | ✅(DevToolsで可) | ✅ 詳細ログあり | △ | +| **用途の柔軟性** | ◎(コードで完全制御・自由度が高い) | ○(簡単なAPIモックに最適) | △(UI開発特化) | ○(高機能) | △(制約あり) | ### 特徴まとめ - **MockAPI-PHPは、動的ロジックを含むモックレスポンスをPHPで直接定義可能** です。 - 特に PHPプロジェクトとの親和性が高く、バックエンド未完成時の開発やAPI分離開発において柔軟に対応可能です。 - OpenAPIベースやGUIツールとは異なり、コード駆動でのモックAPI開発に最適化されています。 +- JSONレスポンスの構造を元に OpenAPI 3.0 スキーマを自動生成する機能を搭載しています(v1.2以降)。 ## ライセンス diff --git a/composer.json b/composer.json index c79bbe1..5d1ec49 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,8 @@ { "require": { - "vlucas/phpdotenv": "^5.6" + "vlucas/phpdotenv": "^5.6", + "symfony/yaml": "^7.2", + "opis/json-schema": "^2.4" }, "require-dev": { "phpunit/phpunit": "^12.0", diff --git a/composer.lock b/composer.lock index 55dd932..8b48874 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8e8e0d5d620e0e9f3b9f4a3234d96e2a", + "content-hash": "1c8b2933cb387b269a887b35628ed86f", "packages": [ { "name": "graham-campbell/result-type", @@ -68,6 +68,196 @@ ], "time": "2024-07-20T21:45:45+00:00" }, + { + "name": "opis/json-schema", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/opis/json-schema.git", + "reference": "712827751c62b465daae6e725bf0cf5ffbf965e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/json-schema/zipball/712827751c62b465daae6e725bf0cf5ffbf965e1", + "reference": "712827751c62b465daae6e725bf0cf5ffbf965e1", + "shasum": "" + }, + "require": { + "ext-json": "*", + "opis/string": "^2.0", + "opis/uri": "^1.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ext-bcmath": "*", + "ext-intl": "*", + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\JsonSchema\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + }, + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + } + ], + "description": "Json Schema Validator for PHP", + "homepage": "https://opis.io/json-schema", + "keywords": [ + "json", + "json-schema", + "schema", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/opis/json-schema/issues", + "source": "https://github.com/opis/json-schema/tree/2.4.1" + }, + "time": "2024-12-30T20:20:21+00:00" + }, + { + "name": "opis/string", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/opis/string.git", + "reference": "ba0b9607b9809462b0e28a11e4881a8d77431feb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/string/zipball/ba0b9607b9809462b0e28a11e4881a8d77431feb", + "reference": "ba0b9607b9809462b0e28a11e4881a8d77431feb", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "ext-json": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\String\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "Multibyte strings as objects", + "homepage": "https://opis.io/string", + "keywords": [ + "multi-byte", + "opis", + "string", + "string manipulation", + "utf-8" + ], + "support": { + "issues": "https://github.com/opis/string/issues", + "source": "https://github.com/opis/string/tree/2.0.2" + }, + "time": "2024-12-30T21:43:22+00:00" + }, + { + "name": "opis/uri", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/opis/uri.git", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/uri/zipball/0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "shasum": "" + }, + "require": { + "opis/string": "^2.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\Uri\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "Build, parse and validate URIs and URI-templates", + "homepage": "https://opis.io", + "keywords": [ + "URI Template", + "parse url", + "punycode", + "uri", + "uri components", + "url", + "validate uri" + ], + "support": { + "issues": "https://github.com/opis/uri/issues", + "source": "https://github.com/opis/uri/tree/1.1.0" + }, + "time": "2021-05-22T15:57:08+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.3", @@ -143,6 +333,73 @@ ], "time": "2024-07-20T21:41:07+00:00" }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.5-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.31.0", @@ -382,6 +639,78 @@ ], "time": "2024-09-09T11:45:10+00:00" }, + { + "name": "symfony/yaml", + "version": "v7.2.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912", + "reference": "4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.2.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-03-03T07:12:39+00:00" + }, { "name": "vlucas/phpdotenv", "version": "v5.6.1", diff --git a/generate-schema.php b/generate-schema.php new file mode 100644 index 0000000..10ecc01 --- /dev/null +++ b/generate-schema.php @@ -0,0 +1,332 @@ +load(); +} + +$LOG_DIR = isset($_ENV['LOG_DIR']) ? trim($_ENV['LOG_DIR']) : __DIR__ . '/logs'; +$SCHEMA_DIR = isset($_ENV['SCHEMA_DIR']) ? trim($_ENV['SCHEMA_DIR']) : __DIR__ . '/schema'; +$SCHEMA_FORMAT = isset($_ENV['SCHEMA_FORMAT']) ? strtolower(trim($_ENV['SCHEMA_FORMAT'])) : 'yaml'; +$SCHEMA_TITLE = isset($_ENV['SCHEMA_TITLE']) ? trim($_ENV['SCHEMA_TITLE']) : 'MockAPI-PHP Auto Schema'; +$SCHEMA_VERSION = isset($_ENV['SCHEMA_VERSION']) ? trim($_ENV['SCHEMA_VERSION']) : '1.0.0'; + +// Create log directory if it doesn't exist +if (!is_dir($LOG_DIR)) { + mkdir($LOG_DIR, 0777, true); +} + +// ----------------------------------------------------------------------------- + +function getOptions(): array { + global $SCHEMA_FORMAT, $SCHEMA_TITLE, $SCHEMA_VERSION; + + $defaultOptions = [ + 'format' => $SCHEMA_FORMAT, + 'title' => $SCHEMA_TITLE, + 'version' => $SCHEMA_VERSION, + ]; + + if (PHP_SAPI === 'cli') { + global $argv; + $format = strtolower($argv[1] ?? $defaultOptions['format']); + if (!in_array($format, ['json', 'yaml'], true)) { + echo "Unsupported format: $format. Defaulting to 'yaml'.\n"; + $format = 'yaml'; + } + return [ + 'format' => $format, + 'title' => $argv[2] ?? $defaultOptions['title'], + 'version' => $argv[3] ?? $defaultOptions['version'], + ]; + } else { + $format = filter_input(INPUT_GET, 'format', FILTER_SANITIZE_SPECIAL_CHARS) ?: $defaultOptions['format']; + $title = filter_input(INPUT_GET, 'title', FILTER_SANITIZE_SPECIAL_CHARS) ?: $defaultOptions['title']; + $version = filter_input(INPUT_GET, 'version', FILTER_SANITIZE_SPECIAL_CHARS) ?: $defaultOptions['version']; + + return [ + 'format' => strtolower($format) === 'json' ? 'json' : 'yaml', + 'title' => $title, + 'version' => $version, + ]; + } +} + +function mapTypes(array $types): string { + $normalized = array_map(fn($t) => match ($t) { + 'integer' => 'integer', + 'double' => 'number', + 'string' => 'string', + 'boolean' => 'boolean', + default => 'string', + }, $types); + + $unique = array_values(array_filter(array_unique($normalized), fn($v) => is_string($v))); + + // In case of integer + number, unify to number + if (in_array('integer', $unique, true) && in_array('number', $unique, true)) { + $unique = array_values(array_diff($unique, ['integer'])); + } + + if (count($unique) === 1) { + return $unique[0]; + } elseif (count($unique) === 0) { + // fallback: nothing found, default to 'string' + return 'string'; + } else { + // multiple types found, default to 'string' + return 'string'; + } +} + +function getJsonSchemaType(mixed $value): string { + return match (gettype($value)) { + 'integer' => 'integer', + 'double' => 'number', + 'string' => 'string', + 'boolean' => 'boolean', + 'NULL' => 'null', + default => 'string', + }; +} + +function jsonToSchema(mixed $data): array { + $type = gettype($data); + + switch ($type) { + case 'array': + // Array: Branches between one-dimensional scalar array and object array + if (array_keys($data) === range(0, count($data) - 1)) { + if (count($data) === 0) { + return [ + 'type' => 'array', + 'items' => ['type' => 'string'] // fallback + ]; + } + + $firstItem = $data[0]; + + if (is_scalar($firstItem)) { + // Scalar array: + $types = array_map('gettype', $data); + return [ + 'type' => 'array', + 'items' => ['type' => mapTypes($types)] + ]; + } elseif (is_array($firstItem)) { + // Object array: merge all elements into one + $merged = []; + foreach ($data as $item) { + if (is_array($item)) { + $merged = array_merge($merged, $item); + } + } + return [ + 'type' => 'array', + 'items' => jsonToSchema($merged) + ]; + } else { + // Others (including null) + return [ + 'type' => 'array', + 'items' => ['type' => getJsonSchemaType($firstItem)] + ]; + } + } else { + // Associative array: object and interpretations + return jsonToSchema((object) $data); + } + + case 'object': + $properties = []; + foreach (get_object_vars($data) as $key => $value) { + $properties[$key] = jsonToSchema($value); + } + return [ + 'type' => 'object', + 'properties' => $properties + ]; + + case 'boolean': + case 'integer': + case 'double': + case 'string': + return ['type' => getJsonSchemaType($data)]; + + case 'NULL': + return ['type' => 'null']; + + default: + return ['type' => 'string']; // fallback + } +} + +function validateJson(array $data): void { + global $LOG_DIR; + $validator = new Validator(new SchemaLoader()); + $validator->setMaxErrors(10); + $validator->setStopAtFirstError(false); + + $schema = Helper::toJson(jsonToSchema($data)); + $converted = Helper::toJson($data); + + /** @var ValidationResult $result */ + $result = $validator->validate($converted, $schema); + if (!$result->isValid()) { + if ($result->hasError()) { + $error = $result->error(); + $log = "Validation error: {$error->message()}\n"; + $log .= json_encode((new ErrorFormatter())->format($error), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + } else { + $log = "Validation error: Unknown error\n"; + } + file_put_contents("$LOG_DIR/validation-error.log", $log); + echo "Validation error occurred. Please see schema/validation-error.log for details.\n"; + if (PHP_SAPI !== 'cli') { + http_response_code(500); + } + exit(1); + } +} + +function scanResponses(string $baseDir, string $title, string $version): array { + $openapi = [ + 'openapi' => '3.0.3', + 'info' => [ + 'title' => $title, + 'version' => $version + ], + 'paths' => [] + ]; + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($baseDir) + ); + + foreach ($iterator as $file) { + if (!$file->isFile() || $file->getExtension() !== 'json') continue; + + $relativePath = str_replace($baseDir . DIRECTORY_SEPARATOR, '', $file->getPathname()); + // Exclusion criteria: Exclude responses/errors below + if (str_starts_with($relativePath, 'errors' . DIRECTORY_SEPARATOR)) continue; + $parts = explode(DIRECTORY_SEPARATOR, $relativePath); + + if (count($parts) < 3) continue; + + $responseFile = array_pop($parts); + $method = array_pop($parts); + $endpointParts = array_map(fn($p) => '/' . $p, $parts); + $endpoint = implode('', $endpointParts); + $statusName = pathinfo($responseFile, PATHINFO_FILENAME) ?: 'default'; + + $raw = file_get_contents($file->getPathname()); + $json = json_decode($raw, true); + if ($json === null) { + echo "JSON Error: {$file->getPathname()} is invalid.\n"; + if (PHP_SAPI !== 'cli') { + http_response_code(500); + } + exit(1); + } + + // Validation (immediate interruption on error) + validateJson($json); + + $schema = jsonToSchema($json); + $openapi['paths'][$endpoint][strtolower($method)]['responses'][$statusName] = [ + 'description' => "Response from $responseFile", + 'content' => [ + 'application/json' => [ + 'schema' => $schema, + 'example' => trimExample($json), + ] + ] + ]; + } + + return $openapi; +} + +function trimExample(mixed $data): mixed { + if (is_array($data)) { + // In the case of an array (sequential index): Only the first item + if (array_keys($data) === range(0, count($data) - 1)) { + return isset($data[0]) ? [trimExample($data[0])] : []; + } + + // Recursive processing for associative arrays (object-like) + $result = []; + foreach ($data as $key => $value) { + $result[$key] = trimExample($value); + } + return $result; + } elseif (is_object($data)) { + // Recursion in stdClass + $result = new stdClass(); + foreach (get_object_vars($data) as $key => $value) { + $result->$key = trimExample($value); + } + return $result; + } + + // Scalars are left as is + return $data; +} + +function outputSchema(array $schema, string $format): void { + global $SCHEMA_DIR; + if (!is_dir($SCHEMA_DIR)) mkdir($SCHEMA_DIR, 0777, true); + + if ($format === 'json') { + $json = json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + file_put_contents("$SCHEMA_DIR/openapi.json", $json); + if (PHP_SAPI !== 'cli') { + header('Content-Type: application/json'); + echo $json; + } + } else { + $yaml = Yaml::dump($schema, 10, 2); + file_put_contents("$SCHEMA_DIR/openapi.yaml", $yaml); + if (PHP_SAPI !== 'cli') { + header('Content-Type: text/yaml'); + echo $yaml; + } + } + + if (PHP_SAPI === 'cli') { + echo "The OpenAPI schema has outputted to schema/ in the format specified by $format.\n"; + } +} + +// Execution +$options = getOptions(); +$responsesDir = __DIR__ . '/responses'; +$schema = scanResponses($responsesDir, $options['title'], $options['version']); +outputSchema($schema, $options['format']); diff --git a/http_status.php b/http_status.php index 23fc594..3ce9189 100644 --- a/http_status.php +++ b/http_status.php @@ -1,5 +1,17 @@ "HTTP/1.0 100 Continue", 101 => "HTTP/1.0 101 Switching Protocols", diff --git a/index.php b/index.php index 2ee6b50..78642ca 100644 --- a/index.php +++ b/index.php @@ -1,4 +1,17 @@ load(); @@ -12,6 +24,6 @@ $PORT = $_ENV['PORT'] ?? 3030; $HOST = 'localhost'; -// PHP 組み込みサーバーを `.env` の PORT で起動 +// Start the PHP built-in server on the PORT in `.env`. echo "Starting Mock API Server on http://$HOST:$PORT\n"; exec("php -S $HOST:$PORT -t ."); diff --git a/tests/MockApiTest.php b/tests/MockApiTest.php index ec0ba72..8f1d71e 100644 --- a/tests/MockApiTest.php +++ b/tests/MockApiTest.php @@ -1,5 +1,21 @@ assertStringContainsString('id', $responseLog, "Response log should contain user ID."); } + public function testOpenApiSchemaGenerationJsonShouldWork(): void + { + $scriptPath = realpath(__DIR__ . '/../generate-schema.php'); + $schemaPath = realpath(__DIR__ . '/../schema/openapi.json'); + $expectedExample = [ + 'id' => 3, + 'name' => 'Mike Born', + 'email' => 'mike@example.com', + ]; + + $this->assertNotFalse($scriptPath, 'Invalid path for generate-schema.php.'); + + // スキーマ出力ファイルがあれば一旦削除 + if (file_exists($schemaPath)) { + unlink($schemaPath); + } + + // Execution format: CLI (format: json) + $cmd = escapeshellcmd("php {$scriptPath} json"); + $output = shell_exec($cmd); + $this->assertFileExists($schemaPath, "Schema files are not generated: {$schemaPath}"); + + $schema = json_decode(file_get_contents($schemaPath), true, 512, JSON_THROW_ON_ERROR); + + // Check the basic structure of your OpenAPI schema + $this->assertArrayHasKey('paths', $schema, "The JSON schema does not contain paths."); + $this->assertArrayHasKey('/users', $schema['paths'], "The JSON schema does not contain /users path."); + $this->assertArrayHasKey('get', $schema['paths']['/users'], "The JSON schema does not contain GET method."); + + $response = $schema['paths']['/users']['get']['responses']['default'] ?? null; + $this->assertNotNull($response, "No Response as HTTP 200 code."); + + $example = $schema['paths']['/users']['get']['responses']['default']['content']['application/json']['example'] ?? null; + $this->assertNotNull($example, "The example field is not included."); + + // Verify that the contents of default.json match + $this->assertEquals($expectedExample, $example, "The content of example does not match default.json."); + } + + public function testOpenApiSchemaGenerationYamlShouldWork(): void + { + $scriptPath = realpath(__DIR__ . '/../generate-schema.php'); + $yamlPath = realpath(__DIR__ . '/../schema') . '/openapi.yaml'; + $expectedExample = [ + 'id' => 3, + 'name' => 'Mike Born', + 'email' => 'mike@example.com', + ]; + + $this->assertNotFalse($scriptPath, 'Invalid path for generate-schema.php.'); + + if (file_exists($yamlPath)) { + unlink($yamlPath); + } + + // Generate a schema in yaml format + $cmd = escapeshellcmd("php {$scriptPath} yaml"); + $output = shell_exec($cmd); + $this->assertFileExists($yamlPath, "YAML schema file was not generated: {$yamlPath}"); + + // Convert yaml to array and validate + $yaml = file_get_contents($yamlPath); + $this->assertNotFalse($yaml, "Failed to read openapi.yaml."); + + if (!class_exists(\Symfony\Component\Yaml\Yaml::class)) { + $this->markTestSkipped('symfony/yaml is not installed.'); + } + + $schema = \Symfony\Component\Yaml\Yaml::parse($yaml); + + // Check the basic structure of your OpenAPI schema + $this->assertArrayHasKey('paths', $schema, "The yaml schema does not contain paths."); + $this->assertArrayHasKey('/users', $schema['paths'], "The yaml schema does not contain /users path."); + $this->assertArrayHasKey('get', $schema['paths']['/users'], "The yaml schema does not contain GET method."); + + $example = $schema['paths']['/users']['get']['responses']['default']['content']['application/json']['example'] ?? null; + $this->assertNotNull($example, "The example field is not included."); + + // Verify that the contents of default.json match + $this->assertEquals($expectedExample, $example, "The content of example does not match default.json."); + } + /** * @param array $data */ diff --git a/version.json b/version.json index 777d8d4..8403ad3 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "version": "1.1.0", - "release_date": "2025-03-17" + "version": "1.2.0", + "release_date": "2025-04-01" } \ No newline at end of file From b81c6368efa8ca82a93339641cb01749837b4017 Mon Sep 17 00:00:00 2001 From: Katsuhiko Maeno Date: Tue, 1 Apr 2025 01:47:22 +0900 Subject: [PATCH 2/9] docs: update README for 1.2.0 --- README_JP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_JP.md b/README_JP.md index de65b9b..4395328 100644 --- a/README_JP.md +++ b/README_JP.md @@ -1,4 +1,4 @@ -# MockAPI-PHP ── 日本語ドキュメント +# MockAPI-PHP このプロジェクトは、PHP製の軽量なモックAPIサーバーです。 開発・テスト環境で実際のAPIを使用せずに、リクエストのシミュレーションが可能です。 From b52fba841437f99522dfcef938b714552afeac9e Mon Sep 17 00:00:00 2001 From: Katsuhiko Maeno Date: Tue, 1 Apr 2025 01:57:02 +0900 Subject: [PATCH 3/9] ci: insert job for verify schema file exists --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 846a287..2c25b50 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,10 @@ jobs: - name: Automatic generation of OpenAPI schema run: php generate-schema.php yaml + - name: Verify schema file exists + run: ls -la schema/openapi.yaml + continue-on-error: true + - name: Upload schema uses: actions/upload-artifact@v3 with: From a08f6c10987b08647972844c43839898b28b2491 Mon Sep 17 00:00:00 2001 From: Katsuhiko Maeno Date: Tue, 1 Apr 2025 02:07:38 +0900 Subject: [PATCH 4/9] ci: update ci settings --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c25b50..d1a6f4b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,7 @@ jobs: continue-on-error: true - name: Upload schema + if: success() uses: actions/upload-artifact@v3 with: name: openapi-schema From 3dabbbee3cbfb7bf609a25d06bb4630dc83a9847 Mon Sep 17 00:00:00 2001 From: Katsuhiko Maeno Date: Tue, 1 Apr 2025 02:14:38 +0900 Subject: [PATCH 5/9] ci: fix upload schema job --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1a6f4b..e924d77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,10 +45,9 @@ jobs: - name: Verify schema file exists run: ls -la schema/openapi.yaml - continue-on-error: true - name: Upload schema - if: success() + if: hashFiles('schema/openapi.yaml') != '' uses: actions/upload-artifact@v3 with: name: openapi-schema From 2c133421a791ae21de255e07f807ace27f98f3b7 Mon Sep 17 00:00:00 2001 From: Katsuhiko Maeno Date: Tue, 1 Apr 2025 02:26:56 +0900 Subject: [PATCH 6/9] ci: update to actions/upload-artifact@v3.1.2 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e924d77..403ce7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: - name: Upload schema if: hashFiles('schema/openapi.yaml') != '' - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v3.1.2 with: name: openapi-schema path: schema/openapi.yaml From 969f5b69ac14448d87be88010ab6d0a5c9275e2d Mon Sep 17 00:00:00 2001 From: Katsuhiko Maeno Date: Tue, 1 Apr 2025 02:32:36 +0900 Subject: [PATCH 7/9] ci: update to actions/upload-artifact@v4 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 403ce7d..8b39429 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,7 @@ jobs: - name: Upload schema if: hashFiles('schema/openapi.yaml') != '' - uses: actions/upload-artifact@v3.1.2 + uses: actions/upload-artifact@v4 with: name: openapi-schema path: schema/openapi.yaml From 87bede08c85b253a27b629280bb4bd9a8d8bab58 Mon Sep 17 00:00:00 2001 From: Katsuhiko Maeno Date: Tue, 1 Apr 2025 02:52:16 +0900 Subject: [PATCH 8/9] fix: adjust code by phpcs --- generate-schema.php | 681 ++++++++++++++++++++++-------------------- http_status.php | 2 + index.php | 1 + start_server.php | 2 + tests/MockApiTest.php | 36 ++- 5 files changed, 374 insertions(+), 348 deletions(-) diff --git a/generate-schema.php b/generate-schema.php index 10ecc01..678aba0 100644 --- a/generate-schema.php +++ b/generate-schema.php @@ -1,332 +1,349 @@ -load(); -} - -$LOG_DIR = isset($_ENV['LOG_DIR']) ? trim($_ENV['LOG_DIR']) : __DIR__ . '/logs'; -$SCHEMA_DIR = isset($_ENV['SCHEMA_DIR']) ? trim($_ENV['SCHEMA_DIR']) : __DIR__ . '/schema'; -$SCHEMA_FORMAT = isset($_ENV['SCHEMA_FORMAT']) ? strtolower(trim($_ENV['SCHEMA_FORMAT'])) : 'yaml'; -$SCHEMA_TITLE = isset($_ENV['SCHEMA_TITLE']) ? trim($_ENV['SCHEMA_TITLE']) : 'MockAPI-PHP Auto Schema'; -$SCHEMA_VERSION = isset($_ENV['SCHEMA_VERSION']) ? trim($_ENV['SCHEMA_VERSION']) : '1.0.0'; - -// Create log directory if it doesn't exist -if (!is_dir($LOG_DIR)) { - mkdir($LOG_DIR, 0777, true); -} - -// ----------------------------------------------------------------------------- - -function getOptions(): array { - global $SCHEMA_FORMAT, $SCHEMA_TITLE, $SCHEMA_VERSION; - - $defaultOptions = [ - 'format' => $SCHEMA_FORMAT, - 'title' => $SCHEMA_TITLE, - 'version' => $SCHEMA_VERSION, - ]; - - if (PHP_SAPI === 'cli') { - global $argv; - $format = strtolower($argv[1] ?? $defaultOptions['format']); - if (!in_array($format, ['json', 'yaml'], true)) { - echo "Unsupported format: $format. Defaulting to 'yaml'.\n"; - $format = 'yaml'; - } - return [ - 'format' => $format, - 'title' => $argv[2] ?? $defaultOptions['title'], - 'version' => $argv[3] ?? $defaultOptions['version'], - ]; - } else { - $format = filter_input(INPUT_GET, 'format', FILTER_SANITIZE_SPECIAL_CHARS) ?: $defaultOptions['format']; - $title = filter_input(INPUT_GET, 'title', FILTER_SANITIZE_SPECIAL_CHARS) ?: $defaultOptions['title']; - $version = filter_input(INPUT_GET, 'version', FILTER_SANITIZE_SPECIAL_CHARS) ?: $defaultOptions['version']; - - return [ - 'format' => strtolower($format) === 'json' ? 'json' : 'yaml', - 'title' => $title, - 'version' => $version, - ]; - } -} - -function mapTypes(array $types): string { - $normalized = array_map(fn($t) => match ($t) { - 'integer' => 'integer', - 'double' => 'number', - 'string' => 'string', - 'boolean' => 'boolean', - default => 'string', - }, $types); - - $unique = array_values(array_filter(array_unique($normalized), fn($v) => is_string($v))); - - // In case of integer + number, unify to number - if (in_array('integer', $unique, true) && in_array('number', $unique, true)) { - $unique = array_values(array_diff($unique, ['integer'])); - } - - if (count($unique) === 1) { - return $unique[0]; - } elseif (count($unique) === 0) { - // fallback: nothing found, default to 'string' - return 'string'; - } else { - // multiple types found, default to 'string' - return 'string'; - } -} - -function getJsonSchemaType(mixed $value): string { - return match (gettype($value)) { - 'integer' => 'integer', - 'double' => 'number', - 'string' => 'string', - 'boolean' => 'boolean', - 'NULL' => 'null', - default => 'string', - }; -} - -function jsonToSchema(mixed $data): array { - $type = gettype($data); - - switch ($type) { - case 'array': - // Array: Branches between one-dimensional scalar array and object array - if (array_keys($data) === range(0, count($data) - 1)) { - if (count($data) === 0) { - return [ - 'type' => 'array', - 'items' => ['type' => 'string'] // fallback - ]; - } - - $firstItem = $data[0]; - - if (is_scalar($firstItem)) { - // Scalar array: - $types = array_map('gettype', $data); - return [ - 'type' => 'array', - 'items' => ['type' => mapTypes($types)] - ]; - } elseif (is_array($firstItem)) { - // Object array: merge all elements into one - $merged = []; - foreach ($data as $item) { - if (is_array($item)) { - $merged = array_merge($merged, $item); - } - } - return [ - 'type' => 'array', - 'items' => jsonToSchema($merged) - ]; - } else { - // Others (including null) - return [ - 'type' => 'array', - 'items' => ['type' => getJsonSchemaType($firstItem)] - ]; - } - } else { - // Associative array: object and interpretations - return jsonToSchema((object) $data); - } - - case 'object': - $properties = []; - foreach (get_object_vars($data) as $key => $value) { - $properties[$key] = jsonToSchema($value); - } - return [ - 'type' => 'object', - 'properties' => $properties - ]; - - case 'boolean': - case 'integer': - case 'double': - case 'string': - return ['type' => getJsonSchemaType($data)]; - - case 'NULL': - return ['type' => 'null']; - - default: - return ['type' => 'string']; // fallback - } -} - -function validateJson(array $data): void { - global $LOG_DIR; - $validator = new Validator(new SchemaLoader()); - $validator->setMaxErrors(10); - $validator->setStopAtFirstError(false); - - $schema = Helper::toJson(jsonToSchema($data)); - $converted = Helper::toJson($data); - - /** @var ValidationResult $result */ - $result = $validator->validate($converted, $schema); - if (!$result->isValid()) { - if ($result->hasError()) { - $error = $result->error(); - $log = "Validation error: {$error->message()}\n"; - $log .= json_encode((new ErrorFormatter())->format($error), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; - } else { - $log = "Validation error: Unknown error\n"; - } - file_put_contents("$LOG_DIR/validation-error.log", $log); - echo "Validation error occurred. Please see schema/validation-error.log for details.\n"; - if (PHP_SAPI !== 'cli') { - http_response_code(500); - } - exit(1); - } -} - -function scanResponses(string $baseDir, string $title, string $version): array { - $openapi = [ - 'openapi' => '3.0.3', - 'info' => [ - 'title' => $title, - 'version' => $version - ], - 'paths' => [] - ]; - - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($baseDir) - ); - - foreach ($iterator as $file) { - if (!$file->isFile() || $file->getExtension() !== 'json') continue; - - $relativePath = str_replace($baseDir . DIRECTORY_SEPARATOR, '', $file->getPathname()); - // Exclusion criteria: Exclude responses/errors below - if (str_starts_with($relativePath, 'errors' . DIRECTORY_SEPARATOR)) continue; - $parts = explode(DIRECTORY_SEPARATOR, $relativePath); - - if (count($parts) < 3) continue; - - $responseFile = array_pop($parts); - $method = array_pop($parts); - $endpointParts = array_map(fn($p) => '/' . $p, $parts); - $endpoint = implode('', $endpointParts); - $statusName = pathinfo($responseFile, PATHINFO_FILENAME) ?: 'default'; - - $raw = file_get_contents($file->getPathname()); - $json = json_decode($raw, true); - if ($json === null) { - echo "JSON Error: {$file->getPathname()} is invalid.\n"; - if (PHP_SAPI !== 'cli') { - http_response_code(500); - } - exit(1); - } - - // Validation (immediate interruption on error) - validateJson($json); - - $schema = jsonToSchema($json); - $openapi['paths'][$endpoint][strtolower($method)]['responses'][$statusName] = [ - 'description' => "Response from $responseFile", - 'content' => [ - 'application/json' => [ - 'schema' => $schema, - 'example' => trimExample($json), - ] - ] - ]; - } - - return $openapi; -} - -function trimExample(mixed $data): mixed { - if (is_array($data)) { - // In the case of an array (sequential index): Only the first item - if (array_keys($data) === range(0, count($data) - 1)) { - return isset($data[0]) ? [trimExample($data[0])] : []; - } - - // Recursive processing for associative arrays (object-like) - $result = []; - foreach ($data as $key => $value) { - $result[$key] = trimExample($value); - } - return $result; - } elseif (is_object($data)) { - // Recursion in stdClass - $result = new stdClass(); - foreach (get_object_vars($data) as $key => $value) { - $result->$key = trimExample($value); - } - return $result; - } - - // Scalars are left as is - return $data; -} - -function outputSchema(array $schema, string $format): void { - global $SCHEMA_DIR; - if (!is_dir($SCHEMA_DIR)) mkdir($SCHEMA_DIR, 0777, true); - - if ($format === 'json') { - $json = json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - file_put_contents("$SCHEMA_DIR/openapi.json", $json); - if (PHP_SAPI !== 'cli') { - header('Content-Type: application/json'); - echo $json; - } - } else { - $yaml = Yaml::dump($schema, 10, 2); - file_put_contents("$SCHEMA_DIR/openapi.yaml", $yaml); - if (PHP_SAPI !== 'cli') { - header('Content-Type: text/yaml'); - echo $yaml; - } - } - - if (PHP_SAPI === 'cli') { - echo "The OpenAPI schema has outputted to schema/ in the format specified by $format.\n"; - } -} - -// Execution -$options = getOptions(); -$responsesDir = __DIR__ . '/responses'; -$schema = scanResponses($responsesDir, $options['title'], $options['version']); -outputSchema($schema, $options['format']); +load(); +} + +$LOG_DIR = isset($_ENV['LOG_DIR']) ? trim($_ENV['LOG_DIR']) : __DIR__ . '/logs'; +$SCHEMA_DIR = isset($_ENV['SCHEMA_DIR']) ? trim($_ENV['SCHEMA_DIR']) : __DIR__ . '/schema'; +$SCHEMA_FORMAT = isset($_ENV['SCHEMA_FORMAT']) ? strtolower(trim($_ENV['SCHEMA_FORMAT'])) : 'yaml'; +$SCHEMA_TITLE = isset($_ENV['SCHEMA_TITLE']) ? trim($_ENV['SCHEMA_TITLE']) : 'MockAPI-PHP Auto Schema'; +$SCHEMA_VERSION = isset($_ENV['SCHEMA_VERSION']) ? trim($_ENV['SCHEMA_VERSION']) : '1.0.0'; + +// Create log directory if it doesn't exist +if (!is_dir($LOG_DIR)) { + mkdir($LOG_DIR, 0777, true); +} + +// ----------------------------------------------------------------------------- + +function getOptions(): array +{ + global $SCHEMA_FORMAT, $SCHEMA_TITLE, $SCHEMA_VERSION; + + $defaultOptions = [ + 'format' => $SCHEMA_FORMAT, + 'title' => $SCHEMA_TITLE, + 'version' => $SCHEMA_VERSION, + ]; + + if (PHP_SAPI === 'cli') { + global $argv; + $format = strtolower($argv[1] ?? $defaultOptions['format']); + if (!in_array($format, ['json', 'yaml'], true)) { + echo "Unsupported format: $format. Defaulting to 'yaml'.\n"; + $format = 'yaml'; + } + return [ + 'format' => $format, + 'title' => $argv[2] ?? $defaultOptions['title'], + 'version' => $argv[3] ?? $defaultOptions['version'], + ]; + } else { + $format = filter_input(INPUT_GET, 'format', FILTER_SANITIZE_SPECIAL_CHARS) ?: $defaultOptions['format']; + $title = filter_input(INPUT_GET, 'title', FILTER_SANITIZE_SPECIAL_CHARS) ?: $defaultOptions['title']; + $version = filter_input(INPUT_GET, 'version', FILTER_SANITIZE_SPECIAL_CHARS) ?: $defaultOptions['version']; + + return [ + 'format' => strtolower($format) === 'json' ? 'json' : 'yaml', + 'title' => $title, + 'version' => $version, + ]; + } +} + +function mapTypes(array $types): string +{ + $normalized = array_map(fn($t) => match ($t) { + 'integer' => 'integer', + 'double' => 'number', + 'string' => 'string', + 'boolean' => 'boolean', + default => 'string', + }, $types); + + $unique = array_values(array_filter(array_unique($normalized), fn($v) => is_string($v))); + + // In case of integer + number, unify to number + if (in_array('integer', $unique, true) && in_array('number', $unique, true)) { + $unique = array_values(array_diff($unique, ['integer'])); + } + + if (count($unique) === 1) { + return $unique[0]; + } elseif (count($unique) === 0) { + // fallback: nothing found, default to 'string' + return 'string'; + } else { + // multiple types found, default to 'string' + return 'string'; + } +} + +function getJsonSchemaType(mixed $value): string +{ + return match (gettype($value)) { + 'integer' => 'integer', + 'double' => 'number', + 'string' => 'string', + 'boolean' => 'boolean', + 'NULL' => 'null', + default => 'string', + }; +} + +function jsonToSchema(mixed $data): array +{ + $type = gettype($data); + + switch ($type) { + case 'array': + // Array: Branches between one-dimensional scalar array and object array + if (array_keys($data) === range(0, count($data) - 1)) { + if (count($data) === 0) { + return [ + 'type' => 'array', + 'items' => ['type' => 'string'] // fallback + ]; + } + + $firstItem = $data[0]; + + if (is_scalar($firstItem)) { + // Scalar array: + $types = array_map('gettype', $data); + return [ + 'type' => 'array', + 'items' => ['type' => mapTypes($types)] + ]; + } elseif (is_array($firstItem)) { + // Object array: merge all elements into one + $merged = []; + foreach ($data as $item) { + if (is_array($item)) { + $merged = array_merge($merged, $item); + } + } + return [ + 'type' => 'array', + 'items' => jsonToSchema($merged) + ]; + } else { + // Others (including null) + return [ + 'type' => 'array', + 'items' => ['type' => getJsonSchemaType($firstItem)] + ]; + } + } else { + // Associative array: object and interpretations + return jsonToSchema((object) $data); + } + + case 'object': + $properties = []; + foreach (get_object_vars($data) as $key => $value) { + $properties[$key] = jsonToSchema($value); + } + return [ + 'type' => 'object', + 'properties' => $properties + ]; + case 'boolean': + case 'integer': + case 'double': + case 'string': + return ['type' => getJsonSchemaType($data)]; + + case 'NULL': + return ['type' => 'null']; + + default: + return ['type' => 'string']; // fallback + } +} + +function validateJson(array $data): void +{ + global $LOG_DIR; + $validator = new Validator(new SchemaLoader()); + $validator->setMaxErrors(10); + $validator->setStopAtFirstError(false); + + $schema = Helper::toJson(jsonToSchema($data)); + $converted = Helper::toJson($data); + + /** @var ValidationResult $result */ + $result = $validator->validate($converted, $schema); + if (!$result->isValid()) { + if ($result->hasError()) { + $error = $result->error(); + $log = "Validation error: {$error->message()}\n"; + $log .= json_encode((new ErrorFormatter())->format($error), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; + } else { + $log = "Validation error: Unknown error\n"; + } + file_put_contents("$LOG_DIR/validation-error.log", $log); + echo "Validation error occurred. Please see schema/validation-error.log for details.\n"; + if (PHP_SAPI !== 'cli') { + http_response_code(500); + } + exit(1); + } +} + +function scanResponses(string $baseDir, string $title, string $version): array +{ + $openapi = [ + 'openapi' => '3.0.3', + 'info' => [ + 'title' => $title, + 'version' => $version + ], + 'paths' => [] + ]; + + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($baseDir) + ); + + foreach ($iterator as $file) { + if (!$file->isFile() || $file->getExtension() !== 'json') { + continue; + } + + $relativePath = str_replace($baseDir . DIRECTORY_SEPARATOR, '', $file->getPathname()); + // Exclusion criteria: Exclude responses/errors below + if (str_starts_with($relativePath, 'errors' . DIRECTORY_SEPARATOR)) { + continue; + } + $parts = explode(DIRECTORY_SEPARATOR, $relativePath); + + if (count($parts) < 3) { + continue; + } + + $responseFile = array_pop($parts); + $method = array_pop($parts); + $endpointParts = array_map(fn($p) => '/' . $p, $parts); + $endpoint = implode('', $endpointParts); + $statusName = pathinfo($responseFile, PATHINFO_FILENAME) ?: 'default'; + + $raw = file_get_contents($file->getPathname()); + $json = json_decode($raw, true); + if ($json === null) { + echo "JSON Error: {$file->getPathname()} is invalid.\n"; + if (PHP_SAPI !== 'cli') { + http_response_code(500); + } + exit(1); + } + + // Validation (immediate interruption on error) + validateJson($json); + + $schema = jsonToSchema($json); + $openapi['paths'][$endpoint][strtolower($method)]['responses'][$statusName] = [ + 'description' => "Response from $responseFile", + 'content' => [ + 'application/json' => [ + 'schema' => $schema, + 'example' => trimExample($json), + ] + ] + ]; + } + + return $openapi; +} + +function trimExample(mixed $data): mixed +{ + if (is_array($data)) { + // In the case of an array (sequential index): Only the first item + if (array_keys($data) === range(0, count($data) - 1)) { + return isset($data[0]) ? [trimExample($data[0])] : []; + } + + // Recursive processing for associative arrays (object-like) + $result = []; + foreach ($data as $key => $value) { + $result[$key] = trimExample($value); + } + return $result; + } elseif (is_object($data)) { + // Recursion in stdClass + $result = new stdClass(); + foreach (get_object_vars($data) as $key => $value) { + $result->$key = trimExample($value); + } + return $result; + } + + // Scalars are left as is + return $data; +} + +function outputSchema(array $schema, string $format): void +{ + global $SCHEMA_DIR; + if (!is_dir($SCHEMA_DIR)) { + mkdir($SCHEMA_DIR, 0777, true); + } + + if ($format === 'json') { + $json = json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + file_put_contents("$SCHEMA_DIR/openapi.json", $json); + if (PHP_SAPI !== 'cli') { + header('Content-Type: application/json'); + echo $json; + } + } else { + $yaml = Yaml::dump($schema, 10, 2); + file_put_contents("$SCHEMA_DIR/openapi.yaml", $yaml); + if (PHP_SAPI !== 'cli') { + header('Content-Type: text/yaml'); + echo $yaml; + } + } + + if (PHP_SAPI === 'cli') { + echo "The OpenAPI schema has outputted to schema/ in the format specified by $format.\n"; + } +} + +// Execution +$options = getOptions(); +$responsesDir = __DIR__ . '/responses'; +$schema = scanResponses($responsesDir, $options['title'], $options['version']); +outputSchema($schema, $options['format']); diff --git a/http_status.php b/http_status.php index 3ce9189..e603a5f 100644 --- a/http_status.php +++ b/http_status.php @@ -1,4 +1,5 @@ "HTTP/1.0 100 Continue", 101 => "HTTP/1.0 101 Switching Protocols", diff --git a/index.php b/index.php index 78642ca..edd1781 100644 --- a/index.php +++ b/index.php @@ -1,4 +1,5 @@ assertFileExists($schemaPath, "Schema files are not generated: {$schemaPath}"); - + $schema = json_decode(file_get_contents($schemaPath), true, 512, JSON_THROW_ON_ERROR); - + // Check the basic structure of your OpenAPI schema $this->assertArrayHasKey('paths', $schema, "The JSON schema does not contain paths."); $this->assertArrayHasKey('/users', $schema['paths'], "The JSON schema does not contain /users path."); $this->assertArrayHasKey('get', $schema['paths']['/users'], "The JSON schema does not contain GET method."); - + $response = $schema['paths']['/users']['get']['responses']['default'] ?? null; $this->assertNotNull($response, "No Response as HTTP 200 code."); - - $example = $schema['paths']['/users']['get']['responses']['default']['content']['application/json']['example'] ?? null; + + $example = $schema['paths']['/users']['get']['responses']['default']['content']['application/json']['example'] + ?? null; $this->assertNotNull($example, "The example field is not included."); - + // Verify that the contents of default.json match $this->assertEquals($expectedExample, $example, "The content of example does not match default.json."); } @@ -220,36 +223,37 @@ public function testOpenApiSchemaGenerationYamlShouldWork(): void 'name' => 'Mike Born', 'email' => 'mike@example.com', ]; - + $this->assertNotFalse($scriptPath, 'Invalid path for generate-schema.php.'); - + if (file_exists($yamlPath)) { unlink($yamlPath); } - + // Generate a schema in yaml format $cmd = escapeshellcmd("php {$scriptPath} yaml"); $output = shell_exec($cmd); $this->assertFileExists($yamlPath, "YAML schema file was not generated: {$yamlPath}"); - + // Convert yaml to array and validate $yaml = file_get_contents($yamlPath); $this->assertNotFalse($yaml, "Failed to read openapi.yaml."); - + if (!class_exists(\Symfony\Component\Yaml\Yaml::class)) { $this->markTestSkipped('symfony/yaml is not installed.'); } - + $schema = \Symfony\Component\Yaml\Yaml::parse($yaml); - + // Check the basic structure of your OpenAPI schema $this->assertArrayHasKey('paths', $schema, "The yaml schema does not contain paths."); $this->assertArrayHasKey('/users', $schema['paths'], "The yaml schema does not contain /users path."); $this->assertArrayHasKey('get', $schema['paths']['/users'], "The yaml schema does not contain GET method."); - - $example = $schema['paths']['/users']['get']['responses']['default']['content']['application/json']['example'] ?? null; + + $example = $schema['paths']['/users']['get']['responses']['default']['content']['application/json']['example'] + ?? null; $this->assertNotNull($example, "The example field is not included."); - + // Verify that the contents of default.json match $this->assertEquals($expectedExample, $example, "The content of example does not match default.json."); } From e7cef7ca3613033b06de0fb7afca88b448c249bc Mon Sep 17 00:00:00 2001 From: Katsuhiko Maeno Date: Tue, 1 Apr 2025 02:59:36 +0900 Subject: [PATCH 9/9] fix: update schema file path --- tests/MockApiTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/MockApiTest.php b/tests/MockApiTest.php index 31102f1..f5d7fc7 100644 --- a/tests/MockApiTest.php +++ b/tests/MockApiTest.php @@ -177,7 +177,7 @@ public function testRequestAndResponseShouldBeLogged(): void public function testOpenApiSchemaGenerationJsonShouldWork(): void { $scriptPath = realpath(__DIR__ . '/../generate-schema.php'); - $schemaPath = realpath(__DIR__ . '/../schema/openapi.json'); + $schemaPath = __DIR__ . '/../schema/openapi.json'; $expectedExample = [ 'id' => 3, 'name' => 'Mike Born', @@ -217,7 +217,7 @@ public function testOpenApiSchemaGenerationJsonShouldWork(): void public function testOpenApiSchemaGenerationYamlShouldWork(): void { $scriptPath = realpath(__DIR__ . '/../generate-schema.php'); - $yamlPath = realpath(__DIR__ . '/../schema') . '/openapi.yaml'; + $yamlPath = __DIR__ . '/../schema' . '/openapi.yaml'; $expectedExample = [ 'id' => 3, 'name' => 'Mike Born',