From 4dc77877a26521a791a5f2d6afa7c9b4217f81ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentinas=20Bartusevi=C4=8Dius?= Date: Fri, 3 Feb 2017 18:10:27 +0200 Subject: [PATCH 01/26] DataTypes added from RAML1.0 --- src/ApiDefinition.php | 36 ++++++++++++++++++++++++++++++++++++ test/ApiDefinitionTest.php | 17 +++++++++++++++++ test/fixture/types.raml | 13 +++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 test/fixture/types.raml diff --git a/src/ApiDefinition.php b/src/ApiDefinition.php index 4d0b28cc..493a620c 100644 --- a/src/ApiDefinition.php +++ b/src/ApiDefinition.php @@ -133,6 +133,15 @@ class ApiDefinition implements ArrayInstantiationInterface */ private $securedBy = []; + /** + * A list of data types + * + * @link https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md/#raml-data-types + * + * @var array + */ + private $types = []; + // --- /** @@ -235,6 +244,12 @@ public static function createFromArray($title, array $data = []) } } + if (isset($data['types'])) { + foreach ($data['types'] as $name => $definition) { + $apiDefinition->addType($name, $definition); + } + } + // --- foreach ($data as $resourceName => $resource) { @@ -568,6 +583,27 @@ public function addDocumentation($title, $documentation) $this->documentationList[$title] = $documentation; } + /** + * Add data type + * + * @param string $name + * @param array $definition + */ + public function addType($name, $definition) + { + $this->types[$name] = $definition; + } + + /** + * Get data types + * + * @return array + */ + public function getTypes() + { + return $this->types; + } + // -- /** diff --git a/test/ApiDefinitionTest.php b/test/ApiDefinitionTest.php index ce4b5d8c..ada8bf38 100644 --- a/test/ApiDefinitionTest.php +++ b/test/ApiDefinitionTest.php @@ -72,6 +72,23 @@ public function shouldReturnURIProtocol() ), $api->getProtocols()); } + /** @test */ + public function shouldProcessTypes() + { + $api = $this->parser->parse(__DIR__.'/fixture/types.raml'); + $this->assertCount(1, $api->getTypes()); + $this->assertSame(array( + 'User' => array( + 'type' => 'object', + 'properties' => array( + 'firstname' => 'string', + 'lastname' => 'string', + 'age' => 'number', + ) + ) + ), $api->getTypes()); + } + /** @test */ public function shouldReturnProtocolsIfSpecified() { diff --git a/test/fixture/types.raml b/test/fixture/types.raml new file mode 100644 index 00000000..27499f35 --- /dev/null +++ b/test/fixture/types.raml @@ -0,0 +1,13 @@ +#%RAML 1.0 + +title: Fancy API +baseUri: http://example.api.com/{version} +version: v1 + +types: + User: + type: object + properties: + firstname: string + lastname: string + age: number From ec7c51768c85bac9b56885ff8c0052666ce8603e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentinas=20Bartusevi=C4=8Dius?= Date: Sat, 4 Feb 2017 16:33:33 +0200 Subject: [PATCH 02/26] Possible to include named traits; Check if protocol is not empty it ApiDefinition::setBaseUrl --- src/ApiDefinition.php | 5 ++++- src/Parser.php | 12 +++++++++--- test/ParseTest.php | 12 ++++++++++++ test/fixture/includedTraits.raml | 9 +++++++++ test/fixture/traits/category.raml | 16 ++++++++++++++++ 5 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 test/fixture/includedTraits.raml create mode 100644 test/fixture/traits/category.raml diff --git a/src/ApiDefinition.php b/src/ApiDefinition.php index 493a620c..2f6c4942 100644 --- a/src/ApiDefinition.php +++ b/src/ApiDefinition.php @@ -416,7 +416,10 @@ public function setBaseUrl($baseUrl) $this->baseUrl = $baseUrl; if (!$this->protocols) { - $this->protocols = [strtoupper(parse_url($this->baseUrl, PHP_URL_SCHEME))]; + $protocol = strtoupper(parse_url($this->baseUrl, PHP_URL_SCHEME)); + if (!empty($protocol)) { + $this->protocols = [$protocol]; + } } } diff --git a/src/Parser.php b/src/Parser.php index a61d5720..1c166767 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -425,9 +425,15 @@ private function parseTraits($ramlData) { if (isset($ramlData['traits'])) { $keyedTraits = []; - foreach ($ramlData['traits'] as $trait) { - foreach ($trait as $k => $t) { - $keyedTraits[$k] = $t; + foreach ($ramlData['traits'] as $key => $trait) { + if (is_int($key)) { + foreach ($trait as $k => $t) { + $keyedTraits[$k] = $t; + } + } else { + foreach ($trait as $k => $t) { + $keyedTraits[$key][$k] = $t; + } } } diff --git a/test/ParseTest.php b/test/ParseTest.php index 1364ebaa..316c66f9 100644 --- a/test/ParseTest.php +++ b/test/ParseTest.php @@ -860,6 +860,18 @@ public function shouldParseCustomSettingsOnMethodWithOAuthParser() $this->assertSame($settingsObject->getAuthorizationUri(), 'https://www.dropbox.com/1/oauth2/authorize'); } + /** @test */ + public function shouldParseIncludedTraits() + { + $apiDefinition = $this->parser->parse(__DIR__ . '/fixture/includedTraits.raml'); + $resource = $apiDefinition->getResourceByUri('/category'); + $method = $resource->getMethod('get'); + $queryParams = $method->getQueryParameters(); + + $this->assertCount(3, $queryParams); + $this->assertSame(['id', 'parent_id', 'title'], array_keys($queryParams)); + } + /** @test */ public function shouldParseResourcePathNameCorrectly() { diff --git a/test/fixture/includedTraits.raml b/test/fixture/includedTraits.raml new file mode 100644 index 00000000..dc6826fe --- /dev/null +++ b/test/fixture/includedTraits.raml @@ -0,0 +1,9 @@ +#%RAML 0.8 +title: Example API +version: v1 + +traits: + Category: !include traits/category.raml +/category: + get: + is: [ Category ] diff --git a/test/fixture/traits/category.raml b/test/fixture/traits/category.raml new file mode 100644 index 00000000..79fef540 --- /dev/null +++ b/test/fixture/traits/category.raml @@ -0,0 +1,16 @@ +#%RAML 0.8 +type: object +displayName: Category +queryParameters: + id: + type: string + required: false + description: Object id + parent_id: + type: string + required: false + description: Category parent id + title: + type: string + required: false + description: Category Title From 90c28b6c348e7048b90217f73598c8781f7a5b90 Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Tue, 14 Feb 2017 02:37:49 +0100 Subject: [PATCH 03/26] Finished implementation of RAML 1.0 processing functions --- src/Parser.php | 146 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 110 insertions(+), 36 deletions(-) diff --git a/src/Parser.php b/src/Parser.php index 1c166767..b41ef690 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -22,6 +22,13 @@ */ class Parser { + const LOWER_CAMEL_CASE = 0, + LOWER_HYPHEN_CASE = 1, + LOWER_UNDERSCORE_CASE = 2, + UPPER_CAMEL_CASE = 4, + UPPER_HYPHEN_CASE = 8, + UPPER_UNDERSCORE_CASE = 16; + /** * Array of cached files * No point in fetching them twice @@ -695,53 +702,120 @@ private function replaceTypes($raml, $types, $path, $name, $parentKey = null) */ private function applyTraitVariables(array $values, array $trait) { - $variables = implode('|', array_keys($values)); $newTrait = []; foreach ($trait as $key => &$value) { - $newKey = preg_replace_callback( - '/<<(' . $variables . ')([\s]*\|[\s]*!(singularize|pluralize))?>>/', - function ($matches) use ($values) { - $transformer = isset($matches[3]) ? $matches[3] : ''; - switch ($transformer) { - case 'singularize': - return Inflect::singularize($values[$matches[1]]); - break; - case 'pluralize': - return Inflect::pluralize($values[$matches[1]]); - break; - default: - return $values[$matches[1]]; - } - }, - $key - ); + $newKey = $this->applyFunctions($key, $values); if (is_array($value)) { $value = $this->applyTraitVariables($values, $value); } else { - $value = preg_replace_callback( - '/<<(' . $variables . ')([\s]*\|[\s]*!(singularize|pluralize))?>>/', - function ($matches) use ($values) { - $transformer = isset($matches[3]) ? $matches[3] : ''; - - switch ($transformer) { - case 'singularize': - return Inflect::singularize($values[$matches[1]]); - break; - case 'pluralize': - return Inflect::pluralize($values[$matches[1]]); - break; - default: - return $values[$matches[1]]; - } - }, - $value - ); + $value = $this->applyFunctions($value, $values); } $newTrait[$newKey] = $value; } return $newTrait; } + + private function applyFunctions($trait, array $values) + { + $variables = implode('|', array_keys($values)); + return preg_replace_callback( + '/<<(' . $variables . ')'. + '('. + '[\s]*\|[\s]*!'. + '('. + 'singularize|pluralize|uppercase|lowercase|lowercamelcase|uppercamelcase|lowerunderscorecase|upperunderscorecase|lowerhyphencase|upperhyphencase'. + ')'. + ')?>>/', + function ($matches) use ($values) { + $transformer = isset($matches[3]) ? $matches[3] : ''; + switch ($transformer) { + case 'singularize': + return Inflect::singularize($values[$matches[1]]); + break; + case 'pluralize': + return Inflect::pluralize($values[$matches[1]]); + break; + case 'uppercase': + return strtoupper($values[$matches[1]]); + break; + case 'lowercase': + return strtolower($values[$matches[1]]); + break; + case 'lowercamelcase': + return $this->convertString($values[$matches[1]],self::LOWER_CAMEL_CASE); + break; + case 'uppercamelcase': + return $this->convertString($values[$matches[1]],self::UPPER_CAMEL_CASE); + break; + case 'lowerunderscorecase': + return $this->convertString($values[$matches[1]],self::LOWER_UNDERSCORE_CASE); + break; + case 'upperunderscorecase': + return $this->convertString($values[$matches[1]],self::UPPER_UNDERSCORE_CASE); + break; + case 'lowerhyphencase': + return $this->convertString($values[$matches[1]],self::LOWER_HYPHEN_CASE); + break; + case 'upperhyphencase': + return $this->convertString($values[$matches[1]],self::UPPER_HYPHEN_CASE); + break; + default: + return $values[$matches[1]]; + } + }, + $trait + ); + } + + private function convertString($string,$convertTo) + { + // make a best possible guess about type + $split = preg_split( + '_|-|[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*', + $string, + null, + PREG_SPLIT_NO_EMPTY + ); + $newString = ''; + for ($i=0, $size = count($split); $i < $size; $i++) { + if ($i === 0) + { + $delimiter = ''; + } + else + { + if ($convertTo === self::LOWER_HYPHEN_CASE || $convertTo === self::UPPER_HYPHEN_CASE) + { + $delimiter = '-'; + } + if ($convertTo === self::LOWER_UNDERSCORE_CASE || $convertTo === self::UPPER_UNDERSCORE_CASE) + { + $delimiter = '_'; + } + } + switch ($convertTo) { + case self::LOWER_CAMEL_CASE: + if ($i === 0) + { + $newString .= lcfirst($split[$i]); + break; + } + case self::UPPER_CAMEL_CASE: + $newString .= ucfirst($split[$i]); + break; + case self::LOWER_HYPHEN_CASE: + case self::LOWER_UNDERSCORE_CASE: + $newString .= $delimiter.strtolower($split[$i]); + break; + case self::UPPER_UNDERSCORE_CASE: + case self::UPPER_HYPHEN_CASE: + $newString .= $delimiter.strtoupper($split[$i]); + break; + } + } + return $newString; + } } From c5293f44c446ecd5db0f9df8ad4df5b61798eee9 Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Tue, 14 Feb 2017 02:46:36 +0100 Subject: [PATCH 04/26] Added explicit parameter check --- src/Parser.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Parser.php b/src/Parser.php index b41ef690..0ae7f78d 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -772,6 +772,18 @@ function ($matches) use ($values) { private function convertString($string,$convertTo) { + if (!in_array($convertTo,[ + LOWER_CAMEL_CASE, + LOWER_HYPHEN_CASE, + LOWER_UNDERSCORE_CASE, + UPPER_CAMEL_CASE, + UPPER_HYPHEN_CASE, + UPPER_UNDERSCORE_CASE + ])) + { + throw new \Exception('Invalid parameter "'.$convertTo.'" given for '.__CLASS__.__METHOD__); + } + // make a best possible guess about type $split = preg_split( '_|-|[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*', From 025dbc1c1c6f47863bd4433402d1177793394bbd Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Thu, 16 Feb 2017 19:47:56 +0100 Subject: [PATCH 05/26] Enabled error & warning to exception conversion in phpunit --- phpunit.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index d79774e2..4a11b3e3 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,9 +1,9 @@ Date: Tue, 28 Feb 2017 16:43:45 +0100 Subject: [PATCH 06/26] Added raml 1.0 datatypes and implemented parsing --- src/ApiDefinition.php | 87 ++++++- src/Body.php | 40 ++- src/Parser.php | 147 ++++------- src/QueryParameter.php | 15 ++ src/Trait.php | 15 ++ src/Type.php | 286 ++++++++++++++++++++++ src/TypeCollection.php | 200 +++++++++++++++ src/TypeInterface.php | 21 ++ src/Types/ArrayType.php | 176 +++++++++++++ src/Types/BooleanType.php | 28 +++ src/Types/DateOnlyType.php | 30 +++ src/Types/DateTimeOnlyType.php | 28 +++ src/Types/DateTimeType.php | 65 +++++ src/Types/FileType.php | 137 +++++++++++ src/Types/IntegerType.php | 33 +++ src/Types/LazyProxyType.php | 134 ++++++++++ src/Types/NullType.php | 30 +++ src/Types/NumberType.php | 169 +++++++++++++ src/Types/ObjectType.php | 272 ++++++++++++++++++++ src/Types/StringType.php | 140 +++++++++++ src/Types/TimeOnlyType.php | 26 ++ src/Types/UnionType.php | 65 +++++ src/Utility/StringTransformer.php | 79 ++++++ test/ApiDefinitionTest.php | 37 ++- test/fixture/raml-1.0/complexTypes.raml | 60 +++++ test/fixture/raml-1.0/example/test.json | 4 + test/fixture/raml-1.0/traits.raml | 31 +++ test/fixture/raml-1.0/traitsAndTypes.raml | 38 +++ test/fixture/raml-1.0/types.raml | 25 ++ 29 files changed, 2311 insertions(+), 107 deletions(-) create mode 100644 src/QueryParameter.php create mode 100644 src/Trait.php create mode 100644 src/Type.php create mode 100644 src/TypeCollection.php create mode 100644 src/TypeInterface.php create mode 100644 src/Types/ArrayType.php create mode 100644 src/Types/BooleanType.php create mode 100644 src/Types/DateOnlyType.php create mode 100644 src/Types/DateTimeOnlyType.php create mode 100644 src/Types/DateTimeType.php create mode 100644 src/Types/FileType.php create mode 100644 src/Types/IntegerType.php create mode 100644 src/Types/LazyProxyType.php create mode 100644 src/Types/NullType.php create mode 100644 src/Types/NumberType.php create mode 100644 src/Types/ObjectType.php create mode 100644 src/Types/StringType.php create mode 100644 src/Types/TimeOnlyType.php create mode 100644 src/Types/UnionType.php create mode 100644 src/Utility/StringTransformer.php create mode 100644 test/fixture/raml-1.0/complexTypes.raml create mode 100644 test/fixture/raml-1.0/example/test.json create mode 100644 test/fixture/raml-1.0/traits.raml create mode 100644 test/fixture/raml-1.0/traitsAndTypes.raml create mode 100644 test/fixture/raml-1.0/types.raml diff --git a/src/ApiDefinition.php b/src/ApiDefinition.php index 2f6c4942..fb95861b 100644 --- a/src/ApiDefinition.php +++ b/src/ApiDefinition.php @@ -12,6 +12,13 @@ use Raml\Exception\BadParameter\InvalidSchemaDefinitionException; use Raml\Exception\BadParameter\InvalidProtocolException; +use Raml\Utility\StringTransformer; + +use Raml\Types\UnionType; +use Raml\Types\ArrayType; +use Raml\Types\ObjectType; +use Raml\Types\LazyProxyType; + /** * The API Definition * @@ -138,9 +145,9 @@ class ApiDefinition implements ArrayInstantiationInterface * * @link https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md/#raml-data-types * - * @var array + * @var \Raml\TypeCollection */ - private $types = []; + private $types = null; // --- @@ -152,6 +159,9 @@ class ApiDefinition implements ArrayInstantiationInterface public function __construct($title) { $this->title = $title; + $this->types = TypeCollection::getInstance(); + // since the TypeCollection is a singleton, we need to clear it for every parse + $this->types->clear(); } /** @@ -180,7 +190,6 @@ public static function createFromArray($title, array $data = []) // -- - if (isset($data['version'])) { $apiDefinition->setVersion($data['version']); } @@ -246,10 +255,13 @@ public static function createFromArray($title, array $data = []) if (isset($data['types'])) { foreach ($data['types'] as $name => $definition) { - $apiDefinition->addType($name, $definition); + $apiDefinition->addType(ApiDefinition::determineType($name, $definition)); } } + // resolve type inheritance + $apiDefinition->getTypes()->applyInheritance(); + // --- foreach ($data as $resourceName => $resource) { @@ -586,21 +598,76 @@ public function addDocumentation($title, $documentation) $this->documentationList[$title] = $documentation; } + /** + * Determines the right Type and returns an instance + * + * @param string $name Name of type. + * @param array $definition Definition of type. + * @param \Raml\TypeCollection|null $typeCollection Type collection object. + * + * @return Raml\TypeInterface Returns a (best) matched type object. + **/ + public static function determineType($name, $definition) + { + // check if we can find a more appropriate Type subclass + $definition = is_string($definition) ? ['type' => $definition] : $definition; + $definition['type'] = isset($definition['type']) ? $definition['type'] : 'string'; + $type = $definition['type']; + $straightForwardTypes = [ + 'time-only', + 'datetime', + 'datetime-only', + 'date-only', + 'number', + 'integer', + 'boolean', + 'string', + 'null', + 'file', + 'array', + 'object' + ]; + + if (!in_array($type, ['','any'])) { + if (in_array($type, $straightForwardTypes)) { + $className = sprintf( + 'Raml\Types\%sType', + StringTransformer::convertString($type, StringTransformer::UPPER_CAMEL_CASE) + ); + return forward_static_call_array([$className,'createFromArray'], [$name, $definition]); + } + // if $type contains a '|' we can savely assume it's a combination of types (union) + if (strpos($type, '|') !== false) { + return UnionType::createFromArray($name, $definition); + } + // if $type contains a '[]' it means we have an array with a item restriction + if (strpos($type, '[]') !== false) { + return ArrayType::createFromArray($name, $definition); + } + // no standard type found so this must be a reference to a custom defined type + // since the actual definition can be defined later then when it is referenced + // we create a proxy object for lazy loading when it is needed + return LazyProxyType::createFromArray($name, $definition); + } + + // No subclass found, let's use base class + return Type::createFromArray($name, $definition); + } + /** * Add data type * - * @param string $name - * @param array $definition + * @param \Raml\TypeInterface $type */ - public function addType($name, $definition) + public function addType(TypeInterface $type) { - $this->types[$name] = $definition; + $this->types->add($type); } /** * Get data types * - * @return array + * @return \Raml\TypeCollection */ public function getTypes() { @@ -634,7 +701,7 @@ public function addResource(Resource $resource) /** * Get a security scheme by it's name * - * @param $schemeName + * @param string $schemeName * * @return SecurityScheme */ diff --git a/src/Body.php b/src/Body.php index d5b19fe8..b2d26152 100644 --- a/src/Body.php +++ b/src/Body.php @@ -5,6 +5,9 @@ use Raml\Schema\SchemaDefinitionInterface; use Raml\Exception\BadParameter\InvalidSchemaDefinitionException; +use Raml\ApiDefinition; +use Raml\TypeInterface; +use Raml\Types\ObjectType; /** * A body @@ -42,6 +45,8 @@ class Body implements BodyInterface, ArrayInstantiationInterface */ private $schema; + private $type; + /** * A list of examples * @@ -75,6 +80,7 @@ public function __construct($mediaType) * @param string $mediaType * @param array $data * [ + * type: ?string * schema: ?string * example: ?string * examples: ?array @@ -96,6 +102,15 @@ public static function createFromArray($mediaType, array $data = []) $body->setSchema($data['schema']); } + if (isset($data['type'])) { + $type = ApiDefinition::determineType($data['type'], ['type' => $data['type']]); + if ($type instanceof ObjectType) + { + $type->inheritFromParent(); + } + $body->setType($type); + } + if (isset($data['example'])) { $body->addExample($data['example']); } @@ -106,7 +121,6 @@ public static function createFromArray($mediaType, array $data = []) } } - return $body; } @@ -172,6 +186,30 @@ public function setSchema($schema) // -- + /** + * Get the type + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Set the type + * + * @param \Raml\TypeInterface $type + * + * @throws \Exception Throws exception when type does not parse + */ + public function setType(TypeInterface $type) + { + $this->type = $type; + } + + // -- + /** * Get the example * diff --git a/src/Parser.php b/src/Parser.php index 0ae7f78d..7ebbad06 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -22,12 +22,6 @@ */ class Parser { - const LOWER_CAMEL_CASE = 0, - LOWER_HYPHEN_CASE = 1, - LOWER_UNDERSCORE_CASE = 2, - UPPER_CAMEL_CASE = 4, - UPPER_HYPHEN_CASE = 8, - UPPER_UNDERSCORE_CASE = 16; /** * Array of cached files @@ -49,6 +43,13 @@ class Parser */ private $schemaParsers = []; + /** + * List of types + * + * @var string + **/ + private $types = []; + /** * List of security settings parsers * @@ -160,6 +161,16 @@ public function addSchemaParser(SchemaParserInterface $schemaParser) } } + /** + * Add a new type + * + * @param TypeInterface $type Type to add. + **/ + public function addType(TypeInterface $type) + { + $this->types[$type->getName()] = $type; + } + /** * Add a new security scheme * @@ -284,7 +295,7 @@ private function parseRamlData($ramlData, $rootDir) * Replaces schema into the raml file * * @param array $array - * @param array $schemas List of available schema definition + * @param array $schemas List of available schema definition. * * @return array */ @@ -316,7 +327,7 @@ private function replaceSchemas($array, $schemas) * * @return array */ - private function recurseAndParseSchemas($array, $rootDir) + private function recurseAndParseSchemas(array $array, $rootDir) { foreach ($array as $key => &$value) { if (is_array($value)) { @@ -351,7 +362,7 @@ private function getCachedFilePath($data) { /** * Parse the security settings data into an array * - * @param array $array + * @param array $schemesArray * * @return array */ @@ -390,30 +401,29 @@ private function parseSecuritySettings($schemesArray) } return $securitySchemes; - } /** * Parse the resource types * - * @param $ramlData + * @param mixed $ramlData * * @return array */ private function parseResourceTypes($ramlData) { if (isset($ramlData['resourceTypes'])) { - $keyedTraits = []; - foreach ($ramlData['resourceTypes'] as $trait) { - foreach ($trait as $k => $t) { - $keyedTraits[$k] = $t; + $keyedResourceTypes = []; + foreach ($ramlData['resourceTypes'] as $resourceType) { + foreach ($resourceType as $k => $t) { + $keyedResourceTypes[$k] = $t; } } foreach ($ramlData as $key => $value) { if (strpos($key, '/') === 0) { $name = (isset($value['displayName'])) ? $value['displayName'] : substr($key, 1); - $ramlData[$key] = $this->replaceTypes($value, $keyedTraits, $key, $name, $key); + $ramlData[$key] = $this->replaceTypes($value, $keyedResourceTypes, $key, $name, $key); } } } @@ -424,7 +434,7 @@ private function parseResourceTypes($ramlData) /** * Parse the traits * - * @param $ramlData + * @param mixed $ramlData * * @return array */ @@ -592,14 +602,14 @@ private function includeAndParseFiles($structure, $rootDir) /** * Insert the traits into the RAML file * - * @param array $raml + * @param string|array $raml * @param array $traits * @param string $path * @param string $name * * @return array */ - private function replaceTraits($raml, $traits, $path, $name) + private function replaceTraits($raml, array $traits, $path, $name) { if (!is_array($raml)) { return $raml; @@ -618,7 +628,7 @@ private function replaceTraits($raml, $traits, $path, $name) $traitVariables['resourcePath'] = $path; $traitVariables['resourcePathName'] = $name; - $trait = $this->applyTraitVariables($traitVariables, $traits[$traitName]); + $trait = $this->applyVariables($traitVariables, $traits[$traitName]); } elseif (isset($traits[$traitName])) { $trait = $traits[$traitName]; } @@ -662,14 +672,14 @@ private function replaceTypes($raml, $types, $path, $name, $parentKey = null) if ($key === 'type' && strpos($parentKey, '/') === 0) { $type = []; - $traitVariables = ['resourcePath' => $path, 'resourcePathName' => $name]; + $typeVariables = ['resourcePath' => $path, 'resourcePathName' => $name]; if (is_array($value)) { - $traitVariables = array_merge($traitVariables, current($value)); - $traitName = key($value); - $type = $this->applyTraitVariables($traitVariables, $types[$traitName]); + $typeVariables = array_merge($typeVariables, current($value)); + $typeName = key($value); + $type = $this->applyVariables($typeVariables, $types[$typeName]); } elseif (isset($types[$value])) { - $type = $this->applyTraitVariables($traitVariables, $types[$value]); + $type = $this->applyVariables($typeVariables, $types[$value]); } $newArray = array_replace_recursive($newArray, $this->replaceTypes($type, $types, $path, $name, $key)); @@ -693,14 +703,14 @@ private function replaceTypes($raml, $types, $path, $name, $parentKey = null) } /** - * Add trait variables + * Add trait/type variables * * @param array $values * @param array $trait * * @return mixed */ - private function applyTraitVariables(array $values, array $trait) + private function applyVariables(array $values, array $trait) { $newTrait = []; @@ -708,7 +718,7 @@ private function applyTraitVariables(array $values, array $trait) $newKey = $this->applyFunctions($key, $values); if (is_array($value)) { - $value = $this->applyTraitVariables($values, $value); + $value = $this->applyVariables($values, $value); } else { $value = $this->applyFunctions($value, $values); } @@ -718,6 +728,14 @@ private function applyTraitVariables(array $values, array $trait) return $newTrait; } + /** + * Applies functions on variable if they are defined + * + * @param mixed $trait + * @param array $values + * + * @return mixed Return input $trait after applying functions (if any) + */ private function applyFunctions($trait, array $values) { $variables = implode('|', array_keys($values)); @@ -745,22 +763,22 @@ function ($matches) use ($values) { return strtolower($values[$matches[1]]); break; case 'lowercamelcase': - return $this->convertString($values[$matches[1]],self::LOWER_CAMEL_CASE); + return StringTransformer::convertString($values[$matches[1]], StringTransformer::LOWER_CAMEL_CASE); break; case 'uppercamelcase': - return $this->convertString($values[$matches[1]],self::UPPER_CAMEL_CASE); + return StringTransformer::convertString($values[$matches[1]], StringTransformer::UPPER_CAMEL_CASE); break; case 'lowerunderscorecase': - return $this->convertString($values[$matches[1]],self::LOWER_UNDERSCORE_CASE); + return StringTransformer::convertString($values[$matches[1]], StringTransformer::LOWER_UNDERSCORE_CASE); break; case 'upperunderscorecase': - return $this->convertString($values[$matches[1]],self::UPPER_UNDERSCORE_CASE); + return StringTransformer::convertString($values[$matches[1]], StringTransformer::UPPER_UNDERSCORE_CASE); break; case 'lowerhyphencase': - return $this->convertString($values[$matches[1]],self::LOWER_HYPHEN_CASE); + return StringTransformer::convertString($values[$matches[1]], StringTransformer::LOWER_HYPHEN_CASE); break; case 'upperhyphencase': - return $this->convertString($values[$matches[1]],self::UPPER_HYPHEN_CASE); + return StringTransformer::convertString($values[$matches[1]], StringTransformer::UPPER_HYPHEN_CASE); break; default: return $values[$matches[1]]; @@ -769,65 +787,4 @@ function ($matches) use ($values) { $trait ); } - - private function convertString($string,$convertTo) - { - if (!in_array($convertTo,[ - LOWER_CAMEL_CASE, - LOWER_HYPHEN_CASE, - LOWER_UNDERSCORE_CASE, - UPPER_CAMEL_CASE, - UPPER_HYPHEN_CASE, - UPPER_UNDERSCORE_CASE - ])) - { - throw new \Exception('Invalid parameter "'.$convertTo.'" given for '.__CLASS__.__METHOD__); - } - - // make a best possible guess about type - $split = preg_split( - '_|-|[A-Z]([A-Z0-9]*[a-z][a-z0-9]*[A-Z]|[a-z0-9]*[A-Z][A-Z0-9]*[a-z])[A-Za-z0-9]*', - $string, - null, - PREG_SPLIT_NO_EMPTY - ); - $newString = ''; - for ($i=0, $size = count($split); $i < $size; $i++) { - if ($i === 0) - { - $delimiter = ''; - } - else - { - if ($convertTo === self::LOWER_HYPHEN_CASE || $convertTo === self::UPPER_HYPHEN_CASE) - { - $delimiter = '-'; - } - if ($convertTo === self::LOWER_UNDERSCORE_CASE || $convertTo === self::UPPER_UNDERSCORE_CASE) - { - $delimiter = '_'; - } - } - switch ($convertTo) { - case self::LOWER_CAMEL_CASE: - if ($i === 0) - { - $newString .= lcfirst($split[$i]); - break; - } - case self::UPPER_CAMEL_CASE: - $newString .= ucfirst($split[$i]); - break; - case self::LOWER_HYPHEN_CASE: - case self::LOWER_UNDERSCORE_CASE: - $newString .= $delimiter.strtolower($split[$i]); - break; - case self::UPPER_UNDERSCORE_CASE: - case self::UPPER_HYPHEN_CASE: - $newString .= $delimiter.strtoupper($split[$i]); - break; - } - } - return $newString; - } } diff --git a/src/QueryParameter.php b/src/QueryParameter.php new file mode 100644 index 00000000..5f5a8043 --- /dev/null +++ b/src/QueryParameter.php @@ -0,0 +1,15 @@ + + */ +class Type implements ArrayInstantiationInterface, TypeInterface +{ + /** + * Parent object + * + * @var ObjectType|string + **/ + private $parent = null; + + /** + * Key used for type + * + * @var string + **/ + private $name; + + /** + * Type + * + * @var string + **/ + private $type; + + /** + * Required + * + * @var bool + **/ + private $required = null; + + /** + * Raml definition + * + * @var array + **/ + private $definition; + + /** + * Create new type + * + * @param string $name + */ + public function __construct($name) + { + $this->name = $name; + } + + /** + * Create a new Type from an array of data + * + * @param string $name + * @param array $data + * + * @return Type + * + * @throws \Exception Thrown when input is incorrect. + */ + public static function createFromArray($name, array $data = []) + { + $class = new static($name); + + $class->setType($data['type']); + if (isset($data['usage'])) { + $class->setUsage($data['usage']); + } + $class->setDefinition($data); + + return $class; + } + + /** + * Dumps object to array + * + * @return array Object dumped to array. + */ + public function toArray() + { + return $this->definition; + } + + /** + * Set the value of name + * + * @param string $name + * + * @return self + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get the value of name + * + * @return string Returns name property. + */ + public function getName() + { + return $this->name; + } + + /** + * Set the value of type + * + * @param string $type + * + * @return self + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * Get the value of type + * + * @return string Returns type property. + */ + public function getType() + { + return $this->type; + } + + /** + * Set definition + * + * @param array $data Definition data of type. + **/ + public function setDefinition(array $data = []) + { + $this->definition = $data; + } + + /** + * Get definition + * + * @return string Returns definition property. + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * Get the value of Required + * + * @return bool + */ + public function getRequired() + { + return $this->required; + } + + /** + * Set the value of Required + * + * @param bool $required + * + * @return self + */ + public function setRequired($required) + { + $this->required = $required; + + return $this; + } + + /** + * Get the value of Parent + * + * @return ObjectType + */ + public function getParent() + { + if (is_string($this->parent)) { + $this->parent = TypeCollection::getInstance()->getTypeByName($this->parent); + } + return $this->parent; + } + + /** + * Returns true when parent property is set + * + * @return bool Returns true when parent exists, false if not. + */ + public function hasParent() + { + return ($this->parent !== null); + } + + /** + * Set the value of Parent + * + * @param ObjectType|string $parent + * + * @return self + */ + public function setParent($parent) + { + $this->parent = $parent; + + return $this; + } + + /** + * Inherit properties from parent (recursively) + * + * @return self Returns the new object with inherited properties. + **/ + public function inheritFromParent() + { + if (!$this->hasParent()) { + return $this; + } + $parent = $this->getParent(); + // recurse if + if ($parent instanceof $this && $parent->hasParent()) { + $this->parent = $parent->inheritFromParent(); + unset($parent); + } + if ($this->getType() === 'reference') { + return $this->getParent(); + } + if (!($this->getParent() instanceof $this)) { + throw new \Exception(sprintf( + 'Inheritance not possible because of incompatible Types, child is instance of %s and parent is instance of %s', + get_class($this), + get_class($this->getParent()) + )); + } + + // retrieve all getter/setters so we can check all properties for possible inheritance + $getters = []; + $setters = []; + foreach (get_class_methods($this) as $method) { + $result = preg_split('/^(get|set)(.*)$/', $method, null, PREG_SPLIT_NO_EMPTY); + if (count($result) === 2) { + if ($result[0] === 'get') { + $getters[lcfirst($result[1])] = $method; + } + if ($result[0] === 'set') { + $setters[lcfirst($result[1])] = $method; + } + } + } + $properties = array_keys(array_merge($getters, $setters)); + + foreach ($properties as $prop) { + if (!isset($getters[$prop]) || !isset($setters[$prop])) + { + continue; + } + $getter = $getters[$prop]; + $setter = $setters[$prop]; + $currentValue = $this->$getter(); + // if it is unset, make sure it is equal to parent + if ($currentValue === null) { + $this->$setter($this->getParent()->$getter()); + } + // if it is an array, add parent values + if (is_array($currentValue)) { + $newValue = array_merge($this->getParent()->$getter(), $currentValue); + $this->$setter($newValue); + continue; + } + } + return $this; + } +} diff --git a/src/TypeCollection.php b/src/TypeCollection.php new file mode 100644 index 00000000..8f686994 --- /dev/null +++ b/src/TypeCollection.php @@ -0,0 +1,200 @@ +collection = []; + $this->position = 0; + } + + /** + * The object is created from within the class itself + * only if the class has no instance. + * + * @return TypeCollection + **/ + public static function getInstance() + { + if (self::$instance == null) { + self::$instance = new TypeCollection(); + } + + return self::$instance; + } + + /** + * {@inheritDoc} + **/ + public function current() + { + return $this->collection[$this->position]; + } + + /** + * {@inheritDoc} + **/ + public function key() + { + return $this->position; + } + + /** + * {@inheritDoc} + **/ + public function next() + { + ++$this->position; + } + + /** + * {@inheritDoc} + **/ + public function rewind() + { + $this->position = 0; + } + + /** + * {@inheritDoc} + **/ + public function valid() + { + return isset($this->collection[$this->position]); + } + + /** + * Adds a Type to the collection + * + * @param \Raml\TypeInterface $type Type to add. + **/ + public function add(\Raml\TypeInterface $type) + { + $this->collection[] = $type; + } + + /** + * Remove given Type from the collection + * + * @param \Raml\TypeInterface $typeToRemove Type to remove. + **/ + public function remove(\Raml\TypeInterface $typeToRemove) + { + foreach ($this->collection as $key => $type) { + if ($type === $typeToRemove) { + unset($this->collection[$key]); + return; + } + } + throw new \Exception(sprintf('Cannot remove given type %s', var_export($type, true))); + } + + /** + * Retrieves a type by name + * + * @param string $name Name of the Type to retrieve. + * + * @return \Raml\TypeInterface Returns Type matching given name if found. + * @throws \Exception When no type is found. + **/ + public function getTypeByName($name) + { + foreach ($this->collection as $type) { + /** @var $type \Raml\TypeInterface */ + if ($type->getName() === $name) { + return $type; + } + } + throw new \Exception(sprintf('No type found for name %s, list: %s', var_export($name, true), var_export($allTypes, true))); + } + + /** + * Applies inheritance on all types that have a parent + **/ + public function applyInheritance() + { + foreach ($this->typesWithInheritance as $key => $type) { + $type->inheritFromParent(); + } + // now clear list to prevent applying multiple times on the same objects + $this->typesWithInheritance = []; + } + + /** + * Adds a Type to the list of typesWithInheritance + * + * @param ObjectType $type Type to add. + * + * @return self Returns self for chaining. + **/ + public function addTypeWithInheritance(ObjectType $type) + { + $this->typesWithInheritance[] = $type; + return $this; + } + + /** + * Returns types in a plain multidimensional array + * + * @return array Returns plain array. + **/ + public function toArray() + { + $types = []; + foreach ($this->collection as $type) + { + $types[$type->getName()] = $type->toArray(); + } + return $types; + } + + /** + * Clears the TypeCollection of any registered types + * + **/ + public function clear() + { + $this->collection = []; + $this->position = 0; + $this->typesWithInheritance = []; + } +} diff --git a/src/TypeInterface.php b/src/TypeInterface.php new file mode 100644 index 00000000..2059818a --- /dev/null +++ b/src/TypeInterface.php @@ -0,0 +1,21 @@ + + */ +interface TypeInterface extends ArrayInstantiationInterface +{ + /** + * Returns the name of the Type + **/ + public function getName(); + + /** + * Returns a multidimensional array of the Type's content + */ + public function toArray(); +} diff --git a/src/Types/ArrayType.php b/src/Types/ArrayType.php new file mode 100644 index 00000000..e9728f6d --- /dev/null +++ b/src/Types/ArrayType.php @@ -0,0 +1,176 @@ + + */ +class ArrayType extends Type +{ + /** + * Boolean value that indicates if items in the array MUST be unique. + * + * @var bool + **/ + private $uniqueItems; + + /** + * Indicates the type all items in the array are inherited from. Can be a reference to an existing type or an inline type declaration. + * + * @var string + **/ + private $items; + + /** + * Minimum amount of items in array. Value MUST be equal to or greater than 0. + * Default: 0. + * + * @var int + **/ + private $minItems; + + /** + * Maximum amount of items in array. Value MUST be equal to or greater than 0. + * Default: 2147483647. + * + * @var int + **/ + private $maxItems; + + /** + * Create a new ArrayType from an array of data + * + * @param string $name + * @param array $data + * + * @return ArrayType + */ + public static function createFromArray($name, array $data = []) + { + $type = parent::createFromArray($name, $data); + $pos = strpos($type->getType(), '[]'); + if ($pos !== false) { + $type->setItems(substr($type->getType(), 0, $pos)); + } + $type->setType('array'); + + foreach ($data as $key => $value) { + switch ($key) { + case 'uniqueItems': + $type->setUniqueItems($value); + break; + case 'items': + $type->setItems($value); + break; + case 'minItems': + $type->setMinItems($value); + break; + case 'maxItems': + $type->setMaxItems($value); + break; + } + } + + return $type; + } + + /** + * Get the value of Unique Items + * + * @return bool + */ + public function getUniqueItems() + { + return $this->uniqueItems; + } + + /** + * Set the value of Unique Items + * + * @param bool $uniqueItems + * + * @return self + */ + public function setUniqueItems($uniqueItems) + { + $this->uniqueItems = $uniqueItems; + + return $this; + } + + /** + * Get the value of Items + * + * @return string + */ + public function getItems() + { + return $this->items; + } + + /** + * Set the value of Items + * + * @param string $items + * + * @return self + */ + public function setItems($items) + { + $this->items = $items; + + return $this; + } + + /** + * Get the value of Min Items + * + * @return int + */ + public function getMinItems() + { + return $this->minItems; + } + + /** + * Set the value of Min Items + * + * @param int $minItems + * + * @return self + */ + public function setMinItems($minItems) + { + $this->minItems = $minItems; + + return $this; + } + + /** + * Get the value of Max Items + * + * @return int + */ + public function getMaxItems() + { + return $this->maxItems; + } + + /** + * Set the value of Max Items + * + * @param int $maxItems + * + * @return self + */ + public function setMaxItems($maxItems) + { + $this->maxItems = $maxItems; + + return $this; + } +} diff --git a/src/Types/BooleanType.php b/src/Types/BooleanType.php new file mode 100644 index 00000000..0e9d8759 --- /dev/null +++ b/src/Types/BooleanType.php @@ -0,0 +1,28 @@ + + */ +class BooleanType extends Type +{ + /** + * Create a new BooleanType from an array of data + * + * @param string $name + * @param array $data + * + * @return BooleanType + */ + public static function createFromArray($name, array $data = []) + { + $type = parent::createFromArray($name, $data); + + return $type; + } +} \ No newline at end of file diff --git a/src/Types/DateOnlyType.php b/src/Types/DateOnlyType.php new file mode 100644 index 00000000..f3fd714b --- /dev/null +++ b/src/Types/DateOnlyType.php @@ -0,0 +1,30 @@ + + */ +class DateOnlyType extends Type +{ + /** + * Create a new DateOnlyType from an array of data + * + * @param string $name + * @param array $data + * + * @return DateOnlyType + */ + public static function createFromArray($name, array $data = []) + { + $type = parent::createFromArray($name, $data); + + return $type; + } +} \ No newline at end of file diff --git a/src/Types/DateTimeOnlyType.php b/src/Types/DateTimeOnlyType.php new file mode 100644 index 00000000..021c7546 --- /dev/null +++ b/src/Types/DateTimeOnlyType.php @@ -0,0 +1,28 @@ + + */ +class DateTimeOnlyType extends Type +{ + /** + * Create a new DateTimeOnlyType from an array of data + * + * @param string $name + * @param array $data + * + * @return DateTimeOnlyType + */ + public static function createFromArray($name, array $data = []) + { + $type = parent::createFromArray($name, $data); + + return $type; + } +} \ No newline at end of file diff --git a/src/Types/DateTimeType.php b/src/Types/DateTimeType.php new file mode 100644 index 00000000..8ade810c --- /dev/null +++ b/src/Types/DateTimeType.php @@ -0,0 +1,65 @@ + $value) { + switch ($key) { + case 'format': + $type->setFormat($value); + break; + } + } + + return $type; + } + + /** + * Get the value of Format + * + * @return mixed + */ + public function getFormat() + { + return $this->format; + } + + /** + * Set the value of Format + * + * @param mixed $format + * + * @return self + */ + public function setFormat($format) + { + $this->format = $format; + + return $this; + } +} diff --git a/src/Types/FileType.php b/src/Types/FileType.php new file mode 100644 index 00000000..4c7884ca --- /dev/null +++ b/src/Types/FileType.php @@ -0,0 +1,137 @@ + + */ +class FileType extends Type +{ + /** + * A list of valid content-type strings for the file. The file type * / * MUST be a valid value. + * + * @var array + **/ + private $fileTypes; + + /** + * Specifies the minimum number of bytes for a parameter value. The value MUST be equal to or greater than 0. + * Default: 0 + * + * @var int + **/ + private $minLength; + + /** + * Specifies the maximum number of bytes for a parameter value. The value MUST be equal to or greater than 0. + * Default: 2147483647 + * + * @var int + **/ + private $maxLength; + + /** + * Create a new FileType from an array of data + * + * @param string $name + * @param array $data + * + * @return FileType + */ + public static function createFromArray($name, array $data = []) + { + $type = parent::createFromArray($name, $data); + + foreach ($data as $key => $value) { + switch ($key) { + case 'fileTypes': + $type->setFileTypes($value); + break; + case 'minLength': + $type->setMinLength($value); + break; + case 'maxLength': + $type->setMaxLength($value); + break; + } + } + + return $type; + } + + /** + * Get the value of File Types + * + * @return mixed + */ + public function getFileTypes() + { + return $this->fileTypes; + } + + /** + * Set the value of File Types + * + * @param mixed $fileTypes + * + * @return self + */ + public function setFileTypes($fileTypes) + { + $this->fileTypes = $fileTypes; + + return $this; + } + + /** + * Get the value of Min Length + * + * @return mixed + */ + public function getMinLength() + { + return $this->minLength; + } + + /** + * Set the value of Min Length + * + * @param mixed $minLength + * + * @return self + */ + public function setMinLength($minLength) + { + $this->minLength = $minLength; + + return $this; + } + + /** + * Get the value of Max Length + * + * @return mixed + */ + public function getMaxLength() + { + return $this->maxLength; + } + + /** + * Set the value of Max Length + * + * @param mixed $maxLength + * + * @return self + */ + public function setMaxLength($maxLength) + { + $this->maxLength = $maxLength; + + return $this; + } +} diff --git a/src/Types/IntegerType.php b/src/Types/IntegerType.php new file mode 100644 index 00000000..640242d1 --- /dev/null +++ b/src/Types/IntegerType.php @@ -0,0 +1,33 @@ + + */ +class IntegerType extends NumberType +{ + /** + * A numeric instance is valid against "multipleOf" if the result of dividing the instance by this keyword's value is an integer. + * + * @var int + **/ + private $multipleOf = 1; + + /** + * Create a new IntegerType from an array of data + * + * @param string $name + * @param array $data + * + * @return IntegerType + */ + public static function createFromArray($name, array $data = []) + { + $type = parent::createFromArray($name, $data); + + return $type; + } +} diff --git a/src/Types/LazyProxyType.php b/src/Types/LazyProxyType.php new file mode 100644 index 00000000..0b0cbfbd --- /dev/null +++ b/src/Types/LazyProxyType.php @@ -0,0 +1,134 @@ +name = $name; + $proxy->definition = $data; + if (!isset($data['type'])) { + throw new \Exception('Missing "type" key in $data param to determine datatype!'); + } + + $proxy->type = $data['type']; + + return $proxy; + } + + /** + * Dumps object to array + * + * @return array Object dumped to array. + */ + public function toArray() + { + return $this->definition; + } + + /** + * Returns type definition + * + * @return array Definition of object. + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * Get the value of name + * + * @return string Returns name property. + */ + public function getName() + { + return $this->name; + } + + /** + * Magic method to proxy all method calls to original object + * @param string $name Name of called method. + * @param mixed $params Parameteres of called method. + * + * @return mixed Returns whatever the actual method returns. + */ + public function __call($name, $params) + { + $original = $this->getResolvedObject(); + return call_user_func_array(array($original, $name), $params); + } + + public function getWrappedObject() + { + if ($this->wrappedObject === null) { + $typeCollection = TypeCollection::getInstance(); + $this->wrappedObject = $typeCollection->getTypeByName($this->type); + } + return $this->wrappedObject; + } + + public function getDefinitionRecursive() + { + $type = $this->getWrappedObject(); + $typeDefinition = ($type instanceof self) ? $type->getDefinitionRecursive() : $type->getDefinition(); + $recursiveDefinition = array_replace_recursive($typeDefinition, $this->getDefinition()); + $recursiveDefinition['type'] = $typeDefinition['type']; + return $recursiveDefinition; + } + + public function getResolvedObject() + { + $object = $this->getWrappedObject(); + if ($object instanceof self) { + $definition = $object->getDefinitionRecursive(); + return ApiDefinition::determineType($this->name, $definition); + } + return $object; + } +} diff --git a/src/Types/NullType.php b/src/Types/NullType.php new file mode 100644 index 00000000..1a0d9c10 --- /dev/null +++ b/src/Types/NullType.php @@ -0,0 +1,30 @@ + + */ +class NullType extends Type +{ + /** + * Create a new NullType from an array of data + * + * @param string $name + * @param array $data + * + * @return NullType + */ + public static function createFromArray($name, array $data = []) + { + $type = parent::createFromArray($name, $data); + + return $type; + } +} \ No newline at end of file diff --git a/src/Types/NumberType.php b/src/Types/NumberType.php new file mode 100644 index 00000000..e19e349d --- /dev/null +++ b/src/Types/NumberType.php @@ -0,0 +1,169 @@ + + */ +class NumberType extends Type +{ + /** + * The minimum value of the parameter. Applicable only to parameters of type number or integer. + * + * @var int + **/ + private $minimum; + + /** + * The maximum value of the parameter. Applicable only to parameters of type number or integer. + * + * @var int + **/ + private $maximum; + + /** + * The format of the value. The value MUST be one of the following: int32, int64, int, long, float, double, int16, int8 + * + * @var string + **/ + private $format; + + /** + * A numeric instance is valid against "multipleOf" if the result of dividing the instance by this keyword's value is an integer. + * + * @var int + **/ + private $multipleOf; + + /** + * Create a new NumberType from an array of data + * + * @param string $name + * @param array $data + * + * @return NumberType + */ + public static function createFromArray($name, array $data = []) + { + $type = parent::createFromArray($name, $data); + + foreach ($data as $key => $value) { + switch ($key) { + case 'minimum': + $type->setMinimum($value); + break; + case 'maximum': + $type->setMaximum($value); + break; + case 'format': + $type->setFormat($value); + break; + case 'multipleOf': + $type->setMultipleOf($value); + break; + } + } + + return $type; + } + + /** + * Get the value of Minimum + * + * @return int + */ + public function getMinimum() + { + return $this->minimum; + } + + /** + * Set the value of Minimum + * + * @param int $minimum + * + * @return self + */ + public function setMinimum($minimum) + { + $this->minimum = $minimum; + + return $this; + } + + /** + * Get the value of Maximum + * + * @return int + */ + public function getMaximum() + { + return $this->maximum; + } + + /** + * Set the value of Maximum + * + * @param int $maximum + * + * @return self + */ + public function setMaximum($maximum) + { + $this->maximum = $maximum; + + return $this; + } + + /** + * Get the value of Format + * + * @return string + */ + public function getFormat() + { + return $this->format; + } + + /** + * Set the value of Format + * + * @param string $format + * + * @return self + */ + public function setFormat($format) + { + $this->format = $format; + + return $this; + } + + /** + * Get the value of Multiple Of + * + * @return int + */ + public function getMultipleOf() + { + return $this->multipleOf; + } + + /** + * Set the value of Multiple Of + * + * @param int $multipleOf + * + * @return self + */ + public function setMultipleOf($multipleOf) + { + $this->multipleOf = $multipleOf; + + return $this; + } +} diff --git a/src/Types/ObjectType.php b/src/Types/ObjectType.php new file mode 100644 index 00000000..0defe80c --- /dev/null +++ b/src/Types/ObjectType.php @@ -0,0 +1,272 @@ + + */ +class ObjectType extends Type +{ + /** + * The properties that instances of this type can or must have. + * + * @var \Raml\Type[] + **/ + private $properties = null; + + /** + * The minimum number of properties allowed for instances of this type. + * + * @var int + **/ + private $minProperties = null; + + /** + * The maximum number of properties allowed for instances of this type. + * + * @var int + **/ + private $maxProperties = null; + + /** + * A Boolean that indicates if an object instance has additional properties. + * Default: true + * + * @var bool + **/ + private $additionalProperties = null; + + /** + * Determines the concrete type of an individual object at runtime when, + * for example, payloads contain ambiguous types due to unions or inheritance. + * The value must match the name of one of the declared properties of a type. + * Unsupported practices are inline type declarations and using discriminator with non-scalar properties. + * + * @var string + **/ + private $discriminator = null; + + /** + * Identifies the declaring type. + * Requires including a discriminator facet in the type declaration. + * A valid value is an actual value that might identify the type of an individual object and is unique in the hierarchy of the type. + * Inline type declarations are not supported. + * Default: The name of the type + * + * @var string + **/ + private $discriminatorValue = null; + + /** + * Create a new ObjectType from an array of data + * + * @param string $name Type name. + * @param array $data Type data. + * + * @return ObjectType + */ + public static function createFromArray($name, array $data = []) + { + $type = parent::createFromArray($name, $data); + $type->setType('object'); + + foreach ($data as $key => $value) { + switch ($key) { + case 'properties': + $type->setProperties($value); + break; + case 'minProperties': + $type->setMinProperties($value); + break; + case 'maxProperties': + $type->setMinProperties($value); + break; + case 'additionalProperties': + $type->setAdditionalProperties($value); + break; + case 'discriminator': + $type->setDiscriminator($value); + break; + case 'discriminatorValue': + $type->setDiscriminatorValue($value); + break; + } + } + + return $type; + } + + /** + * Get the value of Properties + * + * @return mixed + */ + public function getProperties() + { + return $this->properties; + } + + /** + * Set the value of Properties + * + * @param array $properties + * + * @return self + */ + public function setProperties(array $properties) + { + foreach ($properties as $name => $property) { + if ($property instanceof \Raml\TypeInterface === false) { + $property = ApiDefinition::determineType($name, $property); + } + $this->properties[] = $property; + } + + return $this; + } + + /** + * Returns a property by name + * + * @param string $name Name of property. + * + * @return Raml\TypeInterface + **/ + public function getPropertyByName($name) + { + foreach ($this->properties as $property) { + if ($property->getName() === $name) { + return $property; + } + } + throw new \Exception(sprintf('No such property: %s', $name)); + } + + + + /** + * Get the value of Min Properties + * + * @return mixed + */ + public function getMinProperties() + { + return $this->minProperties; + } + + /** + * Set the value of Min Properties + * + * @param mixed $minProperties + * + * @return self + */ + public function setMinProperties($minProperties) + { + $this->minProperties = $minProperties; + + return $this; + } + + /** + * Get the value of Max Properties + * + * @return mixed + */ + public function getMaxProperties() + { + return $this->maxProperties; + } + + /** + * Set the value of Max Properties + * + * @param mixed $maxProperties + * + * @return self + */ + public function setMaxProperties($maxProperties) + { + $this->maxProperties = $maxProperties; + + return $this; + } + + /** + * Get the value of Additional Properties + * + * @return mixed + */ + public function getAdditionalProperties() + { + return $this->additionalProperties; + } + + /** + * Set the value of Additional Properties + * + * @param mixed $additionalProperties + * + * @return self + */ + public function setAdditionalProperties($additionalProperties) + { + $this->additionalProperties = $additionalProperties; + + return $this; + } + + /** + * Get the value of Discriminator + * + * @return mixed + */ + public function getDiscriminator() + { + return $this->discriminator; + } + + /** + * Set the value of Discriminator + * + * @param mixed $discriminator + * + * @return self + */ + public function setDiscriminator($discriminator) + { + $this->discriminator = $discriminator; + + return $this; + } + + /** + * Get the value of Discriminator Value + * + * @return mixed + */ + public function getDiscriminatorValue() + { + return $this->discriminatorValue; + } + + /** + * Set the value of Discriminator Value + * + * @param mixed $discriminatorValue + * + * @return self + */ + public function setDiscriminatorValue($discriminatorValue) + { + $this->discriminatorValue = $discriminatorValue; + + return $this; + } +} diff --git a/src/Types/StringType.php b/src/Types/StringType.php new file mode 100644 index 00000000..c434cfcb --- /dev/null +++ b/src/Types/StringType.php @@ -0,0 +1,140 @@ + + */ +class StringType extends Type +{ + /** + * Regular expression that this string should match. + * + * @var string + **/ + private $pattern; + + /** + * Minimum length of the string. Value MUST be equal to or greater than 0. + * Default: 0 + * + * @var int + **/ + private $minLength; + + /** + * Maximum length of the string. Value MUST be equal to or greater than 0. + * Default: 2147483647 + * + * @var int + **/ + private $maxLength; + + /** + * Create a new StringType from an array of data + * + * @param string $name + * @param array $data + * + * @return StringType + */ + public static function createFromArray($name, array $data = []) + { + $type = parent::createFromArray($name, $data); + /* @var $type StringType */ + + foreach ($data as $key => $value) { + switch ($key) { + case 'pattern': + $type->setPattern($value); + break; + case 'minLength': + $type->setMinLength($value); + break; + case 'maxLength': + $type->setMaxLength($value); + break; + } + } + + return $type; + } + + /** + * Get the value of Pattern + * + * @return mixed + */ + public function getPattern() + { + return $this->pattern; + } + + /** + * Set the value of Pattern + * + * @param mixed $pattern + * + * @return self + */ + public function setPattern($pattern) + { + $this->pattern = $pattern; + + return $this; + } + + /** + * Get the value of Min Length + * + * @return mixed + */ + public function getMinLength() + { + return $this->minLength; + } + + /** + * Set the value of Min Length + * + * @param mixed $minLength + * + * @return self + */ + public function setMinLength($minLength) + { + $this->minLength = $minLength; + + return $this; + } + + /** + * Get the value of Max Length + * + * @return mixed + */ + public function getMaxLength() + { + return $this->maxLength; + } + + /** + * Set the value of Max Length + * + * @param mixed $maxLength + * + * @return self + */ + public function setMaxLength($maxLength) + { + $this->maxLength = $maxLength; + + return $this; + } +} diff --git a/src/Types/TimeOnlyType.php b/src/Types/TimeOnlyType.php new file mode 100644 index 00000000..d054ce57 --- /dev/null +++ b/src/Types/TimeOnlyType.php @@ -0,0 +1,26 @@ + + */ +class UnionType extends Type +{ + /** + * Possible Types + * + * @var array + **/ + private $possibleTypes = []; + + /** + * Create a new UnionType from an array of data + * + * @param string $name + * @param array $data + * + * @return UnionType + */ + public static function createFromArray($name, array $data = []) + { + $type = parent::createFromArray($name, $data); + $type->setPossibleTypes(explode('|', $type->getType())); + $type->setType('union'); + + return $type; + } + + /** + * Get the value of Possible Types + * + * @return array + */ + public function getPossibleTypes() + { + return $this->possibleTypes; + } + + /** + * Set the value of Possible Types + * + * @param array $possibleTypes + * + * @return self + */ + public function setPossibleTypes(array $possibleTypes) + { + foreach ($possibleTypes as $type) { + $this->possibleTypes[] = ApiDefinition::determineType('', ['type' => trim($type)]); + } + + return $this; + } +} diff --git a/src/Utility/StringTransformer.php b/src/Utility/StringTransformer.php new file mode 100644 index 00000000..8cc7cfc9 --- /dev/null +++ b/src/Utility/StringTransformer.php @@ -0,0 +1,79 @@ + 'number', ) ) - ), $api->getTypes()); + ), $api->getTypes()->toArray()); + } + + /** @test */ + public function shouldParseTypesToSubTypes() + { + $api = $this->parser->parse(__DIR__.'/fixture/raml-1.0/types.raml'); + $types = $api->getTypes(); + $object = $types->current(); + $this->assertInstanceOf('\Raml\Types\ObjectType', $object); + $this->assertInstanceOf('\Raml\Types\IntegerType', $object->getPropertyByName('id')); + $this->assertInstanceOf('\Raml\Types\StringType', $object->getPropertyByName('name')); + } + + /** @test */ + public function shouldParseComplexTypes() + { + $api = $this->parser->parse(__DIR__.'/fixture/raml-1.0/complexTypes.raml'); + // check types + $org = $api->getTypes()->getTypeByName('Org'); + $this->assertInstanceOf('\Raml\Types\ObjectType', $org); + // property will return a proxy object so to compare to actual type we will need to ask for the resolved object + $this->assertInstanceOf('\Raml\Types\UnionType', $org->getPropertyByName('onCall')->getResolvedObject()); + $head = $org->getPropertyByName('Head'); + $this->assertInstanceOf('\Raml\Types\ObjectType', $head->getResolvedObject()); + $this->assertInstanceOf('\Raml\Types\StringType', $head->getPropertyByName('firstname')); + $this->assertInstanceOf('\Raml\Types\StringType', $head->getPropertyByName('lastname')); + $this->assertInstanceOf('\Raml\Types\StringType', $head->getPropertyByName('title?')); + $this->assertInstanceOf('\Raml\Types\StringType', $head->getPropertyByName('kind')); + $reports = $head->getPropertyByName('reports'); + $this->assertInstanceOf('\Raml\Types\ArrayType', $reports); + $phone = $head->getPropertyByName('phone')->getResolvedObject(); + $this->assertInstanceOf('\Raml\Types\StringType', $phone); + // check resources + $type = $api->getResourceByPath('/orgs/{orgId}')->getMethod('get')->getResponse(200)->getBodyByType('application/json')->getType(); + $this->assertInstanceOf('\Raml\Types\ObjectType', $type->getResolvedObject()); } /** @test */ diff --git a/test/fixture/raml-1.0/complexTypes.raml b/test/fixture/raml-1.0/complexTypes.raml new file mode 100644 index 00000000..5a5d0e0d --- /dev/null +++ b/test/fixture/raml-1.0/complexTypes.raml @@ -0,0 +1,60 @@ +#%RAML 1.0 +title: My API with Types +mediaType: application/json +types: + Org: + type: object + properties: + onCall: Alertable # inherits all properties from type `Alertable` + Head: Manager # inherits all properties from type `Manager` + Person: + type: object + discriminator: kind # reference to the `kind` property of `Person` + properties: + firstname: string + lastname: string + title?: string + kind: string # may be used to differenciate between classes that extend from `Person` + Phone: + type: string + pattern: "^[0-9|-]+$" # defines pattern for the content of type `Phone` + Manager: + type: Person # inherits all properties from type `Person` + properties: + reports: Person[] # inherits all properties from type `Person`; array type where `[]` is a shortcut + phone: Phone + Admin: + type: Person # inherits all properties from type `Person` + properties: + clearanceLevel: + enum: [ low, high ] + AlertableAdmin: + type: Admin # inherits all properties from type `Admin` + properties: + phone: Phone # inherits all properties from type `Phone`; uses shortcut syntax + Alertable: Manager | AlertableAdmin # union type; either a `Manager` or `AlertableAdmin` +/orgs/{orgId}: + get: + responses: + 200: + body: + application/json: + type: Org # reference to global type definition + example: + onCall: + firstname: nico + lastname: ark + kind: AlertableAdmin + clearanceLevel: low + phone: "12321" + Head: + firstname: nico + lastname: ark + kind: Manager + reports: + - + firstname: nico + lastname: ark + kind: Admin + clearanceLevel: low + phone: "123-23" diff --git a/test/fixture/raml-1.0/example/test.json b/test/fixture/raml-1.0/example/test.json new file mode 100644 index 00000000..75fddf9a --- /dev/null +++ b/test/fixture/raml-1.0/example/test.json @@ -0,0 +1,4 @@ +{ + "id": 1, + "name": "test" +} \ No newline at end of file diff --git a/test/fixture/raml-1.0/traits.raml b/test/fixture/raml-1.0/traits.raml new file mode 100644 index 00000000..bde9aaca --- /dev/null +++ b/test/fixture/raml-1.0/traits.raml @@ -0,0 +1,31 @@ +#%RAML 1.0 + +traits: + secured: + usage: Apply this to any method that needs to be secured + description: Some requests require authentication. + headers: + access_token: + description: Access Token + example: 5757gh76 + required: true + paged: + queryParameters: + numPages: + description: The number of pages to return +types: + TestType: + type: object + properties: + id: integer + name: string + example: !include example/test.json +/test: + is: [ secured ] + get: + is: [ paged ] + responses: + 200: + body: + application/json: + type: TestType \ No newline at end of file diff --git a/test/fixture/raml-1.0/traitsAndTypes.raml b/test/fixture/raml-1.0/traitsAndTypes.raml new file mode 100644 index 00000000..63f1e157 --- /dev/null +++ b/test/fixture/raml-1.0/traitsAndTypes.raml @@ -0,0 +1,38 @@ +#%RAML 1.0 +title: Example API +version: v1 +resourceTypes: + searchableCollection: + get: + queryParameters: + title: + description: Return values that have their title matching the given value +traits: + secured: + usage: Apply this to any method that needs to be secured + description: Some requests require authentication. + headers: + <>: + description: A valid <> is required + example: 5757gh76 + required: true + paged: + queryParameters: + numPages: + description: The number of pages to return +types: + TestType: + type: object + properties: + id: integer + name: string + example: !include example/test.json +/test: + type: searchableCollection + get: + is: [ secured: { tokenName: access_token }, paged ] + responses: + 200: + body: + application/json: + type: TestType \ No newline at end of file diff --git a/test/fixture/raml-1.0/types.raml b/test/fixture/raml-1.0/types.raml new file mode 100644 index 00000000..8af65b6d --- /dev/null +++ b/test/fixture/raml-1.0/types.raml @@ -0,0 +1,25 @@ +#%RAML 1.0 +title: Example API +version: v1 +types: + TestType: + type: object + properties: + id: integer + name: string + example: !include example/test.json +/test: + get: + responses: + 200: + body: + application/json: + type: TestType + post: + body: + type: TestType + responses: + 200: + body: + application/json: + type: TestType \ No newline at end of file From c44f2ace29662a3a9f3e79e661e87842078c3046 Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Wed, 1 Mar 2017 17:26:59 +0100 Subject: [PATCH 07/26] Added JSON & XML types --- src/Body.php | 7 ++ src/Exception/PropertyNotFoundException.php | 10 +++ src/Types/JsonType.php | 81 +++++++++++++++++++++ src/Types/NumberType.php | 75 ++++++++++++++++++- src/Types/ObjectType.php | 3 +- src/Types/XmlType.php | 72 ++++++++++++++++++ 6 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 src/Exception/PropertyNotFoundException.php create mode 100644 src/Types/JsonType.php create mode 100644 src/Types/XmlType.php diff --git a/src/Body.php b/src/Body.php index b2d26152..3fdc35b2 100644 --- a/src/Body.php +++ b/src/Body.php @@ -45,6 +45,13 @@ class Body implements BodyInterface, ArrayInstantiationInterface */ private $schema; + /** + * The type of the body + * + * @see http://raml.org/spec.html#raml-data-types + * + * @var SchemaDefinitionInterface|string + */ private $type; /** diff --git a/src/Exception/PropertyNotFoundException.php b/src/Exception/PropertyNotFoundException.php new file mode 100644 index 00000000..c5425288 --- /dev/null +++ b/src/Exception/PropertyNotFoundException.php @@ -0,0 +1,10 @@ + + */ +class JsonType extends Type +{ + /** + * Json schema + * + * @var string + **/ + private $json; + + /** + * Create a new JsonType from an array of data + * + * @param string $name + * @param array $data + * + * @return StringType + */ + public static function createFromArray($name, array $data = []) + { + $type = parent::createFromArray($name, $data); + /* @var $type StringType */ + + $this->json = $data; + + return $type; + } + + /** + * Validate a JSON string against the schema + * - Converts the string into a JSON object then uses the JsonSchema Validator to validate + * + * @param $string + * + * @return bool + */ + public function validate($string) + { + $json = json_decode($string); + + if (json_last_error() !== JSON_ERROR_NONE) { + return false; + } + + return $this->validateJsonObject($json); + } + + /** + * Validates a json object + * + * @param string $json + * + * @throws InvalidSchemaException + * + * @return bool + */ + public function validateJsonObject($json) + { + $validator = new Validator(); + $jsonSchema = $this->json; + + $validator->check($json, $jsonSchema); + + if (!$validator->isValid()) { + return false; + } + + return true; + } +} diff --git a/src/Types/NumberType.php b/src/Types/NumberType.php index e19e349d..35241e95 100644 --- a/src/Types/NumberType.php +++ b/src/Types/NumberType.php @@ -16,28 +16,28 @@ class NumberType extends Type * * @var int **/ - private $minimum; + private $minimum = null; /** * The maximum value of the parameter. Applicable only to parameters of type number or integer. * * @var int **/ - private $maximum; + private $maximum = null; /** * The format of the value. The value MUST be one of the following: int32, int64, int, long, float, double, int16, int8 * * @var string **/ - private $format; + private $format = null; /** * A numeric instance is valid against "multipleOf" if the result of dividing the instance by this keyword's value is an integer. * * @var int **/ - private $multipleOf; + private $multipleOf = null; /** * Create a new NumberType from an array of data @@ -135,9 +135,13 @@ public function getFormat() * @param string $format * * @return self + * @throws Exception Thrown when given format is not any of allowed types. */ public function setFormat($format) { + if (!in_array($format, ['int32', 'int64', 'int', 'long', 'float', 'double', 'int16', 'int8'])) { + throw new \Exception(sprinf('Incorrect format given: "%s"', $format)); + } $this->format = $format; return $this; @@ -166,4 +170,67 @@ public function setMultipleOf($multipleOf) return $this; } + + public function validate($value) + { + if (!is_null($this->maximum)) { + if ($value > $this->maximum) { + return false; + } + } + if (!is_null($this->minimum)) { + if ($value < $this->minimum) { + return false; + } + } + switch ($this->format) { + case 'int8': + if (filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => -128, 'max_range' => 127]]) === false) { + return false; + } + break; + case 'int16': + if (filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => -32768, 'max_range' => 32767]]) === false) { + return false; + } + break; + case 'int32': + if (filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => -2147483648, 'max_range' => 2147483647]]) === false) { + return false; + } + break; + case 'int64': + if (filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => -9223372036854775808, 'max_range' => 9223372036854775807]]) === false) { + return false; + } + break; + case 'int': + // int === long + case 'long': + if (!is_int($value)) { + return false; + } + break; + case 'float': + // float === double + case 'double': + if (!is_float($value)) { + return false; + } + break; + // if no format is given we check only if it is a number + null: + default: + if (!is_float($value) && !is_int($value)) { + return false; + } + break; + } + if (!is_null($this->multipleOf)) { + if ($value %$this->multipleOf != 0) { + return false; + } + } + return true; + } } diff --git a/src/Types/ObjectType.php b/src/Types/ObjectType.php index 0defe80c..bdd9ccbc 100644 --- a/src/Types/ObjectType.php +++ b/src/Types/ObjectType.php @@ -5,6 +5,7 @@ use Raml\Type; use Raml\ApiDefinition; use Raml\TypeCollection; +use Raml\Exception\PropertyNotFoundException; /** * ObjectType class @@ -145,7 +146,7 @@ public function getPropertyByName($name) return $property; } } - throw new \Exception(sprintf('No such property: %s', $name)); + throw new PropertyNotFoundException(sprintf('No such property: %s', $name)); } diff --git a/src/Types/XmlType.php b/src/Types/XmlType.php new file mode 100644 index 00000000..76132e3f --- /dev/null +++ b/src/Types/XmlType.php @@ -0,0 +1,72 @@ + + */ +class XmlType extends Type +{ + /** + * XML schema + * + * @var string + **/ + private $xml; + + /** + * Create a new JsonType from an array of data + * + * @param string $name + * @param array $data + * + * @return StringType + */ + public static function createFromArray($name, array $data = []) + { + $type = parent::createFromArray($name, $data); + /* @var $type StringType */ + + $this->xml = $data; + + return $type; + } + + /** + * Validate an XML string against the schema + * + * @param $string + * + * @return bool + */ + public function validate($string) + { + $dom = new \DOMDocument; + + $originalErrorLevel = libxml_use_internal_errors(true); + + $dom->loadXML($string); + $errors = libxml_get_errors(); + libxml_clear_errors(); + if ($errors) { + return false; + } + + // --- + + $dom->schemaValidateSource($this->xml); + $errors = libxml_get_errors(); + libxml_clear_errors(); + if ($errors) { + return false; + } + + libxml_use_internal_errors($originalErrorLevel); + + return true; + } +} From b0dbb6ee6c9e0d192cc923586dfcdcf8c3047de6 Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Wed, 1 Mar 2017 17:28:12 +0100 Subject: [PATCH 08/26] Added validation of types --- .../Definition/JsonSchemaDefinition.php | 4 +-- src/Type.php | 9 ++++++ src/TypeInterface.php | 13 ++++++-- src/Types/ArrayType.php | 5 ++++ src/Types/BooleanType.php | 7 ++++- src/Types/DateOnlyType.php | 8 ++++- src/Types/DateTimeOnlyType.php | 8 ++++- src/Types/DateTimeType.php | 7 +++++ src/Types/FileType.php | 5 ++++ src/Types/IntegerType.php | 5 ++++ src/Types/LazyProxyType.php | 6 ++++ src/Types/NullType.php | 7 ++++- src/Types/ObjectType.php | 20 +++++++++++++ src/Types/StringType.php | 30 +++++++++++++++++-- src/Types/TimeOnlyType.php | 6 ++++ src/Types/UnionType.php | 10 +++++++ test/ApiDefinitionTest.php | 18 +++++++++++ 17 files changed, 157 insertions(+), 11 deletions(-) diff --git a/src/Schema/Definition/JsonSchemaDefinition.php b/src/Schema/Definition/JsonSchemaDefinition.php index 8b483546..c22688c2 100644 --- a/src/Schema/Definition/JsonSchemaDefinition.php +++ b/src/Schema/Definition/JsonSchemaDefinition.php @@ -37,9 +37,9 @@ public function __construct(\stdClass $json) * * @param $string * - * @throws \Exception + * @throws Exception * - * @return boolean + * @return bool */ public function validate($string) { diff --git a/src/Type.php b/src/Type.php index 8de44736..7aa7e4f9 100644 --- a/src/Type.php +++ b/src/Type.php @@ -283,4 +283,13 @@ public function inheritFromParent() } return $this; } + + /** + * {@inheritdoc} + */ + public function validate($value) + { + // everything is valid for the any type + return true; + } } diff --git a/src/TypeInterface.php b/src/TypeInterface.php index 2059818a..47c979b2 100644 --- a/src/TypeInterface.php +++ b/src/TypeInterface.php @@ -10,12 +10,21 @@ interface TypeInterface extends ArrayInstantiationInterface { /** - * Returns the name of the Type + * Returns the name of the Type. **/ public function getName(); /** - * Returns a multidimensional array of the Type's content + * Returns a multidimensional array of the Type's content. */ public function toArray(); + + /** + * Returns boolean true when the given $value is valid against the type, false otherwise. + * + * @param mixed $value Value to validate. + * + * @return bool Returns true when valid, false otherwise. + */ + public function validate($value); } diff --git a/src/Types/ArrayType.php b/src/Types/ArrayType.php index e9728f6d..12df3670 100644 --- a/src/Types/ArrayType.php +++ b/src/Types/ArrayType.php @@ -173,4 +173,9 @@ public function setMaxItems($maxItems) return $this; } + + public function validate($value) + { + return is_array($value); + } } diff --git a/src/Types/BooleanType.php b/src/Types/BooleanType.php index 0e9d8759..aff21be5 100644 --- a/src/Types/BooleanType.php +++ b/src/Types/BooleanType.php @@ -25,4 +25,9 @@ public static function createFromArray($name, array $data = []) return $type; } -} \ No newline at end of file + + public function validate($value) + { + return is_bool($value); + } +} diff --git a/src/Types/DateOnlyType.php b/src/Types/DateOnlyType.php index f3fd714b..64c46a2d 100644 --- a/src/Types/DateOnlyType.php +++ b/src/Types/DateOnlyType.php @@ -27,4 +27,10 @@ public static function createFromArray($name, array $data = []) return $type; } -} \ No newline at end of file + + public function validate($value) + { + $d = DateTime::createFromFormat('Y-m-d', $value); + return $d && $d->format('Y-m-d') === $value; + } +} diff --git a/src/Types/DateTimeOnlyType.php b/src/Types/DateTimeOnlyType.php index 021c7546..e46321e6 100644 --- a/src/Types/DateTimeOnlyType.php +++ b/src/Types/DateTimeOnlyType.php @@ -25,4 +25,10 @@ public static function createFromArray($name, array $data = []) return $type; } -} \ No newline at end of file + + public function validate($value) + { + $d = DateTime::createFromFormat(DATE_RFC3339, $value); + return $d && $d->format(DATE_RFC3339) === $value; + } +} diff --git a/src/Types/DateTimeType.php b/src/Types/DateTimeType.php index 8ade810c..4b27a734 100644 --- a/src/Types/DateTimeType.php +++ b/src/Types/DateTimeType.php @@ -62,4 +62,11 @@ public function setFormat($format) return $this; } + + public function validate($value) + { + $format = $this->format ?: DATE_RFC3339; + $d = DateTime::createFromFormat($format, $value); + return $d && $d->format($format) === $value; + } } diff --git a/src/Types/FileType.php b/src/Types/FileType.php index 4c7884ca..a190a058 100644 --- a/src/Types/FileType.php +++ b/src/Types/FileType.php @@ -134,4 +134,9 @@ public function setMaxLength($maxLength) return $this; } + + public function validate($value) + { + return (bool) preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $value); + } } diff --git a/src/Types/IntegerType.php b/src/Types/IntegerType.php index 640242d1..2490603b 100644 --- a/src/Types/IntegerType.php +++ b/src/Types/IntegerType.php @@ -30,4 +30,9 @@ public static function createFromArray($name, array $data = []) return $type; } + + public function validate($value) + { + return is_int($value); + } } diff --git a/src/Types/LazyProxyType.php b/src/Types/LazyProxyType.php index 0b0cbfbd..ceb5ce4e 100644 --- a/src/Types/LazyProxyType.php +++ b/src/Types/LazyProxyType.php @@ -131,4 +131,10 @@ public function getResolvedObject() } return $object; } + + public function validate($value) + { + $original = $this->getResolvedObject(); + return $original->validate($value); + } } diff --git a/src/Types/NullType.php b/src/Types/NullType.php index 1a0d9c10..88e5830d 100644 --- a/src/Types/NullType.php +++ b/src/Types/NullType.php @@ -27,4 +27,9 @@ public static function createFromArray($name, array $data = []) return $type; } -} \ No newline at end of file + + public function validate($value) + { + return is_null($value); + } +} diff --git a/src/Types/ObjectType.php b/src/Types/ObjectType.php index bdd9ccbc..70eeb6c0 100644 --- a/src/Types/ObjectType.php +++ b/src/Types/ObjectType.php @@ -270,4 +270,24 @@ public function setDiscriminatorValue($discriminatorValue) return $this; } + + public function validate($value) + { + // an object is in essence just a group (array) of datatypes + if (!is_array($value)) { + return false; + } + foreach ($value as $name => $propertyValue) { + try { + $property = $this->getPropertyByName($name); + if (!$property->validate($propertyValue)) { + return false; + } + } catch (PropertyNotFoundException $e) { + // if no property found, carry on + return false; + } + } + return true; + } } diff --git a/src/Types/StringType.php b/src/Types/StringType.php index c434cfcb..883d6fa3 100644 --- a/src/Types/StringType.php +++ b/src/Types/StringType.php @@ -18,7 +18,7 @@ class StringType extends Type * * @var string **/ - private $pattern; + private $pattern = null; /** * Minimum length of the string. Value MUST be equal to or greater than 0. @@ -26,7 +26,7 @@ class StringType extends Type * * @var int **/ - private $minLength; + private $minLength = null; /** * Maximum length of the string. Value MUST be equal to or greater than 0. @@ -34,7 +34,7 @@ class StringType extends Type * * @var int **/ - private $maxLength; + private $maxLength = null; /** * Create a new StringType from an array of data @@ -137,4 +137,28 @@ public function setMaxLength($maxLength) return $this; } + + public function validate($value) + { + if (!is_string($value)) { + return false; + } + if (!is_null($this->pattern)) { + if (preg_match('/'.$this->pattern.'/', $value) == false) { + return false; + } + } + if (!is_null($this->minLength)) { + if (strlen($value) < $this->minLength) { + return false; + } + } + if (!is_null($this->maxLength)) { + if (strlen($value) > $this->maxLength) { + return false; + } + } + + return true; + } } diff --git a/src/Types/TimeOnlyType.php b/src/Types/TimeOnlyType.php index d054ce57..530d36d9 100644 --- a/src/Types/TimeOnlyType.php +++ b/src/Types/TimeOnlyType.php @@ -23,4 +23,10 @@ public static function createFromArray($name, array $data = []) return $type; } + + public function validate($value) + { + $d = DateTime::createFromFormat('HH:II:SS', $value); + return $d && $d->format('HH:II:SS') === $value; + } } diff --git a/src/Types/UnionType.php b/src/Types/UnionType.php index 768a8a87..bad0a0ef 100644 --- a/src/Types/UnionType.php +++ b/src/Types/UnionType.php @@ -62,4 +62,14 @@ public function setPossibleTypes(array $possibleTypes) return $this; } + + public function validate($value) + { + foreach ($this->getPossibleTypes() as $type) { + if ($type->validate($value)) { + return true; + } + } + return false; + } } diff --git a/test/ApiDefinitionTest.php b/test/ApiDefinitionTest.php index bc6eb139..e362bb3e 100644 --- a/test/ApiDefinitionTest.php +++ b/test/ApiDefinitionTest.php @@ -124,6 +124,24 @@ public function shouldParseComplexTypes() $this->assertInstanceOf('\Raml\Types\ObjectType', $type->getResolvedObject()); } + /** @test */ + public function shouldValidateResponse() + { + $api = $this->parser->parse(__DIR__.'/fixture/raml-1.0/complexTypes.raml'); + $body = $api->getResourceByPath('/orgs/{orgId}')->getMethod('get')->getResponse(200)->getBodyByType('application/json'); + /* @var $body \Raml\Body */ + + $validResponse = $body->getExample(); + $type = $body->getType(); + $this->assertTrue($type->validate($validResponse)); + + $invalidResponse = [ + 'onCall' => 'this is not an object', + 'Head' => 'this is not an object' + ]; + $this->assertFalse($type->validate($invalidResponse)); + } + /** @test */ public function shouldReturnProtocolsIfSpecified() { From 91e62ea9e5566b185eb584c122665be50d87572b Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Thu, 2 Mar 2017 16:22:15 +0100 Subject: [PATCH 09/26] Added notice and todo for 1.0 spec to README --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 1b69343d..af5d8a6b 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,20 @@ # PHP RAML Parser +## **!!Attention!!** this is a work-in-progress of the RAML 1.0 specification, for RAML 0.8 see the [master branch](https://github.com/alecsammon/php-raml-parser/tree/master) + +### Still TODO: +- [User defined facets](https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md/#user-defined-facets) +- Full implementation of [type expressions](https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md/#type-expressions) + - The shorthand array and the union type have been implemented + - Bi-dimensional array and the array-union combination have **NOT** been implemented yet. +- [Multiple inheritance](https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md/#multiple-inheritance) +- [Annotations](https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md/#annotations) +- [Libraries](https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md/#libraries) +- [Overlays and Extensions](https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md/#overlays-and-extensions) +- [Improved Security Schemes](https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md/#security-schemes) + +## Original documentation + Parses a RAML file into a PHP object. [![Build Status](https://travis-ci.org/alecsammon/php-raml-parser.svg?branch=master)](https://travis-ci.org/alecsammon/php-raml-parser) From f7fce9acaa6ed4dada5991f2c17a8ff4c9de2806 Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Thu, 2 Mar 2017 16:22:55 +0100 Subject: [PATCH 10/26] Added extra checks and more fine-tuning to spec --- src/ApiDefinition.php | 25 +++++++++++++++---- src/Body.php | 4 +++ .../InvalidSchemaDefinitionException.php | 2 +- .../MutuallyExclusiveElementsException.php | 11 ++++++++ 4 files changed, 36 insertions(+), 6 deletions(-) create mode 100644 src/Exception/MutuallyExclusiveElementsException.php diff --git a/src/ApiDefinition.php b/src/ApiDefinition.php index fb95861b..4a0aa761 100644 --- a/src/ApiDefinition.php +++ b/src/ApiDefinition.php @@ -11,6 +11,7 @@ use Raml\Exception\BadParameter\ResourceNotFoundException; use Raml\Exception\BadParameter\InvalidSchemaDefinitionException; use Raml\Exception\BadParameter\InvalidProtocolException; +use Raml\Exception\MutuallyExclusiveElementsException; use Raml\Utility\StringTransformer; @@ -97,6 +98,7 @@ class ApiDefinition implements ArrayInstantiationInterface /** * The schemas the API supplies defined in the root (optional) * + * @deprecated Replaced by types element. * @see http://raml.org/spec.html#schemas * * @var array[] @@ -225,9 +227,13 @@ public static function createFromArray($title, array $data = []) $apiDefinition->setDefaultMediaType($data['defaultMediaType']); } + if (isset($data['schemas']) && isset($data['types'])) { + throw new MutuallyExclusiveElementsException(); + } + if (isset($data['schemas'])) { foreach ($data['schemas'] as $name => $schema) { - $apiDefinition->addSchemaCollection($name, $schema); + $apiDefinition->addType(ApiDefinition::determineType($name, $schema)); } } @@ -539,7 +545,7 @@ public function setDefaultMediaType($defaultMediaType) */ public function getSchemaCollections() { - return $this->schemaCollections; + return $this->types; } /** @@ -610,9 +616,6 @@ public function addDocumentation($title, $documentation) public static function determineType($name, $definition) { // check if we can find a more appropriate Type subclass - $definition = is_string($definition) ? ['type' => $definition] : $definition; - $definition['type'] = isset($definition['type']) ? $definition['type'] : 'string'; - $type = $definition['type']; $straightForwardTypes = [ 'time-only', 'datetime', @@ -627,6 +630,18 @@ public static function determineType($name, $definition) 'array', 'object' ]; + if (is_string($definition)) { + $definition = ['type' => $definition]; + } elseif (is_array($definition)) { + if (!isset($definition['type'])) { + $definition['type'] = isset($definition['properties']) ? 'object' : 'string'; + } + } else { + throw new \Exception('Invalid datatype for $definition parameter.'); + } + + + $type = $definition['type']; if (!in_array($type, ['','any'])) { if (in_array($type, $straightForwardTypes)) { diff --git a/src/Body.php b/src/Body.php index 3fdc35b2..47757d09 100644 --- a/src/Body.php +++ b/src/Body.php @@ -116,6 +116,10 @@ public static function createFromArray($mediaType, array $data = []) $type->inheritFromParent(); } $body->setType($type); + } else { + // nothing defined means a default of the any type + // see https://github.com/raml-org/raml-spec/blob/master/versions/raml-10/raml-10.md/#determine-default-types + $body->setType(new Type('default')); } if (isset($data['example'])) { diff --git a/src/Exception/BadParameter/InvalidSchemaDefinitionException.php b/src/Exception/BadParameter/InvalidSchemaDefinitionException.php index 18c48200..1abdab4f 100644 --- a/src/Exception/BadParameter/InvalidSchemaDefinitionException.php +++ b/src/Exception/BadParameter/InvalidSchemaDefinitionException.php @@ -12,6 +12,6 @@ class InvalidSchemaDefinitionException extends RuntimeException implements { public function __construct() { - parent::__construct('Not a valid schema, must be string or instance of SchemaDefinitionInterface'); + parent::__construct('Not a valid schema, must be string or instance of SchemaDefinitionInterface.'); } } diff --git a/src/Exception/MutuallyExclusiveElementsException.php b/src/Exception/MutuallyExclusiveElementsException.php new file mode 100644 index 00000000..497cde95 --- /dev/null +++ b/src/Exception/MutuallyExclusiveElementsException.php @@ -0,0 +1,11 @@ + Date: Mon, 13 Mar 2017 14:20:01 +0100 Subject: [PATCH 11/26] Added method for retrieving parsed RAML in array form --- src/ApiDefinition.php | 1 + src/Parser.php | 81 ++++++++++++++++++++++++++++++++++--------- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/src/ApiDefinition.php b/src/ApiDefinition.php index 4a0aa761..82f875e5 100644 --- a/src/ApiDefinition.php +++ b/src/ApiDefinition.php @@ -612,6 +612,7 @@ public function addDocumentation($title, $documentation) * @param \Raml\TypeCollection|null $typeCollection Type collection object. * * @return Raml\TypeInterface Returns a (best) matched type object. + * @throws \Exception Thrown when no type is defined. **/ public static function determineType($name, $definition) { diff --git a/src/Parser.php b/src/Parser.php index 7ebbad06..225160ad 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -219,10 +219,34 @@ public function parse($rawFileName) $ramlString = file_get_contents($fileName); $ramlData = $this->parseRamlString($ramlString, $rootDir); - + return $this->parseRamlData($ramlData, $rootDir); } + /** + * Parse a RAML spec from a file to array + * + * @param string $rawFileName + * + * @throws FileNotFoundException + * @throws RamlParserException + * + * @return array + */ + public function parseToArray($rawFileName) + { + $fileName = realpath($rawFileName); + + if (!is_file($fileName)) { + throw new FileNotFoundException($rawFileName); + } + + $rootDir = dirname($fileName); + $ramlString = file_get_contents($fileName); + + return $this->parseRamlString($ramlString, $rootDir); + } + /** * Parse a RAML spec from a string * @@ -263,9 +287,13 @@ private function parseRamlData($ramlData, $rootDir) $ramlData = $this->parseResourceTypes($ramlData); if ($this->configuration->isSchemaParsingEnabled()) { - if (isset($ramlData['schemas'])) { + if (isset($ramlData['schemas']) || isset($ramlData['types'])) { + $collections = isset($ramlData['schemas']) ? $ramlData['schemas'] : $ramlData['types']; $schemas = []; - foreach ($ramlData['schemas'] as $schemaCollection) { + foreach ($collections as $key => $schemaCollection) { + if (!is_array($schemaCollection)) { + continue; + } foreach ($schemaCollection as $schemaName => $schema) { $schemas[$schemaName] = $schema; } @@ -305,7 +333,7 @@ private function replaceSchemas($array, $schemas) return $array; } foreach ($array as $key => $value) { - if ('schema' === $key) { + if (in_array($key, ['schema', 'type'])) { if (isset($schemas[$value])) { $array[$key] = $schemas[$value]; } @@ -526,18 +554,39 @@ private function parseYaml($fileData) */ private function loadAndParseFile($fileName, $rootDir) { - $rootDir = realpath($rootDir); - $fullPath = realpath($rootDir . '/' . $fileName); - - if (is_readable($fullPath) === false) { - return false; - } + // first check if file is local or remote + $host = parse_url($fileName, PHP_URL_HOST); + if ($host === NULL) { + // local file + $rootDir = realpath($rootDir); + $fullPath = realpath($rootDir . '/' . $fileName); + + if (is_readable($fullPath) === false) { + return false; + } - // Prevent LFI directory traversal attacks - if (!$this->configuration->isDirectoryTraversalAllowed() && - substr($fullPath, 0, strlen($rootDir)) !== $rootDir - ) { - return false; + // Prevent LFI directory traversal attacks + if (!$this->configuration->isDirectoryTraversalAllowed() && + substr($fullPath, 0, strlen($rootDir)) !== $rootDir + ) { + return false; + } + $fileExtension = (pathinfo($fileName, PATHINFO_EXTENSION)); + } else { + // remote file + $fullPath = $fileName; + $fileHeaders = get_headers($fileName, true); + $mimeType = isset($fileHeaders['Content-Type']) ? $fileHeaders['Content-Type'] : ''; + $mapping = [ + 'application/json' => 'json', + 'application/xml' => 'xml', + 'application/soap+xml' => 'xml', + 'text/xml' => 'xml' + ]; + if (!isset($mapping[$mimeType])) { + return false; + } + $fileExtension = $mapping[$mimeType]; } $cacheKey = md5($fullPath); @@ -547,8 +596,6 @@ private function loadAndParseFile($fileName, $rootDir) return $this->cachedFiles[$cacheKey]; } - $fileExtension = (pathinfo($fileName, PATHINFO_EXTENSION)); - if (in_array($fileExtension, ['yaml', 'yml', 'raml'])) { $rootDir = dirname($rootDir . '/' . $fileName); From 3f682ec40d9693055645c9a80fed20759318562b Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Mon, 27 Mar 2017 14:52:49 +0200 Subject: [PATCH 12/26] Added traits & resourcetype parsing to array method --- src/Parser.php | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/Parser.php b/src/Parser.php index 225160ad..8209783c 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -244,7 +244,13 @@ public function parseToArray($rawFileName) $rootDir = dirname($fileName); $ramlString = file_get_contents($fileName); - return $this->parseRamlString($ramlString, $rootDir); + $ramlData = $this->parseRamlString($ramlString, $rootDir); + + $ramlData = $this->parseTraits($ramlData); + + $ramlData = $this->parseResourceTypes($ramlData); + + return $ramlData; } /** @@ -517,7 +523,7 @@ private function parseRamlString($ramlString, $rootDir) } if (strpos($header, '#%RAML') === 0) { - // @todo extract the vesion of the raml and do something with it + // @todo extract the version of the raml and do something with it $data = $this->includeAndParseFiles( $data, @@ -799,34 +805,24 @@ function ($matches) use ($values) { switch ($transformer) { case 'singularize': return Inflect::singularize($values[$matches[1]]); - break; case 'pluralize': return Inflect::pluralize($values[$matches[1]]); - break; case 'uppercase': return strtoupper($values[$matches[1]]); - break; case 'lowercase': return strtolower($values[$matches[1]]); - break; case 'lowercamelcase': return StringTransformer::convertString($values[$matches[1]], StringTransformer::LOWER_CAMEL_CASE); - break; case 'uppercamelcase': return StringTransformer::convertString($values[$matches[1]], StringTransformer::UPPER_CAMEL_CASE); - break; case 'lowerunderscorecase': return StringTransformer::convertString($values[$matches[1]], StringTransformer::LOWER_UNDERSCORE_CASE); - break; case 'upperunderscorecase': return StringTransformer::convertString($values[$matches[1]], StringTransformer::UPPER_UNDERSCORE_CASE); - break; case 'lowerhyphencase': return StringTransformer::convertString($values[$matches[1]], StringTransformer::LOWER_HYPHEN_CASE); - break; case 'upperhyphencase': return StringTransformer::convertString($values[$matches[1]], StringTransformer::UPPER_HYPHEN_CASE); - break; default: return $values[$matches[1]]; } From 5b0e014793955bd3fd1e530f5b3cb5aef0a879ac Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Thu, 6 Apr 2017 18:34:40 +0200 Subject: [PATCH 13/26] Refactored namespace Types to Type --- src/ApiDefinition.php | 10 +++-- src/Body.php | 3 +- src/Type.php | 30 +++++++++++-- src/{Types => Type}/ArrayType.php | 21 +++++++-- src/{Types => Type}/BooleanType.php | 8 +++- src/{Types => Type}/DateOnlyType.php | 10 +++-- src/{Types => Type}/DateTimeOnlyType.php | 8 +++- src/{Types => Type}/DateTimeType.php | 8 +++- src/{Types => Type}/FileType.php | 8 +++- src/{Types => Type}/IntegerType.php | 11 +++-- src/{Types => Type}/JsonType.php | 57 ++++++++++++++++++++---- src/{Types => Type}/LazyProxyType.php | 2 +- src/{Types => Type}/NullType.php | 12 ++--- src/{Types => Type}/NumberType.php | 23 +++++----- src/{Types => Type}/ObjectType.php | 30 ++++++++----- src/{Types => Type}/StringType.php | 13 +++--- src/{Types => Type}/TimeOnlyType.php | 7 ++- src/{Types => Type}/UnionType.php | 24 ++++++++-- src/{Types => Type}/XmlType.php | 21 ++++++--- src/TypeCollection.php | 4 +- test/ApiDefinitionTest.php | 53 ++++++++++++---------- 21 files changed, 257 insertions(+), 106 deletions(-) rename src/{Types => Type}/ArrayType.php (81%) rename src/{Types => Type}/BooleanType.php (71%) rename src/{Types => Type}/DateOnlyType.php (71%) rename src/{Types => Type}/DateTimeOnlyType.php (71%) rename src/{Types => Type}/DateTimeType.php (84%) rename src/{Types => Type}/FileType.php (91%) rename src/{Types => Type}/IntegerType.php (76%) rename src/{Types => Type}/JsonType.php (50%) rename src/{Types => Type}/LazyProxyType.php (99%) rename src/{Types => Type}/NullType.php (70%) rename src/{Types => Type}/NumberType.php (80%) rename src/{Types => Type}/ObjectType.php (88%) rename src/{Types => Type}/StringType.php (85%) rename src/{Types => Type}/TimeOnlyType.php (71%) rename src/{Types => Type}/UnionType.php (65%) rename src/{Types => Type}/XmlType.php (74%) diff --git a/src/ApiDefinition.php b/src/ApiDefinition.php index 82f875e5..ea4764f4 100644 --- a/src/ApiDefinition.php +++ b/src/ApiDefinition.php @@ -15,10 +15,12 @@ use Raml\Utility\StringTransformer; -use Raml\Types\UnionType; -use Raml\Types\ArrayType; -use Raml\Types\ObjectType; -use Raml\Types\LazyProxyType; +use Raml\Type\UnionType; +use Raml\Type\ArrayType; +use Raml\Type\ObjectType; +use Raml\Type\JsonType; +use Raml\Type\XmlType; +use Raml\Type\LazyProxyType; /** * The API Definition diff --git a/src/Body.php b/src/Body.php index 47757d09..be3f8552 100644 --- a/src/Body.php +++ b/src/Body.php @@ -7,7 +7,8 @@ use Raml\Exception\BadParameter\InvalidSchemaDefinitionException; use Raml\ApiDefinition; use Raml\TypeInterface; -use Raml\Types\ObjectType; +use Raml\Type\ObjectType; +use Raml\Type; /** * A body diff --git a/src/Type.php b/src/Type.php index 7aa7e4f9..655d02f2 100644 --- a/src/Type.php +++ b/src/Type.php @@ -2,8 +2,9 @@ namespace Raml; -use Raml\Types; +use Raml\Type; use Raml\Utility\StringTransformer; +use Symfony\Component\Yaml\Yaml; /** * Type class @@ -34,11 +35,11 @@ class Type implements ArrayInstantiationInterface, TypeInterface private $type; /** - * Required + * Specifies that the property is required or not. * * @var bool **/ - private $required = null; + private $required = true; /** * Raml definition @@ -75,6 +76,9 @@ public static function createFromArray($name, array $data = []) if (isset($data['usage'])) { $class->setUsage($data['usage']); } + if (isset($data['required'])) { + $class->setRequired($data['required']); + } $class->setDefinition($data); return $class; @@ -168,6 +172,16 @@ public function getRequired() return $this->required; } + /** + * Returns true when property is required, false otherwise. + * + * @return bool + */ + public function isRequired() + { + return ($this->required === true); + } + /** * Set the value of Required * @@ -292,4 +306,14 @@ public function validate($value) // everything is valid for the any type return true; } + + /** + * Returns the original RAML string + * + * @return string + */ + public function __toString() + { + return Yaml::dump($this->definition); + } } diff --git a/src/Types/ArrayType.php b/src/Type/ArrayType.php similarity index 81% rename from src/Types/ArrayType.php rename to src/Type/ArrayType.php index 12df3670..c3577d64 100644 --- a/src/Types/ArrayType.php +++ b/src/Type/ArrayType.php @@ -1,8 +1,9 @@ minItems) { + throw new InvalidTypeException([sprintf('Array should contain a minimal of "%s" items.', $this->minItems)]); + } + if (count($value) > $this->maxItems) { + throw new InvalidTypeException([sprintf('Array should contain a maximum of "%s" items.', $this->maxItems)]); + } + // TODO: implement $this->items check + // TODO: implement $this->uniqueItems check + } + return true; } } diff --git a/src/Types/BooleanType.php b/src/Type/BooleanType.php similarity index 71% rename from src/Types/BooleanType.php rename to src/Type/BooleanType.php index aff21be5..2728a609 100644 --- a/src/Types/BooleanType.php +++ b/src/Type/BooleanType.php @@ -1,8 +1,9 @@ format('Y-m-d') === $value; + if (($d && $d->format('Y-m-d') === $value) === false) { + throw new InvalidTypeException('Value is not a date-only.'); + } + return true; } } diff --git a/src/Types/DateTimeOnlyType.php b/src/Type/DateTimeOnlyType.php similarity index 71% rename from src/Types/DateTimeOnlyType.php rename to src/Type/DateTimeOnlyType.php index e46321e6..bcf11a0a 100644 --- a/src/Types/DateTimeOnlyType.php +++ b/src/Type/DateTimeOnlyType.php @@ -1,8 +1,9 @@ format(DATE_RFC3339) === $value; + if (($d && $d->format(DATE_RFC3339) === $value) === false) { + throw new InvalidTypeException('Value is not a datetime-only.'); + } + return true; } } diff --git a/src/Types/DateTimeType.php b/src/Type/DateTimeType.php similarity index 84% rename from src/Types/DateTimeType.php rename to src/Type/DateTimeType.php index 4b27a734..3068c410 100644 --- a/src/Types/DateTimeType.php +++ b/src/Type/DateTimeType.php @@ -1,8 +1,9 @@ format ?: DATE_RFC3339; $d = DateTime::createFromFormat($format, $value); - return $d && $d->format($format) === $value; + if (($d && $d->format($format) === $value) === false) { + throw new InvalidTypeException('Value is not a datetime-only.'); + } + return true; } } diff --git a/src/Types/FileType.php b/src/Type/FileType.php similarity index 91% rename from src/Types/FileType.php rename to src/Type/FileType.php index a190a058..88b9f2c4 100644 --- a/src/Types/FileType.php +++ b/src/Type/FileType.php @@ -1,8 +1,9 @@ */ class IntegerType extends NumberType @@ -33,6 +35,9 @@ public static function createFromArray($name, array $data = []) public function validate($value) { - return is_int($value); + if (is_int($value) === false) { + throw new InvalidTypeException(['Value is not a integer.']); + } + return true; } } diff --git a/src/Types/JsonType.php b/src/Type/JsonType.php similarity index 50% rename from src/Types/JsonType.php rename to src/Type/JsonType.php index 6327fce1..a212d916 100644 --- a/src/Types/JsonType.php +++ b/src/Type/JsonType.php @@ -1,9 +1,11 @@ json = $data; + $json = $data['type']; + + $type->setJson($json); return $type; } + public function setJson($json) { + if (!is_string($json)) { + throw new InvalidJsonException(); + } + $this->json = $json; + } + /** * Validate a JSON string against the schema * - Converts the string into a JSON object then uses the JsonSchema Validator to validate * - * @param $string + * @param string $string JSON object to validate. * * @return bool + * @throws InvalidJsonException Thrown when string is invalid JSON. */ public function validate($string) { $json = json_decode($string); if (json_last_error() !== JSON_ERROR_NONE) { - return false; + throw new InvalidJsonException(json_last_error()); } return $this->validateJsonObject($json); @@ -61,21 +72,51 @@ public function validate($string) * * @param string $json * - * @throws InvalidSchemaException + * @throws InvalidSchemaException Thrown when the string does not validate against the schema. * * @return bool */ public function validateJsonObject($json) { $validator = new Validator(); - $jsonSchema = $this->json; + $jsonSchema = json_decode($this->json); $validator->check($json, $jsonSchema); if (!$validator->isValid()) { - return false; + throw new InvalidSchemaException($validator->getErrors()); } return true; } + + /** + * Returns the JSON Schema as a \stdClass + * + * @return \stdClass + */ + public function getJsonObject() + { + return json_decode($this->json); + } + + /** + * Returns the JSON Schema as an array + * + * @return array + */ + public function getJsonArray() + { + return json_decode($this->json, true); + } + + /** + * Returns the original JSON schema + * + * @return string + */ + public function __toString() + { + return $this->json; + } } diff --git a/src/Types/LazyProxyType.php b/src/Type/LazyProxyType.php similarity index 99% rename from src/Types/LazyProxyType.php rename to src/Type/LazyProxyType.php index ceb5ce4e..d24d1c63 100644 --- a/src/Types/LazyProxyType.php +++ b/src/Type/LazyProxyType.php @@ -1,6 +1,6 @@ */ class NullType extends Type @@ -30,6 +29,9 @@ public static function createFromArray($name, array $data = []) public function validate($value) { - return is_null($value); + if (is_null($value) === false) { + throw new InvalidTypeException(['Value is not null.']); + } + return true; } } diff --git a/src/Types/NumberType.php b/src/Type/NumberType.php similarity index 80% rename from src/Types/NumberType.php rename to src/Type/NumberType.php index 35241e95..62abb7d7 100644 --- a/src/Types/NumberType.php +++ b/src/Type/NumberType.php @@ -1,8 +1,9 @@ maximum)) { if ($value > $this->maximum) { - return false; + throw new InvalidTypeException([sprintf('Value is larger than the allowed maximum of %s.', $this->maximum)]); } } if (!is_null($this->minimum)) { if ($value < $this->minimum) { - return false; + throw new InvalidTypeException([sprintf('Value is smaller than the allowed minimum of %s.', $this->minimum)]); } } switch ($this->format) { case 'int8': if (filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => -128, 'max_range' => 127]]) === false) { - return false; + throw new InvalidTypeException([sprintf('Value is not of the required format: "%s".', $this->format)]); } break; case 'int16': if (filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => -32768, 'max_range' => 32767]]) === false) { - return false; + throw new InvalidTypeException([sprintf('Value is not of the required format: "%s".', $this->format)]); } break; case 'int32': if (filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => -2147483648, 'max_range' => 2147483647]]) === false) { - return false; + throw new InvalidTypeException([sprintf('Value is not of the required format: "%s".', $this->format)]); } break; case 'int64': if (filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => -9223372036854775808, 'max_range' => 9223372036854775807]]) === false) { - return false; + throw new InvalidTypeException([sprintf('Value is not of the required format: "%s".', $this->format)]); } break; case 'int': // int === long case 'long': if (!is_int($value)) { - return false; + throw new InvalidTypeException([sprintf('Value is not of the required format: "%s".', $this->format)]); } break; case 'float': // float === double case 'double': if (!is_float($value)) { - return false; + throw new InvalidTypeException([sprintf('Value is not of the required format: "%s".', $this->format)]); } break; // if no format is given we check only if it is a number null: default: if (!is_float($value) && !is_int($value)) { - return false; + throw new InvalidTypeException([sprintf('Value is not of the required format: "%s".', $this->format)]); } break; } if (!is_null($this->multipleOf)) { if ($value %$this->multipleOf != 0) { - return false; + throw new InvalidTypeException([sprintf('Value is not a multiplication of "%s".', $this->multipleOf)]); } } return true; diff --git a/src/Types/ObjectType.php b/src/Type/ObjectType.php similarity index 88% rename from src/Types/ObjectType.php rename to src/Type/ObjectType.php index 70eeb6c0..8a5250fb 100644 --- a/src/Types/ObjectType.php +++ b/src/Type/ObjectType.php @@ -1,11 +1,12 @@ $propertyValue) { - try { - $property = $this->getPropertyByName($name); - if (!$property->validate($propertyValue)) { - return false; + $errors = []; + + foreach ($this->getProperties() as $property) { + if ($property->isRequired()) { + if (!in_array($property->getName(), array_keys($value))) { + $errors[] = sprintf('Object does not contain required property "%s".', $property->getName()); + } else { + try { + $property->validate($value[$property->getName()]); + } catch (InvalidTypeException $e) { + $errors = array_merge($errors, $e->getErrors()); + } } - } catch (PropertyNotFoundException $e) { - // if no property found, carry on - return false; } } + + if (!empty($errors)) { + throw new InvalidTypeException($errors); + } + return true; } } diff --git a/src/Types/StringType.php b/src/Type/StringType.php similarity index 85% rename from src/Types/StringType.php rename to src/Type/StringType.php index 883d6fa3..1dc69d96 100644 --- a/src/Types/StringType.php +++ b/src/Type/StringType.php @@ -1,10 +1,9 @@ pattern)) { if (preg_match('/'.$this->pattern.'/', $value) == false) { - return false; + throw new InvalidTypeException([sprintf('String does not match required pattern: %s.', $this->pattern)]); } } if (!is_null($this->minLength)) { if (strlen($value) < $this->minLength) { - return false; + throw new InvalidTypeException([sprintf('String is shorter than the minimal length of %s.', $this->minLength)]); } } if (!is_null($this->maxLength)) { if (strlen($value) > $this->maxLength) { - return false; + throw new InvalidTypeException([sprintf('String is longer than the maximal length of %s.', $this->minLength)]); } } diff --git a/src/Types/TimeOnlyType.php b/src/Type/TimeOnlyType.php similarity index 71% rename from src/Types/TimeOnlyType.php rename to src/Type/TimeOnlyType.php index 530d36d9..cb41c53d 100644 --- a/src/Types/TimeOnlyType.php +++ b/src/Type/TimeOnlyType.php @@ -1,8 +1,9 @@ format('HH:II:SS') === $value; + if (($d && $d->format('HH:II:SS') === $value) !== false) { + throw new InvalidTypeException(['Value is not time-only.']); + } } } diff --git a/src/Types/UnionType.php b/src/Type/UnionType.php similarity index 65% rename from src/Types/UnionType.php rename to src/Type/UnionType.php index bad0a0ef..bd4d2194 100644 --- a/src/Types/UnionType.php +++ b/src/Type/UnionType.php @@ -1,10 +1,11 @@ getPossibleTypes() as $type) { - if ($type->validate($value)) { - return true; + try { + if ($type->validate($value)) { + return true; + } + } catch (InvalidTypeException $e) { + // ignore validation errors since it can be any of possible types } } - return false; + + throw new InvalidTypeException( + [ + sprintf( + 'Value is not any of the following types: %s', + array_reduce($this->getPossibleTypes(), function ($carry, $item) { + $carry = $carry . ', ' . $item->getName(); + return $carry; + }) + ) + ] + ); } } diff --git a/src/Types/XmlType.php b/src/Type/XmlType.php similarity index 74% rename from src/Types/XmlType.php rename to src/Type/XmlType.php index 76132e3f..7dc2e039 100644 --- a/src/Types/XmlType.php +++ b/src/Type/XmlType.php @@ -1,8 +1,9 @@ xml = $data; + $type->xml = $data['type']; return $type; } @@ -39,7 +40,7 @@ public static function createFromArray($name, array $data = []) /** * Validate an XML string against the schema * - * @param $string + * @param string $string Value to validate. * * @return bool */ @@ -53,7 +54,7 @@ public function validate($string) $errors = libxml_get_errors(); libxml_clear_errors(); if ($errors) { - return false; + throw new InvalidSchemaException($errors); } // --- @@ -62,11 +63,21 @@ public function validate($string) $errors = libxml_get_errors(); libxml_clear_errors(); if ($errors) { - return false; + throw new InvalidSchemaException($errors); } libxml_use_internal_errors($originalErrorLevel); return true; } + + /** + * Returns the original XML schema + * + * @return string + */ + public function __toString() + { + return $this->xml; + } } diff --git a/src/TypeCollection.php b/src/TypeCollection.php index 8f686994..471a0bb4 100644 --- a/src/TypeCollection.php +++ b/src/TypeCollection.php @@ -2,7 +2,7 @@ namespace Raml; -use Raml\Types\ObjectType; +use Raml\Type\ObjectType; /** * Singleton class used to register all types in one place @@ -144,7 +144,7 @@ public function getTypeByName($name) return $type; } } - throw new \Exception(sprintf('No type found for name %s, list: %s', var_export($name, true), var_export($allTypes, true))); + throw new \Exception(sprintf('No type found for name %s', var_export($name, true))); } /** diff --git a/test/ApiDefinitionTest.php b/test/ApiDefinitionTest.php index e362bb3e..1ffefcef 100644 --- a/test/ApiDefinitionTest.php +++ b/test/ApiDefinitionTest.php @@ -95,9 +95,9 @@ public function shouldParseTypesToSubTypes() $api = $this->parser->parse(__DIR__.'/fixture/raml-1.0/types.raml'); $types = $api->getTypes(); $object = $types->current(); - $this->assertInstanceOf('\Raml\Types\ObjectType', $object); - $this->assertInstanceOf('\Raml\Types\IntegerType', $object->getPropertyByName('id')); - $this->assertInstanceOf('\Raml\Types\StringType', $object->getPropertyByName('name')); + $this->assertInstanceOf('\Raml\Type\ObjectType', $object); + $this->assertInstanceOf('\Raml\Type\IntegerType', $object->getPropertyByName('id')); + $this->assertInstanceOf('\Raml\Type\StringType', $object->getPropertyByName('name')); } /** @test */ @@ -106,40 +106,45 @@ public function shouldParseComplexTypes() $api = $this->parser->parse(__DIR__.'/fixture/raml-1.0/complexTypes.raml'); // check types $org = $api->getTypes()->getTypeByName('Org'); - $this->assertInstanceOf('\Raml\Types\ObjectType', $org); + $this->assertInstanceOf('\Raml\Type\ObjectType', $org); // property will return a proxy object so to compare to actual type we will need to ask for the resolved object - $this->assertInstanceOf('\Raml\Types\UnionType', $org->getPropertyByName('onCall')->getResolvedObject()); + $this->assertInstanceOf('\Raml\Type\UnionType', $org->getPropertyByName('onCall')->getResolvedObject()); $head = $org->getPropertyByName('Head'); - $this->assertInstanceOf('\Raml\Types\ObjectType', $head->getResolvedObject()); - $this->assertInstanceOf('\Raml\Types\StringType', $head->getPropertyByName('firstname')); - $this->assertInstanceOf('\Raml\Types\StringType', $head->getPropertyByName('lastname')); - $this->assertInstanceOf('\Raml\Types\StringType', $head->getPropertyByName('title?')); - $this->assertInstanceOf('\Raml\Types\StringType', $head->getPropertyByName('kind')); + $this->assertInstanceOf('\Raml\Type\ObjectType', $head->getResolvedObject()); + $this->assertInstanceOf('\Raml\Type\StringType', $head->getPropertyByName('firstname')); + $this->assertInstanceOf('\Raml\Type\StringType', $head->getPropertyByName('lastname')); + $this->assertInstanceOf('\Raml\Type\StringType', $head->getPropertyByName('title?')); + $this->assertInstanceOf('\Raml\Type\StringType', $head->getPropertyByName('kind')); $reports = $head->getPropertyByName('reports'); - $this->assertInstanceOf('\Raml\Types\ArrayType', $reports); + $this->assertInstanceOf('\Raml\Type\ArrayType', $reports); $phone = $head->getPropertyByName('phone')->getResolvedObject(); - $this->assertInstanceOf('\Raml\Types\StringType', $phone); + $this->assertInstanceOf('\Raml\Type\StringType', $phone); // check resources $type = $api->getResourceByPath('/orgs/{orgId}')->getMethod('get')->getResponse(200)->getBodyByType('application/json')->getType(); - $this->assertInstanceOf('\Raml\Types\ObjectType', $type->getResolvedObject()); + $this->assertInstanceOf('\Raml\Type\ObjectType', $type->getResolvedObject()); } /** @test */ public function shouldValidateResponse() { - $api = $this->parser->parse(__DIR__.'/fixture/raml-1.0/complexTypes.raml'); - $body = $api->getResourceByPath('/orgs/{orgId}')->getMethod('get')->getResponse(200)->getBodyByType('application/json'); - /* @var $body \Raml\Body */ - - $validResponse = $body->getExample(); - $type = $body->getType(); - $this->assertTrue($type->validate($validResponse)); - - $invalidResponse = [ + $api = $this->parser->parse(__DIR__.'/fixture/raml-1.0/complexTypes.raml'); + $body = $api->getResourceByPath('/orgs/{orgId}')->getMethod('get')->getResponse(200)->getBodyByType('application/json'); + /* @var $body \Raml\Body */ + + $validResponse = $body->getExample(); + $type = $body->getType(); + $this->assertTrue($type->validate($validResponse)); + + + $invalidResponse = [ 'onCall' => 'this is not an object', 'Head' => 'this is not an object' - ]; - $this->assertFalse($type->validate($invalidResponse)); + ]; + $this->setExpectedException( + '\Raml\Exception\InvalidTypeException', + 'Invalid Schema.' + ); + $type->validate($invalidResponse); } /** @test */ From bbac7078e6128c7c2623e5ab151a5eb722c0734c Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Thu, 6 Apr 2017 18:37:32 +0200 Subject: [PATCH 14/26] Implemented type validator class based on schema validator --- src/ApiDefinition.php | 75 ++++--- src/Body.php | 21 +- src/Exception/InvalidTypeException.php | 22 ++ src/FileLoader/JsonSchemaFileLoader.php | 5 + src/NamedParameter.php | 6 +- src/Parser.php | 100 ++++++--- src/Validator/Validator.php | 269 ++++++++++++++++++++++++ src/Validator/ValidatorSchemaHelper.php | 2 +- 8 files changed, 429 insertions(+), 71 deletions(-) create mode 100644 src/Exception/InvalidTypeException.php create mode 100644 src/Validator/Validator.php diff --git a/src/ApiDefinition.php b/src/ApiDefinition.php index ea4764f4..ea78805a 100644 --- a/src/ApiDefinition.php +++ b/src/ApiDefinition.php @@ -52,6 +52,13 @@ class ApiDefinition implements ArrayInstantiationInterface */ private $version; + /** + * Raml version + * + * @var string + */ + private $ramlVersion; + /** * The Base URL (optional for development, required in production) * @@ -151,7 +158,7 @@ class ApiDefinition implements ArrayInstantiationInterface * * @var \Raml\TypeCollection */ - private $types = null; + private $types = []; // --- @@ -181,6 +188,7 @@ public function __construct($title) * protocols: ?array * defaultMediaType: ?string * schemas: ?array + * types: ?array * securitySchemes: ?array * documentation: ?array * /* @@ -229,16 +237,6 @@ public static function createFromArray($title, array $data = []) $apiDefinition->setDefaultMediaType($data['defaultMediaType']); } - if (isset($data['schemas']) && isset($data['types'])) { - throw new MutuallyExclusiveElementsException(); - } - - if (isset($data['schemas'])) { - foreach ($data['schemas'] as $name => $schema) { - $apiDefinition->addType(ApiDefinition::determineType($name, $schema)); - } - } - if (isset($data['securitySchemes'])) { foreach ($data['securitySchemes'] as $name => $securityScheme) { $apiDefinition->addSecurityScheme(SecurityScheme::createFromArray($name, $securityScheme)); @@ -261,8 +259,13 @@ public static function createFromArray($title, array $data = []) } } - if (isset($data['types'])) { - foreach ($data['types'] as $name => $definition) { + if (isset($data['schemas']) && isset($data['types'])) { + throw new MutuallyExclusiveElementsException(); + } + + if (isset($data['schemas']) || isset($data['types'])) { + $types = isset($data['schemas']) ? $data['schemas'] : $data['types']; + foreach ($types as $name => $definition) { $apiDefinition->addType(ApiDefinition::determineType($name, $definition)); } } @@ -541,6 +544,7 @@ public function setDefaultMediaType($defaultMediaType) // -- /** + * @deprecated Use types instead! * Get the schemas defined in the root of the API * * @return array[] @@ -551,6 +555,7 @@ public function getSchemaCollections() } /** + * @deprecated Use types instead! * Add an schema * * @param string $collectionName @@ -566,6 +571,7 @@ public function addSchemaCollection($collectionName, $schemas) } /** + * @deprecated Use types instead! * Add a new schema to a collection * * @param string $collectionName @@ -607,7 +613,7 @@ public function addDocumentation($title, $documentation) } /** - * Determines the right Type and returns an instance + * Determines the right Type and returns a type instance * * @param string $name Name of type. * @param array $definition Definition of type. @@ -639,21 +645,29 @@ public static function determineType($name, $definition) if (!isset($definition['type'])) { $definition['type'] = isset($definition['properties']) ? 'object' : 'string'; } + } elseif ($definition instanceof \stdClass) { + return JsonType::createFromArray('schema', $definition); } else { throw new \Exception('Invalid datatype for $definition parameter.'); } - + if (is_object($name)) { + throw new \Exception(var_export($name, true)); + } + if (strpos($name, '?') !== false) { + $definition['required'] = false; + } $type = $definition['type']; + if (in_array($type, $straightForwardTypes)) { + $className = sprintf( + 'Raml\Type\%sType', + StringTransformer::convertString($type, StringTransformer::UPPER_CAMEL_CASE) + ); + return forward_static_call_array([$className,'createFromArray'], [$name, $definition]); + } + if (!in_array($type, ['','any'])) { - if (in_array($type, $straightForwardTypes)) { - $className = sprintf( - 'Raml\Types\%sType', - StringTransformer::convertString($type, StringTransformer::UPPER_CAMEL_CASE) - ); - return forward_static_call_array([$className,'createFromArray'], [$name, $definition]); - } // if $type contains a '|' we can savely assume it's a combination of types (union) if (strpos($type, '|') !== false) { return UnionType::createFromArray($name, $definition); @@ -662,12 +676,21 @@ public static function determineType($name, $definition) if (strpos($type, '[]') !== false) { return ArrayType::createFromArray($name, $definition); } - // no standard type found so this must be a reference to a custom defined type - // since the actual definition can be defined later then when it is referenced + // is it a XML schema? + if (substr(ltrim($type), 0, 1) === '<') { + return XmlType::createFromArray('schema', $definition); + } + // is it a JSON schema? + if (substr(ltrim($type), 0, 1) === '{') { + return JsonType::createFromArray('schema', $definition); + } + + // no? then no standard type found so this must be a reference to a custom defined type. + // since the actual definition can be defined later then when it is referenced, // we create a proxy object for lazy loading when it is needed return LazyProxyType::createFromArray($name, $definition); } - + // No subclass found, let's use base class return Type::createFromArray($name, $definition); } @@ -683,7 +706,7 @@ public function addType(TypeInterface $type) } /** - * Get data types + * Get all data types defined in the root of the API * * @return \Raml\TypeCollection */ diff --git a/src/Body.php b/src/Body.php index be3f8552..eac40583 100644 --- a/src/Body.php +++ b/src/Body.php @@ -5,6 +5,7 @@ use Raml\Schema\SchemaDefinitionInterface; use Raml\Exception\BadParameter\InvalidSchemaDefinitionException; +use Raml\Exception\MutuallyExclusiveElementsException; use Raml\ApiDefinition; use Raml\TypeInterface; use Raml\Type\ObjectType; @@ -106,14 +107,16 @@ public static function createFromArray($mediaType, array $data = []) $body->setDescription($data['description']); } - if (isset($data['schema'])) { - $body->setSchema($data['schema']); + if (isset($data['schema']) && isset($data['type'])) { + throw new MutuallyExclusiveElementsException(); } if (isset($data['type'])) { - $type = ApiDefinition::determineType($data['type'], ['type' => $data['type']]); - if ($type instanceof ObjectType) - { + $name = ''; + $definition = $data['type']; + + $type = ApiDefinition::determineType($name, $definition); + if ($type instanceof ObjectType) { $type->inheritFromParent(); } $body->setType($type); @@ -171,16 +174,18 @@ public function setDescription($description) // -- /** + * @deprecated Schema has been deprecated and is superseded by type. * Get the schema * * @return SchemaDefinitionInterface|string */ public function getSchema() { - return $this->schema; + return $this->type; } /** + * @deprecated Schema has been deprecated and is superseded by type. * Set the schema * * @param SchemaDefinitionInterface|string $schema @@ -189,11 +194,11 @@ public function getSchema() */ public function setSchema($schema) { - if (!is_string($schema) && !$schema instanceof SchemaDefinitionInterface) { + if (!is_string($schema) && !$schema instanceof TypeInterface) { throw new InvalidSchemaDefinitionException(); } - $this->schema = $schema; + $this->type = $schema; } // -- diff --git a/src/Exception/InvalidTypeException.php b/src/Exception/InvalidTypeException.php new file mode 100644 index 00000000..c4d537c8 --- /dev/null +++ b/src/Exception/InvalidTypeException.php @@ -0,0 +1,22 @@ +errors = $errors; + + parent::__construct('Invalid Schema.'); + } + + public function getErrors() + { + return $this->errors; + } +} diff --git a/src/FileLoader/JsonSchemaFileLoader.php b/src/FileLoader/JsonSchemaFileLoader.php index 16db2e5d..98c4fbd0 100644 --- a/src/FileLoader/JsonSchemaFileLoader.php +++ b/src/FileLoader/JsonSchemaFileLoader.php @@ -39,9 +39,14 @@ public function loadFile($filePath) $jsonSchemaParser = new RefResolver($retriever); try { $json = $jsonSchemaParser->fetchRef('file://' . $filePath, null); + } catch (\JsonSchema\Exception\JsonDecodingException $e) { + throw new InvalidJsonException($e->getMessage()); } catch (\Exception $e) { $json = json_decode(file_get_contents($filePath)); } + if ($json === null) { + throw new InvalidJsonException(sprintf('Cannot load JSON file at %s', $filePath)); + } try { return json_encode($json); diff --git a/src/NamedParameter.php b/src/NamedParameter.php index 0160e9db..95f1cebc 100644 --- a/src/NamedParameter.php +++ b/src/NamedParameter.php @@ -665,9 +665,9 @@ public function setDefault($default) } /** - * Validate a paramater via RAML specifications + * Validate a parameter via RAML specifications * - * @param mixed $param The value of the paramater to validate + * @param mixed $param The value of the parameter to validate * @throws \Exception The code corresponds to the error that occured. */ public function validate($param) @@ -683,7 +683,6 @@ public function validate($param) } return; - } switch ($this->getType()) { @@ -797,7 +796,6 @@ public function validate($param) // File type cannot be reliably validated based on its type alone. break; - } /** diff --git a/src/Parser.php b/src/Parser.php index 8209783c..d289714a 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -16,12 +16,22 @@ use Raml\SecurityScheme\SecuritySettingsParser\OAuth2SecuritySettingsParser; use Raml\SecurityScheme\SecuritySettingsParserInterface; use Symfony\Component\Yaml\Yaml; +use JsonSchema\Uri\UriRetriever; +use JsonSchema\RefResolver; +use Raml\Exception\InvalidJsonException; /** * Converts a RAML file into a API Documentation tree */ class Parser { + // @TODO: pass the parsed RAML version to the ApiDefinition + /** + * RAML version + * + * @var string + */ + private $ramlVersion; /** * Array of cached files @@ -91,6 +101,7 @@ public function __construct( $this->configuration = $config ?: new ParseConfiguration(); // --- + // @deprecated schema have been replaced by a dedicated (json/xml) type // add schema parsers // if null then use a default list @@ -292,31 +303,31 @@ private function parseRamlData($ramlData, $rootDir) $ramlData = $this->parseResourceTypes($ramlData); - if ($this->configuration->isSchemaParsingEnabled()) { - if (isset($ramlData['schemas']) || isset($ramlData['types'])) { - $collections = isset($ramlData['schemas']) ? $ramlData['schemas'] : $ramlData['types']; - $schemas = []; - foreach ($collections as $key => $schemaCollection) { - if (!is_array($schemaCollection)) { - continue; - } - foreach ($schemaCollection as $schemaName => $schema) { - $schemas[$schemaName] = $schema; - } + // if ($this->configuration->isSchemaParsingEnabled()) { + if (isset($ramlData['schemas']) || isset($ramlData['types'])) { + $collections = isset($ramlData['schemas']) ? $ramlData['schemas'] : $ramlData['types']; + $schemas = []; + foreach ($collections as $key => $schemaCollection) { + if (!is_array($schemaCollection)) { + continue; + } + foreach ($schemaCollection as $schemaName => $schema) { + $schemas[$schemaName] = $schema; } } - foreach ($ramlData as $key => $value) { - if (0 === strpos($key, '/')) { - if (isset($schemas)) { - $value = $this->replaceSchemas($value, $schemas); - } - if (is_array($value)) { - $value = $this->recurseAndParseSchemas($value, $rootDir); - } - $ramlData[$key] = $value; + } + foreach ($ramlData as $key => $value) { + if (0 === strpos($key, '/')) { + if (isset($schemas)) { + $value = $this->replaceSchemas($value, $schemas); } + if (is_array($value)) { + $value = $this->recurseAndParseSchemas($value, $rootDir); + } + $ramlData[$key] = $value; } } + // } if (isset($ramlData['securitySchemes'])) { $ramlData['securitySchemes'] = $this->parseSecuritySettings($ramlData['securitySchemes']); @@ -339,8 +350,8 @@ private function replaceSchemas($array, $schemas) return $array; } foreach ($array as $key => $value) { - if (in_array($key, ['schema', 'type'])) { - if (isset($schemas[$value])) { + if (is_string($key) && in_array($key, ['schema', 'type'])) { + if (array_key_exists($value, $schemas)) { $array[$key] = $schemas[$value]; } } else { @@ -366,13 +377,35 @@ private function recurseAndParseSchemas(array $array, $rootDir) foreach ($array as $key => &$value) { if (is_array($value)) { if (isset($value['schema'])) { - if (in_array($key, array_keys($this->schemaParsers))) { - $schemaParser = $this->schemaParsers[$key]; - $fileDir = $this->getCachedFilePath($value['schema']); - $schemaParser->setSourceUri('file:' . ($fileDir ? $fileDir : $rootDir . DIRECTORY_SEPARATOR)); - $value['schema'] = $schemaParser->createSchemaDefinition($value['schema']); - } else { - throw new InvalidSchemaTypeException($key); + if ($value['schema'] === false) { + throw new \Exception('Schema is false: '.var_export($array, true)); + } + $value['type'] = $value['schema']; + unset($value['schema']); + } + if (isset($value['type'])) { + if ($value['type'] === false) { + throw new \Exception('Type is false: '.var_export($array, true)); + } + if (substr(ltrim($value['type']), 0, 1) === '{') { + if (in_array($key, array_keys($this->schemaParsers))) { + $fileDir = $this->getCachedFilePath($value['type']); + $retriever = new UriRetriever; + $jsonSchemaParser = new RefResolver($retriever); + + $data = json_decode($value['type']); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new InvalidJsonException(json_last_error()); + } + $jsonSchemaParser->resolve($data, 'file:' . ($fileDir ? $fileDir : $rootDir . DIRECTORY_SEPARATOR)); + $value['type'] = json_encode((array) $data); + } else { + throw new InvalidJsonException($key); + } + // } elseif (substr(ltrim($value['type']), 0, 1) === '<') { + // } else { + // throw new InvalidJsonException(''); } } else { $value = $this->recurseAndParseSchemas($value, $rootDir); @@ -524,7 +557,7 @@ private function parseRamlString($ramlString, $rootDir) if (strpos($header, '#%RAML') === 0) { // @todo extract the version of the raml and do something with it - + $data = $this->includeAndParseFiles( $data, $rootDir @@ -646,7 +679,11 @@ private function includeAndParseFiles($structure, $rootDir) return $result; } elseif (strpos($structure, '!include') === 0) { - return $this->loadAndParseFile(str_replace('!include ', '', $structure), $rootDir); + $result = $this->loadAndParseFile(str_replace('!include ', '', $structure), $rootDir); + if ($result === false) { + throw new RamlParserException('Could not load and/or parse include file.'); + } + return $result; } else { return $structure; } @@ -749,7 +786,6 @@ private function replaceTypes($raml, $types, $path, $name, $parentKey = null) $newArray[$key] = $newValue; } } - } return $newArray; diff --git a/src/Validator/Validator.php b/src/Validator/Validator.php new file mode 100644 index 00000000..c0360f87 --- /dev/null +++ b/src/Validator/Validator.php @@ -0,0 +1,269 @@ +schemaHelper = $schema; + } + + /** + * @param RequestInterface $request + * @throws Exception + */ + public function validateRequest(RequestInterface $request) + { + $this->assertNoMissingParameters($request); + $this->assertValidParameters($request); + $this->assertValidRequestBody($request); + } + + /** + * @param RequestInterface $request + * @throws ValidatorRequestException + */ + private function assertNoMissingParameters(RequestInterface $request) + { + $method = $request->getMethod(); + $path = $request->getUri()->getPath(); + + $schemaParameters = $this->schemaHelper->getQueryParameters($method, $path, true); + $requestParameters = $this->getRequestParameters($request); + + $missingParameters = array_diff_key($schemaParameters, $requestParameters); + if (count($missingParameters) === 0) { + return; + } + + throw new ValidatorRequestException(sprintf( + 'Missing request parameters required by the schema for `%s %s`: %s', + strtoupper($method), + $path, + join(', ', array_keys($missingParameters)) + )); + } + + /** + * @param RequestInterface $request + * @throws ValidatorRequestException + */ + private function assertValidParameters(RequestInterface $request) + { + $method = $request->getMethod(); + $path = $request->getUri()->getPath(); + + $schemaParameters = $this->schemaHelper->getQueryParameters($method, $path); + $requestParameters = $this->getRequestParameters($request); + + /** @var NamedParameter $schemaParameter */ + foreach ($schemaParameters as $schemaParameter) { + $key = $schemaParameter->getKey(); + + if (!array_key_exists($key, $requestParameters)) { + continue; + } + + try { + $schemaParameter->validate($requestParameters[$key]); + } catch (ValidationException $exception) { + $message = sprintf( + 'Request parameter does not match schema for `%s %s`: %s', + strtoupper($method), + $path, + $exception->getMessage() + ); + + throw new ValidatorRequestException($message, 0, $exception); + } + } + } + + /** + * @param RequestInterface $request + * @throws ValidatorRequestException + */ + private function assertValidRequestBody(RequestInterface $request) + { + $body = $request->getBody()->getContents(); + + $method = $request->getMethod(); + $path = $request->getUri()->getPath(); + $contentType = $request->getHeaderLine('Content-Type'); + + $schemaBody = $this->schemaHelper->getRequestBody($method, $path, $contentType); + + try { + $schemaBody->getType()->validate($body); + } catch (InvalidTypeException $exception) { + $message = sprintf( + 'Request body for %s %s with content type %s does not match schema: %s', + strtoupper($method), + $path, + $contentType, + $this->getSchemaErrorsAsString($exception->getErrors()) + ); + + throw new ValidatorRequestException($message, 0, $exception); + } + } + + /** + * @param RequestInterface $request + * @return array + */ + private function getRequestParameters(RequestInterface $request) + { + parse_str($request->getUri()->getQuery(), $requestParameters); + + return $requestParameters; + } + + /** + * @param array $errors + * @return string + */ + private function getSchemaErrorsAsString(array $errors) + { + return join(', ', array_map(function (array $error) { + return sprintf('%s (%s)', $error['property'], $error['constraint']); + }, $errors)); + } + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + */ + public function validateResponse(RequestInterface $request, ResponseInterface $response) + { + $this->assertNoMissingHeaders($request, $response); + $this->assertValidHeaders($request, $response); + $this->assertValidResponseBody($request, $response); + } + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @throws ValidatorResponseException + */ + private function assertNoMissingHeaders(RequestInterface $request, ResponseInterface $response) + { + $method = $request->getMethod(); + $path = $request->getUri()->getPath(); + $statusCode = $response->getStatusCode(); + + $schemaHeaders = $this->schemaHelper->getResponseHeaders($method, $path, $statusCode, true); + + $missingHeaders = array_diff_key($schemaHeaders, $response->getHeaders()); + if (count($missingHeaders) === 0) { + return; + } + + throw new ValidatorResponseException(sprintf( + 'Missing response headers required by the schema for %s %s with status code %s: %s', + strtoupper($method), + $path, + $statusCode, + $this->getNamedParametersAsString($missingHeaders) + )); + } + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @throws ValidatorResponseException + */ + private function assertValidHeaders(RequestInterface $request, ResponseInterface $response) + { + $method = $request->getMethod(); + $path = $request->getUri()->getPath(); + $statusCode = $response->getStatusCode(); + + $schemaHeaders = $this->schemaHelper->getResponseHeaders($method, $path, $statusCode); + + /** @var NamedParameter $schemaHeader */ + foreach ($schemaHeaders as $schemaHeader) { + $key = $schemaHeader->getKey(); + + /** @var string[] $header */ + foreach ($response->getHeader($key) as $header) { + foreach ($header as $headerValue) { + try { + $schemaHeader->validate($headerValue); + } catch (ValidationException $exception) { + $message = sprintf( + 'Response header %s with value "%s" for %s %s '. + 'with status code %s does not match schema: %s', + $key, + $headerValue, + strtoupper($method), + $path, + $statusCode, + $exception->getMessage() + ); + + throw new ValidatorResponseException($message, 0, $exception); + } + } + } + } + } + + /** + * @param RequestInterface $request + * @param ResponseInterface $response + * @throws ValidatorResponseException + */ + private function assertValidResponseBody(RequestInterface $request, ResponseInterface $response) + { + $method = $request->getMethod(); + $path = $request->getUri()->getPath(); + $statusCode = $response->getStatusCode(); + $contentType = $response->getHeaderLine('Content-Type'); + + $schemaBody = $this->schemaHelper->getResponseBody($method, $path, $statusCode, $contentType); + + $body = $response->getBody()->getContents(); + + try { + $schemaBody->getType()->validate($body); + } catch (InvalidSchemaException $exception) { + $message = sprintf( + 'Invalid Schema: %s', + $this->getSchemaErrorsAsString($exception->getErrors()) + ); + + throw new ValidatorResponseException($message, 0, $exception); + } + } + + /** + * @param array $errors + * @return string + */ + private function getNamedParametersAsString(array $errors) + { + return join(', ', array_map(function (NamedParameter $parameter) { + return $parameter->getDisplayName(); + }, $errors)); + } +} diff --git a/src/Validator/ValidatorSchemaHelper.php b/src/Validator/ValidatorSchemaHelper.php index eeb507cd..52eb6386 100644 --- a/src/Validator/ValidatorSchemaHelper.php +++ b/src/Validator/ValidatorSchemaHelper.php @@ -51,7 +51,7 @@ public function getQueryParameters($method, $path, $requiredOnly = false) * @param string $method * @param string $path * @param string $contentType - * @return Body + * @return \Raml\Body */ public function getRequestBody($method, $path, $contentType) { From b2ad193263f20241483004eea20370eaa17bf0ce Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Thu, 6 Apr 2017 18:47:08 +0200 Subject: [PATCH 15/26] Updated/fixed tests --- test/ParseTest.php | 66 +++------- test/Validator/ValidatorTest.php | 212 +++++++++++++++++++++++++++++++ test/XmlSchemaTest.php | 6 +- test/fixture/invalid/bad.raml | 4 +- 4 files changed, 234 insertions(+), 54 deletions(-) create mode 100644 test/Validator/ValidatorTest.php diff --git a/test/ParseTest.php b/test/ParseTest.php index 316c66f9..23560025 100644 --- a/test/ParseTest.php +++ b/test/ParseTest.php @@ -81,8 +81,11 @@ public function shouldThrowCorrectExceptionOnBadJson() /** @test */ public function shouldThrowCorrectExceptionOnBadRamlFile() { - $this->setExpectedException('\Raml\Exception\InvalidJsonException'); - $this->parser->parse(__DIR__.'/fixture/invalid/bad.raml'); + $this->setExpectedException('\Raml\Exception\RamlParserException'); + // exit(var_dump( + $this->parser->parse(__DIR__.'/fixture/invalid/bad.raml') + // )) + ; } /** @test */ @@ -91,14 +94,14 @@ public function shouldThrowExceptionOnPathManipulationIfNotAllowed() $config = new \Raml\ParseConfiguration(); $config->disableDirectoryTraversal(); $this->parser->setConfiguration($config); - $this->setExpectedException('\Raml\Exception\InvalidJsonException'); + $this->setExpectedException('\Raml\Exception\RamlParserException'); $this->parser->parse(__DIR__.'/fixture/treeTraversal/bad.raml'); } /** @test */ public function shouldPreventDirectoryTraversalByDefault() { - $this->setExpectedException('\Raml\Exception\InvalidJsonException'); + $this->setExpectedException('\Raml\Exception\RamlParserException'); $this->parser->parse(__DIR__.'/fixture/treeTraversal/bad.raml'); } @@ -118,7 +121,7 @@ public function shouldNotThrowExceptionOnPathManipulationIfAllowed() $body = $response->getBodyByType('application/json'); $schema = $body->getSchema(); - $this->assertInstanceOf('\Raml\Schema\Definition\JsonSchemaDefinition', $schema); + $this->assertInstanceOf('\Raml\Type\JsonType', $schema); } /** @test */ @@ -213,7 +216,7 @@ public function shouldReturnAMethodObjectForAMethod() $this->assertCount(3, $resource->getMethods()); $this->assertInstanceOf('\Raml\Method', $method); $this->assertEquals('POST', $method->getType()); - $this->assertInstanceOf('\Raml\Schema\Definition\JsonSchemaDefinition', $schema); + $this->assertInstanceOf('\Raml\Type\JsonType', $schema); } /** @test */ @@ -256,7 +259,7 @@ public function shouldParseJson() $body = $response->getBodyByType('application/json'); $schema = $body->getSchema(); - $this->assertInstanceOf('\Raml\Schema\Definition\JsonSchemaDefinition', $schema); + $this->assertInstanceOf('\Raml\Type\JsonType', $schema); } /** @test */ @@ -270,7 +273,7 @@ public function shouldParseJsonSchemaInRaml() $body = $response->getBodyByType('application/json'); $schema = $body->getSchema(); - $this->assertInstanceOf('\Raml\Schema\Definition\JsonSchemaDefinition', $schema); + $this->assertInstanceOf('\Raml\Type\JsonType', $schema); } /** @test */ @@ -284,24 +287,7 @@ public function shouldIncludeChildJsonObjects() $body = $response->getBodyByType('application/json'); $schema = $body->getSchema(); - $this->assertInstanceOf('\Raml\Schema\Definition\JsonSchemaDefinition', $schema); - } - - /** @test */ - public function shouldNotParseJsonIfNotRequested() - { - $config = new \Raml\ParseConfiguration(); - $config->disableSchemaParsing(); - $this->parser->setConfiguration($config); - - $simpleRaml = $this->parser->parse(__DIR__.'/fixture/simple.raml'); - $resource = $simpleRaml->getResourceByUri('/songs/1'); - $method = $resource->getMethod('get'); - $response = $method->getResponse(200); - $body = $response->getBodyByType('application/json'); - $schema = $body->getSchema(); - - $this->assertInternalType('string', $schema); + $this->assertInstanceOf('\Raml\Type\JsonType', $schema); } /** @test */ @@ -344,25 +330,7 @@ public function shouldParseIncludedJson() $body = $response->getBodyByType('application/json'); $schema = $body->getSchema(); - $this->assertInstanceOf('\Raml\Schema\Definition\JsonSchemaDefinition', $schema); - } - - /** @test */ - public function shouldNotParseIncludedJsonIfNotRequired() - { - $config = new \Raml\ParseConfiguration(); - $config->disableSchemaParsing(); - $this->parser->setConfiguration($config); - - $simpleRaml = $this->parser->parse(__DIR__.'/fixture/includeSchema.raml'); - - $resource = $simpleRaml->getResourceByUri('/songs'); - $method = $resource->getMethod('get'); - $response = $method->getResponse(200); - $body = $response->getBodyByType('application/json'); - $schema = $body->getSchema(); - - $this->assertInternalType('string', $schema); + $this->assertInstanceOf('\Raml\Type\JsonType', $schema); } /** @test */ @@ -414,7 +382,7 @@ public function shouldThrowErrorIfNoTitle() /** @test */ public function shouldThrowErrorIfUnknownIncluded() { - $this->setExpectedException('\Raml\Exception\InvalidSchemaTypeException'); + $this->setExpectedException('\Raml\Exception\InvalidJsonException'); try { $this->parser->parse(__DIR__.'/fixture/includeUnknownSchema.raml'); @@ -440,7 +408,7 @@ public function shouldBeAbleToAddAdditionalSchemaTypes() $schema = $body->getSchema(); - $this->assertInstanceOf('\Raml\Schema\Definition\JsonSchemaDefinition', $schema); + $this->assertInstanceOf('\Raml\Type\JsonType', $schema); } /** @test */ @@ -707,7 +675,7 @@ public function shouldReplaceSchemaByRootSchema() $body = $response->getBodyByType('application/json'); $schema = $body->getSchema(); - $this->assertInstanceOf('Raml\Schema\Definition\JsonSchemaDefinition', $schema); + $this->assertInstanceOf('Raml\Type\JsonType', $schema); $schema = $schema->getJsonArray(); @@ -719,7 +687,7 @@ public function shouldParseAndReplaceSchemaOnlyInResources() { $def = $this->parser->parse(__DIR__ . '/fixture/schemaInTypes.raml'); $schema = $def->getResourceByUri('/projects')->getMethod('post')->getBodyByType('application/json')->getSchema(); - $this->assertInstanceOf('Raml\Schema\Definition\JsonSchemaDefinition', $schema); + $this->assertInstanceOf('Raml\Type\JsonType', $schema); } /** @test */ diff --git a/test/Validator/ValidatorTest.php b/test/Validator/ValidatorTest.php new file mode 100644 index 00000000..3d636704 --- /dev/null +++ b/test/Validator/ValidatorTest.php @@ -0,0 +1,212 @@ +parser = new \Raml\Parser(); + $this->uri = $this->getMock('\Psr\Http\Message\UriInterface'); + $this->request = $this->getMock('\Psr\Http\Message\RequestInterface'); + $this->request->method('getUri')->willReturn($this->uri); + $this->response = $this->getMock('\Psr\Http\Message\ResponseInterface'); + } + + /** + * @param string $fixturePath + * @return Validator + */ + private function getValidatorForSchema($fixturePath) + { + $apiDefinition = $this->parser->parse($fixturePath); + $helper = new ValidatorSchemaHelper($apiDefinition); + + return new Validator($helper); + } + + /** @test */ + public function shouldCatchMissingParameters() + { + $this->request->method('getMethod')->willReturn('get'); + $this->uri->method('getPath')->willReturn('/songs'); + $this->uri->method('getQuery')->willReturn(''); + + $this->setExpectedException( + '\Raml\Validator\ValidatorRequestException', + 'required_number' + ); + + $validator = $this->getValidatorForSchema(__DIR__ . '/../fixture/validator/queryParameters.raml'); + $validator->validateRequest($this->request); + } + + /** @test */ + public function shouldCatchInvalidParameters() + { + $this->request->method('getMethod')->willReturn('get'); + $this->uri->method('getPath')->willReturn('/songs'); + $this->uri->method('getQuery')->willReturn('required_number=5&optional_long_string=ABC'); + + $this->setExpectedException( + '\Raml\Validator\ValidatorRequestException', + 'optional_long_string' + ); + + $validator = $this->getValidatorForSchema(__DIR__ . '/../fixture/validator/queryParameters.raml'); + $validator->validateRequest($this->request); + } + + /** @test */ + public function shouldCatchInvalidRequestBody() + { + $body = $this->getMock('\Psr\Http\Message\StreamInterface'); + $body->method('getContents')->willReturn('{"title":"Aaa"}'); + + $this->request->method('getMethod')->willReturn('post'); + $this->uri->method('getPath')->willReturn('/songs'); + $this->request->method('getHeaderLine')->with('Content-Type')->willReturn('application/json'); + $this->request->method('getBody')->willReturn($body); + + $this->setExpectedException( + '\Raml\Exception\InvalidSchemaException', + 'Invalid Schema.' + ); + + $validator = $this->getValidatorForSchema(__DIR__ . '/../fixture/validator/requestBody.raml'); + $validator->validateRequest($this->request); + } + + /** @test */ + public function shouldCatchMissingHeaders() + { + $this->request->method('getMethod')->willReturn('get'); + $this->uri->method('getPath')->willReturn('/songs'); + $this->response->method('getStatusCode')->willReturn(200); + $this->response->method('getHeaders')->willReturn([]); + + $this->setExpectedException( + '\Raml\Validator\ValidatorResponseException', + 'X-Required-Header' + ); + + $validator = $this->getValidatorForSchema(__DIR__ . '/../fixture/validator/responseHeaders.raml'); + $validator->validateResponse($this->request, $this->response); + } + + /** @test */ + public function shouldCatchInvalidHeaders() + { + $headers = [ + 'X-Required-Header' => ['123456'], + 'X-Long-Optional-Header' => ['Abcdefg', 'Abc'], + ]; + + $map = [ + ['X-Required-Header', [['123456']]], + ['X-Long-Optional-Header', [['Abcdefg', 'Abc']]], + ]; + + $this->request->method('getMethod')->willReturn('get'); + $this->uri->method('getPath')->willReturn('/songs'); + $this->response->method('getStatusCode')->willReturn(200); + $this->response->method('getHeader')->willReturnMap($map); + $this->response->method('getHeaders')->willReturn($headers); + + $this->setExpectedException( + '\Raml\Validator\ValidatorResponseException', + 'X-Long-Optional-Header' + ); + + $validator = $this->getValidatorForSchema(__DIR__ . '/../fixture/validator/responseHeaders.raml'); + $validator->validateResponse($this->request, $this->response); + } + + /** @test */ + public function shouldPassOnEmptyBodyIfNotRequired() + { + $json = ''; + + $headers = [ + 'X-Required-Header' => ['123456'], + 'X-Long-Optional-Header' => ['Abcdefghijkl'], + ]; + + $map = [ + ['X-Required-Header', [['123456']]], + ['X-Long-Optional-Header', [['Abcdefg', 'Abc']]], + ]; + + $body = $this->getMock('\Psr\Http\Message\StreamInterface'); + $body->method('getContents')->willReturn($json); + + $this->request->method('getMethod')->willReturn('get'); + $this->uri->method('getPath')->willReturn('/songs'); + $this->response->method('getStatusCode')->willReturn(200); + $this->response->method('getHeader')->willReturnMap($map); + $this->response->method('getHeaders')->willReturn($headers); + $this->response->method('getBody')->willReturn($body); + + $this->setExpectedException( + '\Raml\Validator\ValidatorResponseException', + 'X-Long-Optional-Header' + ); + + $validator = $this->getValidatorForSchema(__DIR__ . '/../fixture/validator/responseHeaders.raml'); + $validator->validateResponse($this->request, $this->response); + } + + /** @test */ + public function shouldCatchInvalidResponseBody() + { + $json = '{}'; + + $headers = [ + 'X-Required-Header' => ['123456'], + 'X-Long-Optional-Header' => ['Abcdefghijkl'], + ]; + + $map = [ + ['X-Required-Header', [['123456']]], + ['X-Long-Optional-Header', [['Abcdefg', 'Abc']]], + ]; + + $body = $this->getMock('\Psr\Http\Message\StreamInterface'); + $body->method('getContents')->willReturn($json); + + $this->request->method('getMethod')->willReturn('post'); + $this->uri->method('getPath')->willReturn('/songs'); + $this->response->method('getStatusCode')->willReturn(200); + $this->response->method('getHeader')->willReturnMap($map); + $this->response->method('getHeaders')->willReturn($headers); + $this->response->method('getHeaderLine')->with('Content-Type')->willReturn('application/json'); + $this->response->method('getBody')->willReturn($body); + + $this->setExpectedException( + '\Raml\Validator\ValidatorResponseException', + 'Invalid Schema:' + ); + + $validator = $this->getValidatorForSchema(__DIR__ . '/../fixture/validator/responseBody.raml'); + $validator->validateResponse($this->request, $this->response); + } +} diff --git a/test/XmlSchemaTest.php b/test/XmlSchemaTest.php index 5db8bac6..8ca9e054 100644 --- a/test/XmlSchemaTest.php +++ b/test/XmlSchemaTest.php @@ -57,7 +57,7 @@ private function getSchema() /** @test */ public function shouldReturnXmlSchemeDefinition() { - $this->assertInstanceOf('Raml\Schema\Definition\XmlSchemaDefinition', $this->getSchema()); + $this->assertInstanceOf('Raml\Type\XmlType', $this->getSchema()); } /** @test */ @@ -101,7 +101,7 @@ public function shouldCorrectlyValidateIncorrectXml() /** @test */ public function shouldCorrectlyValidateInvalidXml() { - $this->setExpectedException('\Raml\Exception\InvalidXmlException', 'Invalid Xml.'); + $this->setExpectedException('\Raml\Exception\InvalidSchemaException', 'Invalid Schema.'); $xml = <<<'XML' @@ -167,7 +167,7 @@ public function shouldThrowExceptionOnIncorrectXml() public function shouldThrowExceptionOnInvalidXml() { $invalidXml = 'api-request>setExpectedException('\Raml\Exception\InvalidXmlException'); + $this->setExpectedException('\Raml\Exception\InvalidSchemaException'); $this->loadXmlSchema()->validate($invalidXml); } } diff --git a/test/fixture/invalid/bad.raml b/test/fixture/invalid/bad.raml index bbe47945..6e73a461 100644 --- a/test/fixture/invalid/bad.raml +++ b/test/fixture/invalid/bad.raml @@ -1,6 +1,6 @@ #%RAML 0.8 -title: World Music API +title: World Music API BAD baseUri: http://example.api.com/{version} version: v1 traits: @@ -19,4 +19,4 @@ traits: 200: body: application/json: - schema: !include nonexistantfile.json + schema: !include nonexistentfile.json From c0e10f72c6c74251db216bb61495abe9eacaa1e1 Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Wed, 12 Apr 2017 13:41:31 +0200 Subject: [PATCH 16/26] Fixed and optimized type validations --- src/ApiDefinition.php | 39 +++++++------- src/Body.php | 2 +- src/Exception/BodyNotFoundException.php | 10 ++++ src/Exception/InvalidTypeException.php | 30 +++++++++-- src/Method.php | 4 +- src/Type.php | 10 ++-- src/Type/ArrayType.php | 68 +++++++++++++++++++++++-- src/Type/BooleanType.php | 2 +- src/Type/DateOnlyType.php | 6 ++- src/Type/DateTimeOnlyType.php | 2 +- src/Type/DateTimeType.php | 2 +- src/Type/FileType.php | 2 +- src/Type/IntegerType.php | 2 +- src/Type/JsonType.php | 12 +++-- src/Type/NullType.php | 2 +- src/Type/NumberType.php | 20 ++++---- src/Type/ObjectType.php | 29 +++++++---- src/Type/StringType.php | 8 +-- src/Type/TimeOnlyType.php | 2 +- src/Type/UnionType.php | 3 +- src/Validator/Validator.php | 39 ++++++++++++-- src/Validator/ValidatorSchemaHelper.php | 7 ++- test/ParseTest.php | 5 +- 23 files changed, 224 insertions(+), 82 deletions(-) create mode 100644 src/Exception/BodyNotFoundException.php diff --git a/src/ApiDefinition.php b/src/ApiDefinition.php index ea78805a..9352ab99 100644 --- a/src/ApiDefinition.php +++ b/src/ApiDefinition.php @@ -31,6 +31,7 @@ class ApiDefinition implements ArrayInstantiationInterface { const PROTOCOL_HTTP = 'HTTP'; const PROTOCOL_HTTPS = 'HTTPS'; + const ROOT_ELEMENT_NAME = '__ROOT_ELEMENT__'; // --- @@ -624,21 +625,6 @@ public function addDocumentation($title, $documentation) **/ public static function determineType($name, $definition) { - // check if we can find a more appropriate Type subclass - $straightForwardTypes = [ - 'time-only', - 'datetime', - 'datetime-only', - 'date-only', - 'number', - 'integer', - 'boolean', - 'string', - 'null', - 'file', - 'array', - 'object' - ]; if (is_string($definition)) { $definition = ['type' => $definition]; } elseif (is_array($definition)) { @@ -653,10 +639,27 @@ public static function determineType($name, $definition) if (is_object($name)) { throw new \Exception(var_export($name, true)); } - if (strpos($name, '?') !== false) { + if (strpos($definition['type'], '?') !== false) { + $definition['type'] = substr($definition['type'], 0, strlen($definition['type']) - 1); $definition['required'] = false; } + // check if we can find a more appropriate Type subclass + $straightForwardTypes = [ + 'time-only', + 'datetime', + 'datetime-only', + 'date-only', + 'number', + 'integer', + 'boolean', + 'string', + 'null', + 'file', + 'array', + 'object' + ]; + $type = $definition['type']; if (in_array($type, $straightForwardTypes)) { @@ -678,11 +681,11 @@ public static function determineType($name, $definition) } // is it a XML schema? if (substr(ltrim($type), 0, 1) === '<') { - return XmlType::createFromArray('schema', $definition); + return XmlType::createFromArray(self::ROOT_ELEMENT_NAME, $definition); } // is it a JSON schema? if (substr(ltrim($type), 0, 1) === '{') { - return JsonType::createFromArray('schema', $definition); + return JsonType::createFromArray(self::ROOT_ELEMENT_NAME, $definition); } // no? then no standard type found so this must be a reference to a custom defined type. diff --git a/src/Body.php b/src/Body.php index eac40583..2799b174 100644 --- a/src/Body.php +++ b/src/Body.php @@ -115,7 +115,7 @@ public static function createFromArray($mediaType, array $data = []) $name = ''; $definition = $data['type']; - $type = ApiDefinition::determineType($name, $definition); + $type = ApiDefinition::determineType(ApiDefinition::ROOT_ELEMENT_NAME, $definition); if ($type instanceof ObjectType) { $type->inheritFromParent(); } diff --git a/src/Exception/BodyNotFoundException.php b/src/Exception/BodyNotFoundException.php new file mode 100644 index 00000000..7679a74d --- /dev/null +++ b/src/Exception/BodyNotFoundException.php @@ -0,0 +1,10 @@ +errors = $errors; + // check if errors is correctly structured + if (!isset($error['property']) || !isset($error['constraint'])) { + throw new RuntimeException('Errors parameter is missing required elements "property" and/or "constraint.'); + } + if ($previous !== null && $previous instanceof Self) { + $this->errors = $previous->getErrors(); + } + $this->errors[] = $error; - parent::__construct('Invalid Schema.'); + parent::__construct('Type does not validate.', 500, $previous); } + /** + * Returns the collected validation errors + * + * @return array Returns array of arrays [['property' => 'propertyname', 'constraint' => 'constraint description']]. + */ public function getErrors() { return $this->errors; } + + public function setErrors(array $errors) + { + $this->errors = $errors; + } } diff --git a/src/Method.php b/src/Method.php index 4fbfad68..89cfc296 100644 --- a/src/Method.php +++ b/src/Method.php @@ -1,6 +1,8 @@ bodyList[$type])) { - throw new \Exception('No body of type "' . $type . '"'); + throw new BodyNotFoundException('No body of type "' . $type . '"'); } return $this->bodyList[$type]; diff --git a/src/Type.php b/src/Type.php index 655d02f2..5e563666 100644 --- a/src/Type.php +++ b/src/Type.php @@ -18,35 +18,35 @@ class Type implements ArrayInstantiationInterface, TypeInterface * * @var ObjectType|string **/ - private $parent = null; + protected $parent = null; /** * Key used for type * * @var string **/ - private $name; + protected $name; /** * Type * * @var string **/ - private $type; + protected $type; /** * Specifies that the property is required or not. * * @var bool **/ - private $required = true; + protected $required = true; /** * Raml definition * * @var array **/ - private $definition; + protected $definition; /** * Create new type diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index c3577d64..f0abed89 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -4,6 +4,7 @@ use Raml\Type; use Raml\Exception\InvalidTypeException; +use Raml\ApiDefinition; /** * ArrayType class @@ -122,7 +123,12 @@ public function getItems() */ public function setItems($items) { - $this->items = $items; + if (!is_array($items)) { + $items = [$items]; + } + foreach ($items as $item) { + $this->items[] = ApiDefinition::determineType($item, $item); + } return $this; } @@ -178,15 +184,67 @@ public function setMaxItems($maxItems) public function validate($value) { if (!is_array($value)) { - throw new InvalidTypeException(['Value is not an array.']); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not an array: %s', var_export($value, true))]); } else { if (count($value) < $this->minItems) { - throw new InvalidTypeException([sprintf('Array should contain a minimal of "%s" items.', $this->minItems)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Array should contain a minimal of "%s" items.', $this->minItems)]); } if (count($value) > $this->maxItems) { - throw new InvalidTypeException([sprintf('Array should contain a maximum of "%s" items.', $this->maxItems)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Array should contain a maximum of "%s" items.', $this->maxItems)]); + } + + if (!empty($this->items)) { + $lastException = null; + foreach ($value as $item) { + // check if array element is of any of the defined types + foreach ($this->items as $allowedType) { + try { + $allowedType->validate($item); + continue 2; + } catch (InvalidTypeException $e) { + $lastException = $e; + } + } + // none found means validation failure + if (count($this->items) === 1) { + throw new InvalidTypeException([ + 'property' => $this->name, + 'constraint' => sprintf( + 'Array element can only be of allowed type "%s" and fails requirements: %s', + implode( + ',', + array_map( + function ($item) { + return $item->getName(); + }, + $this->items + ) + ), + implode(', ', array_map(function ($error) { + return sprintf('%s (%s)', $error['property'], $error['constraint']); + }, $lastException->getErrors())) + ) + ], $lastException); + } else { + throw new InvalidTypeException([ + 'property' => $this->name, + 'constraint' => sprintf( + 'Array element can only be of allowed types: %s', + implode( + ',', + array_map( + function ($item) { + return $item->getName(); + }, + $this->items + ) + ), + var_export($item, true) + ) + ], $lastException); + } + } } - // TODO: implement $this->items check // TODO: implement $this->uniqueItems check } return true; diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index 2728a609..efce1f7d 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -30,7 +30,7 @@ public static function createFromArray($name, array $data = []) public function validate($value) { if (!is_bool($value)) { - throw new InvalidTypeException(['Value is not a boolean.']); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not a boolean.']); } return true; } diff --git a/src/Type/DateOnlyType.php b/src/Type/DateOnlyType.php index f693d30d..8896cc9e 100644 --- a/src/Type/DateOnlyType.php +++ b/src/Type/DateOnlyType.php @@ -2,6 +2,7 @@ namespace Raml\Type; +use DateTime; use Raml\Type; use Raml\Exception\InvalidTypeException; @@ -29,9 +30,12 @@ public static function createFromArray($name, array $data = []) public function validate($value) { + if (!is_string($value)) { + throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not a date-only string.']); + } $d = DateTime::createFromFormat('Y-m-d', $value); if (($d && $d->format('Y-m-d') === $value) === false) { - throw new InvalidTypeException('Value is not a date-only.'); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not a date-only.']); } return true; } diff --git a/src/Type/DateTimeOnlyType.php b/src/Type/DateTimeOnlyType.php index bcf11a0a..ea2fb961 100644 --- a/src/Type/DateTimeOnlyType.php +++ b/src/Type/DateTimeOnlyType.php @@ -31,7 +31,7 @@ public function validate($value) { $d = DateTime::createFromFormat(DATE_RFC3339, $value); if (($d && $d->format(DATE_RFC3339) === $value) === false) { - throw new InvalidTypeException('Value is not a datetime-only.'); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not a datetime-only.']); } return true; } diff --git a/src/Type/DateTimeType.php b/src/Type/DateTimeType.php index 3068c410..150ca955 100644 --- a/src/Type/DateTimeType.php +++ b/src/Type/DateTimeType.php @@ -69,7 +69,7 @@ public function validate($value) $format = $this->format ?: DATE_RFC3339; $d = DateTime::createFromFormat($format, $value); if (($d && $d->format($format) === $value) === false) { - throw new InvalidTypeException('Value is not a datetime-only.'); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not a datetime-only.']); } return true; } diff --git a/src/Type/FileType.php b/src/Type/FileType.php index 88b9f2c4..8b99cd26 100644 --- a/src/Type/FileType.php +++ b/src/Type/FileType.php @@ -139,7 +139,7 @@ public function setMaxLength($maxLength) public function validate($value) { if ((bool) preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $value) === false) { - throw new InvalidTypeException(['Value is not a file.']); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not a file.']); } return true; } diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index 657c579c..e724ee7f 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -36,7 +36,7 @@ public static function createFromArray($name, array $data = []) public function validate($value) { if (is_int($value) === false) { - throw new InvalidTypeException(['Value is not a integer.']); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not a integer.']); } return true; } diff --git a/src/Type/JsonType.php b/src/Type/JsonType.php index a212d916..a00bd66b 100644 --- a/src/Type/JsonType.php +++ b/src/Type/JsonType.php @@ -58,10 +58,14 @@ public function setJson($json) { */ public function validate($string) { - $json = json_decode($string); - - if (json_last_error() !== JSON_ERROR_NONE) { - throw new InvalidJsonException(json_last_error()); + if (is_string($json)) { + $json = json_decode($string); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new InvalidJsonException(json_last_error()); + } + } else { + $json = (object) $string; } return $this->validateJsonObject($json); diff --git a/src/Type/NullType.php b/src/Type/NullType.php index f7c4ffa1..0cfe0ce7 100644 --- a/src/Type/NullType.php +++ b/src/Type/NullType.php @@ -30,7 +30,7 @@ public static function createFromArray($name, array $data = []) public function validate($value) { if (is_null($value) === false) { - throw new InvalidTypeException(['Value is not null.']); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not null.']); } return true; } diff --git a/src/Type/NumberType.php b/src/Type/NumberType.php index 62abb7d7..6fbd1926 100644 --- a/src/Type/NumberType.php +++ b/src/Type/NumberType.php @@ -176,60 +176,60 @@ public function validate($value) { if (!is_null($this->maximum)) { if ($value > $this->maximum) { - throw new InvalidTypeException([sprintf('Value is larger than the allowed maximum of %s.', $this->maximum)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is larger than the allowed maximum of %s.', $this->maximum)]); } } if (!is_null($this->minimum)) { if ($value < $this->minimum) { - throw new InvalidTypeException([sprintf('Value is smaller than the allowed minimum of %s.', $this->minimum)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is smaller than the allowed minimum of %s.', $this->minimum)]); } } switch ($this->format) { case 'int8': if (filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => -128, 'max_range' => 127]]) === false) { - throw new InvalidTypeException([sprintf('Value is not of the required format: "%s".', $this->format)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not of the required format: "%s".', $this->format)]); } break; case 'int16': if (filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => -32768, 'max_range' => 32767]]) === false) { - throw new InvalidTypeException([sprintf('Value is not of the required format: "%s".', $this->format)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not of the required format: "%s".', $this->format)]); } break; case 'int32': if (filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => -2147483648, 'max_range' => 2147483647]]) === false) { - throw new InvalidTypeException([sprintf('Value is not of the required format: "%s".', $this->format)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not of the required format: "%s".', $this->format)]); } break; case 'int64': if (filter_var($value, FILTER_VALIDATE_INT, ['options' => ['min_range' => -9223372036854775808, 'max_range' => 9223372036854775807]]) === false) { - throw new InvalidTypeException([sprintf('Value is not of the required format: "%s".', $this->format)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not of the required format: "%s".', $this->format)]); } break; case 'int': // int === long case 'long': if (!is_int($value)) { - throw new InvalidTypeException([sprintf('Value is not of the required format: "%s".', $this->format)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not of the required format: "%s".', $this->format)]); } break; case 'float': // float === double case 'double': if (!is_float($value)) { - throw new InvalidTypeException([sprintf('Value is not of the required format: "%s".', $this->format)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not of the required format: "%s".', $this->format)]); } break; // if no format is given we check only if it is a number null: default: if (!is_float($value) && !is_int($value)) { - throw new InvalidTypeException([sprintf('Value is not of the required format: "%s".', $this->format)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not of the required format: "%s".', $this->format)]); } break; } if (!is_null($this->multipleOf)) { if ($value %$this->multipleOf != 0) { - throw new InvalidTypeException([sprintf('Value is not a multiplication of "%s".', $this->multipleOf)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not a multiplication of "%s".', $this->multipleOf)]); } } return true; diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 8a5250fb..ada99877 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -276,26 +276,33 @@ public function validate($value) { // an object is in essence just a group (array) of datatypes if (!is_array($value)) { - throw new InvalidTypeException(['Value is not an array.']); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not an array.']); } - $errors = []; + $previousException = null; foreach ($this->getProperties() as $property) { - if ($property->isRequired()) { + // we catch the validation exceptions so we can validate the entire object + try { if (!in_array($property->getName(), array_keys($value))) { - $errors[] = sprintf('Object does not contain required property "%s".', $property->getName()); - } else { - try { - $property->validate($value[$property->getName()]); - } catch (InvalidTypeException $e) { - $errors = array_merge($errors, $e->getErrors()); + if ($property->isRequired()) { + throw new InvalidTypeException([ + 'property' => $property->getName(), + 'constraint' => sprintf('Object does not contain required property "%s".', $property->getName()) + ], $previousException); } + } else { + $property->validate($value[$property->getName()]); } + } catch (InvalidTypeException $e) { + $previousException = $e; } } - if (!empty($errors)) { - throw new InvalidTypeException($errors); + if ($previousException !== null) { + throw new InvalidTypeException([ + 'property' => $this->name, + 'constraint' => 'One or more object properties is invalid.' + ]); } return true; diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 1dc69d96..779731ed 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -140,21 +140,21 @@ public function setMaxLength($maxLength) public function validate($value) { if (!is_string($value)) { - throw new InvalidTypeException(['Value is not a string.']); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not a string.']); } if (!is_null($this->pattern)) { if (preg_match('/'.$this->pattern.'/', $value) == false) { - throw new InvalidTypeException([sprintf('String does not match required pattern: %s.', $this->pattern)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('String does not match required pattern: %s.', $this->pattern)]); } } if (!is_null($this->minLength)) { if (strlen($value) < $this->minLength) { - throw new InvalidTypeException([sprintf('String is shorter than the minimal length of %s.', $this->minLength)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('String is shorter than the minimal length of %s.', $this->minLength)]); } } if (!is_null($this->maxLength)) { if (strlen($value) > $this->maxLength) { - throw new InvalidTypeException([sprintf('String is longer than the maximal length of %s.', $this->minLength)]); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('String is longer than the maximal length of %s.', $this->minLength)]); } } diff --git a/src/Type/TimeOnlyType.php b/src/Type/TimeOnlyType.php index cb41c53d..fba6934a 100644 --- a/src/Type/TimeOnlyType.php +++ b/src/Type/TimeOnlyType.php @@ -29,7 +29,7 @@ public function validate($value) { $d = DateTime::createFromFormat('HH:II:SS', $value); if (($d && $d->format('HH:II:SS') === $value) !== false) { - throw new InvalidTypeException(['Value is not time-only.']); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not time-only.']); } } } diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index bd4d2194..4c60ef98 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -78,7 +78,8 @@ public function validate($value) throw new InvalidTypeException( [ - sprintf( + 'property' => $this->name, + 'constraint' => sprintf( 'Value is not any of the following types: %s', array_reduce($this->getPossibleTypes(), function ($carry, $item) { $carry = $carry . ', ' . $item->getName(); diff --git a/src/Validator/Validator.php b/src/Validator/Validator.php index c0360f87..6f641427 100644 --- a/src/Validator/Validator.php +++ b/src/Validator/Validator.php @@ -8,6 +8,9 @@ use Raml\Exception\InvalidSchemaException; use Raml\Exception\ValidationException; use Exception; +use Raml\Exception\BodyNotFoundException; +use Raml\Validator\ValidatorSchemaException; +use Raml\Exception\InvalidTypeException; /** * Validator @@ -109,8 +112,15 @@ private function assertValidRequestBody(RequestInterface $request) $method = $request->getMethod(); $path = $request->getUri()->getPath(); $contentType = $request->getHeaderLine('Content-Type'); - - $schemaBody = $this->schemaHelper->getRequestBody($method, $path, $contentType); + try { + $schemaBody = $this->schemaHelper->getRequestBody($method, $path, $contentType); + } catch (BodyNotFoundException $e) { + // no body is defined for this request, nothing to check! + return; + } catch (ValidatorSchemaException $e) { + // no body is defined for this request, nothing to check! + return; + } try { $schemaBody->getType()->validate($body); @@ -242,13 +252,32 @@ private function assertValidResponseBody(RequestInterface $request, ResponseInte $schemaBody = $this->schemaHelper->getResponseBody($method, $path, $statusCode, $contentType); + // psr7 response object should always be rewinded else getContents() only returns the remaining body + $response->getBody()->rewind(); $body = $response->getBody()->getContents(); + // parse body to PHP datatypes + switch ($contentType) { + case 'application/json': + $parsedBody = json_decode($body, true); + if (json_last_error() !== JSON_ERROR_NONE) { + throw new InvalidJsonException(json_last_error()); + } + break; + case 'application/xml': + // do nothing + $parsedBody = $body; + break; + + default: + throw new ValidatorResponseException(sprintf('Unsupported content-type given: %s', $contentType)); + } + try { - $schemaBody->getType()->validate($body); - } catch (InvalidSchemaException $exception) { + $schemaBody->getType()->validate($parsedBody); + } catch (InvalidTypeException $exception) { $message = sprintf( - 'Invalid Schema: %s', + 'Invalid type: %s', $this->getSchemaErrorsAsString($exception->getErrors()) ); diff --git a/src/Validator/ValidatorSchemaHelper.php b/src/Validator/ValidatorSchemaHelper.php index 52eb6386..f88d09d2 100644 --- a/src/Validator/ValidatorSchemaHelper.php +++ b/src/Validator/ValidatorSchemaHelper.php @@ -132,7 +132,12 @@ public function getResponseHeaders($method, $path, $statusCode, $requiredOnly = private function getBody(MessageSchemaInterface $schema, $method, $path, $contentType) { try { - $body = $schema->getBodyByType($contentType); + if (empty($contentType)) { + // no content-type defined? fallback to first defined + $body = current($schema->getBodies()); + } else { + $body = $schema->getBodyByType($contentType); + } } catch (Exception $exception) { $message = sprintf( 'Schema for %s %s with content type %s was not found in API definition', diff --git a/test/ParseTest.php b/test/ParseTest.php index 23560025..64a1e0ff 100644 --- a/test/ParseTest.php +++ b/test/ParseTest.php @@ -82,10 +82,7 @@ public function shouldThrowCorrectExceptionOnBadJson() public function shouldThrowCorrectExceptionOnBadRamlFile() { $this->setExpectedException('\Raml\Exception\RamlParserException'); - // exit(var_dump( - $this->parser->parse(__DIR__.'/fixture/invalid/bad.raml') - // )) - ; + $this->parser->parse(__DIR__.'/fixture/invalid/bad.raml'); } /** @test */ From 122516e25cca9b2ca8c140299535a4125ccbdb00 Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Wed, 12 Apr 2017 13:47:44 +0200 Subject: [PATCH 17/26] Fix: missing adding of previous validation errors --- src/Type/ObjectType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index ada99877..fda65ec4 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -302,7 +302,7 @@ public function validate($value) throw new InvalidTypeException([ 'property' => $this->name, 'constraint' => 'One or more object properties is invalid.' - ]); + ], $previousException); } return true; From 2a9aa0efa4a54da918693bc11783c76eb9d3d832 Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Wed, 12 Apr 2017 17:11:34 +0200 Subject: [PATCH 18/26] Fix: null type should be nil type --- src/ApiDefinition.php | 2 +- src/Type/{NullType.php => NilType.php} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/Type/{NullType.php => NilType.php} (93%) diff --git a/src/ApiDefinition.php b/src/ApiDefinition.php index 9352ab99..c5ecd3ca 100644 --- a/src/ApiDefinition.php +++ b/src/ApiDefinition.php @@ -654,7 +654,7 @@ public static function determineType($name, $definition) 'integer', 'boolean', 'string', - 'null', + 'nil', 'file', 'array', 'object' diff --git a/src/Type/NullType.php b/src/Type/NilType.php similarity index 93% rename from src/Type/NullType.php rename to src/Type/NilType.php index 0cfe0ce7..66872e94 100644 --- a/src/Type/NullType.php +++ b/src/Type/NilType.php @@ -6,11 +6,11 @@ use Raml\Exception\InvalidTypeException; /** - * NullType class + * NilType class * * @author Melvin Loos */ -class NullType extends Type +class NilType extends Type { /** * Create a new NullType from an array of data From 135b521bc797c23f4f4b4f4107da3458ddef9548 Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Wed, 12 Apr 2017 17:20:24 +0200 Subject: [PATCH 19/26] Improved validation error message --- src/Type/DateOnlyType.php | 7 ++++--- src/Type/DateTimeOnlyType.php | 7 ++++--- src/Type/DateTimeType.php | 4 ++-- src/Type/TimeOnlyType.php | 7 ++++--- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Type/DateOnlyType.php b/src/Type/DateOnlyType.php index 8896cc9e..bc9c536d 100644 --- a/src/Type/DateOnlyType.php +++ b/src/Type/DateOnlyType.php @@ -33,9 +33,10 @@ public function validate($value) if (!is_string($value)) { throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not a date-only string.']); } - $d = DateTime::createFromFormat('Y-m-d', $value); - if (($d && $d->format('Y-m-d') === $value) === false) { - throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not a date-only.']); + $format = 'Y-m-d'; + $d = DateTime::createFromFormat($format, $value); + if (($d && $d->format($format) === $value) === false) { + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not conform format: %s.', $format)]); } return true; } diff --git a/src/Type/DateTimeOnlyType.php b/src/Type/DateTimeOnlyType.php index ea2fb961..c09ce6ef 100644 --- a/src/Type/DateTimeOnlyType.php +++ b/src/Type/DateTimeOnlyType.php @@ -29,9 +29,10 @@ public static function createFromArray($name, array $data = []) public function validate($value) { - $d = DateTime::createFromFormat(DATE_RFC3339, $value); - if (($d && $d->format(DATE_RFC3339) === $value) === false) { - throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not a datetime-only.']); + $format = \DateTime::DATE_RFC3339; + $d = DateTime::createFromFormat($format, $value); + if (($d && $d->format($format) === $value) === false) { + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not conform format: %s.', $format)]); } return true; } diff --git a/src/Type/DateTimeType.php b/src/Type/DateTimeType.php index 150ca955..c7a140d9 100644 --- a/src/Type/DateTimeType.php +++ b/src/Type/DateTimeType.php @@ -66,10 +66,10 @@ public function setFormat($format) public function validate($value) { - $format = $this->format ?: DATE_RFC3339; + $format = $this->format ?: \DateTime::DATE_RFC3339; $d = DateTime::createFromFormat($format, $value); if (($d && $d->format($format) === $value) === false) { - throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not a datetime-only.']); + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not conform format: %s.', $format)]); } return true; } diff --git a/src/Type/TimeOnlyType.php b/src/Type/TimeOnlyType.php index fba6934a..74436196 100644 --- a/src/Type/TimeOnlyType.php +++ b/src/Type/TimeOnlyType.php @@ -27,9 +27,10 @@ public static function createFromArray($name, array $data = []) public function validate($value) { - $d = DateTime::createFromFormat('HH:II:SS', $value); - if (($d && $d->format('HH:II:SS') === $value) !== false) { - throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not time-only.']); + $format = 'HH:II:SS'; + $d = DateTime::createFromFormat($format, $value); + if (($d && $d->format($format) === $value) !== false) { + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not conform format: %s.', $format)]); } } } From 0133c21e899e5b5488ead021ebbc399452b803c5 Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Thu, 13 Apr 2017 11:53:19 +0200 Subject: [PATCH 20/26] Moved raml type name to class name mapping to corresponding classes --- src/ApiDefinition.php | 32 ++++++++++++++------------------ src/Type.php | 2 ++ src/Type/ArrayType.php | 2 ++ src/Type/BooleanType.php | 2 ++ src/Type/DateOnlyType.php | 2 ++ src/Type/DateTimeOnlyType.php | 2 ++ src/Type/DateTimeType.php | 2 ++ src/Type/FileType.php | 2 ++ src/Type/IntegerType.php | 2 ++ src/Type/JsonType.php | 2 ++ src/Type/NilType.php | 2 ++ src/Type/NumberType.php | 2 ++ src/Type/ObjectType.php | 2 ++ src/Type/StringType.php | 2 ++ src/Type/TimeOnlyType.php | 2 ++ src/Type/UnionType.php | 2 ++ src/Type/XmlType.php | 2 ++ 17 files changed, 46 insertions(+), 18 deletions(-) diff --git a/src/ApiDefinition.php b/src/ApiDefinition.php index c5ecd3ca..3f809319 100644 --- a/src/ApiDefinition.php +++ b/src/ApiDefinition.php @@ -646,28 +646,24 @@ public static function determineType($name, $definition) // check if we can find a more appropriate Type subclass $straightForwardTypes = [ - 'time-only', - 'datetime', - 'datetime-only', - 'date-only', - 'number', - 'integer', - 'boolean', - 'string', - 'nil', - 'file', - 'array', - 'object' + \Raml\Type\TimeOnlyType::TYPE_NAME => 'Raml\Type\TimeOnlyType', + \Raml\Type\DateTimeType::TYPE_NAME => 'Raml\Type\DateTimeType', + \Raml\Type\DateTimeOnlyType::TYPE_NAME => 'Raml\Type\DateTimeOnlyType', + \Raml\Type\DateOnlyType::TYPE_NAME => 'Raml\Type\DateOnlyType', + \Raml\Type\NumberType::TYPE_NAME => 'Raml\Type\NumberType', + \Raml\Type\IntegerType::TYPE_NAME => 'Raml\Type\IntegerType', + \Raml\Type\BooleanType::TYPE_NAME => 'Raml\Type\BooleanType', + \Raml\Type\StringType::TYPE_NAME => 'Raml\Type\StringType', + \Raml\Type\NilType::TYPE_NAME => 'Raml\Type\NilType', + \Raml\Type\FileType::TYPE_NAME => 'Raml\Type\FileType', + \Raml\Type\ArrayType::TYPE_NAME => 'Raml\Type\ArrayType', + \Raml\Type\ObjectType::TYPE_NAME => 'Raml\Type\ObjectType', ]; $type = $definition['type']; - if (in_array($type, $straightForwardTypes)) { - $className = sprintf( - 'Raml\Type\%sType', - StringTransformer::convertString($type, StringTransformer::UPPER_CAMEL_CASE) - ); - return forward_static_call_array([$className,'createFromArray'], [$name, $definition]); + if (in_array($type, array_keys($straightForwardTypes))) { + return forward_static_call_array([$straightForwardTypes[$type],'createFromArray'], [$name, $definition]); } if (!in_array($type, ['','any'])) { diff --git a/src/Type.php b/src/Type.php index 5e563666..ff6cd094 100644 --- a/src/Type.php +++ b/src/Type.php @@ -13,6 +13,8 @@ */ class Type implements ArrayInstantiationInterface, TypeInterface { + const TYPE_NAME = 'any'; + /** * Parent object * diff --git a/src/Type/ArrayType.php b/src/Type/ArrayType.php index f0abed89..2d8517e4 100644 --- a/src/Type/ArrayType.php +++ b/src/Type/ArrayType.php @@ -13,6 +13,8 @@ */ class ArrayType extends Type { + const TYPE_NAME = 'array'; + /** * Boolean value that indicates if items in the array MUST be unique. * diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php index efce1f7d..99625625 100644 --- a/src/Type/BooleanType.php +++ b/src/Type/BooleanType.php @@ -12,6 +12,8 @@ */ class BooleanType extends Type { + const TYPE_NAME = 'boolean'; + /** * Create a new BooleanType from an array of data * diff --git a/src/Type/DateOnlyType.php b/src/Type/DateOnlyType.php index bc9c536d..b1cc5869 100644 --- a/src/Type/DateOnlyType.php +++ b/src/Type/DateOnlyType.php @@ -13,6 +13,8 @@ */ class DateOnlyType extends Type { + const TYPE_NAME = 'date-only'; + /** * Create a new DateOnlyType from an array of data * diff --git a/src/Type/DateTimeOnlyType.php b/src/Type/DateTimeOnlyType.php index c09ce6ef..ec5ce103 100644 --- a/src/Type/DateTimeOnlyType.php +++ b/src/Type/DateTimeOnlyType.php @@ -12,6 +12,8 @@ */ class DateTimeOnlyType extends Type { + const TYPE_NAME = 'datetime-only'; + /** * Create a new DateTimeOnlyType from an array of data * diff --git a/src/Type/DateTimeType.php b/src/Type/DateTimeType.php index c7a140d9..9b0e616e 100644 --- a/src/Type/DateTimeType.php +++ b/src/Type/DateTimeType.php @@ -10,6 +10,8 @@ */ class DateTimeType extends Type { + const TYPE_NAME = 'datetime'; + /** * DateTime format to use * diff --git a/src/Type/FileType.php b/src/Type/FileType.php index 8b99cd26..cee9213a 100644 --- a/src/Type/FileType.php +++ b/src/Type/FileType.php @@ -12,6 +12,8 @@ */ class FileType extends Type { + const TYPE_NAME = 'file'; + /** * A list of valid content-type strings for the file. The file type * / * MUST be a valid value. * diff --git a/src/Type/IntegerType.php b/src/Type/IntegerType.php index e724ee7f..97dfb0d5 100644 --- a/src/Type/IntegerType.php +++ b/src/Type/IntegerType.php @@ -11,6 +11,8 @@ */ class IntegerType extends NumberType { + const TYPE_NAME = 'integer'; + /** * A numeric instance is valid against "multipleOf" if the result of dividing the instance by this keyword's value is an integer. * diff --git a/src/Type/JsonType.php b/src/Type/JsonType.php index a00bd66b..ba6d57a4 100644 --- a/src/Type/JsonType.php +++ b/src/Type/JsonType.php @@ -14,6 +14,8 @@ */ class JsonType extends Type { + const TYPE_NAME = 'json'; + /** * Json schema * diff --git a/src/Type/NilType.php b/src/Type/NilType.php index 66872e94..39ae94dd 100644 --- a/src/Type/NilType.php +++ b/src/Type/NilType.php @@ -12,6 +12,8 @@ */ class NilType extends Type { + const TYPE_NAME = 'nil'; + /** * Create a new NullType from an array of data * diff --git a/src/Type/NumberType.php b/src/Type/NumberType.php index 6fbd1926..5cd845ac 100644 --- a/src/Type/NumberType.php +++ b/src/Type/NumberType.php @@ -12,6 +12,8 @@ */ class NumberType extends Type { + const TYPE_NAME = 'number'; + /** * The minimum value of the parameter. Applicable only to parameters of type number or integer. * diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index fda65ec4..685ff35c 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -15,6 +15,8 @@ */ class ObjectType extends Type { + const TYPE_NAME = 'object'; + /** * The properties that instances of this type can or must have. * diff --git a/src/Type/StringType.php b/src/Type/StringType.php index 779731ed..63d553f3 100644 --- a/src/Type/StringType.php +++ b/src/Type/StringType.php @@ -12,6 +12,8 @@ */ class StringType extends Type { + const TYPE_NAME = 'string'; + /** * Regular expression that this string should match. * diff --git a/src/Type/TimeOnlyType.php b/src/Type/TimeOnlyType.php index 74436196..53af5130 100644 --- a/src/Type/TimeOnlyType.php +++ b/src/Type/TimeOnlyType.php @@ -10,6 +10,8 @@ */ class TimeOnlyType extends Type { + const TYPE_NAME = 'time-only'; + /** * Create a new TimeOnlyType from an array of data * diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php index 4c60ef98..eb9d4197 100644 --- a/src/Type/UnionType.php +++ b/src/Type/UnionType.php @@ -14,6 +14,8 @@ */ class UnionType extends Type { + const TYPE_NAME = 'union'; + /** * Possible Types * diff --git a/src/Type/XmlType.php b/src/Type/XmlType.php index 7dc2e039..5100f1c2 100644 --- a/src/Type/XmlType.php +++ b/src/Type/XmlType.php @@ -12,6 +12,8 @@ */ class XmlType extends Type { + const TYPE_NAME = 'xsd'; + /** * XML schema * From c60161fdf08a1091d2f265a8e6a7e8fe51c3d8f9 Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Thu, 13 Apr 2017 11:53:34 +0200 Subject: [PATCH 21/26] Fixed datetime constants --- src/Type/DateTimeOnlyType.php | 2 +- src/Type/DateTimeType.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Type/DateTimeOnlyType.php b/src/Type/DateTimeOnlyType.php index ec5ce103..22e4cb26 100644 --- a/src/Type/DateTimeOnlyType.php +++ b/src/Type/DateTimeOnlyType.php @@ -31,7 +31,7 @@ public static function createFromArray($name, array $data = []) public function validate($value) { - $format = \DateTime::DATE_RFC3339; + $format = \DateTime::RFC3339; $d = DateTime::createFromFormat($format, $value); if (($d && $d->format($format) === $value) === false) { throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not conform format: %s.', $format)]); diff --git a/src/Type/DateTimeType.php b/src/Type/DateTimeType.php index 9b0e616e..82b7f4d8 100644 --- a/src/Type/DateTimeType.php +++ b/src/Type/DateTimeType.php @@ -4,6 +4,7 @@ use Raml\Type; use Raml\Exception\InvalidTypeException; +use DateTime; /** * DateTimeType type class @@ -68,7 +69,7 @@ public function setFormat($format) public function validate($value) { - $format = $this->format ?: \DateTime::DATE_RFC3339; + $format = $this->format ?: DateTime::RFC3339; $d = DateTime::createFromFormat($format, $value); if (($d && $d->format($format) === $value) === false) { throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not conform format: %s.', $format)]); From b4fd5d24ef23e281ebb0c8d25b0abd1eb3811f37 Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Thu, 13 Apr 2017 15:26:35 +0200 Subject: [PATCH 22/26] Set todo --- src/Type/ObjectType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/ObjectType.php b/src/Type/ObjectType.php index 685ff35c..21e5f37f 100644 --- a/src/Type/ObjectType.php +++ b/src/Type/ObjectType.php @@ -65,7 +65,7 @@ class ObjectType extends Type * * @var string **/ - private $discriminatorValue = null; + private $discriminatorValue = null; // TODO: set correct default value /** * Create a new ObjectType from an array of data From 67c765c53ed199296eb03da2ec089904698c6804 Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Thu, 13 Apr 2017 15:27:18 +0200 Subject: [PATCH 23/26] Fix: forgot variable after refactoring --- src/Type/JsonType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/JsonType.php b/src/Type/JsonType.php index ba6d57a4..56ea5625 100644 --- a/src/Type/JsonType.php +++ b/src/Type/JsonType.php @@ -60,7 +60,7 @@ public function setJson($json) { */ public function validate($string) { - if (is_string($json)) { + if (is_string($string)) { $json = json_decode($string); if (json_last_error() !== JSON_ERROR_NONE) { From 96d4916ebb49c86640363ad0080a818869848751 Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Thu, 13 Apr 2017 15:28:04 +0200 Subject: [PATCH 24/26] Added deprecation comments to old schema validators --- src/Validator/RequestValidator.php | 3 +++ src/Validator/ResponseValidator.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/Validator/RequestValidator.php b/src/Validator/RequestValidator.php index 6e0834d8..7f22db24 100644 --- a/src/Validator/RequestValidator.php +++ b/src/Validator/RequestValidator.php @@ -7,6 +7,9 @@ use Raml\Exception\ValidationException; use Raml\NamedParameter; +/** + * @deprecated See Raml\Validator\Validator + */ class RequestValidator { /** diff --git a/src/Validator/ResponseValidator.php b/src/Validator/ResponseValidator.php index ba24503f..75c057b7 100644 --- a/src/Validator/ResponseValidator.php +++ b/src/Validator/ResponseValidator.php @@ -8,6 +8,9 @@ use Raml\Exception\ValidationException; use Raml\NamedParameter; +/** + * @deprecated See Raml\Validator\Validator + */ class ResponseValidator { /** From d7cdcd012c379bfb7fe8ba4efaa003c1a256bbfe Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Thu, 13 Apr 2017 15:28:32 +0200 Subject: [PATCH 25/26] Fixed incomplete optional shorthand parsing --- src/ApiDefinition.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ApiDefinition.php b/src/ApiDefinition.php index 3f809319..265b8706 100644 --- a/src/ApiDefinition.php +++ b/src/ApiDefinition.php @@ -639,9 +639,12 @@ public static function determineType($name, $definition) if (is_object($name)) { throw new \Exception(var_export($name, true)); } - if (strpos($definition['type'], '?') !== false) { - $definition['type'] = substr($definition['type'], 0, strlen($definition['type']) - 1); - $definition['required'] = false; + + + if (strpos($definition['type'], '?') !== false || + $pos = strpos($name, '?') !== false) { + // shorthand for required = false + $definition['required'] = isset($definition['required']) ? $definition['required'] : false; } // check if we can find a more appropriate Type subclass From 3e74f208930fcf2344e86def8cd8384aba237e0d Mon Sep 17 00:00:00 2001 From: Melvin Loos Date: Thu, 13 Apr 2017 15:28:40 +0200 Subject: [PATCH 26/26] Fixed unit tests --- test/ApiDefinitionTest.php | 2 +- test/Validator/ValidatorTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/ApiDefinitionTest.php b/test/ApiDefinitionTest.php index 1ffefcef..07609206 100644 --- a/test/ApiDefinitionTest.php +++ b/test/ApiDefinitionTest.php @@ -142,7 +142,7 @@ public function shouldValidateResponse() ]; $this->setExpectedException( '\Raml\Exception\InvalidTypeException', - 'Invalid Schema.' + 'Type does not validate.' ); $type->validate($invalidResponse); } diff --git a/test/Validator/ValidatorTest.php b/test/Validator/ValidatorTest.php index 3d636704..d286871d 100644 --- a/test/Validator/ValidatorTest.php +++ b/test/Validator/ValidatorTest.php @@ -202,8 +202,8 @@ public function shouldCatchInvalidResponseBody() $this->response->method('getBody')->willReturn($body); $this->setExpectedException( - '\Raml\Validator\ValidatorResponseException', - 'Invalid Schema:' + '\Raml\Exception\InvalidSchemaException', + 'Invalid Schema.' ); $validator = $this->getValidatorForSchema(__DIR__ . '/../fixture/validator/responseBody.raml');