diff --git a/config/api-postman.php b/config/api-postman.php index 7ffd626..0bc7c99 100644 --- a/config/api-postman.php +++ b/config/api-postman.php @@ -35,8 +35,8 @@ | */ - 'structured' => false, - 'crud_folders' => true, + 'structured' => true, + 'crud_folders' => false, /* |-------------------------------------------------------------------------- @@ -117,6 +117,7 @@ | */ + 'formdata' => [ // 'email' => 'john@example.com', // 'password' => 'changeme', diff --git a/src/Commands/ExportPostmanCommand.php b/src/Commands/ExportPostmanCommand.php index f2b2095..6f522f0 100644 --- a/src/Commands/ExportPostmanCommand.php +++ b/src/Commands/ExportPostmanCommand.php @@ -2,6 +2,7 @@ namespace AndreasElia\PostmanGenerator\Commands; +use AndreasElia\PostmanGenerator\Exporter; use Closure; use Illuminate\Console\Command; use Illuminate\Contracts\Config\Repository; @@ -60,421 +61,21 @@ public function __construct(Router $router, Repository $config) $this->config = $config['api-postman']; } - public function handle(): void + public function handle(Exporter $exporter): void { - $this->setFilename(); - $this->setAuthToken(); - $this->initializeStructure(); - - foreach ($this->router->getRoutes() as $route) { - $methods = array_filter($route->methods(), fn ($value) => $value !== 'HEAD'); - $middlewares = $route->gatherMiddleware(); - - foreach ($methods as $method) { - $includedMiddleware = false; - - foreach ($middlewares as $middleware) { - if (in_array($middleware, $this->config['include_middleware'])) { - $includedMiddleware = true; - } - } - - if (empty($middlewares) || ! $includedMiddleware) { - continue; - } - - $requestRules = []; - - $routeAction = $route->getAction(); - - $reflectionMethod = $this->getReflectionMethod($routeAction); - - if (! $reflectionMethod) { - continue; - } - - if ($this->config['enable_formdata']) { - $rulesParameter = collect($reflectionMethod->getParameters()) - ->filter(function ($value, $key) { - $value = $value->getType(); - - return $value && is_subclass_of($value->getName(), FormRequest::class); - }) - ->first(); - - if ($rulesParameter) { - $rulesParameter = $rulesParameter->getType()->getName(); - $rulesParameter = new $rulesParameter; - $rules = method_exists($rulesParameter, 'rules') ? $rulesParameter->rules() : []; - - foreach ($rules as $fieldName => $rule) { - if (is_string($rule)) { - $rule = preg_split('/\s*\|\s*/', $rule); - } - - $printRules = $this->config['print_rules']; - - $requestRules[] = [ - 'name' => $fieldName, - 'description' => $printRules ? $rule : '', - ]; - - if (is_array($rule) && in_array('confirmed', $rule)) { - $requestRules[] = [ - 'name' => $fieldName.'_confirmation', - 'description' => $printRules ? $rule : '', - ]; - } - } - } - } - - $routeHeaders = $this->config['headers']; - - if ($this->token && in_array($this->config['auth_middleware'], $middlewares)) { - switch ($this->authType) { - case 'bearer': - $routeHeaders[] = [ - 'key' => 'Authorization', - 'value' => 'Bearer {{token}}', - ]; - break; - - case 'basic': - $routeHeaders[] = [ - 'key' => 'Authorization', - 'value' => 'Basic {{token}}', - ]; - break; - } - } - - $request = $this->makeRequest($route, $method, $routeHeaders, $requestRules); - - if ($this->isStructured()) { - $routeNames = $route->action['as'] ?? null; - - if (! $routeNames) { - $routeUri = explode('/', $route->uri()); - - // remove "api" from the start - unset($routeUri[0]); - - $routeNames = implode('.', $routeUri); - } - - $routeNames = explode('.', $routeNames); - $routeNames = array_filter($routeNames, function ($value) { - return ! is_null($value) && $value !== ''; - }); - - if (! $this->createCrudFolders()) { - if (in_array(end($routeNames), ['index', 'store', 'show', 'update', 'destroy'])) { - unset($routeNames[array_key_last($routeNames)]); - } - - if ($routeNames[0] == 'api') { - unset($routeNames[0]); - } - } - - $this->buildTree($this->structure, $routeNames, $request); - } else { - $this->structure['item'][] = $request; - } - } - } - - Storage::disk($this->config['disk'])->put($exportName = "postman/$this->filename", json_encode($this->structure)); - - $this->info('Postman Collection Exported: '.storage_path('app/'.$exportName)); - } - - protected function getReflectionMethod(array $routeAction): ?object - { - // Hydrates the closure if it is an instance of Opis\Closure\SerializableClosure - if ($this->containsSerializedClosure($routeAction)) { - $routeAction['uses'] = unserialize($routeAction['uses'])->getClosure(); - } - - if ($routeAction['uses'] instanceof Closure) { - return new ReflectionFunction($routeAction['uses']); - } - - $routeData = explode('@', $routeAction['uses']); - $reflection = new ReflectionClass($routeData[0]); - - if (! $reflection->hasMethod($routeData[1])) { - return null; - } - - return $reflection->getMethod($routeData[1]); - } - - public static function containsSerializedClosure(array $action): bool - { - return is_string($action['uses']) && Str::startsWith($action['uses'], [ - 'C:32:"Opis\\Closure\\SerializableClosure', - 'O:47:"Laravel\SerializableClosure\\SerializableClosure', - 'O:55:"Laravel\\SerializableClosure\\UnsignedSerializableClosure', - ]); - } - - protected function buildTree(array &$routes, array $segments, array $request): void - { - $parent = &$routes; - $destination = end($segments); - - foreach ($segments as $segment) { - $matched = false; - - foreach ($parent['item'] as &$item) { - if ($item['name'] === $segment) { - $parent = &$item; - - if ($segment === $destination) { - $parent['item'][] = $request; - } - - $matched = true; - - break; - } - } - - unset($item); - - if (! $matched) { - $item = [ - 'name' => $segment, - 'item' => $segment === $destination ? [$request] : [], - ]; - - $parent['item'][] = &$item; - $parent = &$item; - } - - unset($item); - } - } - - public function makeRequest($route, $method, $routeHeaders, $requestRules) - { - $printRules = $this->config['print_rules']; - - $uri = Str::of($route->uri())->replaceMatches('/{([[:alnum:]]+)}/', ':$1'); - - $variables = $uri->matchAll('/(?<={)[[:alnum:]]+(?=})/m'); - - $data = [ - 'name' => $route->uri(), - 'request' => [ - 'method' => strtoupper($method), - 'header' => $routeHeaders, - 'url' => [ - 'raw' => '{{base_url}}/'.$uri, - 'host' => ['{{base_url}}'], - 'path' => $uri->explode('/')->filter(), - 'variable' => $variables->transform(function ($variable) { - return ['key' => $variable, 'value' => '']; - })->all(), - ], - ], - ]; - - if ($requestRules) { - $ruleData = []; - - foreach ($requestRules as $rule) { - $ruleData[] = [ - 'key' => $rule['name'], - 'value' => $this->config['formdata'][$rule['name']] ?? null, - 'type' => 'text', - 'description' => $printRules ? $this->parseRulesIntoHumanReadable($rule['name'], $rule['description']) : '', - ]; - } - - $data['request']['body'] = [ - 'mode' => 'urlencoded', - 'urlencoded' => $ruleData, - ]; - } - - return $data; - } - - /** - * Process a rule set and utilize the Validator to create human readable descriptions - * to help users provide valid data. - * - * @param $attribute - * @param $rules - * @return string - */ - protected function parseRulesIntoHumanReadable($attribute, $rules): string - { - // ... bail if user has asked for non interpreted strings: - if (! $this->config['rules_to_human_readable']) { - foreach ($rules as $i => $rule) { - // because we don't support custom rule classes, we remove them from the rules - if (is_subclass_of($rule, Rule::class)) { - unset($rules[$i]); - } - } - - return is_array($rules) ? implode(', ', $rules) : $this->safelyStringifyClassBasedRule($rules); - } - - /* - * An object based rule is presumably a Laravel default class based rule or one that implements the Illuminate - * Rule interface. Lets try to safely access the string representation... - */ - if (is_object($rules)) { - $rules = [$this->safelyStringifyClassBasedRule($rules)]; - } - - /* - * Handle string based rules (e.g. required|string|max:30) - */ - if (is_array($rules)) { - foreach ($rules as $i => $rule) { - if (is_object($rule)) { - unset($rules[$i]); - } - } - - $this->validator = Validator::make([], [$attribute => implode('|', $rules)]); - - foreach ($rules as $rule) { - [$rule, $parameters] = ValidationRuleParser::parse($rule); - - $this->validator->addFailure($attribute, $rule, $parameters); - } - - $messages = $this->validator->getMessageBag()->toArray()[$attribute]; - - if (is_array($messages)) { - $messages = $this->handleEdgeCases($messages); - } - - return implode(', ', is_array($messages) ? $messages : $messages->toArray()); - } - - // ...safely return a safe value if we encounter neither a string or object based rule set: - return ''; - } - - protected function initializeStructure(): void - { - $this->structure = [ - 'variable' => [ - [ - 'key' => 'base_url', - 'value' => $this->config['base_url'], - ], - ], - 'info' => [ - 'name' => $this->filename, - 'schema' => 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json', - ], - 'item' => [], - 'event' => [], - ]; - - $prerequestPath = $this->config['prerequest_script']; - $testPath = $this->config['test_script']; - - if ($prerequestPath || $testPath) { - $scripts = [ - 'prerequest' => $prerequestPath, - 'test' => $testPath, - ]; - - foreach ($scripts as $type => $path) { - if (file_exists($path)) { - $this->structure['event'][] = [ - 'listen' => $type, - 'script' => [ - 'type' => 'text/javascript', - 'exec' => file_get_contents($path), - ], - ]; - } - } - } - - if ($this->token) { - $this->structure['variable'][] = [ - 'key' => 'token', - 'value' => $this->token, - ]; - } - } - - protected function setFilename() - { - $this->filename = str_replace( + $filename = str_replace( ['{timestamp}', '{app}'], [date('Y_m_d_His'), Str::snake(config('app.name'))], - $this->config['filename'] + config('api-postman.filename') ); - } - - protected function setAuthToken() - { - foreach (self::AUTH_OPTIONS as $option) { - if ($token = $this->option($option)) { - $this->token = $token ?? null; - $this->authType = $option; - } - } - } - - protected function isStructured() - { - return $this->config['structured']; - } - - protected function createCrudFolders() - { - return $this->config['crud_folders']; - } - - /** - * Certain fields are not handled via the normal throw failure method in the validator - * We need to add a human readable message. - * - * @param array $messages - * @return array - */ - protected function handleEdgeCases(array $messages): array - { - foreach ($messages as $key => $message) { - if ($message === 'validation.nullable') { - $messages[$key] = '(Nullable)'; - continue; - } - if ($message === 'validation.sometimes') { - $messages[$key] = '(Optional)'; - } - } + $exporter + ->to($filename) + ->export(); - return $messages; - } - - /** - * In this case we have received what is most likely a Rule Object but are not certain. - * - * @param $probableRule - * @return string - */ - protected function safelyStringifyClassBasedRule($probableRule): string - { - if (! is_object($probableRule) || is_subclass_of($probableRule, Rule::class) || ! method_exists($probableRule, '__toString')) { - return ''; - } + Storage::disk(config('api-postman.disk')) + ->put('postman/'.$filename, $exporter->getOutput()); - return (string) $probableRule; + $this->info('Postman Collection Exported: '.storage_path('app/postman/'.$filename)); } } diff --git a/src/Concerns/HasAuthentication.php b/src/Concerns/HasAuthentication.php new file mode 100644 index 0000000..d45b528 --- /dev/null +++ b/src/Concerns/HasAuthentication.php @@ -0,0 +1,26 @@ +config['authentication']; + + if ($config['method']) { + $className = Str::of('AndreasElia\\PostmanGenerator\\Authentication\\') + ->append(ucfirst($config['method'])) + ->toString(); + + $this->authentication = new $className; + } + + return $this; + } +} diff --git a/src/Exporter.php b/src/Exporter.php new file mode 100644 index 0000000..b53b62c --- /dev/null +++ b/src/Exporter.php @@ -0,0 +1,268 @@ +config = $config['api-postman']; + } + + public function to(string $filename): self + { + $this->filename = $filename; + + return $this; + } + +// public function token(string $token): self +// { +// $this->token = $token; +// +// return $this; +// } + + public function getOutput() + { + return json_encode($this->output); + } + + public function export(): void + { + $this->resolveAuth(); + + $this->output = $this->generateStructure(); + } + + protected function generateStructure(): array + { + $this->output = [ + 'variable' => [ + [ + 'key' => 'base_url', + 'value' => $this->config['base_url'], + ], + ], + 'info' => [ + 'name' => $this->filename, + 'schema' => 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json', + ], + 'item' => [], + ]; + + if ($token = $this->config['authentication']['token']) { + $this->output['variable'][] = [ + 'key' => 'token', + 'value' => $token, + ]; + } + + dd(app(RouteProcessor::class)->process($this->output)); + + return app(RouteProcessor::class)->process($this->output); + + foreach ($router->getRoutes() as $route) { + $methods = array_filter($route->methods(), fn ($value) => $value !== 'HEAD'); + $middlewares = $route->gatherMiddleware(); + + foreach ($methods as $method) { + $includedMiddleware = false; + + foreach ($middlewares as $middleware) { + if (in_array($middleware, $this->config['include_middleware'])) { + $includedMiddleware = true; + } + } + + if (empty($middlewares) || ! $includedMiddleware) { + continue; + } + + $requestRules = []; + + $reflectionMethod = $this->getReflectionMethod($route->getAction()); + + if (! $reflectionMethod) { + continue; + } + +// dd((new FormDataProcessor)->process($reflectionMethod)); + +// if ($this->config['enable_formdata']) { +// $rulesParameter = collect($reflectionMethod->getParameters()) +// ->filter(function ($value, $key) { +// $value = $value->getType(); +// +// return $value && is_subclass_of($value->getName(), FormRequest::class); +// }) +// ->first(); +// +// if ($rulesParameter) { +// $rulesParameter = $rulesParameter->getType()->getName(); +// $rulesParameter = new $rulesParameter; +// $rules = method_exists($rulesParameter, 'rules') ? $rulesParameter->rules() : []; +// +// foreach ($rules as $fieldName => $rule) { +// if (is_string($rule)) { +// $rule = preg_split('/\s*\|\s*/', $rule); +// } +// +// $printRules = $this->config['print_rules']; +// +// $requestRules[] = [ +// 'name' => $fieldName, +// 'description' => $printRules ? $rule : '', +// ]; +// +// if (is_array($rule) && in_array('confirmed', $rule)) { +// $requestRules[] = [ +// 'name' => $fieldName.'_confirmation', +// 'description' => $printRules ? $rule : '', +// ]; +// } +// } +// } +// } + + $routeHeaders = $this->config['headers']; + + if ($this->authentication && in_array($this->config['auth_middleware'], $middlewares)) { + $routeHeaders[] = $this->authentication->toArray(); + } + + $request = (new RouteRequest)($route, $method, $routeHeaders, $requestRules); + + if ($this->config['structured']) { + $routeNames = $route->action['as'] ?? null; + + if (! $routeNames) { + $routeUri = explode('/', $route->uri()); + + // remove "api" from the start + unset($routeUri[0]); + + $routeNames = implode('.', $routeUri); + } + + $routeNames = explode('.', $routeNames); + $routeNames = array_filter($routeNames, function ($value) { + return ! is_null($value) && $value !== ''; + }); + + if (! $this->config['crud_folders']) { + if (in_array(end($routeNames), ['index', 'store', 'show', 'update', 'destroy'])) { + unset($routeNames[array_key_last($routeNames)]); + } + + if ($routeNames[0] == 'api') { + unset($routeNames[0]); + } + } + + $this->buildTree($this->output, $routeNames, $request); + } else { + $this->output['item'][] = $request; + } + } + } + + dd($this->output); + + return $this->output; + } + + protected function buildTree(array &$routes, array $segments, array $request): void + { + $parent = &$routes; + $destination = end($segments); + + foreach ($segments as $segment) { + $matched = false; + + foreach ($parent['item'] as &$item) { + if ($item['name'] === $segment) { + $parent = &$item; + + if ($segment === $destination) { + $parent['item'][] = $request; + } + + $matched = true; + + break; + } + } + + unset($item); + + if (! $matched) { + $item = [ + 'name' => $segment, + 'item' => $segment === $destination ? [$request] : [], + ]; + + $parent['item'][] = &$item; + $parent = &$item; + } + + unset($item); + } + } + + /** + * @throws ReflectionException + */ + protected function getReflectionMethod(array $routeAction): ?object + { + if ($this->containsSerializedClosure($routeAction)) { + $routeAction['uses'] = unserialize($routeAction['uses'])->getClosure(); + } + + if ($routeAction['uses'] instanceof Closure) { + return new ReflectionFunction($routeAction['uses']); + } + + $routeData = explode('@', $routeAction['uses']); + $reflection = new ReflectionClass($routeData[0]); + + if (! $reflection->hasMethod($routeData[1])) { + return null; + } + + return $reflection->getMethod($routeData[1]); + } + + public static function containsSerializedClosure(array $action): bool + { + return is_string($action['uses']) && Str::startsWith($action['uses'], [ + 'C:32:"Opis\\Closure\\SerializableClosure', + 'O:47:"Laravel\SerializableClosure\\SerializableClosure', + 'O:55:"Laravel\\SerializableClosure\\UnsignedSerializableClosure', + ]); + } +} diff --git a/src/Processors/FormDataProcessor.php b/src/Processors/FormDataProcessor.php new file mode 100644 index 0000000..916f05c --- /dev/null +++ b/src/Processors/FormDataProcessor.php @@ -0,0 +1,52 @@ +getParameters()) + ->first(function ($value) { + $value = $value->getType(); + + return $value && is_subclass_of($value->getName(), FormRequest::class); + }); + + if ($rulesParameter) { + /** @var FormRequest $class */ + $class = new ($rulesParameter->getType()->getName()); + + $classRules = method_exists($class, 'rules') ? $class->rules() : []; + + foreach ($classRules as $fieldName => $rule) { + if (is_string($rule)) { + $rule = preg_split('/\s*\|\s*/', $rule); + } + + $printRules = config('api-postman.print_rules'); + + $rules[] = [ + 'name' => $fieldName, + 'description' => $printRules ? $rule : '', + ]; + + if (is_array($rule) && in_array('confirmed', $rule)) { + $rules[] = [ + 'name' => $fieldName.'_confirmation', + 'description' => $printRules ? $rule : '', + ]; + } + } + } + + return $rules; + } +} diff --git a/src/Processors/RouteProcessor.php b/src/Processors/RouteProcessor.php new file mode 100644 index 0000000..0fa03dc --- /dev/null +++ b/src/Processors/RouteProcessor.php @@ -0,0 +1,319 @@ +config = $config['api-postman']; + + $this->routes = $router->getRoutes(); + + $this->resolveAuth(); + } + + public function process(array $output): array + { + $this->output = $output; + + /** @var Route $route */ + foreach ($this->routes as $route) { + $this->processRoute($route); + } + + return $this->output; + } + + /** + * @throws \ReflectionException + */ + protected function processRoute(Route $route) + { + $methods = array_filter($route->methods(), fn ($value) => $value !== 'HEAD'); + $middlewares = $route->gatherMiddleware(); + + foreach ($methods as $method) { + $includedMiddleware = false; + + foreach ($middlewares as $middleware) { + if (in_array($middleware, $this->config['include_middleware'])) { + $includedMiddleware = true; + } + } + + if (empty($middlewares) || ! $includedMiddleware) { + continue; + } + + $reflectionMethod = $this->getReflectionMethod($route->getAction()); + + if (! $reflectionMethod) { + continue; + } + + $routeHeaders = $this->config['headers']; + + if ($this->authentication && in_array($this->config['auth_middleware'], $middlewares)) { + $routeHeaders[] = $this->authentication->toArray(); + } + + $uri = Str::of($route->uri()) + ->after('/') + ->replaceMatches('/{([[:alnum:]]+)}/', ':$1'); + +// if (!$uri->toString()) { +// return []; +// } + + $data = [ + 'name' => $route->uri(), + 'request' => $this->processRequest($method, $uri, (new FormDataProcessor)->process($reflectionMethod)), + 'response' => [], + ]; + + if ($this->config['structured']) { + $routeNameSegments = ( + $route->getName() + ? Str::of($route->getName())->explode('.') + : Str::of($route->uri())->after('api/')->explode('/') + )->filter(fn ($value) => ! is_null($value) && $value !== ''); + + if (! $this->config['crud_folders']) { + if (in_array($routeNameSegments->last(), ['index', 'store', 'show', 'update', 'destroy'])) { + $routeNameSegments->forget($routeNameSegments->count() - 1); + } + } + + $this->buildTree($this->output, $routeNameSegments->all(), $data); + } else { + $this->output['item'][] = $data; + } + } + } + + protected function processRequest(string $method, Stringable $uri, Collection $rules): array + { + $variables = $uri->matchAll('/(?<={)[[:alnum:]]+(?=})/m'); + + return collect([ + 'method' => strtoupper($method), +// 'header' => $headers, + 'url' => [ + 'raw' => '{{base_url}}/'.$uri, + 'host' => ['{{base_url}}'], + 'path' => $uri->explode('/')->filter()->all(), + 'variable' => $variables->transform(function ($variable) { + return ['key' => $variable, 'value' => '']; + })->all(), + ], + ]) + ->when($rules, function (Collection $collection, Collection $rules) { + if ($rules->isEmpty()) { + return $collection; + } + + return $collection->put('body', [ + 'mode' => 'urlencoded', + 'urlencoded' => $rules->map(fn ($rule) => [ + 'key' => $rule['name'], + 'value' => $this->config['formdata'][$rule['name']] ?? null, + 'type' => 'text', + 'description' => false ? $this->parseRulesIntoHumanReadable($rule['name'], $rule['description']) : null, + ]), + ]); + }) + ->all(); + } + + protected function processResponse(string $method, array $action): array + { + return [ + 'code' => 200, + 'body' => [ + 'mode' => 'raw', + 'raw' => '', + ], + ]; + } + + /** + * @throws \ReflectionException + */ + private function getReflectionMethod(array $routeAction): ?object + { + if ($this->containsSerializedClosure($routeAction)) { + $routeAction['uses'] = unserialize($routeAction['uses'])->getClosure(); + } + + if ($routeAction['uses'] instanceof Closure) { + return new ReflectionFunction($routeAction['uses']); + } + + $routeData = explode('@', $routeAction['uses']); + $reflection = new ReflectionClass($routeData[0]); + + if (! $reflection->hasMethod($routeData[1])) { + return null; + } + + return $reflection->getMethod($routeData[1]); + } + + private function containsSerializedClosure(array $action): bool + { + return is_string($action['uses']) && Str::startsWith($action['uses'], [ + 'C:32:"Opis\\Closure\\SerializableClosure', + 'O:47:"Laravel\SerializableClosure\\SerializableClosure', + 'O:55:"Laravel\\SerializableClosure\\UnsignedSerializableClosure', + ]); + } + + protected function buildTree(array &$routes, array $segments, array $request): void + { + $parent = &$routes; + $destination = end($segments); + + foreach ($segments as $segment) { + $matched = false; + + foreach ($parent['item'] as &$item) { + if ($item['name'] === $segment) { + $parent = &$item; + + if ($segment === $destination) { + $parent['item'][] = $request; + } + + $matched = true; + + break; + } + } + + unset($item); + + if (! $matched) { + $item = [ + 'name' => $segment, + 'item' => $segment === $destination ? [$request] : [], + ]; + + $parent['item'][] = &$item; + $parent = &$item; + } + + unset($item); + } + } + + protected function parseRulesIntoHumanReadable($attribute, $rules): string + { + // ... bail if user has asked for non interpreted strings: + if (! $this->config['rules_to_human_readable']) { + foreach ($rules as $i => $rule) { + // because we don't support custom rule classes, we remove them from the rules + if (is_subclass_of($rule, Rule::class)) { + unset($rules[$i]); + } + } + + return is_array($rules) ? implode(', ', $rules) : $this->safelyStringifyClassBasedRule($rules); + } + + /* + * An object based rule is presumably a Laravel default class based rule or one that implements the Illuminate + * Rule interface. Lets try to safely access the string representation... + */ + if (is_object($rules)) { + $rules = [$this->safelyStringifyClassBasedRule($rules)]; + } + + /* + * Handle string based rules (e.g. required|string|max:30) + */ + if (is_array($rules)) { + foreach ($rules as $i => $rule) { + if (is_object($rule)) { + unset($rules[$i]); + } + } + + $validator = Validator::make([], [$attribute => implode('|', $rules)]); + + foreach ($rules as $rule) { + [$rule, $parameters] = ValidationRuleParser::parse($rule); + + $validator->addFailure($attribute, $rule, $parameters); + } + + $messages = $validator->getMessageBag()->toArray()[$attribute]; + + if (is_array($messages)) { + $messages = $this->handleEdgeCases($messages); + } + + return implode(', ', is_array($messages) ? $messages : $messages->toArray()); + } + + // ...safely return a safe value if we encounter neither a string or object based rule set: + return ''; + } + + protected function handleEdgeCases(array $messages): array + { + foreach ($messages as $key => $message) { + if ($message === 'validation.nullable') { + $messages[$key] = '(Nullable)'; + continue; + } + + if ($message === 'validation.sometimes') { + $messages[$key] = '(Optional)'; + } + } + + return $messages; + } + + /** + * In this case we have received what is most likely a Rule Object but are not certain. + * + * @param $probableRule + * @return string + */ + protected function safelyStringifyClassBasedRule($probableRule): string + { + if (! is_object($probableRule) || is_subclass_of($probableRule, Rule::class) || ! method_exists($probableRule, '__toString')) { + return ''; + } + + return (string) $probableRule; + } +} diff --git a/src/RouteRequest.php b/src/RouteRequest.php new file mode 100644 index 0000000..26b7401 --- /dev/null +++ b/src/RouteRequest.php @@ -0,0 +1,132 @@ +uri())->replaceMatches('/{([[:alnum:]]+)}/', ':$1'); + + $variables = $uri->matchAll('/(?<={)[[:alnum:]]+(?=})/m'); + + $data = [ + 'name' => $route->uri(), + 'request' => [ + 'method' => strtoupper($method), + 'header' => $headers, + 'url' => [ + 'raw' => '{{base_url}}/'.$uri, + 'host' => ['{{base_url}}'], + 'path' => $uri->explode('/')->filter(), + 'variable' => $variables->transform(function ($variable) { + return ['key' => $variable, 'value' => '']; + })->all(), + ], + ], + 'response' => [], + ]; + + if ($rules) { + $ruleData = []; + + foreach ($rules as $rule) { + $value = config('api-postman.formdata')[$rule['name']] ?? null; + $description = $printRules + ? $this->parseRulesIntoHumanReadable($rule['name'], $rule['description']) + : ''; + + $ruleData[] = [ + 'key' => $rule['name'], + 'value' => $value, + 'type' => 'text', + 'description' => $description, + ]; + } + + $data['request']['body'] = [ + 'mode' => 'urlencoded', + 'urlencoded' => $ruleData, + ]; + } + + return $data; + } + + protected function parseRulesIntoHumanReadable($attribute, $rules): string + { + if (! config('api-postman.rules_to_human_readable')) { + return is_array($rules) + ? implode(', ', $rules) + : $this->safelyStringifyClassBasedRule($rules); + } + + if (is_object($rules)) { + $rules = [$this->safelyStringifyClassBasedRule($rules)]; + } + + if (! is_array($rules)) { + return ''; + } + + $this->validator = Validator::make([], [$attribute => implode('|', $rules)]); + + foreach ($rules as $rule) { + [$rule, $parameters] = ValidationRuleParser::parse($rule); + + $this->validator->addFailure($attribute, $rule, $parameters); + } + + $messages = $this->validator->getMessageBag()->toArray()[$attribute]; + + if (is_array($messages)) { + $messages = $this->handleEdgeCases($messages); + } + + return implode(', ', is_array($messages) ? $messages : $messages->toArray()); + } + + /** + * Certain fields are not handled via the normal throw failure method in the validator + * We need to add a human readable message. + * + * @param array $messages + * @return array + */ + protected function handleEdgeCases(array $messages): array + { + foreach ($messages as $key => $message) { + if ($message === 'validation.nullable') { + $messages[$key] = '(Nullable)'; + continue; + } + + if ($message === 'validation.sometimes') { + $messages[$key] = '(Optional)'; + } + } + + return $messages; + } + + /** + * In this case we have received what is most likely a Rule Object but are not certain. + * + * @param $probableRule + * @return string + */ + protected function safelyStringifyClassBasedRule($probableRule): string + { + if (! is_object($probableRule) || is_subclass_of($probableRule, Rule::class) || ! method_exists($probableRule, '__toString')) { + return ''; + } + + return (string) $probableRule; + } +}