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) diff --git a/phpunit.xml b/phpunit.xml index d79774e2..4a11b3e3 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,9 +1,9 @@ title = $title; + $this->types = TypeCollection::getInstance(); + // since the TypeCollection is a singleton, we need to clear it for every parse + $this->types->clear(); } /** @@ -158,6 +189,7 @@ public function __construct($title) * protocols: ?array * defaultMediaType: ?string * schemas: ?array + * types: ?array * securitySchemes: ?array * documentation: ?array * /* @@ -171,7 +203,6 @@ public static function createFromArray($title, array $data = []) // -- - if (isset($data['version'])) { $apiDefinition->setVersion($data['version']); } @@ -207,12 +238,6 @@ public static function createFromArray($title, array $data = []) $apiDefinition->setDefaultMediaType($data['defaultMediaType']); } - if (isset($data['schemas'])) { - foreach ($data['schemas'] as $name => $schema) { - $apiDefinition->addSchemaCollection($name, $schema); - } - } - if (isset($data['securitySchemes'])) { foreach ($data['securitySchemes'] as $name => $securityScheme) { $apiDefinition->addSecurityScheme(SecurityScheme::createFromArray($name, $securityScheme)); @@ -235,6 +260,20 @@ public static function createFromArray($title, array $data = []) } } + 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)); + } + } + + // resolve type inheritance + $apiDefinition->getTypes()->applyInheritance(); + // --- foreach ($data as $resourceName => $resource) { @@ -401,7 +440,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]; + } } } @@ -503,16 +545,18 @@ public function setDefaultMediaType($defaultMediaType) // -- /** + * @deprecated Use types instead! * Get the schemas defined in the root of the API * * @return array[] */ public function getSchemaCollections() { - return $this->schemaCollections; + return $this->types; } /** + * @deprecated Use types instead! * Add an schema * * @param string $collectionName @@ -528,6 +572,7 @@ public function addSchemaCollection($collectionName, $schemas) } /** + * @deprecated Use types instead! * Add a new schema to a collection * * @param string $collectionName @@ -568,6 +613,110 @@ public function addDocumentation($title, $documentation) $this->documentationList[$title] = $documentation; } + /** + * Determines the right Type and returns a type 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. + * @throws \Exception Thrown when no type is defined. + **/ + public static function determineType($name, $definition) + { + if (is_string($definition)) { + $definition = ['type' => $definition]; + } elseif (is_array($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($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 + $straightForwardTypes = [ + \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, array_keys($straightForwardTypes))) { + return forward_static_call_array([$straightForwardTypes[$type],'createFromArray'], [$name, $definition]); + } + + if (!in_array($type, ['','any'])) { + // 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); + } + // is it a XML schema? + if (substr(ltrim($type), 0, 1) === '<') { + return XmlType::createFromArray(self::ROOT_ELEMENT_NAME, $definition); + } + // is it a JSON schema? + if (substr(ltrim($type), 0, 1) === '{') { + 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. + // 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 \Raml\TypeInterface $type + */ + public function addType(TypeInterface $type) + { + $this->types->add($type); + } + + /** + * Get all data types defined in the root of the API + * + * @return \Raml\TypeCollection + */ + public function getTypes() + { + return $this->types; + } + // -- /** @@ -595,7 +744,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..2799b174 100644 --- a/src/Body.php +++ b/src/Body.php @@ -5,6 +5,11 @@ use Raml\Schema\SchemaDefinitionInterface; use Raml\Exception\BadParameter\InvalidSchemaDefinitionException; +use Raml\Exception\MutuallyExclusiveElementsException; +use Raml\ApiDefinition; +use Raml\TypeInterface; +use Raml\Type\ObjectType; +use Raml\Type; /** * A body @@ -42,6 +47,15 @@ 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; + /** * A list of examples * @@ -75,6 +89,7 @@ public function __construct($mediaType) * @param string $mediaType * @param array $data * [ + * type: ?string * schema: ?string * example: ?string * examples: ?array @@ -92,8 +107,23 @@ 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'])) { + $name = ''; + $definition = $data['type']; + + $type = ApiDefinition::determineType(ApiDefinition::ROOT_ELEMENT_NAME, $definition); + if ($type instanceof ObjectType) { + $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'])) { @@ -106,7 +136,6 @@ public static function createFromArray($mediaType, array $data = []) } } - return $body; } @@ -145,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 @@ -163,11 +194,35 @@ 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; + } + + // -- + + /** + * 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; } // -- 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/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 = $previous->getErrors(); + } + $this->errors[] = $error; + + 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/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 @@ +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/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/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 a61d5720..d289714a 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -16,12 +16,23 @@ 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 * No point in fetching them twice @@ -42,6 +53,13 @@ class Parser */ private $schemaParsers = []; + /** + * List of types + * + * @var string + **/ + private $types = []; + /** * List of security settings parsers * @@ -83,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 @@ -153,6 +172,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 * @@ -201,10 +230,40 @@ 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); + + $ramlData = $this->parseRamlString($ramlString, $rootDir); + + $ramlData = $this->parseTraits($ramlData); + + $ramlData = $this->parseResourceTypes($ramlData); + + return $ramlData; + } + /** * Parse a RAML spec from a string * @@ -244,27 +303,31 @@ private function parseRamlData($ramlData, $rootDir) $ramlData = $this->parseResourceTypes($ramlData); - if ($this->configuration->isSchemaParsingEnabled()) { - if (isset($ramlData['schemas'])) { - $schemas = []; - foreach ($ramlData['schemas'] as $schemaCollection) { - 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']); @@ -277,7 +340,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 */ @@ -287,8 +350,8 @@ private function replaceSchemas($array, $schemas) return $array; } foreach ($array as $key => $value) { - if ('schema' === $key) { - if (isset($schemas[$value])) { + if (is_string($key) && in_array($key, ['schema', 'type'])) { + if (array_key_exists($value, $schemas)) { $array[$key] = $schemas[$value]; } } else { @@ -309,18 +372,40 @@ 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)) { 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); @@ -344,7 +429,7 @@ private function getCachedFilePath($data) { /** * Parse the security settings data into an array * - * @param array $array + * @param array $schemesArray * * @return array */ @@ -383,30 +468,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); } } } @@ -417,7 +501,7 @@ private function parseResourceTypes($ramlData) /** * Parse the traits * - * @param $ramlData + * @param mixed $ramlData * * @return array */ @@ -425,9 +509,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; + } } } @@ -466,8 +556,8 @@ 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, $rootDir @@ -503,18 +593,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); @@ -524,8 +635,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); @@ -570,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; } @@ -579,14 +692,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; @@ -605,7 +718,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]; } @@ -649,14 +762,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)); @@ -673,69 +786,84 @@ private function replaceTypes($raml, $types, $path, $name, $parentKey = null) $newArray[$key] = $newValue; } } - } return $newArray; } /** - * 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) { - $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); + $value = $this->applyVariables($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; } + + /** + * 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)); + 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]]); + case 'pluralize': + return Inflect::pluralize($values[$matches[1]]); + case 'uppercase': + return strtoupper($values[$matches[1]]); + case 'lowercase': + return strtolower($values[$matches[1]]); + case 'lowercamelcase': + return StringTransformer::convertString($values[$matches[1]], StringTransformer::LOWER_CAMEL_CASE); + case 'uppercamelcase': + return StringTransformer::convertString($values[$matches[1]], StringTransformer::UPPER_CAMEL_CASE); + case 'lowerunderscorecase': + return StringTransformer::convertString($values[$matches[1]], StringTransformer::LOWER_UNDERSCORE_CASE); + case 'upperunderscorecase': + return StringTransformer::convertString($values[$matches[1]], StringTransformer::UPPER_UNDERSCORE_CASE); + case 'lowerhyphencase': + return StringTransformer::convertString($values[$matches[1]], StringTransformer::LOWER_HYPHEN_CASE); + case 'upperhyphencase': + return StringTransformer::convertString($values[$matches[1]], StringTransformer::UPPER_HYPHEN_CASE); + default: + return $values[$matches[1]]; + } + }, + $trait + ); + } } 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 +{ + const TYPE_NAME = 'any'; + + /** + * Parent object + * + * @var ObjectType|string + **/ + protected $parent = null; + + /** + * Key used for type + * + * @var string + **/ + protected $name; + + /** + * Type + * + * @var string + **/ + protected $type; + + /** + * Specifies that the property is required or not. + * + * @var bool + **/ + protected $required = true; + + /** + * Raml definition + * + * @var array + **/ + protected $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']); + } + if (isset($data['required'])) { + $class->setRequired($data['required']); + } + $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; + } + + /** + * Returns true when property is required, false otherwise. + * + * @return bool + */ + public function isRequired() + { + return ($this->required === true); + } + + /** + * 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; + } + + /** + * {@inheritdoc} + */ + 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/Type/ArrayType.php b/src/Type/ArrayType.php new file mode 100644 index 00000000..2d8517e4 --- /dev/null +++ b/src/Type/ArrayType.php @@ -0,0 +1,254 @@ + + */ +class ArrayType extends Type +{ + const TYPE_NAME = 'array'; + + /** + * 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 = 0; + + /** + * Maximum amount of items in array. Value MUST be equal to or greater than 0. + * Default: 2147483647. + * + * @var int + **/ + private $maxItems = 2147483647; + + /** + * 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) + { + if (!is_array($items)) { + $items = [$items]; + } + foreach ($items as $item) { + $this->items[] = ApiDefinition::determineType($item, $item); + } + + 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; + } + + public function validate($value) + { + if (!is_array($value)) { + 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(['property' => $this->name, 'constraint' => sprintf('Array should contain a minimal of "%s" items.', $this->minItems)]); + } + if (count($value) > $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->uniqueItems check + } + return true; + } +} diff --git a/src/Type/BooleanType.php b/src/Type/BooleanType.php new file mode 100644 index 00000000..99625625 --- /dev/null +++ b/src/Type/BooleanType.php @@ -0,0 +1,39 @@ + + */ +class BooleanType extends Type +{ + const TYPE_NAME = 'boolean'; + + /** + * 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; + } + + public function validate($value) + { + if (!is_bool($value)) { + 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 new file mode 100644 index 00000000..b1cc5869 --- /dev/null +++ b/src/Type/DateOnlyType.php @@ -0,0 +1,45 @@ + + */ +class DateOnlyType extends Type +{ + const TYPE_NAME = 'date-only'; + + /** + * 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; + } + + public function validate($value) + { + if (!is_string($value)) { + throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not a date-only string.']); + } + $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 new file mode 100644 index 00000000..22e4cb26 --- /dev/null +++ b/src/Type/DateTimeOnlyType.php @@ -0,0 +1,41 @@ + + */ +class DateTimeOnlyType extends Type +{ + const TYPE_NAME = 'datetime-only'; + + /** + * 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; + } + + public function validate($value) + { + $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)]); + } + return true; + } +} diff --git a/src/Type/DateTimeType.php b/src/Type/DateTimeType.php new file mode 100644 index 00000000..82b7f4d8 --- /dev/null +++ b/src/Type/DateTimeType.php @@ -0,0 +1,79 @@ + $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; + } + + public function validate($value) + { + $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)]); + } + return true; + } +} diff --git a/src/Type/FileType.php b/src/Type/FileType.php new file mode 100644 index 00000000..cee9213a --- /dev/null +++ b/src/Type/FileType.php @@ -0,0 +1,148 @@ + + */ +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. + * + * @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; + } + + public function validate($value) + { + if ((bool) preg_match('/^[a-zA-Z0-9\/\r\n+]*={0,2}$/', $value) === false) { + 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 new file mode 100644 index 00000000..97dfb0d5 --- /dev/null +++ b/src/Type/IntegerType.php @@ -0,0 +1,45 @@ + + */ +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. + * + * @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; + } + + public function validate($value) + { + if (is_int($value) === false) { + 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 new file mode 100644 index 00000000..56ea5625 --- /dev/null +++ b/src/Type/JsonType.php @@ -0,0 +1,128 @@ + + */ +class JsonType extends Type +{ + const TYPE_NAME = 'json'; + + /** + * 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); + + $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 $string JSON object to validate. + * + * @return bool + * @throws InvalidJsonException Thrown when string is invalid JSON. + */ + public function validate($string) + { + if (is_string($string)) { + $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); + } + + /** + * Validates a json object + * + * @param string $json + * + * @throws InvalidSchemaException Thrown when the string does not validate against the schema. + * + * @return bool + */ + public function validateJsonObject($json) + { + $validator = new Validator(); + $jsonSchema = json_decode($this->json); + + $validator->check($json, $jsonSchema); + + if (!$validator->isValid()) { + 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/Type/LazyProxyType.php b/src/Type/LazyProxyType.php new file mode 100644 index 00000000..d24d1c63 --- /dev/null +++ b/src/Type/LazyProxyType.php @@ -0,0 +1,140 @@ +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; + } + + public function validate($value) + { + $original = $this->getResolvedObject(); + return $original->validate($value); + } +} diff --git a/src/Type/NilType.php b/src/Type/NilType.php new file mode 100644 index 00000000..39ae94dd --- /dev/null +++ b/src/Type/NilType.php @@ -0,0 +1,39 @@ + + */ +class NilType extends Type +{ + const TYPE_NAME = 'nil'; + + /** + * 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; + } + + public function validate($value) + { + if (is_null($value) === false) { + 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 new file mode 100644 index 00000000..5cd845ac --- /dev/null +++ b/src/Type/NumberType.php @@ -0,0 +1,239 @@ + + */ +class NumberType extends Type +{ + const TYPE_NAME = 'number'; + + /** + * The minimum value of the parameter. Applicable only to parameters of type number or integer. + * + * @var int + **/ + private $minimum = null; + + /** + * The maximum value of the parameter. Applicable only to parameters of type number or integer. + * + * @var int + **/ + 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 = 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 = null; + + /** + * 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 + * @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; + } + + /** + * 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; + } + + public function validate($value) + { + if (!is_null($this->maximum)) { + if ($value > $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(['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(['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(['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(['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(['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(['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(['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(['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(['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 new file mode 100644 index 00000000..21e5f37f --- /dev/null +++ b/src/Type/ObjectType.php @@ -0,0 +1,312 @@ + + */ +class ObjectType extends Type +{ + const TYPE_NAME = 'object'; + + /** + * 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; // TODO: set correct default value + + /** + * 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 PropertyNotFoundException(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; + } + + public function validate($value) + { + // an object is in essence just a group (array) of datatypes + if (!is_array($value)) { + throw new InvalidTypeException(['property' => $this->name, 'constraint' => 'Value is not an array.']); + } + $previousException = null; + + foreach ($this->getProperties() as $property) { + // we catch the validation exceptions so we can validate the entire object + try { + if (!in_array($property->getName(), array_keys($value))) { + 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 ($previousException !== null) { + throw new InvalidTypeException([ + 'property' => $this->name, + 'constraint' => 'One or more object properties is invalid.' + ], $previousException); + } + + return true; + } +} diff --git a/src/Type/StringType.php b/src/Type/StringType.php new file mode 100644 index 00000000..63d553f3 --- /dev/null +++ b/src/Type/StringType.php @@ -0,0 +1,165 @@ + + */ +class StringType extends Type +{ + const TYPE_NAME = 'string'; + + /** + * Regular expression that this string should match. + * + * @var string + **/ + private $pattern = null; + + /** + * Minimum length of the string. Value MUST be equal to or greater than 0. + * Default: 0 + * + * @var int + **/ + private $minLength = null; + + /** + * Maximum length of the string. Value MUST be equal to or greater than 0. + * Default: 2147483647 + * + * @var int + **/ + private $maxLength = null; + + /** + * 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; + } + + public function validate($value) + { + if (!is_string($value)) { + 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(['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(['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(['property' => $this->name, 'constraint' => sprintf('String is longer than the maximal length of %s.', $this->minLength)]); + } + } + + return true; + } +} diff --git a/src/Type/TimeOnlyType.php b/src/Type/TimeOnlyType.php new file mode 100644 index 00000000..53af5130 --- /dev/null +++ b/src/Type/TimeOnlyType.php @@ -0,0 +1,38 @@ +format($format) === $value) !== false) { + throw new InvalidTypeException(['property' => $this->name, 'constraint' => sprintf('Value is not conform format: %s.', $format)]); + } + } +} diff --git a/src/Type/UnionType.php b/src/Type/UnionType.php new file mode 100644 index 00000000..eb9d4197 --- /dev/null +++ b/src/Type/UnionType.php @@ -0,0 +1,94 @@ + + */ +class UnionType extends Type +{ + const TYPE_NAME = 'union'; + + /** + * 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; + } + + public function validate($value) + { + foreach ($this->getPossibleTypes() as $type) { + try { + if ($type->validate($value)) { + return true; + } + } catch (InvalidTypeException $e) { + // ignore validation errors since it can be any of possible types + } + } + + throw new InvalidTypeException( + [ + '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(); + return $carry; + }) + ) + ] + ); + } +} diff --git a/src/Type/XmlType.php b/src/Type/XmlType.php new file mode 100644 index 00000000..5100f1c2 --- /dev/null +++ b/src/Type/XmlType.php @@ -0,0 +1,85 @@ + + */ +class XmlType extends Type +{ + const TYPE_NAME = 'xsd'; + + /** + * 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 */ + + $type->xml = $data['type']; + + return $type; + } + + /** + * Validate an XML string against the schema + * + * @param string $string Value to validate. + * + * @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) { + throw new InvalidSchemaException($errors); + } + + // --- + + $dom->schemaValidateSource($this->xml); + $errors = libxml_get_errors(); + libxml_clear_errors(); + if ($errors) { + 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 new file mode 100644 index 00000000..471a0bb4 --- /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', var_export($name, 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..47c979b2 --- /dev/null +++ b/src/TypeInterface.php @@ -0,0 +1,30 @@ + + */ +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(); + + /** + * 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/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 @@ +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'); + 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); + } 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); + + // 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($parsedBody); + } catch (InvalidTypeException $exception) { + $message = sprintf( + 'Invalid type: %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..f88d09d2 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) { @@ -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/ApiDefinitionTest.php b/test/ApiDefinitionTest.php index ce4b5d8c..07609206 100644 --- a/test/ApiDefinitionTest.php +++ b/test/ApiDefinitionTest.php @@ -72,6 +72,81 @@ 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()->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\Type\ObjectType', $object); + $this->assertInstanceOf('\Raml\Type\IntegerType', $object->getPropertyByName('id')); + $this->assertInstanceOf('\Raml\Type\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\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\Type\UnionType', $org->getPropertyByName('onCall')->getResolvedObject()); + $head = $org->getPropertyByName('Head'); + $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\Type\ArrayType', $reports); + $phone = $head->getPropertyByName('phone')->getResolvedObject(); + $this->assertInstanceOf('\Raml\Type\StringType', $phone); + // check resources + $type = $api->getResourceByPath('/orgs/{orgId}')->getMethod('get')->getResponse(200)->getBodyByType('application/json')->getType(); + $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 = [ + 'onCall' => 'this is not an object', + 'Head' => 'this is not an object' + ]; + $this->setExpectedException( + '\Raml\Exception\InvalidTypeException', + 'Type does not validate.' + ); + $type->validate($invalidResponse); + } + /** @test */ public function shouldReturnProtocolsIfSpecified() { diff --git a/test/ParseTest.php b/test/ParseTest.php index 1364ebaa..64a1e0ff 100644 --- a/test/ParseTest.php +++ b/test/ParseTest.php @@ -81,7 +81,7 @@ public function shouldThrowCorrectExceptionOnBadJson() /** @test */ public function shouldThrowCorrectExceptionOnBadRamlFile() { - $this->setExpectedException('\Raml\Exception\InvalidJsonException'); + $this->setExpectedException('\Raml\Exception\RamlParserException'); $this->parser->parse(__DIR__.'/fixture/invalid/bad.raml'); } @@ -91,14 +91,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 +118,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 +213,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 +256,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 +270,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 +284,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 +327,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 +379,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 +405,7 @@ public function shouldBeAbleToAddAdditionalSchemaTypes() $schema = $body->getSchema(); - $this->assertInstanceOf('\Raml\Schema\Definition\JsonSchemaDefinition', $schema); + $this->assertInstanceOf('\Raml\Type\JsonType', $schema); } /** @test */ @@ -707,7 +672,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 +684,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 */ @@ -860,6 +825,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/Validator/ValidatorTest.php b/test/Validator/ValidatorTest.php new file mode 100644 index 00000000..d286871d --- /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\Exception\InvalidSchemaException', + '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/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/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 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 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 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