├── .editorconfig
├── .github
├── FUNDING.yml
└── workflows
│ ├── CS.yml
│ └── ci.yml
├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── config
└── api-postman.php
├── examples
├── 2021_02_04_151948_postman.json
├── 2021_02_04_155327_postman.json
└── api.php
├── header.png
├── phpunit.xml
├── phpunit.xml.bak
├── src
├── Authentication
│ ├── AuthenticationMethod.php
│ ├── Basic.php
│ └── Bearer.php
├── Commands
│ └── ExportPostmanCommand.php
├── Concerns
│ └── HasAuthentication.php
├── Exporter.php
├── PostmanGeneratorServiceProvider.php
└── Processors
│ ├── DocBlockProcessor.php
│ ├── FormDataProcessor.php
│ └── RouteProcessor.php
└── tests
├── Feature
├── ExportPostmanTest.php
└── ExportPostmanWithCacheTest.php
├── Fixtures
├── AuditLogController.php
├── CollectionHelpersTrait.php
├── ExampleController.php
├── ExampleEvent.js
├── ExampleFormRequest.php
├── ExampleModel.php
├── ExampleService.php
└── UppercaseRule.php
├── TestCase.php
└── Unit
└── .gitkeep
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | indent_style = space
8 | indent_size = 4
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | trim_trailing_whitespace = false
13 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [andreaselia,tomirons]
2 |
--------------------------------------------------------------------------------
/.github/workflows/CS.yml:
--------------------------------------------------------------------------------
1 | name: CS
2 |
3 | on:
4 | pull_request:
5 |
6 | jobs:
7 | tests:
8 | runs-on: ubuntu-latest
9 | name: Code Style
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Install Dependencies
13 | run: php /usr/bin/composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist
14 | - name: Verify
15 | run: php vendor/bin/pint --test
16 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | schedule:
7 | - cron: '0 0 * * *'
8 |
9 | jobs:
10 | tests:
11 | runs-on: ubuntu-latest
12 | strategy:
13 | fail-fast: true
14 | matrix:
15 | testbench: ['^8.0']
16 | name: Testbench ${{ matrix.testbench }}
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: Install Dependencies
20 | run: php /usr/bin/composer install -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist
21 | - name: Install Testbench
22 | run: php /usr/bin/composer require -q --dev -W --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist orchestra/testbench:${{ matrix.testbench }}
23 | - name: Execute tests via PHPUnit
24 | run: php vendor/bin/phpunit --stop-on-failure
25 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.idea
2 | /vendor
3 | composer.lock
4 | .phpunit.result.cache
5 | .phpunit.cache/test-results
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Andreas Elia
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](//packagist.org/packages/andreaselia/laravel-api-to-postman)
4 | [](https://github.styleci.io/repos/323709695?branch=master)
5 |
6 | # Laravel API to Postman
7 |
8 | This package allows you to automatically generate a Postman collection based on your API routes. It also provides basic configuration and support for bearer auth tokens and basic auth for routes behind an auth middleware.
9 |
10 | For ```POST``` and ```PUT``` requests that utilizes a FormRequest, you can optionally scaffold the request, and publish rules in raw or human readable format.
11 | ## Postman Schema
12 |
13 | The generator works for the latest version of the Postman Schema at the time of publication (v2.1.0).
14 |
15 | ## Installation
16 |
17 | Install the package:
18 |
19 | ```bash
20 | composer require andreaselia/laravel-api-to-postman
21 | ```
22 |
23 | Publish the config file:
24 |
25 | ```bash
26 | php artisan vendor:publish --provider="AndreasElia\PostmanGenerator\PostmanGeneratorServiceProvider"
27 | ```
28 |
29 | ## Configuration
30 |
31 | You can modify any of the `api-postman.php` config values to suit your export requirements.
32 |
33 | Click [here](/config/api-postman.php) to view the config attributes.
34 |
35 | ## Usage
36 |
37 | The output of the command being ran is your storage/app directory.
38 |
39 | To use the command simply run:
40 |
41 | ```bash
42 | php artisan export:postman
43 | ```
44 |
45 | The following usage will generate routes with the bearer token specified.
46 |
47 | ```bash
48 | php artisan export:postman --bearer="1|XXNKXXqJjfzG8XXSvXX1Q4pxxnkXmp8tT8TXXKXX"
49 | ```
50 |
51 | The following usage will generate routes with the basic auth specified.
52 |
53 | ```bash
54 | php artisan export:postman --basic="username:password123"
55 | ```
56 |
57 | If both auths are specified, bearer will be favored.
58 |
59 | ## Examples
60 |
61 | This is with the default configuration and a bearer token passed in:
62 |
63 | ```bash
64 | php artisan export:postman --bearer=123456789
65 | ```
66 |
67 | - [Example routes](/examples/api.php)
68 | - [Example default output](/examples/2021_02_04_151948_postman.json)
69 | - [Example structured output](/examples/2021_02_04_155327_postman.json)
70 |
71 | ## Contributing
72 |
73 | You're more than welcome to submit a pull request, or if you're not feeling up to it - create an issue so someone else can pick it up.
74 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "andreaselia/laravel-api-to-postman",
3 | "description": "Generate a Postman collection automatically from your Laravel API",
4 | "keywords": [
5 | "laravel",
6 | "postman",
7 | "api",
8 | "collection",
9 | "generate"
10 | ],
11 | "type": "library",
12 | "license": "MIT",
13 | "authors": [
14 | {
15 | "name": "Andreas Elia",
16 | "email": "andreaselia@live.co.uk"
17 | }
18 | ],
19 | "autoload": {
20 | "psr-4": {
21 | "AndreasElia\\PostmanGenerator\\": "src/"
22 | }
23 | },
24 | "autoload-dev": {
25 | "psr-4": {
26 | "AndreasElia\\PostmanGenerator\\Tests\\": "tests"
27 | }
28 | },
29 | "require": {
30 | "php": "^7.4|^8.0",
31 | "ext-json": "*",
32 | "illuminate/config": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
33 | "illuminate/console": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
34 | "illuminate/contracts": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
35 | "illuminate/support": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
36 | "illuminate/routing": "^7.0|^8.0|^9.0|^10.0|^11.0|^12.0",
37 | "phpstan/phpdoc-parser": "^1.26"
38 | },
39 | "extra": {
40 | "laravel": {
41 | "providers": [
42 | "AndreasElia\\PostmanGenerator\\PostmanGeneratorServiceProvider"
43 | ]
44 | }
45 | },
46 | "minimum-stability": "dev",
47 | "prefer-stable": true,
48 | "require-dev": {
49 | "orchestra/testbench": "^8.0|^9.0",
50 | "phpunit/phpunit": "^10.0|^11.0",
51 | "laravel/pint": "^1.13"
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/config/api-postman.php:
--------------------------------------------------------------------------------
1 | env('APP_URL', 'http://localhost'),
15 |
16 | /*
17 | |--------------------------------------------------------------------------
18 | | Collection Filename
19 | |--------------------------------------------------------------------------
20 | |
21 | | The name for the collection file to be saved.
22 | |
23 | */
24 |
25 | 'filename' => '{timestamp}_{app}_collection.json',
26 |
27 | /*
28 | |--------------------------------------------------------------------------
29 | | Structured
30 | |--------------------------------------------------------------------------
31 | |
32 | | If you want folders to be generated based on namespace.
33 | |
34 | | Set "crud_folders" to "false" if you don't want the api, index, store, show etc. folders.
35 | |
36 | */
37 |
38 | 'structured' => false,
39 | 'crud_folders' => true,
40 |
41 | /*
42 | |--------------------------------------------------------------------------
43 | | Auth Middleware
44 | |--------------------------------------------------------------------------
45 | |
46 | | The middleware which wraps your authenticated API routes.
47 | |
48 | | E.g. auth:api, auth:sanctum
49 | |
50 | */
51 |
52 | 'auth_middleware' => 'auth:api',
53 |
54 | /*
55 | |--------------------------------------------------------------------------
56 | | Headers
57 | |--------------------------------------------------------------------------
58 | |
59 | | The headers applied to all routes within the collection.
60 | |
61 | */
62 |
63 | 'headers' => [
64 | [
65 | 'key' => 'Accept',
66 | 'value' => 'application/json',
67 | ],
68 | [
69 | 'key' => 'Content-Type',
70 | 'value' => 'application/json',
71 | ],
72 | ],
73 |
74 | /*
75 | |--------------------------------------------------------------------------
76 | | Events
77 | |--------------------------------------------------------------------------
78 | |
79 | | If you want to configure the prequest and test scripts for the collection,
80 | | then please provide paths to the JavaScript files.
81 | |
82 | */
83 |
84 | 'prerequest_script' => '', // This script will execute before every request in the collection.
85 | 'test_script' => '', // This script will execute after every request in the collection.
86 |
87 | /*
88 | |--------------------------------------------------------------------------
89 | | Include Doc Comments
90 | |--------------------------------------------------------------------------
91 | |
92 | | Determines whether to set the PHP Doc comments to the description
93 | | in postman.
94 | |
95 | */
96 |
97 | 'include_doc_comments' => false,
98 |
99 | /*
100 | |--------------------------------------------------------------------------
101 | | Enable Form Data
102 | |--------------------------------------------------------------------------
103 | |
104 | | Determines whether or not form data should be handled.
105 | |
106 | */
107 |
108 | 'enable_formdata' => false,
109 |
110 | /*
111 | |--------------------------------------------------------------------------
112 | | Parse Form Request Rules
113 | |--------------------------------------------------------------------------
114 | |
115 | | If you want form requests to be printed in the field description field,
116 | | and if so, whether they will be in a human readable form.
117 | |
118 | */
119 |
120 | 'print_rules' => true, // @requires: 'enable_formdata' === true
121 | 'rules_to_human_readable' => true, // @requires: 'parse_rules' === true
122 |
123 | /*
124 | |--------------------------------------------------------------------------
125 | | Form Data
126 | |--------------------------------------------------------------------------
127 | |
128 | | The key/values to requests for form data dummy information.
129 | |
130 | */
131 |
132 | 'formdata' => [
133 | // 'email' => 'john@example.com',
134 | // 'password' => 'changeme',
135 | ],
136 |
137 | /*
138 | |--------------------------------------------------------------------------
139 | | Include Middleware
140 | |--------------------------------------------------------------------------
141 | |
142 | | The routes of the included middleware are included in the export.
143 | |
144 | */
145 |
146 | 'include_middleware' => ['api'],
147 |
148 | /*
149 | |--------------------------------------------------------------------------
150 | | Disk Driver
151 | |--------------------------------------------------------------------------
152 | |
153 | | Specify the configured disk for storing the postman collection file.
154 | |
155 | */
156 |
157 | 'disk' => 'local',
158 |
159 | /*
160 | |--------------------------------------------------------------------------
161 | | Authentication
162 | |--------------------------------------------------------------------------
163 | |
164 | | Specify the authentication to be used for the endpoints.
165 | |
166 | */
167 |
168 | 'authentication' => [
169 | 'method' => env('POSTMAN_EXPORT_AUTH_METHOD'),
170 | 'token' => env('POSTMAN_EXPORT_AUTH_TOKEN'),
171 | ],
172 |
173 | /*
174 | |--------------------------------------------------------------------------
175 | | Protocol Profile Behavior
176 | |--------------------------------------------------------------------------
177 | |
178 | | Set of configurations used to alter the usual behavior of sending the request.
179 | | These can be defined in a collection at Item or ItemGroup level which will be inherited if applicable.
180 | |
181 | */
182 |
183 | 'protocol_profile_behavior' => [
184 | 'disable_body_pruning' => false, // Control request body pruning for following methods: GET, COPY, HEAD, PURGE, UNLOCK
185 | ],
186 |
187 | ];
188 |
--------------------------------------------------------------------------------
/examples/2021_02_04_151948_postman.json:
--------------------------------------------------------------------------------
1 | {
2 | "variable": [{
3 | "key": "base_url",
4 | "value": "https:\/\/api.example.com\/"
5 | }, {
6 | "key": "token",
7 | "value": "123456789"
8 | }],
9 | "info": {
10 | "name": "2021_02_04_151948_postman",
11 | "schema": "https:\/\/schema.getpostman.com\/json\/collection\/v2.1.0\/collection.json"
12 | },
13 | "item": [{
14 | "name": "api",
15 | "request": {
16 | "method": "GET",
17 | "header": [{
18 | "key": "Content-Type",
19 | "value": "application\/json"
20 | }],
21 | "url": {
22 | "raw": "{{base_url}}\/api",
23 | "host": "{{base_url}}\/api"
24 | }
25 | }
26 | }, {
27 | "name": "api\/posts",
28 | "request": {
29 | "method": "GET",
30 | "header": [{
31 | "key": "Content-Type",
32 | "value": "application\/json"
33 | }],
34 | "url": {
35 | "raw": "{{base_url}}\/api\/posts",
36 | "host": "{{base_url}}\/api\/posts"
37 | }
38 | }
39 | }, {
40 | "name": "api\/posts",
41 | "request": {
42 | "method": "POST",
43 | "header": [{
44 | "key": "Content-Type",
45 | "value": "application\/json"
46 | }],
47 | "url": {
48 | "raw": "{{base_url}}\/api\/posts",
49 | "host": "{{base_url}}\/api\/posts"
50 | }
51 | }
52 | }, {
53 | "name": "api\/posts\/{post}",
54 | "request": {
55 | "method": "GET",
56 | "header": [{
57 | "key": "Content-Type",
58 | "value": "application\/json"
59 | }],
60 | "url": {
61 | "raw": "{{base_url}}\/api\/posts\/{post}",
62 | "host": "{{base_url}}\/api\/posts\/{post}"
63 | }
64 | }
65 | }, {
66 | "name": "api\/posts\/{post}",
67 | "request": {
68 | "method": "PUT",
69 | "header": [{
70 | "key": "Content-Type",
71 | "value": "application\/json"
72 | }],
73 | "url": {
74 | "raw": "{{base_url}}\/api\/posts\/{post}",
75 | "host": "{{base_url}}\/api\/posts\/{post}"
76 | }
77 | }
78 | }, {
79 | "name": "api\/posts\/{post}",
80 | "request": {
81 | "method": "PATCH",
82 | "header": [{
83 | "key": "Content-Type",
84 | "value": "application\/json"
85 | }],
86 | "url": {
87 | "raw": "{{base_url}}\/api\/posts\/{post}",
88 | "host": "{{base_url}}\/api\/posts\/{post}"
89 | }
90 | }
91 | }, {
92 | "name": "api\/posts\/{post}",
93 | "request": {
94 | "method": "DELETE",
95 | "header": [{
96 | "key": "Content-Type",
97 | "value": "application\/json"
98 | }],
99 | "url": {
100 | "raw": "{{base_url}}\/api\/posts\/{post}",
101 | "host": "{{base_url}}\/api\/posts\/{post}"
102 | }
103 | }
104 | }, {
105 | "name": "api\/user",
106 | "request": {
107 | "method": "POST",
108 | "header": [{
109 | "key": "Content-Type",
110 | "value": "application\/json"
111 | }, {
112 | "key": "Authorization",
113 | "value": "Bearer {{token}}"
114 | }],
115 | "url": {
116 | "raw": "{{base_url}}\/api\/user",
117 | "host": "{{base_url}}\/api\/user"
118 | }
119 | }
120 | }]
121 | }
122 |
--------------------------------------------------------------------------------
/examples/2021_02_04_155327_postman.json:
--------------------------------------------------------------------------------
1 | {
2 | "variable": [{
3 | "key": "base_url",
4 | "value": "https:\/\/api.example.com\/"
5 | }, {
6 | "key": "token",
7 | "value": "123456789"
8 | }],
9 | "info": {
10 | "name": "2021_02_04_155327_postman",
11 | "schema": "https:\/\/schema.getpostman.com\/json\/collection\/v2.1.0\/collection.json"
12 | },
13 | "item": [{
14 | "name": "home",
15 | "item": [{
16 | "name": "api",
17 | "request": {
18 | "method": "GET",
19 | "header": [{
20 | "key": "Content-Type",
21 | "value": "application\/json"
22 | }],
23 | "url": {
24 | "raw": "{{base_url}}\/api",
25 | "host": "{{base_url}}\/api"
26 | }
27 | }
28 | }]
29 | }, {
30 | "name": "posts",
31 | "item": [{
32 | "name": "api\/posts",
33 | "request": {
34 | "method": "GET",
35 | "header": [{
36 | "key": "Content-Type",
37 | "value": "application\/json"
38 | }],
39 | "url": {
40 | "raw": "{{base_url}}\/api\/posts",
41 | "host": "{{base_url}}\/api\/posts"
42 | }
43 | }
44 | }, {
45 | "name": "api\/posts",
46 | "request": {
47 | "method": "POST",
48 | "header": [{
49 | "key": "Content-Type",
50 | "value": "application\/json"
51 | }],
52 | "url": {
53 | "raw": "{{base_url}}\/api\/posts",
54 | "host": "{{base_url}}\/api\/posts"
55 | }
56 | }
57 | }, {
58 | "name": "api\/posts\/{post}",
59 | "request": {
60 | "method": "GET",
61 | "header": [{
62 | "key": "Content-Type",
63 | "value": "application\/json"
64 | }],
65 | "url": {
66 | "raw": "{{base_url}}\/api\/posts\/{post}",
67 | "host": "{{base_url}}\/api\/posts\/{post}"
68 | }
69 | }
70 | }, {
71 | "name": "api\/posts\/{post}",
72 | "request": {
73 | "method": "PUT",
74 | "header": [{
75 | "key": "Content-Type",
76 | "value": "application\/json"
77 | }],
78 | "url": {
79 | "raw": "{{base_url}}\/api\/posts\/{post}",
80 | "host": "{{base_url}}\/api\/posts\/{post}"
81 | }
82 | }
83 | }, {
84 | "name": "api\/posts\/{post}",
85 | "request": {
86 | "method": "PATCH",
87 | "header": [{
88 | "key": "Content-Type",
89 | "value": "application\/json"
90 | }],
91 | "url": {
92 | "raw": "{{base_url}}\/api\/posts\/{post}",
93 | "host": "{{base_url}}\/api\/posts\/{post}"
94 | }
95 | }
96 | }, {
97 | "name": "api\/posts\/{post}",
98 | "request": {
99 | "method": "DELETE",
100 | "header": [{
101 | "key": "Content-Type",
102 | "value": "application\/json"
103 | }],
104 | "url": {
105 | "raw": "{{base_url}}\/api\/posts\/{post}",
106 | "host": "{{base_url}}\/api\/posts\/{post}"
107 | }
108 | }
109 | }]
110 | }, {
111 | "name": "user",
112 | "item": [{
113 | "name": "api\/user",
114 | "request": {
115 | "method": "POST",
116 | "header": [{
117 | "key": "Content-Type",
118 | "value": "application\/json"
119 | }, {
120 | "key": "Authorization",
121 | "value": "Bearer {{token}}"
122 | }],
123 | "url": {
124 | "raw": "{{base_url}}\/api\/user",
125 | "host": "{{base_url}}\/api\/user"
126 | }
127 | }
128 | }]
129 | }]
130 | }
131 |
--------------------------------------------------------------------------------
/examples/api.php:
--------------------------------------------------------------------------------
1 | name('home');
8 |
9 | Route::apiResource('posts', 'PostController');
10 |
11 | Route::middleware('auth:api')->group(function () {
12 | Route::post('/user', UserController::class)->name('user');
13 | });
14 |
--------------------------------------------------------------------------------
/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andreaselia/laravel-api-to-postman/24d1fa6764bcc0cfd84bfd745ec7c6f0d7de5776/header.png
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 | src/
16 |
17 |
18 |
19 |
20 | ./tests/Unit
21 |
22 |
23 | ./tests/Feature
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/phpunit.xml.bak:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 | src/
16 |
17 |
18 |
19 |
20 | ./tests/Unit
21 |
22 |
23 | ./tests/Feature
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/Authentication/AuthenticationMethod.php:
--------------------------------------------------------------------------------
1 | 'Authorization',
18 | 'value' => sprintf('%s %s', $this->prefix(), $this->token ?? '{{token}}'),
19 | ];
20 | }
21 |
22 | public function getToken(): string
23 | {
24 | return $this->token;
25 | }
26 |
27 | abstract public function prefix(): string;
28 | }
29 |
--------------------------------------------------------------------------------
/src/Authentication/Basic.php:
--------------------------------------------------------------------------------
1 | set('api-postman.authentication', [
29 | 'method' => $this->option('bearer') ? 'bearer' : ($this->option('basic') ? 'basic' : null),
30 | 'token' => $this->option('bearer') ?? $this->option('basic') ?? null,
31 | ]);
32 |
33 | $exporter
34 | ->to($filename)
35 | ->setAuthentication(value(function () {
36 | if (filled($this->option('bearer'))) {
37 | return new \AndreasElia\PostmanGenerator\Authentication\Bearer($this->option('bearer'));
38 | }
39 |
40 | if (filled($this->option('basic'))) {
41 | return new \AndreasElia\PostmanGenerator\Authentication\Basic($this->option('basic'));
42 | }
43 |
44 | return null;
45 | }))
46 | ->export();
47 |
48 | Storage::disk(config('api-postman.disk'))
49 | ->put('postman/'.$filename, $exporter->getOutput());
50 |
51 | $this->info('Postman Collection Exported: '.storage_path('app/postman/'.$filename));
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Concerns/HasAuthentication.php:
--------------------------------------------------------------------------------
1 | config['authentication'];
15 |
16 | if ($config['method']) {
17 | $className = Str::of('AndreasElia\\PostmanGenerator\\Authentication\\')
18 | ->append(ucfirst($config['method']))
19 | ->toString();
20 |
21 | $this->authentication = new $className($config['token']);
22 | }
23 |
24 | return $this;
25 | }
26 |
27 | public function setAuthentication(?AuthenticationMethod $authentication): self
28 | {
29 | if (isset($authentication)) {
30 | $this->authentication = $authentication;
31 | }
32 |
33 | return $this;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Exporter.php:
--------------------------------------------------------------------------------
1 | config = $config['api-postman'];
22 | }
23 |
24 | public function to(string $filename): self
25 | {
26 | $this->filename = $filename;
27 |
28 | return $this;
29 | }
30 |
31 | public function getOutput()
32 | {
33 | return json_encode($this->output);
34 | }
35 |
36 | public function export(): void
37 | {
38 | $this->resolveAuth();
39 |
40 | $this->output = $this->generateStructure();
41 | }
42 |
43 | protected function generateStructure(): array
44 | {
45 | $this->output = [
46 | 'variable' => [
47 | [
48 | 'key' => 'base_url',
49 | 'value' => $this->config['base_url'],
50 | ],
51 | ],
52 | 'info' => [
53 | 'name' => $this->filename,
54 | 'schema' => 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
55 | ],
56 | 'item' => [],
57 | 'event' => [],
58 | ];
59 |
60 | $preRequestPath = $this->config['prerequest_script'];
61 | $testPath = $this->config['test_script'];
62 |
63 | if ($preRequestPath || $testPath) {
64 | $scripts = [
65 | 'prerequest' => $preRequestPath,
66 | 'test' => $testPath,
67 | ];
68 |
69 | foreach ($scripts as $type => $path) {
70 | if (file_exists($path)) {
71 | $this->output['event'][] = [
72 | 'listen' => $type,
73 | 'script' => [
74 | 'type' => 'text/javascript',
75 | 'exec' => file_get_contents($path),
76 | ],
77 | ];
78 | }
79 | }
80 | }
81 |
82 | if ($this->authentication) {
83 | $this->output['variable'][] = [
84 | 'key' => 'token',
85 | 'value' => $this->authentication->getToken(),
86 | ];
87 | }
88 |
89 | return app(RouteProcessor::class)->process($this->output);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/PostmanGeneratorServiceProvider.php:
--------------------------------------------------------------------------------
1 | app->runningInConsole()) {
18 | $this->publishes([
19 | __DIR__.'/../config/api-postman.php' => config_path('api-postman.php'),
20 | ], 'postman-config');
21 | }
22 |
23 | $this->commands(ExportPostmanCommand::class);
24 | }
25 |
26 | /**
27 | * Register any application services.
28 | *
29 | * @return void
30 | */
31 | public function register()
32 | {
33 | $this->mergeConfigFrom(
34 | __DIR__.'/../config/api-postman.php', 'api-postman'
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Processors/DocBlockProcessor.php:
--------------------------------------------------------------------------------
1 | getDocComment();
27 | $tokens = new TokenIterator($lexer->tokenize($comment));
28 | $phpDocNode = $parser->parse($tokens);
29 |
30 | foreach ($phpDocNode->children as $child) {
31 | if ($child instanceof PhpDocTextNode) {
32 | $description .= ' '.$child->text;
33 | }
34 | }
35 |
36 | return Str::squish($description);
37 | } catch (Throwable $e) {
38 | return '';
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Processors/FormDataProcessor.php:
--------------------------------------------------------------------------------
1 | getParameters())
17 | ->first(function ($value) {
18 | $value = $value->getType();
19 |
20 | return $value && is_subclass_of($value->getName(), FormRequest::class);
21 | });
22 |
23 | if ($rulesParameter) {
24 | /** @var FormRequest $class */
25 | $class = new ($rulesParameter->getType()->getName());
26 |
27 | $classRules = method_exists($class, 'rules') ? $class->rules() : [];
28 |
29 | foreach ($classRules as $fieldName => $rule) {
30 | if (is_string($rule)) {
31 | $rule = preg_split('/\s*\|\s*/', $rule);
32 | }
33 |
34 | $printRules = config('api-postman.print_rules');
35 |
36 | $rules->push([
37 | 'name' => $fieldName,
38 | 'description' => $printRules ? $rule : '',
39 | ]);
40 |
41 | if (is_array($rule) && in_array('confirmed', $rule)) {
42 | $rules->push([
43 | 'name' => $fieldName.'_confirmation',
44 | 'description' => $printRules ? $rule : '',
45 | ]);
46 | }
47 | }
48 | }
49 |
50 | return $rules;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Processors/RouteProcessor.php:
--------------------------------------------------------------------------------
1 | config = $config['api-postman'];
33 |
34 | $this->router = $router;
35 |
36 | $this->resolveAuth();
37 | }
38 |
39 | public function process(array $output): array
40 | {
41 | $this->output = $output;
42 |
43 | $routes = collect($this->router->getRoutes());
44 |
45 | /** @var Route $route */
46 | foreach ($routes as $route) {
47 | $this->processRoute($route);
48 | }
49 |
50 | return $this->output;
51 | }
52 |
53 | /**
54 | * @throws \ReflectionException
55 | */
56 | protected function processRoute(Route $route)
57 | {
58 | try {
59 | $methods = array_filter($route->methods(), fn ($value) => $value !== 'HEAD');
60 | $middlewares = $route->gatherMiddleware();
61 |
62 | foreach ($methods as $method) {
63 | $includedMiddleware = false;
64 |
65 | foreach ($middlewares as $middleware) {
66 | if (in_array($middleware, $this->config['include_middleware'])) {
67 | $includedMiddleware = true;
68 | }
69 | }
70 |
71 | if (empty($middlewares) || ! $includedMiddleware) {
72 | continue;
73 | }
74 |
75 | $reflectionMethod = $this->getReflectionMethod($route->getAction());
76 |
77 | if (! $reflectionMethod) {
78 | continue;
79 | }
80 |
81 | $routeHeaders = $this->config['headers'];
82 |
83 | if ($this->authentication && in_array($this->config['auth_middleware'], $middlewares)) {
84 | $routeHeaders[] = $this->authentication->toArray();
85 | }
86 |
87 | $uri = Str::of($route->uri())->replaceMatches('/{([[:alnum:]_]+)}/', ':$1');
88 |
89 | if ($this->config['include_doc_comments']) {
90 | $description = (new DocBlockProcessor)($reflectionMethod);
91 | }
92 |
93 | $data = [
94 | 'name' => $route->uri(),
95 | 'request' => array_merge(
96 | $this->processRequest(
97 | $method,
98 | $uri,
99 | $this->config['enable_formdata'] ? (new FormDataProcessor)->process($reflectionMethod) : collect()
100 | ),
101 | ['description' => $description ?? '']
102 | ),
103 | 'response' => [],
104 |
105 | 'protocolProfileBehavior' => [
106 | 'disableBodyPruning' => $this->config['protocol_profile_behavior']['disable_body_pruning'] ?? false,
107 | ],
108 | ];
109 |
110 | if ($this->config['structured']) {
111 | $routeNameSegments = (
112 | $route->getName()
113 | ? Str::of($route->getName())->explode('.')
114 | : Str::of($route->uri())->after('api/')->explode('/')
115 | )->filter(fn ($value) => ! is_null($value) && $value !== '');
116 |
117 | if (! $this->config['crud_folders']) {
118 | if (in_array($routeNameSegments->last(), ['index', 'store', 'show', 'update', 'destroy'])) {
119 | $routeNameSegments->forget($routeNameSegments->count() - 1);
120 | }
121 | }
122 |
123 | $this->buildTree($this->output, $routeNameSegments->all(), $data);
124 | } else {
125 | $this->output['item'][] = $data;
126 | }
127 | }
128 | } catch (\Exception $e) {
129 | Log::warning('Failed to process route: '.$route->uri());
130 | }
131 | }
132 |
133 | protected function processRequest(string $method, Stringable $uri, Collection $rules): array
134 | {
135 | return collect([
136 | 'method' => strtoupper($method),
137 | 'header' => collect($this->config['headers'])
138 | ->push($this->authentication?->toArray())
139 | ->filter()
140 | ->values()
141 | ->all(),
142 | 'url' => [
143 | 'raw' => '{{base_url}}/'.$uri,
144 | 'host' => ['{{base_url}}'],
145 | 'path' => $uri->explode('/')->filter()->all(),
146 | 'variable' => $uri
147 | ->matchAll('/(?<={)[[:alnum:]]+(?=})/m')
148 | ->transform(function ($variable) {
149 | return ['key' => $variable, 'value' => ''];
150 | })
151 | ->all(),
152 | ],
153 | ])
154 | ->when($rules, function (Collection $collection, Collection $rules) use ($method) {
155 | if ($rules->isEmpty()) {
156 | return $collection;
157 | }
158 |
159 | $rules->transform(fn ($rule) => [
160 | 'key' => $rule['name'],
161 | 'value' => $this->config['formdata'][$rule['name']] ?? null,
162 | 'description' => $this->config['print_rules'] ? $this->parseRulesIntoHumanReadable($rule['name'], $rule['description']) : null,
163 | ]);
164 |
165 | if ($method === 'GET') {
166 | return $collection->mergeRecursive([
167 | 'url' => [
168 | 'query' => $rules->map(fn ($value) => array_merge($value, ['disabled' => false])),
169 | ],
170 | ]);
171 | }
172 |
173 | return $collection->put('body', [
174 | 'mode' => 'urlencoded',
175 | 'urlencoded' => $rules->map(fn ($value) => array_merge($value, ['type' => 'text'])),
176 | ]);
177 | })
178 | ->all();
179 | }
180 |
181 | protected function processResponse(string $method, array $action): array
182 | {
183 | return [
184 | 'code' => 200,
185 | 'body' => [
186 | 'mode' => 'raw',
187 | 'raw' => '',
188 | ],
189 | ];
190 | }
191 |
192 | /**
193 | * @throws \ReflectionException
194 | */
195 | private function getReflectionMethod(array $routeAction): ?object
196 | {
197 | if ($this->containsSerializedClosure($routeAction)) {
198 | $routeAction['uses'] = unserialize($routeAction['uses'])->getClosure();
199 | }
200 |
201 | if ($routeAction['uses'] instanceof Closure) {
202 | return new ReflectionFunction($routeAction['uses']);
203 | }
204 |
205 | $routeData = explode('@', $routeAction['uses']);
206 | $reflection = new ReflectionClass($routeData[0]);
207 |
208 | if (! $reflection->hasMethod($routeData[1])) {
209 | return null;
210 | }
211 |
212 | return $reflection->getMethod($routeData[1]);
213 | }
214 |
215 | private function containsSerializedClosure(array $action): bool
216 | {
217 | return is_string($action['uses']) && Str::startsWith($action['uses'], [
218 | 'C:32:"Opis\\Closure\\SerializableClosure',
219 | 'O:47:"Laravel\SerializableClosure\\SerializableClosure',
220 | 'O:55:"Laravel\\SerializableClosure\\UnsignedSerializableClosure',
221 | ]);
222 | }
223 |
224 | protected function buildTree(array &$routes, array $segments, array $request): void
225 | {
226 | $parent = &$routes;
227 | $destination = end($segments);
228 |
229 | foreach ($segments as $segment) {
230 | $matched = false;
231 |
232 | foreach ($parent['item'] as &$item) {
233 | if ($item['name'] === $segment) {
234 | $parent = &$item;
235 |
236 | if ($segment === $destination) {
237 | $parent['item'][] = $request;
238 | }
239 |
240 | $matched = true;
241 |
242 | break;
243 | }
244 | }
245 |
246 | unset($item);
247 |
248 | if (! $matched) {
249 | $item = [
250 | 'name' => $segment,
251 | 'item' => $segment === $destination ? [$request] : [],
252 | ];
253 |
254 | $parent['item'][] = &$item;
255 | $parent = &$item;
256 | }
257 |
258 | unset($item);
259 | }
260 | }
261 |
262 | protected function parseRulesIntoHumanReadable($attribute, $rules): string
263 | {
264 | // ... bail if user has asked for non interpreted strings:
265 | if (! $this->config['rules_to_human_readable']) {
266 | foreach ($rules as $i => $rule) {
267 | // because we don't support custom rule classes, we remove them from the rules
268 | if (is_subclass_of($rule, Rule::class)) {
269 | unset($rules[$i]);
270 | }
271 | }
272 |
273 | return is_array($rules) ? implode(', ', $rules) : $this->safelyStringifyClassBasedRule($rules);
274 | }
275 |
276 | /*
277 | * An object based rule is presumably a Laravel default class based rule or one that implements the Illuminate
278 | * Rule interface. Lets try to safely access the string representation...
279 | */
280 | if (is_object($rules)) {
281 | $rules = [$this->safelyStringifyClassBasedRule($rules)];
282 | }
283 |
284 | /*
285 | * Handle string based rules (e.g. required|string|max:30)
286 | */
287 | if (is_array($rules)) {
288 | foreach ($rules as $i => $rule) {
289 | if (is_object($rule)) {
290 | unset($rules[$i]);
291 | }
292 | }
293 |
294 | $validator = Validator::make([], [$attribute => implode('|', $rules)]);
295 |
296 | foreach ($rules as $rule) {
297 | [$rule, $parameters] = ValidationRuleParser::parse($rule);
298 |
299 | $validator->addFailure($attribute, $rule, $parameters);
300 | }
301 |
302 | $messages = $validator->getMessageBag()->toArray()[$attribute];
303 |
304 | if (is_array($messages)) {
305 | $messages = $this->handleEdgeCases($messages);
306 | }
307 |
308 | return implode(', ', is_array($messages) ? $messages : $messages->toArray());
309 | }
310 |
311 | // ...safely return a safe value if we encounter neither a string or object based rule set:
312 | return '';
313 | }
314 |
315 | protected function handleEdgeCases(array $messages): array
316 | {
317 | foreach ($messages as $key => $message) {
318 | if ($message === 'validation.nullable') {
319 | $messages[$key] = '(Nullable)';
320 |
321 | continue;
322 | }
323 |
324 | if ($message === 'validation.sometimes') {
325 | $messages[$key] = '(Optional)';
326 | }
327 | }
328 |
329 | return $messages;
330 | }
331 |
332 | /**
333 | * In this case we have received what is most likely a Rule Object but are not certain.
334 | */
335 | protected function safelyStringifyClassBasedRule($probableRule): string
336 | {
337 | if (! is_object($probableRule) || is_subclass_of($probableRule, Rule::class) || ! method_exists($probableRule, '__toString')) {
338 | return '';
339 | }
340 |
341 | return (string) $probableRule;
342 | }
343 | }
344 |
--------------------------------------------------------------------------------
/tests/Feature/ExportPostmanTest.php:
--------------------------------------------------------------------------------
1 | set('api-postman.filename', 'test.json');
19 |
20 | Storage::disk()->deleteDirectory('postman');
21 | }
22 |
23 | /**
24 | * @dataProvider providerFormDataEnabled
25 | */
26 | public function test_standard_export_works(bool $formDataEnabled)
27 | {
28 | config()->set('api-postman.enable_formdata', $formDataEnabled);
29 |
30 | $this->artisan('export:postman')->assertExitCode(0);
31 |
32 | $collection = json_decode(Storage::get('postman/'.config('api-postman.filename')), true);
33 |
34 | $routes = $this->app['router']->getRoutes();
35 |
36 | $collectionItems = $collection['item'];
37 |
38 | $totalCollectionItems = $this->countCollectionItems($collection['item']);
39 |
40 | $this->assertEquals(count($routes), $totalCollectionItems);
41 |
42 | foreach ($routes as $route) {
43 | $methods = $route->methods();
44 |
45 | $collectionRoutes = Arr::where($collectionItems, function ($item) use ($route) {
46 | return $item['name'] == $route->uri();
47 | });
48 |
49 | $collectionRoute = Arr::first($collectionRoutes);
50 |
51 | if (! in_array($collectionRoute['request']['method'], $methods)) {
52 | $methods = collect($collectionRoutes)->pluck('request.method')->toArray();
53 | }
54 |
55 | $this->assertNotNull($collectionRoute);
56 | $this->assertTrue(in_array($collectionRoute['request']['method'], $methods));
57 | }
58 | }
59 |
60 | /**
61 | * @dataProvider providerFormDataEnabled
62 | */
63 | public function test_bearer_export_works(bool $formDataEnabled)
64 | {
65 | config()->set('api-postman.enable_formdata', $formDataEnabled);
66 |
67 | $this->artisan('export:postman --bearer=1234567890')->assertExitCode(0);
68 |
69 | $collection = json_decode(Storage::get('postman/'.config('api-postman.filename')), true);
70 |
71 | $routes = $this->app['router']->getRoutes();
72 |
73 | $collectionVariables = $collection['variable'];
74 |
75 | foreach ($collectionVariables as $variable) {
76 | if ($variable['key'] != 'token') {
77 | continue;
78 | }
79 |
80 | $this->assertEquals($variable['value'], '1234567890');
81 | }
82 |
83 | $this->assertCount(2, $collectionVariables);
84 |
85 | $totalCollectionItems = $this->countCollectionItems($collection['item']);
86 |
87 | $this->assertEquals(count($routes), $totalCollectionItems);
88 |
89 | foreach ($routes as $route) {
90 | $methods = $route->methods();
91 |
92 | $collectionRoutes = Arr::where($collection['item'], function ($item) use ($route) {
93 | return $item['name'] == $route->uri();
94 | });
95 |
96 | $collectionRoute = Arr::first($collectionRoutes);
97 |
98 | if (! in_array($collectionRoute['request']['method'], $methods)) {
99 | $methods = collect($collectionRoutes)->pluck('request.method')->toArray();
100 | }
101 |
102 | $this->assertNotNull($collectionRoute);
103 | $this->assertTrue(in_array($collectionRoute['request']['method'], $methods));
104 | }
105 | }
106 |
107 | /**
108 | * @dataProvider providerFormDataEnabled
109 | */
110 | public function test_basic_export_works(bool $formDataEnabled)
111 | {
112 | config()->set('api-postman.enable_formdata', $formDataEnabled);
113 |
114 | $this->artisan('export:postman --basic=username:password1234')->assertExitCode(0);
115 |
116 | $collection = json_decode(Storage::get('postman/'.config('api-postman.filename')), true);
117 |
118 | $routes = $this->app['router']->getRoutes();
119 |
120 | $collectionVariables = $collection['variable'];
121 |
122 | foreach ($collectionVariables as $variable) {
123 | if ($variable['key'] != 'token') {
124 | continue;
125 | }
126 |
127 | $this->assertEquals($variable['value'], 'username:password1234');
128 | }
129 |
130 | $this->assertCount(2, $collectionVariables);
131 |
132 | $totalCollectionItems = $this->countCollectionItems($collection['item']);
133 |
134 | $this->assertEquals(count($routes), $totalCollectionItems);
135 |
136 | foreach ($routes as $route) {
137 | $methods = $route->methods();
138 |
139 | $collectionRoutes = Arr::where($collection['item'], function ($item) use ($route) {
140 | return $item['name'] == $route->uri();
141 | });
142 |
143 | $collectionRoute = Arr::first($collectionRoutes);
144 |
145 | if (! in_array($collectionRoute['request']['method'], $methods)) {
146 | $methods = collect($collectionRoutes)->pluck('request.method')->toArray();
147 | }
148 |
149 | $this->assertNotNull($collectionRoute);
150 | $this->assertTrue(in_array($collectionRoute['request']['method'], $methods));
151 | }
152 | }
153 |
154 | /**
155 | * @dataProvider providerFormDataEnabled
156 | */
157 | public function test_structured_export_works(bool $formDataEnabled)
158 | {
159 | config([
160 | 'api-postman.structured' => true,
161 | 'api-postman.enable_formdata' => $formDataEnabled,
162 | ]);
163 |
164 | $this->artisan('export:postman')->assertExitCode(0);
165 |
166 | $collection = json_decode(Storage::get('postman/'.config('api-postman.filename')), true);
167 |
168 | $routes = $this->app['router']->getRoutes();
169 |
170 | $totalCollectionItems = $this->countCollectionItems($collection['item']);
171 |
172 | $this->assertEquals(count($routes), $totalCollectionItems);
173 | }
174 |
175 | public function test_rules_printing_export_works()
176 | {
177 | config([
178 | 'api-postman.enable_formdata' => true,
179 | 'api-postman.print_rules' => true,
180 | 'api-postman.rules_to_human_readable' => false,
181 | ]);
182 |
183 | $this->artisan('export:postman')->assertExitCode(0);
184 |
185 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']);
186 |
187 | $targetRequest = $collection
188 | ->where('name', 'example/storeWithFormRequest')
189 | ->first();
190 |
191 | $fields = collect($targetRequest['request']['body']['urlencoded']);
192 | $this->assertCount(1, $fields->where('key', 'field_1')->where('description', 'required'));
193 | $this->assertCount(1, $fields->where('key', 'field_2')->where('description', 'required, integer'));
194 | $this->assertCount(1, $fields->where('key', 'field_5')->where('description', 'required, integer, max:30, min:1'));
195 | $this->assertCount(1, $fields->where('key', 'field_6')->where('description', 'in:"1","2","3"'));
196 | }
197 |
198 | public function test_rules_printing_get_export_works()
199 | {
200 | config([
201 | 'api-postman.enable_formdata' => true,
202 | 'api-postman.print_rules' => true,
203 | 'api-postman.rules_to_human_readable' => false,
204 | ]);
205 |
206 | $this->artisan('export:postman')->assertExitCode(0);
207 |
208 | $this->assertTrue(true);
209 |
210 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']);
211 |
212 | $targetRequest = $collection
213 | ->where('name', 'example/getWithFormRequest')
214 | ->first();
215 |
216 | $this->assertEqualsCanonicalizing([
217 | 'raw' => '{{base_url}}/example/getWithFormRequest',
218 | 'host' => [
219 | '{{base_url}}',
220 | ],
221 | 'path' => [
222 | 'example',
223 | 'getWithFormRequest',
224 | ],
225 | 'variable' => [],
226 | ], array_slice($targetRequest['request']['url'], 0, 4));
227 |
228 | $fields = collect($targetRequest['request']['url']['query']);
229 | $this->assertCount(1, $fields->where('key', 'field_1')->where('description', 'required'));
230 | $this->assertCount(1, $fields->where('key', 'field_2')->where('description', 'required, integer'));
231 | $this->assertCount(1, $fields->where('key', 'field_5')->where('description', 'required, integer, max:30, min:1'));
232 | $this->assertCount(1, $fields->where('key', 'field_6')->where('description', 'in:"1","2","3"'));
233 |
234 | // Check for the required structure of the get request query
235 | foreach ($fields as $field) {
236 | $this->assertEqualsCanonicalizing([
237 | 'key' => $field['key'],
238 | 'value' => null,
239 | 'disabled' => false,
240 | 'description' => $field['description'],
241 | ], $field);
242 | }
243 | }
244 |
245 | public function test_rules_printing_export_to_human_readable_works()
246 | {
247 | config([
248 | 'api-postman.enable_formdata' => true,
249 | 'api-postman.print_rules' => true,
250 | 'api-postman.rules_to_human_readable' => true,
251 | ]);
252 |
253 | $this->artisan('export:postman')->assertExitCode(0);
254 |
255 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']);
256 |
257 | $targetRequest = $collection
258 | ->where('name', 'example/storeWithFormRequest')
259 | ->first();
260 |
261 | $fields = collect($targetRequest['request']['body']['urlencoded']);
262 | $this->assertCount(1, $fields->where('key', 'field_1')->where('description', 'The field 1 field is required.'));
263 | $this->assertCount(1, $fields->where('key', 'field_2')->where('description', 'The field 2 field is required., The field 2 field must be an integer.'));
264 | $this->assertCount(1, $fields->where('key', 'field_3')->where('description', '(Optional), The field 3 field must be an integer.'));
265 | $this->assertCount(1, $fields->where('key', 'field_4')->where('description', '(Nullable), The field 4 field must be an integer.'));
266 | // the below fails locally, but passes on GitHub actions?
267 | $this->assertCount(1, $fields->where('key', 'field_5')->where('description', 'The field 5 field is required., The field 5 field must be an integer., The field 5 field must not be greater than 30., The field 5 field must be at least 1.'));
268 |
269 | /** This looks bad, but this is the default message in lang/en/validation.php, you can update to:.
270 | *
271 | * "'in' => 'The selected :attribute is invalid. Allowable values: :values',"
272 | **/
273 | $this->assertCount(1, $fields->where('key', 'field_6')->where('description', 'The selected field 6 is invalid.'));
274 | $this->assertCount(1, $fields->where('key', 'field_7')->where('description', 'The field 7 field is required.'));
275 | $this->assertCount(1, $fields->where('key', 'field_8')->where('description', 'The field 8 field must be uppercase.'));
276 | $this->assertCount(1, $fields->where('key', 'field_9')->where('description', 'The field 9 field is required., The field 9 field must be a string.'));
277 | }
278 |
279 | public function test_event_export_works()
280 | {
281 | $eventScriptPath = 'tests/Fixtures/ExampleEvent.js';
282 |
283 | config([
284 | 'api-postman.prerequest_script' => $eventScriptPath,
285 | 'api-postman.test_script' => $eventScriptPath,
286 | ]);
287 |
288 | $this->artisan('export:postman')->assertExitCode(0);
289 |
290 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['event']);
291 |
292 | $events = $collection
293 | ->whereIn('listen', ['prerequest', 'test'])
294 | ->all();
295 |
296 | $this->assertCount(2, $events);
297 |
298 | $content = file_get_contents($eventScriptPath);
299 |
300 | foreach ($events as $event) {
301 | $this->assertEquals($event['script']['exec'], $content);
302 | }
303 | }
304 |
305 | public function test_php_doc_comment_export()
306 | {
307 | config([
308 | 'api-postman.include_doc_comments' => true,
309 | ]);
310 |
311 | $this->artisan('export:postman')->assertExitCode(0);
312 |
313 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']);
314 |
315 | $targetRequest = $collection
316 | ->where('name', 'example/phpDocRoute')
317 | ->first();
318 |
319 | $this->assertEquals($targetRequest['request']['description'], 'This is the php doc route. Which is also multi-line. and has a blank line.');
320 | }
321 |
322 | public function test_uri_is_correct()
323 | {
324 | $this->artisan('export:postman')->assertExitCode(0);
325 |
326 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']);
327 |
328 | $targetRequest = $collection
329 | ->where('name', 'example/phpDocRoute')
330 | ->first();
331 |
332 | $this->assertEquals($targetRequest['name'], 'example/phpDocRoute');
333 | $this->assertEquals($targetRequest['request']['url']['raw'], '{{base_url}}/example/phpDocRoute');
334 | }
335 |
336 | public function test_api_resource_routes_set_parameters_correctly_with_hyphens()
337 | {
338 | $this->artisan('export:postman')->assertExitCode(0);
339 |
340 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']);
341 |
342 | $targetRequest = $collection
343 | ->where('name', 'example/users/{user}/audit-logs/{audit_log}')
344 | ->where('request.method', 'PATCH')
345 | ->first();
346 |
347 | $this->assertEquals($targetRequest['name'], 'example/users/{user}/audit-logs/{audit_log}');
348 | $this->assertEquals($targetRequest['request']['url']['raw'], '{{base_url}}/example/users/:user/audit-logs/:audit_log');
349 | }
350 |
351 | public function test_api_resource_routes_set_parameters_correctly_with_underscores()
352 | {
353 | $this->artisan('export:postman')->assertExitCode(0);
354 |
355 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']);
356 |
357 | $targetRequest = $collection
358 | ->where('name', 'example/users/{user}/other_logs/{other_log}')
359 | ->where('request.method', 'PATCH')
360 | ->first();
361 |
362 | $this->assertEquals($targetRequest['name'], 'example/users/{user}/other_logs/{other_log}');
363 | $this->assertEquals($targetRequest['request']['url']['raw'], '{{base_url}}/example/users/:user/other_logs/:other_log');
364 | }
365 |
366 | public function test_api_resource_routes_set_parameters_correctly_with_camel_case()
367 | {
368 | $this->artisan('export:postman')->assertExitCode(0);
369 |
370 | $collection = collect(json_decode(Storage::get('postman/'.config('api-postman.filename')), true)['item']);
371 |
372 | $targetRequest = $collection
373 | ->where('name', 'example/users/{user}/someLogs/{someLog}')
374 | ->where('request.method', 'PATCH')
375 | ->first();
376 |
377 | $this->assertEquals($targetRequest['name'], 'example/users/{user}/someLogs/{someLog}');
378 | $this->assertEquals($targetRequest['request']['url']['raw'], '{{base_url}}/example/users/:user/someLogs/:someLog');
379 | }
380 |
381 | public static function providerFormDataEnabled(): array
382 | {
383 | return [
384 | [
385 | false,
386 | ],
387 | [
388 | true,
389 | ],
390 | ];
391 | }
392 | }
393 |
--------------------------------------------------------------------------------
/tests/Feature/ExportPostmanWithCacheTest.php:
--------------------------------------------------------------------------------
1 | defineCacheRoutes(<<<'PHP'
18 | group(function () {
20 | Route::get('serialized-route', function () {
21 | return 'Serialized Route';
22 | });
23 | });
24 | PHP);
25 |
26 | config()->set('api-postman.filename', 'test.json');
27 |
28 | Storage::disk()->deleteDirectory('postman');
29 | }
30 |
31 | public function test_cached_export_works()
32 | {
33 | $this->markTestSkipped('Vendor routes are included in the cached routes, so this test fails');
34 |
35 | $this->get('serialized-route')
36 | ->assertOk()
37 | ->assertSee('Serialized Route');
38 |
39 | $this->artisan('export:postman')->assertExitCode(0);
40 |
41 | $collection = json_decode(Storage::get('postman/'.config('api-postman.filename')), true);
42 |
43 | $routes = $this->app['router']->getRoutes()->getRoutesByName();
44 |
45 | // Filter out workbench routes from orchestra/workbench
46 | $routes = array_filter($routes, function ($route) {
47 | return strpos($route->uri(), 'workbench') === false;
48 | });
49 |
50 | $collectionItems = $collection['item'];
51 |
52 | $this->assertCount(count($routes), $collectionItems);
53 |
54 | foreach ($routes as $route) {
55 | $collectionRoute = Arr::first($collectionItems, function ($item) use ($route) {
56 | return $item['name'] == $route->uri();
57 | });
58 | $this->assertNotNull($collectionRoute);
59 | $this->assertTrue(in_array($collectionRoute['request']['method'], $route->methods()));
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/tests/Fixtures/AuditLogController.php:
--------------------------------------------------------------------------------
1 | retrieveRoutes($item);
19 | }
20 |
21 | return $sum;
22 | }
23 |
24 | return 1;
25 | }
26 |
27 | private function countCollectionItems(array $collectionItems)
28 | {
29 | $sum = 0;
30 |
31 | foreach ($collectionItems as $item) {
32 | $sum += $this->retrieveRoutes($item);
33 | }
34 |
35 | return $sum;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/Fixtures/ExampleController.php:
--------------------------------------------------------------------------------
1 | getRequestData();
32 | }
33 |
34 | public function storeWithFormRequest(ExampleFormRequest $request): string
35 | {
36 | return 'storeWithFormRequest';
37 | }
38 |
39 | public function getWithFormRequest(ExampleFormRequest $request): string
40 | {
41 | return 'getWithFormRequest';
42 | }
43 |
44 | /**
45 | * This is the php doc route.
46 | * Which is also multi-line.
47 | *
48 | * and has a blank line.
49 | *
50 | * @param string $non-existing param
51 | */
52 | public function phpDocRoute(): string
53 | {
54 | return 'phpDocRoute';
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/tests/Fixtures/ExampleEvent.js:
--------------------------------------------------------------------------------
1 | console.log("example event");
2 |
--------------------------------------------------------------------------------
/tests/Fixtures/ExampleFormRequest.php:
--------------------------------------------------------------------------------
1 | 'required',
14 | 'field_2' => 'required|integer',
15 | 'field_3' => 'sometimes|integer',
16 | 'field_4' => 'nullable|integer',
17 | 'field_5' => 'required|integer|max:30|min:1',
18 | 'field_6' => new In([1, 2, 3]),
19 | 'field_7' => ['required', new In([1, 2, 3])],
20 | 'field_8' => new UppercaseRule,
21 | 'field_9' => ['required', 'string', new UppercaseRule],
22 | ];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/Fixtures/ExampleModel.php:
--------------------------------------------------------------------------------
1 | request = $request;
14 | }
15 |
16 | public function getRequestData(): array
17 | {
18 | return $this->request->all();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/Fixtures/UppercaseRule.php:
--------------------------------------------------------------------------------
1 | middleware('api')->prefix('example')->name('example.')->group(function ($router) {
18 | $router->get('index', [ExampleController::class, 'index'])->name('index');
19 | $router->get('show', [ExampleController::class, 'show'])->name('show');
20 | $router->post('store', [ExampleController::class, 'store'])->name('store');
21 | $router->delete('delete', [ExampleController::class, 'delete'])->name('delete');
22 | $router->get('showWithReflectionMethod', [ExampleController::class, 'showWithReflectionMethod'])->name('show-with-reflection-method');
23 | $router->post('storeWithFormRequest', [ExampleController::class, 'storeWithFormRequest'])->name('store-with-form-request');
24 | $router->get('getWithFormRequest', [ExampleController::class, 'getWithFormRequest'])->name('get-with-form-request');
25 | $router->get('phpDocRoute', [ExampleController::class, 'phpDocRoute'])->name('php-doc-route');
26 | $router->apiResource('users.audit-logs', AuditLogController::class);
27 | $router->apiResource('users.other_logs', AuditLogController::class);
28 | $router->apiResource('users.someLogs', AuditLogController::class);
29 | });
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/Unit/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/andreaselia/laravel-api-to-postman/24d1fa6764bcc0cfd84bfd745ec7c6f0d7de5776/tests/Unit/.gitkeep
--------------------------------------------------------------------------------