diff --git a/composer.json b/composer.json index 7fb96db..8d5276c 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ "symfony/console": "^2.8", "pimple/pimple": "^3.0", "league/flysystem": "^1.0", - "composer/semver": "^1.4" + "composer/semver": "^1.4", + "gossi/docblock": "^1.5" }, "require-dev": { "squizlabs/php_codesniffer": "^2.7", diff --git a/resources/examples/Showtimes/Controllers/Movie.php b/resources/examples/Showtimes/Controllers/Movie.php index c4c767b..728a8fd 100644 --- a/resources/examples/Showtimes/Controllers/Movie.php +++ b/resources/examples/Showtimes/Controllers/Movie.php @@ -14,7 +14,7 @@ class Movie * @api-label Get a single movie. * * @api-uri:public {Movies} /movies/+id - * @api-uriSegment {/movies/+id} {integer} id Movie ID + * @api-uriSegment {/movies/+id} id (integer) - Movie ID * * @api-contentType application/json * @@ -34,22 +34,31 @@ public function GET() * @api-label Update a movie. * * @api-uri:public {Movies} /movies/+id - * @api-uriSegment {/movies/+id} {integer} id Movie ID + * @api-uriSegment {/movies/+id} id (integer) - Movie ID * * @api-contentType application/json * @api-scope edit * @api-minVersion 1.1 * - * @api-param:public {string} name Name of the movie. - * @api-param:public {string} description Description, or tagline, for the movie. - * @api-param:public {string} runtime (optional) Movie runtime, in `HHhr MMmin` format. - * @api-param:public {string} content_rating [G|PG|PG-13|R|NC-17|X|NR|UR] (optional) MPAA rating - * @api-param:public {array} genres (optional) Array of movie genres. - * @api-param:public {string} trailer (optional) Trailer URL - * @api-param:public {string} director (optional) Name of the director. - * @api-param:public {array} cast (optional) Array of names of the cast. - * @api-param:public {boolean} is_kid_friendly (optional) Is this movie kid friendly? - * @api-param:public {integer} rotten_tomatoes_score (optional) Rotten Tomatoes score + * @api-param:public name (string, required) - Name of the movie. + * @api-param:public description (string, required) - Description, or tagline, for the movie. + * @api-param:public runtime (string, optional) - Movie runtime, in `HHhr MMmin` format. + * @api-param:public content_rating (string, optional) - MPAA rating + * + Members + * - `G` + * - `PG` + * - `PG-13` + * - `R` + * - `NC-17` + * - `X` + * - `NR` + * - `UR` + * @api-param:public genres (array, optional) - Array of movie genres. + * @api-param:public trailer (string, optional) - Trailer URL + * @api-param:public director (string, optional) - Name of the director. + * @api-param:public cast (array, optional) - Array of names of the cast. + * @api-param:public is_kid_friendly (boolean, optional) - Is this movie kid friendly? + * @api-param:public rotten_tomatoes_score (integer, optional) - Rotten Tomatoes score * * @api-return:public {object} \Mill\Examples\Showtimes\Representations\Movie * @@ -59,7 +68,7 @@ public function GET() * @api-throws:public {404} \Mill\Examples\Showtimes\Representations\Error If the movie could not be found. * * @api-version >=1.1.1 - * @api-param:public {string} imdb (optional) IMDB URL + * @api-param:public imdb (string, optional) - IMDB URL */ public function PATCH() { @@ -72,7 +81,7 @@ public function PATCH() * @api-label Delete a movie. * * @api-uri:private {Movies} /movies/+id - * @api-uriSegment {/movies/+id} {integer} id Movie ID + * @api-uriSegment {/movies/+id} id (integer) - Movie ID * * @api-contentType application/json * @api-scope delete diff --git a/resources/examples/Showtimes/Controllers/Movies.php b/resources/examples/Showtimes/Controllers/Movies.php index ecb74ee..2f23c16 100644 --- a/resources/examples/Showtimes/Controllers/Movies.php +++ b/resources/examples/Showtimes/Controllers/Movies.php @@ -15,7 +15,7 @@ class Movies * * @api-contentType application/json * - * @api-param:public {string} location Location you want movies for. + * @api-param:public location (string) - Location you want movies for. * * @api-return:public {collection} \Mill\Examples\Showtimes\Representations\Movie * @@ -36,15 +36,24 @@ public function GET() * @api-contentType application/json * @api-scope create * - * @api-param:public {string} name Name of the movie. - * @api-param:public {string} description Description, or tagline, for the movie. - * @api-param:public {string} runtime (optional) Movie runtime, in `HHhr MMmin` format. - * @api-param:public {string} content_rating [G|PG|PG-13|R|NC-17|X|NR|UR] (optional) MPAA rating - * @api-param:public {array} genres (optional) Array of movie genres. - * @api-param:public {string} director (optional) Name of the director. - * @api-param:public {array} cast (optional) Array of names of the cast. - * @api-param:public {boolean} is_kid_friendly (optional) Is this movie kid friendly? - * @api-param:public {integer} rotten_tomatoes_score (optional) Rotten Tomatoes score + * @api-param:public name (string, required) - Name of the movie. + * @api-param:public name (string, required) - Description, or tagline, for the movie. + * @api-param:public runtime (string, optional) - Movie runtime, in `HHhr MMmin` format. + * @api-param:public content_rating (string, optional) - MPAA rating + * + Members + * - `G` + * - `PG` + * - `PG-13` + * - `R` + * - `NC-17` + * - `X` + * - `NR` + * - `UR` + * @api-param:public genres (array, optional) - Array of movie genres. + * @api-param:public director (string, optional) - Name of the director. + * @api-param:public cast (array, optional) - Array of names of the cast. + * @api-param:public is_kid_friendly (boolean, optional) - Is this movie kid friendly? + * @api-param:public rotten_tomatoes_score (integer, optional) - Rotten Tomatoes score * * @api-return:public {object} \Mill\Examples\Showtimes\Representations\Movie * @@ -53,8 +62,8 @@ public function GET() * @api-throws:public {400} \Mill\Examples\Showtimes\Representations\Error If the IMDB URL could not be validated. * * @api-version >=1.1 - * @api-param:public {string} imdb (optional) IMDB URL - * @api-param:public {string} trailer (optional) Trailer URL + * @api-param:public imdb (string, optional) - IMDB URL + * @api-param:public trailer (string, optional) - Trailer URL */ public function POST() { diff --git a/resources/examples/Showtimes/Controllers/Theater.php b/resources/examples/Showtimes/Controllers/Theater.php index 258ff7e..adae4f0 100644 --- a/resources/examples/Showtimes/Controllers/Theater.php +++ b/resources/examples/Showtimes/Controllers/Theater.php @@ -14,7 +14,7 @@ class Theater * @api-label Get a single movie theater * * @api-uri:public {Theaters} /theaters/+id - * @api-uriSegment {/theaters/+id} {integer} id Theater ID + * @api-uriSegment {/theaters/+id} id (integer) - Theater ID * * @api-contentType application/json * @@ -35,14 +35,14 @@ public function GET() * @api-label Update a movie theater * * @api-uri:public {Theaters} /theaters/+id - * @api-uriSegment {/theaters/+id} {integer} id Theater ID + * @api-uriSegment {/theaters/+id} id (integer) - Theater ID * * @api-contentType application/json * @api-scope create * - * @api-param:public {string} name Name of the theater. - * @api-param:public {string} address Theater address - * @api-param:public {string} phone_number Theater phone number + * @api-param:public name (string, required) - Name of the theater. + * @api-param:public address (string, required) - Theater address + * @api-param:public phone_number (string, required) - Theater phone number * * @api-return:public {object} \Mill\Examples\Showtimes\Representations\Theater * @@ -62,7 +62,7 @@ public function PATCH() * @api-label Delete a movie movie. * * @api-uri:private {Theaters} /theaters/+id - * @api-uriSegment {/theaters/+id} {integer} id Theater ID + * @api-uriSegment {/theaters/+id} id (integer) - Theater ID * * @api-contentType application/json * @api-scope delete diff --git a/resources/examples/Showtimes/Controllers/Theaters.php b/resources/examples/Showtimes/Controllers/Theaters.php index 117fc83..7500b31 100644 --- a/resources/examples/Showtimes/Controllers/Theaters.php +++ b/resources/examples/Showtimes/Controllers/Theaters.php @@ -15,7 +15,7 @@ class Theaters * * @api-contentType application/json * - * @api-param:public {string} location Location you want theaters in. + * @api-param:public location (string, required) - Location you want theaters in. * * @api-return:public {collection} \Mill\Examples\Showtimes\Representations\Theater * @@ -36,9 +36,9 @@ public function GET() * @api-contentType application/json * @api-scope create * - * @api-param:public {string} name Name of the theater. - * @api-param:public {string} address Theater address - * @api-param:public {string} phone_number Theater phone number + * @api-param:public name (string, required) - Name of the theater. + * @api-param:public address (string, required) - Theater address + * @api-param:public phone_number (string, required) - Theater phone number * * @api-return:public {object} \Mill\Examples\Showtimes\Representations\Theater * diff --git a/src/Exceptions/Resource/Annotations/InvalidMSONSyntaxException.php b/src/Exceptions/Resource/Annotations/InvalidMSONSyntaxException.php new file mode 100644 index 0000000..7854be2 --- /dev/null +++ b/src/Exceptions/Resource/Annotations/InvalidMSONSyntaxException.php @@ -0,0 +1,34 @@ +annotation = $annotation; + $exception->docblock = $docblock; + $exception->class = $class; + $exception->method = $method; + + return $exception; + } +} diff --git a/src/Exceptions/Resource/Annotations/UnsupportedTypeException.php b/src/Exceptions/Resource/Annotations/UnsupportedTypeException.php index 3c34620..6d79234 100644 --- a/src/Exceptions/Resource/Annotations/UnsupportedTypeException.php +++ b/src/Exceptions/Resource/Annotations/UnsupportedTypeException.php @@ -14,7 +14,7 @@ class UnsupportedTypeException extends \Exception public static function create($annotation, $class, $method) { $message = sprintf( - 'The type on `@api-param %s`in %s::%s is unsupported. Please check the documentation for supported types.', + 'The type on `@api-param %s` in %s::%s is unsupported. Please check the documentation for supported types.', $annotation, $class, $method diff --git a/src/Parser.php b/src/Parser.php index 953eda8..5a2eacb 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -1,9 +1,12 @@ (:\w+)+)?/u'; const ANNOTATION_REGEX = '/^\s?@api-(\w+)((:\w+)+)?(\n|\s*([^\n]*))/m'; /** @@ -93,13 +97,19 @@ protected function parseDocblock($docblock, $parse_description = true) $annotations = []; $matches = null; - $docblock = self::cleanDocblock($docblock); + $parser = self::getAnnotationsFromDocblock($docblock); + $tags = $parser->getTags(); + if (!empty($tags)) { + $annotations = $this->parseAnnotations($tags, $original_docblock); + } + + /*$docblock = self::cleanDocblock($docblock); $matches = self::getAnnotationsFromDocblock($docblock); if (is_array($matches)) { $docblock = preg_replace('/^\s?@(\w+)\s*([^\n]*)/m', '', $docblock); $annotations = $this->parseAnnotations($matches, $original_docblock); - } + }*/ // Only parse out a `description` annotation if we need to (like in the instance of not parsing a // representation). @@ -107,6 +117,14 @@ protected function parseDocblock($docblock, $parse_description = true) return $annotations; } + + $description = $parser->getLongDescription(); + if (empty($long_description)) { + $description = $parser->getShortDescription(); + } + + /*$description = $parser->getShortDescription(); + // If there's anything left over, clean it up, and store it as the `description` annotation. // // We're doing this instead of having a more structured `@description` annotation because matching multiple @@ -132,7 +150,7 @@ protected function parseDocblock($docblock, $parse_description = true) $description = rtrim($description); // Trim any empty lines off the front, but leave the indent level if there is one. - $description = preg_replace('/^\s*\n/', '', $description); + $description = preg_replace('/^\s*\n/', '', $description);*/ if (!empty($description)) { $annotations['description'][] = $this->buildAnnotationData('description', null, $description); @@ -144,15 +162,52 @@ protected function parseDocblock($docblock, $parse_description = true) /** * Parse a group of our custom annotations. * - * @param array $matches - * @param string $original_docblock + * @param ArrayList $tags * @return array */ - protected function parseAnnotations($matches, $original_docblock) + protected function parseAnnotations(ArrayList $tags) { $annotations = []; $version = null; - foreach ($matches as $match) { + + /** @var \gossi\docblock\tags\UnknownTag $tag */ + foreach ($tags as $tag) { + $annotation = $tag->getTagName(); + $data = $tag->getDescription(); + $decorators = null; + + // If this isn't a Mill annotation, then ignore it. + if (substr($annotation, 0, 4) !== 'api-') { + continue; + } + + $annotation = substr($annotation, 4); + + preg_match_all(self::REGEX_DECORATOR, $data, $matches); + if (!empty($matches['decorator'][0])) { + $decorators = $matches['decorator'][0]; + $data = preg_replace(self::REGEX_DECORATOR, '', $data); + } + + $data = trim($data); + switch ($annotation) { + // Handle the `@api-version` annotation block. + case 'version': + $version = new Version($data, $this->controller, $this->method); + break; + + // Parse all other annotations. + default: + $annotations[$annotation][] = $this->buildAnnotationData( + $annotation, + $decorators, + $data, + $version + ); + } + } + + /*foreach ($matches as $match) { list($_, $annotation, $decorators, $_last_decorator, $data) = $match; if ($annotation !== 'version' && empty($annotations[$annotation])) { @@ -175,7 +230,7 @@ protected function parseAnnotations($matches, $original_docblock) $version ); } - } + }*/ return $annotations; } @@ -194,6 +249,11 @@ private function buildAnnotationData($name, $decorators, $data, Version $version { $class = $this->getAnnotationClass($name); + // If this annotation class does not support MSON, then let's clean up any multi-line content within its data. + if (!$class::SUPPORTS_MSON) { + $data = preg_replace(MSON::REGEX_CLEAN_MULTILINE, ' ', $data); + } + /** @var Annotation $annotation */ $annotation = new $class($data, $this->controller, $this->method, $version); @@ -257,50 +317,13 @@ private function getAnnotationClass($annotation) } /** - * Clean a supplied docblock by removing comments and normalizing multi-line annotations. - * - * @param string $docblock - * @return string - */ - public static function cleanDocblock($docblock) - { - // Strip off comments. - $docblock = trim($docblock); - $docblock = preg_replace('@^/\*\*@', '', $docblock); - $docblock = preg_replace('@\*/$@', '', $docblock); - $docblock = preg_replace('@^\s*\*@m', '', $docblock); - - // Normalize multi-line annotations. - $lines = explode("\n", $docblock); - /** @var boolean|string $last */ - $last = false; - foreach ($lines as $k => $line) { - if (preg_match('/^\s?@\w/i', $line)) { - $last = $k; - } elseif (preg_match('/^\s*$/', $line)) { - $last = false; - } elseif ($last !== false) { - $lines[$last] = rtrim($lines[$last]).' '.trim($line); - unset($lines[$k]); - } - } - - return implode("\n", $lines); - } - - /** - * Parse out annotations from a supplied, cleaned, docblock. + * Parse out annotations from a supplied docblock. * * @param string $docblock - * @return array|false + * @return Docblock */ public static function getAnnotationsFromDocblock($docblock) { - $has_annotations = preg_match_all(self::ANNOTATION_REGEX, $docblock, $matches, PREG_SET_ORDER); - if (!$has_annotations) { - return false; - } - - return $matches; + return new Docblock($docblock); } } diff --git a/src/Parser/Annotation.php b/src/Parser/Annotation.php index 34ed555..68ede69 100644 --- a/src/Parser/Annotation.php +++ b/src/Parser/Annotation.php @@ -2,6 +2,7 @@ namespace Mill\Parser; use Mill\Exceptions\Resource\Annotations\BadOptionsListException; +use Mill\Exceptions\Resource\Annotations\InvalidMSONSyntaxException; use Mill\Exceptions\Resource\Annotations\MissingRequiredFieldException; /** @@ -105,6 +106,13 @@ abstract class Annotation */ const SUPPORTS_DEPRECATION = null; + /** + * Is this annotation written using MSON? + * + * @return bool|null + */ + const SUPPORTS_MSON = null; + /** * @param string $doc * @param string $controller @@ -133,12 +141,26 @@ public function __construct($doc, $controller, $method, Version $version = null, * Extract a required field from the parsed dataset. * * @param string $field + * @param bool $is_mson_field * @return mixed + * @throws InvalidMSONSyntaxException If the annotation contains invalid MSON. * @throws MissingRequiredFieldException If the supplied field is missing in the parsed dataset. */ - protected function required($field) + protected function required($field, $is_mson_field = true) { if (empty($this->parsed_data[$field])) { + // If this field was written in MSON, but isn't present, and this annotation supports MSON, let's return an + // invalid MSON exception because that means that we just weren't able to parse the MSON that they supplied. + if ($is_mson_field && static::SUPPORTS_MSON) { + throw InvalidMSONSyntaxException::create( + $field, + $this->getAnnotationName(), + $this->docblock, + $this->controller, + $this->method + ); + } + throw MissingRequiredFieldException::create( $field, $this->getAnnotationName(), diff --git a/src/Parser/Annotations/ParamAnnotation.php b/src/Parser/Annotations/ParamAnnotation.php index afa68bb..cd4c41c 100644 --- a/src/Parser/Annotations/ParamAnnotation.php +++ b/src/Parser/Annotations/ParamAnnotation.php @@ -2,10 +2,9 @@ namespace Mill\Parser\Annotations; use Mill\Container; -use Mill\Exceptions\Representation\Types\InvalidTypeException; -use Mill\Exceptions\Resource\Annotations\BadOptionsListException; use Mill\Exceptions\Resource\Annotations\UnsupportedTypeException; use Mill\Parser\Annotation; +use Mill\Parser\MSON; /** * Handler for the `@api-param` annotation. @@ -16,10 +15,7 @@ class ParamAnnotation extends Annotation const REQUIRES_VISIBILITY_DECORATOR = true; const SUPPORTS_VERSIONING = true; const SUPPORTS_DEPRECATION = true; - - const REGEX_TYPE = '/^({[^}]*})/'; - const REGEX_OPTIONAL = '/(\(optional\))/'; - const REGEX_VALUES = '/(\[[^\]]*\])(\ |$)/'; + const SUPPORTS_MSON = true; /** * Name of this parameter's field. @@ -28,6 +24,13 @@ class ParamAnnotation extends Annotation */ protected $field; + /** + * Sample data that this parameter might accept. + * + * @var string + */ + protected $sample_data; + /** * Type of data that this parameter supports. * @@ -66,88 +69,49 @@ class ParamAnnotation extends Annotation 'description', 'field', 'required', + 'sample_data', 'type', 'values', 'visible' ]; - /** - * Array of supported parameter types. - * - * @var array - */ - protected $supported_types = [ - 'array', - 'boolean', - 'datetime', - 'float', - 'enum', - 'integer', - 'number', - 'object', - 'string', - 'timestamp' - ]; - /** * Parse the annotation out and return an array of data that we can use to then interpret this annotations' * representation. * * @return array * @throws UnsupportedTypeException If an unsupported parameter type has been supplied. - * @throws BadOptionsListException If values are not in the right format. */ protected function parser() { - $parsed = []; - $doc = trim($this->docblock); + $content = trim($this->docblock); // Swap in shortcode tokens (if present). $tokens = Container::getConfig()->getParameterTokens(); if (!empty($tokens)) { - $doc = str_replace(array_keys($tokens), array_values($tokens), $doc); - } - - // Parameter type is surrounded by `{curly braces}`. - if (preg_match(self::REGEX_TYPE, $doc, $matches)) { - $parsed['type'] = substr($matches[1], 1, -1); - - // Verify that the supplied type is supported. - if (!in_array(strtolower($parsed['type']), $this->supported_types)) { - throw UnsupportedTypeException::create($doc, $this->controller, $this->method); - } - - $doc = trim(preg_replace(self::REGEX_TYPE, '', $doc)); - } - - // Parameter capability is surrounded by `+plusses+`. - if (preg_match(self::REGEX_CAPABILITY, $doc, $matches)) { - $capability = substr($matches[1], 1, -1); - $parsed['capability'] = new CapabilityAnnotation($capability, $this->controller, $this->method); - - $doc = trim(preg_replace(self::REGEX_CAPABILITY, '', $doc)); + $content = str_replace(array_keys($tokens), array_values($tokens), $content); } - // Optional flag is marked with `(optional)` parens. - if (preg_match(self::REGEX_OPTIONAL, $doc, $matches)) { - $parsed['required'] = false; - $doc = trim(preg_replace(self::REGEX_OPTIONAL, '', $doc)); - } else { - $parsed['required'] = true; + $mson = (new MSON($this->controller, $this->method))->parse($content); + $parsed = [ + 'field' => $mson->getField(), + 'sample_data' => $mson->getSampleData(), + 'type' => $mson->getType(), + 'required' => $mson->isRequired(), + 'capability' => $mson->getCapability(), + 'description' => $mson->getDescription(), + 'values' => $mson->getValues() + ]; + + // Create a capability annotation if one was supplied. + if (!empty($parsed['capability'])) { + $parsed['capability'] = new CapabilityAnnotation( + $parsed['capability'], + $this->controller, + $this->method + ); } - // Parameter values are provided `[in|braces]`. - if (preg_match(self::REGEX_VALUES, $doc, $matches)) { - $parsed['values'] = $this->parseEnumValues('param', substr($matches[1], 1, -1)); - $doc = trim(preg_replace(self::REGEX_VALUES, '', $doc)); - } - - $parts = explode(' ', $doc); - - // Field and description will be the last two parts, field space description - $parsed['field'] = array_shift($parts); - $parsed['description'] = trim(implode(' ', $parts)); - return $parsed; } @@ -162,6 +126,7 @@ protected function parser() protected function interpreter() { $this->field = $this->required('field'); + $this->sample_data = $this->optional('sample_data'); // @todo make this required $this->type = $this->required('type'); $this->description = $this->required('description'); $this->required = $this->boolean('required'); @@ -180,6 +145,16 @@ public function getField() return $this->field; } + /** + * Get the sample data that this parameter might accept. + * + * @return string + */ + public function getSampleData() + { + return $this->sample_data; + } + /** * Get the type of variable that this parameter is. * diff --git a/src/Parser/Annotations/ReturnAnnotation.php b/src/Parser/Annotations/ReturnAnnotation.php index e76bb69..abd3221 100644 --- a/src/Parser/Annotations/ReturnAnnotation.php +++ b/src/Parser/Annotations/ReturnAnnotation.php @@ -58,19 +58,19 @@ class ReturnAnnotation extends Annotation protected function parser() { $parsed = []; - $doc = trim($this->docblock); + $content = trim($this->docblock); // Parameter type is surrounded by `{curly braces}`. - if (preg_match(self::REGEX_TYPE, $doc, $matches)) { + if (preg_match(self::REGEX_TYPE, $content, $matches)) { $parsed['type'] = substr($matches[1], 1, -1); $code = $this->findReturnCodeForType($parsed['type']); $parsed['http_code'] = $code . ' ' . $this->getHttpCodeMessage($code); - $doc = trim(preg_replace(self::REGEX_TYPE, '', $doc)); + $content = trim(preg_replace(self::REGEX_TYPE, '', $content)); } - $parts = explode(' ', $doc); + $parts = explode(' ', $content); $representation = array_shift($parts); $description = trim(implode(' ', $parts)); diff --git a/src/Parser/Annotations/ThrowsAnnotation.php b/src/Parser/Annotations/ThrowsAnnotation.php index 9377141..d20a2f7 100644 --- a/src/Parser/Annotations/ThrowsAnnotation.php +++ b/src/Parser/Annotations/ThrowsAnnotation.php @@ -71,11 +71,11 @@ protected function parser() { $config = Container::getConfig(); - $doc = trim($this->docblock); $parsed = []; + $content = trim($this->docblock); // Capability is surrounded by +plusses+. - if (preg_match(self::REGEX_THROW_HTTP_CODE, $doc, $matches)) { + if (preg_match(self::REGEX_THROW_HTTP_CODE, $content, $matches)) { $parsed['http_code'] = $matches[1]; if (!$this->isValidHttpCode($parsed['http_code'])) { @@ -83,14 +83,14 @@ protected function parser() } $parsed['http_code'] .= ' ' . $this->getHttpCodeMessage($parsed['http_code']); - $doc = trim(preg_replace(self::REGEX_THROW_HTTP_CODE, '', $doc)); + $content = trim(preg_replace(self::REGEX_THROW_HTTP_CODE, '', $content)); } - $parts = explode(' ', $doc); + $parts = explode(' ', $content); $parsed['representation'] = array_shift($parts); // Representation is by itself, so put the pieces back together so we can do some more regex. - $doc = implode(' ', $parts); + $content = implode(' ', $parts); if (!empty($parsed['representation'])) { $representation = $parsed['representation']; @@ -108,7 +108,7 @@ protected function parser() } // Error codes are marked with `(\SomeError\Class::CASE)` or `(1337)` parens. - if (preg_match(self::REGEX_ERROR_CODE, $doc, $matches)) { + if (preg_match(self::REGEX_ERROR_CODE, $content, $matches)) { $error_code = substr($matches[1], 1, -1); if (is_numeric($error_code)) { $parsed['error_code'] = $error_code; @@ -120,26 +120,26 @@ protected function parser() $parsed['error_code'] = constant($error_code); } - $doc = trim(preg_replace(self::REGEX_ERROR_CODE, '', $doc)); + $content = trim(preg_replace(self::REGEX_ERROR_CODE, '', $content)); } // Capability is surrounded by +plusses+. - if (preg_match(self::REGEX_CAPABILITY, $doc, $matches)) { + if (preg_match(self::REGEX_CAPABILITY, $content, $matches)) { $capability = substr($matches[1], 1, -1); $parsed['capability'] = new CapabilityAnnotation($capability, $this->controller, $this->method); - $doc = trim(preg_replace(self::REGEX_CAPABILITY, '', $doc)); + $content = trim(preg_replace(self::REGEX_CAPABILITY, '', $content)); } - $message = trim($doc); - if (!empty($message)) { - if (preg_match(self::REGEX_THROW_SUB_TYPE, $message, $matches)) { - $message = sprintf('If the %s cannot be found in the %s', $matches[1], $matches[2]); - } elseif (preg_match(self::REGEX_THROW_TYPE, $message, $matches)) { - $message = sprintf('If the %s cannot be found', $matches[1]); + $description = trim($content); + if (!empty($description)) { + if (preg_match(self::REGEX_THROW_SUB_TYPE, $description, $matches)) { + $description = sprintf('If the %s cannot be found in the %s.', $matches[1], $matches[2]); + } elseif (preg_match(self::REGEX_THROW_TYPE, $description, $matches)) { + $description = sprintf('If the %s cannot be found.', $matches[1]); } - $parsed['description'] = $message; + $parsed['description'] = $description; } // Now that we've parsed out both the representation and error code, make sure that a representation that diff --git a/src/Parser/Annotations/UriSegmentAnnotation.php b/src/Parser/Annotations/UriSegmentAnnotation.php index 06d26e2..4549fc1 100644 --- a/src/Parser/Annotations/UriSegmentAnnotation.php +++ b/src/Parser/Annotations/UriSegmentAnnotation.php @@ -1,17 +1,19 @@ docblock); + $content = trim($this->docblock); // URI is surrounded by `{curly braces}`. - if (preg_match(self::REGEX_URI, $doc, $matches)) { + if (preg_match(self::REGEX_URI, $content, $matches)) { $parsed['uri'] = substr($matches[1], 1, -1); - $doc = trim(preg_replace(self::REGEX_URI, '', $doc)); + $content = trim(preg_replace(self::REGEX_URI, '', $content)); } - // Parameter type is surrounded by `{curly braces}`. - if (preg_match(self::REGEX_TYPE, $doc, $matches)) { - $parsed['type'] = substr($matches[1], 1, -1); - $doc = trim(preg_replace(self::REGEX_TYPE, '', $doc)); - } - - // Parameter values are provided `[in|braces]`. - if (preg_match(self::REGEX_VALUES, $doc, $matches)) { - $parsed['values'] = $this->parseEnumValues('options', substr($matches[1], 1, -1)); - $doc = trim(preg_replace(self::REGEX_VALUES, '', $doc)); - } - - $parts = explode(' ', $doc); - - // Field and description will be the last two parts, field space description - $parsed['field'] = array_shift($parts); - $parsed['description'] = trim(implode(' ', $parts)); + $mson = (new MSON($this->controller, $this->method))->parse($content); + $parsed = array_merge($parsed, [ + 'field' => $mson->getField(), + 'type' => $mson->getType(), + 'description' => $mson->getDescription(), + 'values' => $mson->getValues() + ]); return $parsed; } @@ -84,7 +75,7 @@ protected function parser() */ protected function interpreter() { - $this->uri = $this->required('uri'); + $this->uri = $this->required('uri', false); $this->field = $this->required('field'); $this->type = $this->required('type'); diff --git a/src/Parser/MSON.php b/src/Parser/MSON.php new file mode 100644 index 0000000..566511c --- /dev/null +++ b/src/Parser/MSON.php @@ -0,0 +1,290 @@ +\w+) (`(?P.+)` )?' . + '\((?P\w+)(, (?Prequired|optional))?(, (?P\w+))?\) - (?P.+))/uis'; + + /** + * This is the regex to match Mill-flavor MSON enum members. + * + * Examples: + * + * - content_rating `G` (string, optional, MOVIE_RATINGS) - This denotes the + * [MPAA rating](http://www.mpaa.org/film-ratings/) for the movie. + * + Members + * - `G` - G rated + * - `PG` - PG rated + * - `PG-13` - PG-13 rated + * + * - content_rating `G` (string, optional, MOVIE_RATINGS) - MPAA rating + * + Members + * - `G` + * - `PG` + * - `PG-13` + * + * @var string + */ + const REGEX_MSON_ENUM = '/(?:\+ Members\n(?:\s*?))?(?:- `(?P.*?)`( - (?P.*?))?)(?:$|\n)/ui'; + + /** + * Take a multi-line string/pagraph, remove any multi-lines and contract sentences. + * + * Examples: + * + * - "If there is a problem with the + * request." becomes "If there is a problem with the request." + * + * @todo This does not currently support multi-paragraph strings as those seem a bit of overkill for their usages, + * but it'd be nice to support at some point regardless. + */ + const REGEX_CLEAN_MULTILINE = '/(\s)?[ \t]*(\r\n|\n)[ \t]*(\s)/'; + + /** + * Name of the controller that this MSON is being parsed from. + * + * @var string + */ + protected $controller; + + /** + * Name of the controller method that MSON is being parsed from. + * + * @var mixed + */ + protected $method; + + /** + * Name of the field that was parsed out of the MSON content. + * + * @var string|false + */ + protected $field; + + /** + * Sample data that was parsed out of the MSON content. + * + * @var string|false + */ + protected $sample_data; + + /** + * Type of field that this MSON content represents. + * + * @var string|false + */ + protected $type; + + /** + * Is this MSON content designated as being required? + * + * @var bool + */ + protected $is_required = false; + + /** + * Application-specific capability that was parsed out of the MSON content. + * + * @var string|false + */ + protected $capability; + + /** + * Parsed description from the MSON content. + * + * @var string|false + */ + protected $description; + + /** + * Array of enumerated values from the MSON content. + * + * @var array + */ + protected $values = []; + + /** + * Supported MSON field types. + * + * @var array + */ + protected $supported_types = [ + 'array', + 'boolean', + 'datetime', + 'float', + 'enum', + 'integer', + 'number', + 'object', + 'string', + 'timestamp' + ]; + + /** + * @param string $controller + * @param string$method + */ + public function __construct($controller, $method) + { + $this->controller = $controller; + $this->method = $method; + } + + /** + * Given a piece of Mill-flavored MSON content, parse it out. + * + * @param string $content + * @return MSON + * @throws UnsupportedTypeException If an unsupported MSON field type has been supplied. + */ + public function parse($content) + { + preg_match(self::REGEX_MSON, $content, $matches); + + $this->field = (isset($matches['field'])) ? $matches['field'] : false; + $this->sample_data = (isset($matches['sample_data'])) ? $matches['sample_data'] : false; + $this->type = (isset($matches['type'])) ? $matches['type'] : false; + $this->capability = (isset($matches['capability'])) ? $matches['capability'] : false; + $this->description = (isset($matches['description'])) ? $matches['description'] : false; + + if (isset($matches['required'])) { + if (!empty($matches['required']) && strtolower($matches['required']) == 'required') { + $this->is_required = true; + } + } + + // Verify that the supplied type is supported. + if (!empty($this->type)) { + if (!in_array(strtolower($this->type), $this->supported_types)) { + throw UnsupportedTypeException::create($content, $this->controller, $this->method); + } + } + + // Parse out enum values if present, and remove them from the parsed description afterwards. + if (!empty($this->description)) { + preg_match_all(self::REGEX_MSON_ENUM, $this->description, $matches); + if (!empty($matches['value']) && !empty($matches['description'])) { + $this->values = $this->parseValues($matches['value'], $matches['description']); + + // Remove any parsed enum values from the description. + $this->description = preg_replace(self::REGEX_MSON_ENUM, '', $this->description); + $this->description = trim($this->description); + } + + // The description might be on multiple lines, so let's clean it up a bit. + // @todo Multi-paragraph descriptions seems like a bit of overkill, but it'd be nice to add support. + $this->description = preg_replace(self::REGEX_CLEAN_MULTILINE, ' ', $this->description); + } + + return $this; + } + + /** + * Given an array of values and descriptions. + * + * @param array $values + * @param array $descriptions + * @return array + */ + protected function parseValues($values, $descriptions) + { + $enum = []; + foreach ($values as $k => $value) { + $value = trim($value); + $description = trim($descriptions[$k]); + + $enum[$value] = $description; + } + + // Keep the array of values alphabetical so it's cleaner when generated into documentation. + ksort($enum); + + return $enum; + } + + /** + * Name of the field that was parsed out of the MSON content. + * + * @return false|string + */ + public function getField() + { + return $this->field; + } + + /** + * Sample data that was parsed out of the MSON content. + * + * @return false|string + */ + public function getSampleData() + { + return $this->sample_data; + } + + /** + * Type of field that this MSON content represents. + * + * @return false|string + */ + public function getType() + { + return $this->type; + } + + /** + * Is this MSON content designated as being required? + * + * @return bool + */ + public function isRequired() + { + return $this->is_required; + } + + /** + * Application-specific capability that was parsed out of the MSON content. + * + * @return false|string + */ + public function getCapability() + { + return $this->capability; + } + + /** + * Parsed description from the MSON content. + * + * @return false|string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Array of enumerated values from the MSON content. + * + * @return array + */ + public function getValues() + { + return $this->values; + } +} diff --git a/tests/Parser/Annotations/FieldAnnotationTest.php b/tests/Parser/Annotations/FieldAnnotationTest.php index 20296a6..c43ea9c 100644 --- a/tests/Parser/Annotations/FieldAnnotationTest.php +++ b/tests/Parser/Annotations/FieldAnnotationTest.php @@ -1,8 +1,6 @@ [ + 'param' => 'content_rating `G` (string, optional, MOVIE_RATINGS) - MPAA rating + + Members + - `G` - G rated + - `PG` - PG rated + - `PG-13` - PG-13 rated', + 'version' => null, + 'visible' => true, + 'deprecated' => false, + 'expected' => [ + 'capability' => 'MOVIE_RATINGS', + 'deprecated' => false, + 'description' => 'MPAA rating', + 'field' => 'content_rating', + 'required' => false, + 'sample_data' => 'G', + 'type' => 'string', + 'values' => [ + 'G' => 'G rated', + 'PG' => 'PG rated', + 'PG-13' => 'PG-13 rated' + ], + 'version' => false, + 'visible' => true + ] + ], + '_complete-with-markdown-description' => [ + 'param' => 'content_rating `G` (string, optional, MOVIE_RATINGS) - This denotes the + [MPAA rating](http://www.mpaa.org/film-ratings/) for the movie. + + Members + - `G` - G rated + - `PG` - PG rated + - `PG-13` - PG-13 rated', + 'version' => null, + 'visible' => true, + 'deprecated' => false, + 'expected' => [ + 'capability' => 'MOVIE_RATINGS', + 'deprecated' => false, + 'description' => 'This denotes the [MPAA rating](http://www.mpaa.org/film-ratings/) for the movie.', + 'field' => 'content_rating', + 'required' => false, + 'sample_data' => 'G', + 'type' => 'string', + 'values' => [ + 'G' => 'G rated', + 'PG' => 'PG rated', + 'PG-13' => 'PG-13 rated' + ], + 'version' => false, + 'visible' => true + ] + ], 'capability' => [ - 'param' => '{string} content_rating +MOVIE_RATINGS+ MPAA rating', + 'param' => 'content_rating `G` (string, REQUIRED, MOVIE_RATINGS) - MPAA rating', 'version' => null, 'visible' => true, 'deprecated' => false, @@ -59,6 +112,7 @@ public function providerAnnotation() 'description' => 'MPAA rating', 'field' => 'content_rating', 'required' => true, + 'sample_data' => 'G', 'type' => 'string', 'values' => false, 'version' => false, @@ -66,38 +120,97 @@ public function providerAnnotation() ] ], 'deprecated' => [ - 'param' => '{page}', + 'param' => 'content_rating `G` (string, required) - MPAA rating', 'version' => null, - 'visible' => false, + 'visible' => true, 'deprecated' => true, 'expected' => [ 'capability' => false, 'deprecated' => true, - 'description' => 'The page number to show.', - 'field' => 'page', - 'required' => false, - 'type' => 'integer', + 'description' => 'MPAA rating', + 'field' => 'content_rating', + 'required' => true, + 'sample_data' => 'G', + 'type' => 'string', 'values' => false, 'version' => false, - 'visible' => false + 'visible' => true + ] + ], + 'enum-with-no-descriptions' => [ + 'param' => 'is_kid_friendly `yes` (string, optional) - Is this movie kid friendly? + + Members + - `yes` + - `no`', + 'version' => null, + 'visible' => true, + 'deprecated' => false, + 'expected' => [ + 'capability' => false, + 'deprecated' => false, + 'description' => 'Is this movie kid friendly?', + 'field' => 'is_kid_friendly', + 'required' => false, + 'sample_data' => 'yes', + 'type' => 'string', + 'values' => [ + 'no' => '', + 'yes' => '' + ], + 'version' => false, + 'visible' => true + ] + ], + 'enum-with-no-set-default' => [ + 'param' => 'content_rating `G` (string, optional) - MPAA rating + + Members + - `G` - G rated + - `PG` - PG rated + - `PG-13` - PG-13 rated + - `R` - R rated + - `NC-17` - NC-17 rated + - `X` - X-rated + - `NR` - No rating + - `UR` - Unrated', + 'version' => null, + 'visible' => true, + 'deprecated' => false, + 'expected' => [ + 'capability' => false, + 'deprecated' => false, + 'description' => 'MPAA rating', + 'field' => 'content_rating', + 'required' => false, + 'sample_data' => 'G', + 'type' => 'string', + 'values' => [ + 'G' => 'G rated', + 'NC-17' => 'NC-17 rated', + 'NR' => 'No rating', + 'PG' => 'PG rated', + 'PG-13' => 'PG-13 rated', + 'R' => 'R rated', + 'UR' => 'Unrated', + 'X' => 'X-rated' + ], + 'version' => false, + 'visible' => true ] ], 'private' => [ - 'param' => '{string} __testing [true|false] Because reasons', + 'param' => 'content_rating `G` (string, required) - MPAA rating', 'version' => null, 'visible' => false, 'deprecated' => false, 'expected' => [ 'capability' => false, 'deprecated' => false, - 'description' => 'Because reasons', - 'field' => '__testing', + 'description' => 'MPAA rating', + 'field' => 'content_rating', 'required' => true, + 'sample_data' => 'G', 'type' => 'string', - 'values' => [ - 'false', - 'true' - ], + 'values' => false, 'version' => false, 'visible' => false ] @@ -113,6 +226,7 @@ public function providerAnnotation() 'description' => 'The page number to show.', 'field' => 'page', 'required' => false, + 'sample_data' => false, 'type' => 'integer', 'values' => false, 'version' => false, @@ -120,7 +234,10 @@ public function providerAnnotation() ] ], 'tokens.acceptable_values' => [ - 'param' => '{filter} [embeddable|playable]', + 'param' => '{filter} + + Members + - `embeddable` - Embeddable + - `playable` - Playable', 'version' => null, 'visible' => true, 'deprecated' => false, @@ -130,82 +247,106 @@ public function providerAnnotation() 'description' => 'Filter to apply to the results.', 'field' => 'filter', 'required' => false, + 'sample_data' => false, 'type' => 'string', 'values' => [ - 'embeddable', - 'playable' + 'embeddable' => 'Embeddable', + 'playable' => 'Playable' ], 'version' => false, 'visible' => true ] ], 'versioned' => [ - 'param' => '{page}', + 'param' => 'content_rating `G` (string) - MPAA rating', 'version' => new Version('1.1 - 1.2', __CLASS__, __METHOD__), 'visible' => true, 'deprecated' => false, 'expected' => [ 'capability' => false, 'deprecated' => false, - 'description' => 'The page number to show.', - 'field' => 'page', + 'description' => 'MPAA rating', + 'field' => 'content_rating', 'required' => false, - 'type' => 'integer', + 'sample_data' => 'G', + 'type' => 'string', 'values' => false, 'version' => '1.1 - 1.2', 'visible' => true ] ], - '_complete' => [ - 'param' => '{string} content_rating [G|PG|PG-13|R|NC-17|X|NR|UR] (optional) +MOVIE_RATINGS+ ' . - 'MPAA rating', + 'with-a-long-description' => [ + 'param' => 'content_rating `G` (string, required) - Voluptate culpa ex, eiusmod rump sint id. Venison + non ribeye landjaeger laboris, enim jowl culpa meatloaf dolore mollit anim. Bacon shankle eiusmod + hamburger enim. Laboris lorem pastrami t-bone tempor ullamco swine commodo tri-tip in sirloin.', + 'version' => null, + 'visible' => false, + 'deprecated' => false, + 'expected' => [ + 'capability' => false, + 'deprecated' => false, + 'description' => 'Voluptate culpa ex, eiusmod rump sint id. Venison non ribeye landjaeger ' . + 'laboris, enim jowl culpa meatloaf dolore mollit anim. Bacon shankle eiusmod hamburger enim. ' . + 'Laboris lorem pastrami t-bone tempor ullamco swine commodo tri-tip in sirloin.', + 'field' => 'content_rating', + 'required' => true, + 'sample_data' => 'G', + 'type' => 'string', + 'values' => false, + 'version' => false, + 'visible' => false + ] + ], + 'without-defined-requirement' => [ + 'param' => 'content_rating `G` (string) - MPAA rating', 'version' => null, 'visible' => true, 'deprecated' => false, 'expected' => [ - 'capability' => 'MOVIE_RATINGS', + 'capability' => false, 'deprecated' => false, 'description' => 'MPAA rating', 'field' => 'content_rating', 'required' => false, + 'sample_data' => 'G', 'type' => 'string', - 'values' => [ - 'G', - 'NC-17', - 'NR', - 'PG', - 'PG-13', - 'R', - 'UR', - 'X' - ], + 'values' => false, 'version' => false, 'visible' => true ] ], - '_complete.with-markdown-description' => [ - 'param' => '{string} content_rating [G|PG|PG-13|R|NC-17|X|NR|UR] (optional) +MOVIE_RATINGS+ ' . - '[MPAA rating](http://www.mpaa.org/film-ratings/)', + 'without-defined-requirement-but-capability' => [ + 'param' => 'content_rating `G` (string, MOVIE_RATINGS) - MPAA rating', 'version' => null, 'visible' => true, 'deprecated' => false, 'expected' => [ 'capability' => 'MOVIE_RATINGS', 'deprecated' => false, - 'description' => '[MPAA rating](http://www.mpaa.org/film-ratings/)', + 'description' => 'MPAA rating', 'field' => 'content_rating', 'required' => false, + 'sample_data' => 'G', 'type' => 'string', - 'values' => [ - 'G', - 'NC-17', - 'NR', - 'PG', - 'PG-13', - 'R', - 'UR', - 'X' - ], + 'values' => false, + 'version' => false, + 'visible' => true + ] + ], + 'without-sample-data' => [ + 'param' => 'content_rating (string) - MPAA rating', + 'version' => null, + 'visible' => true, + 'deprecated' => false, + 'expected' => [ + 'capability' => false, + 'deprecated' => false, + 'description' => 'MPAA rating', + 'field' => 'content_rating', + 'required' => false, + 'sample_data' => false, + 'type' => 'string', + 'values' => false, 'version' => false, 'visible' => true ] @@ -219,60 +360,24 @@ public function providerAnnotation() public function providerAnnotationFailsOnInvalidAnnotations() { return [ - 'missing-field-name' => [ + 'invalid-mson' => [ 'annotation' => '\Mill\Parser\Annotations\ParamAnnotation', - 'docblock' => '{string}', - 'expected.exception' => '\Mill\Exceptions\Resource\Annotations\MissingRequiredFieldException', + 'docblock' => '', + 'expected.exception' => '\Mill\Exceptions\Resource\Annotations\InvalidMSONSyntaxException', 'expected.exception.asserts' => [ - 'getRequiredField' => 'field', 'getAnnotation' => 'param', - 'getDocblock' => '{string}', + 'getDocblock' => '', 'getValues' => [] ] ], - 'missing-type' => [ + 'unsupported-type' => [ 'annotation' => '\Mill\Parser\Annotations\ParamAnnotation', - 'docblock' => '__testing', - 'expected.exception' => '\Mill\Exceptions\Resource\Annotations\MissingRequiredFieldException', - 'expected.exception.asserts' => [ - 'getRequiredField' => 'type', - 'getAnnotation' => 'param', - 'getDocblock' => '__testing', - 'getValues' => [] - ] - ], - 'missing-field-name' => [ - 'annotation' => '\Mill\Parser\Annotations\ParamAnnotation', - 'docblock' => '{int} __testing', + 'docblock' => 'content_rating `G` (str) - MPAA rating', 'expected.exception' => '\Mill\Exceptions\Resource\Annotations\UnsupportedTypeException', 'expected.exception.asserts' => [ - 'getAnnotation' => '{int} __testing', + 'getAnnotation' => 'content_rating `G` (str) - MPAA rating', 'getDocblock' => null ] - ], - 'values-are-in-the-wrong-format' => [ - 'annotation' => '\Mill\Parser\Annotations\ParamAnnotation', - 'docblock' => '{string} __testing [true,false] Because reasons', - 'expected.exception' => '\Mill\Exceptions\Resource\Annotations\BadOptionsListException', - 'expected.exception.asserts' => [ - 'getRequiredField' => null, - 'getAnnotation' => 'param', - 'getDocblock' => '{string} __testing [true,false] Because reasons', - 'getValues' => [ - 'true,false' - ] - ] - ], - 'missing-description' => [ - 'annotation' => '\Mill\Parser\Annotations\ParamAnnotation', - 'docblock' => '{string} __testing [true|false]', - 'expected.exception' => '\Mill\Exceptions\Resource\Annotations\MissingRequiredFieldException', - 'expected.exception.asserts' => [ - 'getRequiredField' => 'description', - 'getAnnotation' => 'param', - 'getDocblock' => '{string} __testing [true|false]', - 'getValues' => [] - ] ] ]; } diff --git a/tests/Parser/Annotations/ThrowsAnnotationTest.php b/tests/Parser/Annotations/ThrowsAnnotationTest.php index 354bd80..83d3f0b 100644 --- a/tests/Parser/Annotations/ThrowsAnnotationTest.php +++ b/tests/Parser/Annotations/ThrowsAnnotationTest.php @@ -110,7 +110,7 @@ public function providerAnnotation() 'visible' => true, 'expected' => [ 'capability' => false, - 'description' => 'If the movie cannot be found', + 'description' => 'If the movie cannot be found.', 'error_code' => false, 'http_code' => '404 Not Found', 'representation' => '\Mill\Examples\Showtimes\Representations\Error', @@ -124,7 +124,7 @@ public function providerAnnotation() 'visible' => true, 'expected' => [ 'capability' => false, - 'description' => 'If the movie cannot be found in the theater', + 'description' => 'If the movie cannot be found in the theater.', 'error_code' => false, 'http_code' => '404 Not Found', 'representation' => '\Mill\Examples\Showtimes\Representations\Error', @@ -169,7 +169,7 @@ public function providerAnnotation() 'visible' => false, 'expected' => [ 'capability' => false, - 'description' => 'If the movie cannot be found', + 'description' => 'If the movie cannot be found.', 'error_code' => false, 'http_code' => '404 Not Found', 'representation' => '\Mill\Examples\Showtimes\Representations\Error', @@ -183,7 +183,7 @@ public function providerAnnotation() 'visible' => false, 'expected' => [ 'capability' => false, - 'description' => 'If the movie cannot be found', + 'description' => 'If the movie cannot be found.', 'error_code' => false, 'http_code' => '404 Not Found', 'representation' => '\Mill\Examples\Showtimes\Representations\Error', @@ -214,7 +214,7 @@ public function providerAnnotation() 'visible' => true, 'expected' => [ 'capability' => 'BUY_TICKETS', - 'description' => 'If the movie cannot be found in the theater', + 'description' => 'If the movie cannot be found in the theater.', 'error_code' => '666', 'http_code' => '404 Not Found', 'representation' => '\Mill\Examples\Showtimes\Representations\CodedError', @@ -229,7 +229,7 @@ public function providerAnnotation() 'visible' => true, 'expected' => [ 'capability' => 'BUY_TICKETS', - 'description' => 'If the movie cannot be found in the theater', + 'description' => 'If the movie cannot be found in the theater.', 'error_code' => false, 'http_code' => '404 Not Found', 'representation' => '\Mill\Examples\Showtimes\Representations\Error', diff --git a/tests/Parser/Annotations/UriSegmentAnnotationTest.php b/tests/Parser/Annotations/UriSegmentAnnotationTest.php index cf71aef..af2f677 100644 --- a/tests/Parser/Annotations/UriSegmentAnnotationTest.php +++ b/tests/Parser/Annotations/UriSegmentAnnotationTest.php @@ -34,7 +34,7 @@ public function providerAnnotation() return [ 'bare' => [ 'uri' => '/movies/+id', - 'segment' => '{/movies/+id} {string} id Movie ID', + 'segment' => '{/movies/+id} id (string) - Movie ID', 'expected' => [ 'description' => 'Movie ID', 'field' => 'id', @@ -45,16 +45,18 @@ public function providerAnnotation() ], '_complete' => [ 'uri' => '/movies/+id/showtimes/*date', - 'segment' => '{/movies/+id/showtimes/*date} {string} date [today|tomorrow] Date to look for movie ' . - 'showtimes.', + 'segment' => '{/movies/+id/showtimes/*date} date (string) - Date to look for movie showtimes. + + Members + - `today` + - `tomorrow`', 'expected' => [ 'description' => 'Date to look for movie showtimes.', 'field' => 'date', 'type' => 'string', 'uri' => '/movies/+id/showtimes/*date', 'values' => [ - 'today', - 'tomorrow' + 'today' => '', + 'tomorrow' => '' ] ] ] @@ -67,63 +69,34 @@ public function providerAnnotation() public function providerAnnotationFailsOnInvalidAnnotations() { return [ - 'missing-uri' => [ - 'annotation' => '\Mill\Parser\Annotations\UriSegmentAnnotation', - 'docblock' => '', - 'expected.exception' => '\Mill\Exceptions\Resource\Annotations\MissingRequiredFieldException', - 'expected.exception.asserts' => [ - 'getRequiredField' => 'uri', - 'getAnnotation' => 'urisegment', - 'getDocblock' => '', - 'getValues' => [] - ] - ], - 'missing-field-name' => [ + 'invalid-mson' => [ 'annotation' => '\Mill\Parser\Annotations\UriSegmentAnnotation', 'docblock' => '{/movies/+id}', - 'expected.exception' => '\Mill\Exceptions\Resource\Annotations\MissingRequiredFieldException', + 'expected.exception' => '\Mill\Exceptions\Resource\Annotations\InvalidMSONSyntaxException', 'expected.exception.asserts' => [ - 'getRequiredField' => 'field', 'getAnnotation' => 'urisegment', 'getDocblock' => '{/movies/+id}', 'getValues' => [] ] ], - 'missing-type' => [ + 'missing-uri' => [ 'annotation' => '\Mill\Parser\Annotations\UriSegmentAnnotation', - 'docblock' => '{/movies/+id} id', + 'docblock' => '', 'expected.exception' => '\Mill\Exceptions\Resource\Annotations\MissingRequiredFieldException', 'expected.exception.asserts' => [ - 'getRequiredField' => 'type', + 'getRequiredField' => 'uri', 'getAnnotation' => 'urisegment', - 'getDocblock' => '{/movies/+id} id', + 'getDocblock' => '', 'getValues' => [] ] ], - 'values-are-in-the-wrong-format' => [ - 'annotation' => '\Mill\Parser\Annotations\UriSegmentAnnotation', - 'docblock' => '{/movies/+id/showtimes/*date} {string} date [today,tomorrow] Date to look for movie ' . - 'showtimes.', - 'expected.exception' => '\Mill\Exceptions\Resource\Annotations\BadOptionsListException', - 'expected.exception.asserts' => [ - 'getRequiredField' => null, - 'getAnnotation' => 'options', - 'getDocblock' => '{/movies/+id/showtimes/*date} {string} date [today,tomorrow] Date to look ' . - 'for movie showtimes.', - 'getValues' => [ - 'today,tomorrow' - ] - ] - ], - 'missing-description' => [ + 'unsupported-type' => [ 'annotation' => '\Mill\Parser\Annotations\UriSegmentAnnotation', - 'docblock' => '{/movies/+id} {string} id', - 'expected.exception' => '\Mill\Exceptions\Resource\Annotations\MissingRequiredFieldException', + 'docblock' => '{/movies/+id} id (str) - Movie ID', + 'expected.exception' => '\Mill\Exceptions\Resource\Annotations\UnsupportedTypeException', 'expected.exception.asserts' => [ - 'getRequiredField' => 'description', - 'getAnnotation' => 'urisegment', - 'getDocblock' => '{/movies/+id} {string} id', - 'getValues' => [] + 'getAnnotation' => 'id (str) - Movie ID', + 'getDocblock' => null ] ] ]; diff --git a/tests/Parser/Resource/Action/DocumentationTest.php b/tests/Parser/Resource/Action/DocumentationTest.php index c323ff3..09dfd84 100644 --- a/tests/Parser/Resource/Action/DocumentationTest.php +++ b/tests/Parser/Resource/Action/DocumentationTest.php @@ -218,6 +218,7 @@ public function providerParseMethodDocumentation() 'description' => 'Array of names of the cast.', 'field' => 'cast', 'required' => false, + 'sample_data' => false, 'type' => 'array', 'values' => false, 'version' => false, @@ -229,16 +230,17 @@ public function providerParseMethodDocumentation() 'description' => 'MPAA rating', 'field' => 'content_rating', 'required' => false, + 'sample_data' => false, 'type' => 'string', 'values' => [ - 'G', - 'NC-17', - 'NR', - 'PG', - 'PG-13', - 'R', - 'UR', - 'X' + 'G' => '', + 'NC-17' => '', + 'NR' => '', + 'PG' => '', + 'PG-13' => '', + 'R' => '', + 'UR' => '', + 'X' => '' ], 'version' => false, 'visible' => true @@ -249,6 +251,7 @@ public function providerParseMethodDocumentation() 'description' => 'Description, or tagline, for the movie.', 'field' => 'description', 'required' => true, + 'sample_data' => false, 'type' => 'string', 'values' => false, 'version' => false, @@ -260,6 +263,7 @@ public function providerParseMethodDocumentation() 'description' => 'Name of the director.', 'field' => 'director', 'required' => false, + 'sample_data' => false, 'type' => 'string', 'values' => false, 'version' => false, @@ -271,6 +275,7 @@ public function providerParseMethodDocumentation() 'description' => 'Is this movie kid friendly?', 'field' => 'is_kid_friendly', 'required' => false, + 'sample_data' => false, 'type' => 'boolean', 'values' => false, 'version' => false, @@ -282,6 +287,7 @@ public function providerParseMethodDocumentation() 'description' => 'Name of the movie.', 'field' => 'name', 'required' => true, + 'sample_data' => false, 'type' => 'string', 'values' => false, 'version' => false, @@ -293,6 +299,7 @@ public function providerParseMethodDocumentation() 'description' => 'Array of movie genres.', 'field' => 'genres', 'required' => false, + 'sample_data' => false, 'type' => 'array', 'values' => false, 'version' => false, @@ -304,6 +311,7 @@ public function providerParseMethodDocumentation() 'description' => 'IMDB URL', 'field' => 'imdb', 'required' => false, + 'sample_data' => false, 'type' => 'string', 'values' => false, 'version' => '>=1.1.1', @@ -315,6 +323,7 @@ public function providerParseMethodDocumentation() 'description' => 'Rotten Tomatoes score', 'field' => 'rotten_tomatoes_score', 'required' => false, + 'sample_data' => false, 'type' => 'integer', 'values' => false, 'version' => false, @@ -326,6 +335,7 @@ public function providerParseMethodDocumentation() 'description' => 'Movie runtime, in `HHhr MMmin` format.', 'field' => 'runtime', 'required' => false, + 'sample_data' => false, 'type' => 'string', 'values' => false, 'version' => false, @@ -337,6 +347,7 @@ public function providerParseMethodDocumentation() 'description' => 'Trailer URL', 'field' => 'trailer', 'required' => false, + 'sample_data' => false, 'type' => 'string', 'values' => false, 'version' => false, diff --git a/tests/_fixtures/mill.test.xml b/tests/_fixtures/mill.test.xml index 1219684..0211728 100644 --- a/tests/_fixtures/mill.test.xml +++ b/tests/_fixtures/mill.test.xml @@ -63,9 +63,9 @@ - {integer} page (optional) The page number to show. - {integer} per_page (optional) Number of items to show on each page. Max 100. - {string} filter (optional) Filter to apply to the results. + page (integer, optional) - The page number to show. + per_page (integer, optional) - Number of items to show on each page. Max 100. + filter (string, optional) - Filter to apply to the results.