diff --git a/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker.mustache b/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker.mustache index 80d6cd2e9db..4a335b5c312 100644 --- a/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker.mustache +++ b/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker.mustache @@ -34,6 +34,7 @@ namespace {{mockPackage}}; use {{mockPackage}}\{{interfaceNamePrefix}}OpenApiDataMocker{{interfaceNameSuffix}} as IMocker; +use StdClass; use InvalidArgumentException; /** @@ -82,6 +83,13 @@ final class OpenApiDataMocker implements IMocker $maxItems = $options['maxItems'] ?? null; $uniqueItems = $options['uniqueItems'] ?? false; return $this->mockArray($items, $minItems, $maxItems, $uniqueItems); + case IMocker::DATA_TYPE_OBJECT: + $properties = $options['properties'] ?? null; + $minProperties = $options['minProperties'] ?? 0; + $maxProperties = $options['maxProperties'] ?? null; + $additionalProperties = $options['additionalProperties'] ?? null; + $required = $options['required'] ?? null; + return $this->mockObject($properties, $minProperties, $maxProperties, $additionalProperties, $required); default: throw new InvalidArgumentException('"dataType" must be one of ' . implode(', ', [ IMocker::DATA_TYPE_INTEGER, @@ -89,6 +97,7 @@ final class OpenApiDataMocker implements IMocker IMocker::DATA_TYPE_STRING, IMocker::DATA_TYPE_BOOLEAN, IMocker::DATA_TYPE_ARRAY, + IMocker::DATA_TYPE_OBJECT, ])); } } @@ -220,10 +229,10 @@ final class OpenApiDataMocker implements IMocker * Shortcut to mock array type * Equivalent to mockData(DATA_TYPE_ARRAY); * - * @param array $items Array of described items - * @param int|null $minItems (optional) An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. - * @param int|null $maxItems (optional) An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword - * @param bool|null $uniqueItems (optional) If it has boolean value true, the instance validates successfully if all of its elements are unique + * @param object|array $items Object or assoc array of described items + * @param int|null $minItems (optional) An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. + * @param int|null $maxItems (optional) An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword + * @param bool|null $uniqueItems (optional) If it has boolean value true, the instance validates successfully if all of its elements are unique * * @throws \InvalidArgumentException when invalid arguments passed * @@ -239,8 +248,12 @@ final class OpenApiDataMocker implements IMocker $minSize = 0; $maxSize = \PHP_INT_MAX; - if (is_array($items) === false || array_key_exists('type', $items) === false) { - throw new InvalidArgumentException('"items" must be assoc array with "type" key'); + if ( + (is_array($items) === false && is_object($items) === false) + || (is_array($items) && array_key_exists('type', $items) === false) + || (is_object($items) && isset($items->type) === false) + ) { + new InvalidArgumentException('"items" must be object or assoc array with "type" key'); } if ($minItems !== null) { @@ -260,9 +273,9 @@ final class OpenApiDataMocker implements IMocker $maxSize = $maxItems; } - $dataType = $items['type']; - $dataFormat = $items['format'] ?? null; $options = $this->extractSchemaProperties($items); + $dataType = $options['type']; + $dataFormat = $options['format'] ?? null; // always genarate smallest possible array to avoid huge JSON responses $arrSize = ($maxSize < 1) ? $maxSize : max($minSize, 1); @@ -273,17 +286,104 @@ final class OpenApiDataMocker implements IMocker } /** - * @internal Extract OAS properties from array or object. + * Shortcut to mock object type. + * Equivalent to mockData(DATA_TYPE_OBJECT); * - * @param array $arr Processed array + * @param object|array $properties Object or array of described properties + * @param int|null $minProperties (optional) An object instance is valid against "minProperties" if its number of properties is greater than, or equal to, the value of this keyword. + * @param int|null $maxProperties (optional) An object instance is valid against "maxProperties" if its number of properties is less than, or equal to, the value of this keyword. + * @param bool|object|array|null $additionalProperties (optional) If "additionalProperties" is true, validation always succeeds. + * If "additionalProperties" is false, validation succeeds only if the instance is an object and all properties on the instance were covered by "properties" and/or "patternProperties". + * If "additionalProperties" is an object, validate the value as a schema to all of the properties that weren't validated by "properties" nor "patternProperties". + * @param array|null $required (optional) This array MUST have at least one element. Elements of this array must be strings, and MUST be unique. + * An object instance is valid if its property set contains all elements in this array value. + * + * @throws \InvalidArgumentException when invalid arguments passed + * + * @return object + */ + public function mockObject( + $properties, + $minProperties = 0, + $maxProperties = null, + $additionalProperties = null, + $required = null + ) { + $obj = new StdClass(); + + if (is_object($properties) === false && is_array($properties) === false) { + throw new InvalidArgumentException('The value of "properties" must be an array or object'); + } + + foreach ($properties as $propName => $propValue) { + if (is_object($propValue) === false && is_array($propValue) === false) { + throw new InvalidArgumentException('Each value of "properties" must be an array or object'); + } + } + + if ($minProperties !== null) { + if (is_integer($minProperties) === false || $minProperties < 0) { + throw new InvalidArgumentException('"minProperties" must be an integer. This integer must be greater than, or equal to, 0'); + } + } + + if ($maxProperties !== null) { + if (is_integer($maxProperties) === false || $maxProperties < 0) { + throw new InvalidArgumentException('"maxProperties" must be an integer. This integer must be greater than, or equal to, 0.'); + } + if ($maxProperties < $minProperties) { + throw new InvalidArgumentException('"maxProperties" value cannot be less than "minProperties"'); + } + } + + if ($additionalProperties !== null) { + if (is_bool($additionalProperties) === false && is_object($additionalProperties) === false && is_array($additionalProperties) === false) { + throw new InvalidArgumentException('The value of "additionalProperties" must be a boolean or object or array.'); + } + } + + if ($required !== null) { + if ( + is_array($required) === false + || count($required) > count(array_unique($required)) + ) { + throw new InvalidArgumentException('The value of "required" must be an array. Elements of this array must be unique.'); + } + foreach ($required as $requiredPropName) { + if (is_string($requiredPropName) === false) { + throw new InvalidArgumentException('Elements of "required" array must be strings'); + } + } + } + + foreach ($properties as $propName => $propValue) { + $options = $this->extractSchemaProperties($propValue); + $dataType = $options['type']; + $dataFormat = $options['dataFormat'] ?? null; + $obj->$propName = $this->mock($dataType, $dataFormat, $options); + } + + return $obj; + } + + /** + * @internal Extract OAS properties from array or object. + * @codeCoverageIgnore + * + * @param array|object $val Processed array or object * * @return array */ - private function extractSchemaProperties($arr) + private function extractSchemaProperties($val) { - $props = []; + $props = [ + 'type' => null, + 'format' => null, + ]; foreach ( [ + 'type', + 'format', 'minimum', 'maximum', 'exclusiveMinimum', @@ -304,8 +404,10 @@ final class OpenApiDataMocker implements IMocker 'example', ] as $propName ) { - if (array_key_exists($propName, $arr)) { - $props[$propName] = $arr[$propName]; + if (is_array($val) && array_key_exists($propName, $val)) { + $props[$propName] = $val[$propName]; + } elseif (is_object($val) && isset($val->$propName)) { + $props[$propName] = $val->$propName; } } return $props; @@ -313,6 +415,7 @@ final class OpenApiDataMocker implements IMocker /** * @internal + * @codeCoverageIgnore * * @return float|int */ diff --git a/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker_interface.mustache b/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker_interface.mustache index e94d55f1afd..cbed4323a22 100644 --- a/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker_interface.mustache +++ b/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker_interface.mustache @@ -63,6 +63,9 @@ interface {{interfaceNamePrefix}}OpenApiDataMocker{{interfaceNameSuffix}} /** @var string DATA_TYPE_ARRAY */ public const DATA_TYPE_ARRAY = 'array'; + /** @var string DATA_TYPE_OBJECT */ + public const DATA_TYPE_OBJECT = 'object'; + /** @var string DATA_FORMAT_INT32 Signed 32 bits */ public const DATA_FORMAT_INT32 = 'int32'; @@ -194,10 +197,10 @@ interface {{interfaceNamePrefix}}OpenApiDataMocker{{interfaceNameSuffix}} * Shortcut to mock array type * Equivalent to mockData(DATA_TYPE_ARRAY); * - * @param array $items Array of described items - * @param int|null $minItems (optional) An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. - * @param int|null $maxItems (optional) An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword - * @param bool|null $uniqueItems (optional) If it has boolean value true, the instance validates successfully if all of its elements are unique + * @param object|array $items Object or assoc array of described items + * @param int|null $minItems (optional) An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. + * @param int|null $maxItems (optional) An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword + * @param bool|null $uniqueItems (optional) If it has boolean value true, the instance validates successfully if all of its elements are unique * * @throws \InvalidArgumentException when invalid arguments passed * @@ -209,5 +212,30 @@ interface {{interfaceNamePrefix}}OpenApiDataMocker{{interfaceNameSuffix}} $maxItems = null, $uniqueItems = false ); + + /** + * Shortcut to mock object type. + * Equivalent to mockData(DATA_TYPE_OBJECT); + * + * @param object|array $properties Object or array of described properties + * @param int|null $minProperties (optional) An object instance is valid against "minProperties" if its number of properties is greater than, or equal to, the value of this keyword. + * @param int|null $maxProperties (optional) An object instance is valid against "maxProperties" if its number of properties is less than, or equal to, the value of this keyword. + * @param bool|object|array|null $additionalProperties (optional) If "additionalProperties" is true, validation always succeeds. + * If "additionalProperties" is false, validation succeeds only if the instance is an object and all properties on the instance were covered by "properties" and/or "patternProperties". + * If "additionalProperties" is an object, validate the value as a schema to all of the properties that weren't validated by "properties" nor "patternProperties". + * @param array|null $required (optional) This array MUST have at least one element. Elements of this array must be strings, and MUST be unique. + * An object instance is valid if its property set contains all elements in this array value. + * + * @throws \InvalidArgumentException when invalid arguments passed + * + * @return object + */ + public function mockObject( + $properties, + $minProperties = 0, + $maxProperties = null, + $additionalProperties = null, + $required = null + ); } {{/apiInfo}} diff --git a/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker_test.mustache b/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker_test.mustache index eef16fffd93..53a700bed73 100644 --- a/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker_test.mustache +++ b/modules/openapi-generator/src/main/resources/php-slim4-server/openapi_data_mocker_test.mustache @@ -37,6 +37,7 @@ use {{mockPackage}}\OpenApiDataMocker; use {{mockPackage}}\{{interfaceNamePrefix}}OpenApiDataMocker{{interfaceNameSuffix}} as IMocker; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Constraint\IsType; +use StdClass; /** * OpenApiDataMockerTest Class Doc Comment @@ -55,7 +56,8 @@ class OpenApiDataMockerTest extends TestCase public function testMockCorrectArguments($dataType, $dataFormat, $options, $expectedType) { $mocker = new OpenApiDataMocker(); - $this->assertInternalType($expectedType, $mocker->mock($dataType)); + $data = $mocker->mock($dataType, $dataFormat, $options); + $this->assertInternalType($expectedType, $data); } public function provideMockCorrectArguments() @@ -65,6 +67,39 @@ class OpenApiDataMockerTest extends TestCase [IMocker::DATA_TYPE_NUMBER, null, null, IsType::TYPE_FLOAT], [IMocker::DATA_TYPE_STRING, null, null, IsType::TYPE_STRING], [IMocker::DATA_TYPE_BOOLEAN, null, null, IsType::TYPE_BOOL], + [IMocker::DATA_TYPE_ARRAY, null, [ + 'items' => [ + 'type' => IMocker::DATA_TYPE_INTEGER, + ], + ], IsType::TYPE_ARRAY], + [IMocker::DATA_TYPE_OBJECT, null, [ + 'properties' => [ + 'username' => [ + 'type' => IMocker::DATA_TYPE_INTEGER, + ], + ], + ], IsType::TYPE_OBJECT], + ]; + } + + /** + * @covers ::mock + * @dataProvider provideMockInvalidArguments + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage "dataType" must be one of integer, number, string, boolean, array, object + */ + public function testMockInvalidArguments($dataType, $dataFormat, $options) + { + $mocker = new OpenApiDataMocker(); + $data = $mocker->mock($dataType, $dataFormat, $options); + } + + public function provideMockInvalidArguments() + { + return [ + ['foobar', null, null], + [3.14, null, null], + [null, null, null], ]; } @@ -440,28 +475,54 @@ class OpenApiDataMockerTest extends TestCase $this->assertContainsOnly($expectedItemsType, $arr, true); } - $dataFormat = $items['dataFormat'] ?? null; + if (is_array($items)) { + $dataType = $items['type']; + $dataFormat = $items['dataFormat'] ?? null; - // items field numeric properties - $minimum = $items['minimum'] ?? null; - $maximum = $items['maximum'] ?? null; - $exclusiveMinimum = $items['exclusiveMinimum'] ?? null; - $exclusiveMaximum = $items['exclusiveMaximum'] ?? null; + // items field numeric properties + $minimum = $items['minimum'] ?? null; + $maximum = $items['maximum'] ?? null; + $exclusiveMinimum = $items['exclusiveMinimum'] ?? null; + $exclusiveMaximum = $items['exclusiveMaximum'] ?? null; - // items field string properties - $minLength = $items['minLength'] ?? null; - $maxLength = $items['maxLength'] ?? null; - $enum = $items['enum'] ?? null; - $pattern = $items['pattern'] ?? null; + // items field string properties + $minLength = $items['minLength'] ?? null; + $maxLength = $items['maxLength'] ?? null; + $enum = $items['enum'] ?? null; + $pattern = $items['pattern'] ?? null; - // items field array properties - $subItems = $items['items'] ?? null; - $subMinItems = $items['minItems'] ?? null; - $subMaxItems = $items['maxItems'] ?? null; - $subUniqueItems = $items['uniqueItems'] ?? null; + // items field array properties + $subItems = $items['items'] ?? null; + $subMinItems = $items['minItems'] ?? null; + $subMaxItems = $items['maxItems'] ?? null; + $subUniqueItems = $items['uniqueItems'] ?? null; + } else { + // is object + $dataType = $items->type; + $dataFormat = $items->dataFormat ?? null; + + // items field numeric properties + $minimum = $items->minimum ?? null; + $maximum = $items->maximum ?? null; + $exclusiveMinimum = $items->exclusiveMinimum ?? null; + $exclusiveMaximum = $items->exclusiveMaximum ?? null; + + // items field string properties + $minLength = $items->minLength ?? null; + $maxLength = $items->maxLength ?? null; + $enum = $items->enum ?? null; + $pattern = $items->pattern ?? null; + + // items field array properties + $subItems = $items->items ?? null; + $subMinItems = $items->minItems ?? null; + $subMaxItems = $items->maxItems ?? null; + $subUniqueItems = $items->uniqueItems ?? null; + } + foreach ($arr as $item) { - switch ($items['type']) { + switch ($dataType) { case IMocker::DATA_TYPE_INTEGER: $this->internalAssertNumber($item, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); break; @@ -486,13 +547,15 @@ class OpenApiDataMockerTest extends TestCase $intItems = ['type' => IMocker::DATA_TYPE_INTEGER, 'minimum' => 5, 'maximum' => 10]; $floatItems = ['type' => IMocker::DATA_TYPE_NUMBER, 'minimum' => -32.4, 'maximum' => 88.6, 'exclusiveMinimum' => true, 'exclusiveMaximum' => true]; $strItems = ['type' => IMocker::DATA_TYPE_STRING, 'minLength' => 20, 'maxLength' => 50]; - $boolItems = ['type' => IMocker::DATA_TYPE_BOOLEAN]; - $arrayItems = ['type' => IMocker::DATA_TYPE_ARRAY, 'items' => ['type' => IMocker::DATA_TYPE_STRING, 'minItems' => 3, 'maxItems' => 10]]; + $boolItems = (object) ['type' => IMocker::DATA_TYPE_BOOLEAN]; + $arrayItems = (object) ['type' => IMocker::DATA_TYPE_ARRAY, 'items' => ['type' => IMocker::DATA_TYPE_STRING, 'minItems' => 3, 'maxItems' => 10]]; + $objectItems = (object) ['type' => IMocker::DATA_TYPE_OBJECT, 'properties' => (object)['username' => ['type' => IMocker::DATA_TYPE_STRING]]]; $expectedInt = IsType::TYPE_INT; $expectedFloat = IsType::TYPE_FLOAT; $expectedStr = IsType::TYPE_STRING; $expectedBool = IsType::TYPE_BOOL; $expectedArray = IsType::TYPE_ARRAY; + $expectedObject = IsType::TYPE_OBJECT; return [ 'empty array' => [ @@ -531,6 +594,9 @@ class OpenApiDataMockerTest extends TestCase 'array of one array of strings' => [ $arrayItems, null, null, false, $expectedArray, 1, ], + 'array of one object' => [ + $objectItems, null, null, false, $expectedObject, 1 + ], ]; } @@ -583,5 +649,125 @@ class OpenApiDataMockerTest extends TestCase ], ]; } + + /** + * @dataProvider provideMockObjectCorrectArguments + * @covers ::mockObject + */ + public function testMockObjectWithCorrectArguments( + $properties, + $minProperties, + $maxProperties, + $additionalProperties, + $required, + $expectedKeys + ) { + $mocker = new OpenApiDataMocker(); + $obj = $mocker->mockObject( + $properties, + $minProperties, + $maxProperties, + $additionalProperties, + $required + ); + + $this->assertInternalType(IsType::TYPE_OBJECT, $obj); + $this->assertSame($expectedKeys, array_keys(get_object_vars($obj))); + } + + public function provideMockObjectCorrectArguments() + { + $additionProps = [ + 'extra' => [ + 'type' => IMocker::DATA_TYPE_STRING, + ], + ]; + return [ + 'empty object' => [ + [], 1, 10, true, null, [], + ], + 'empty object from StdClass' => [ + new StdClass(), 1, 5, false, null, [], + ], + 'object with username property' => [ + [ + 'username' => [ + 'type' => IMocker::DATA_TYPE_STRING, + ], + ], 0, 5, $additionProps, null, ['username'], + ], + 'object with foobar property' => [ + (object) [ + 'foobar' => [ + 'type' => IMocker::DATA_TYPE_INTEGER, + ], + ], 1, 1, (object) $additionProps, null, ['foobar'], + ], + ]; + } + + /** + * @dataProvider provideMockObjectInvalidArguments + * @expectedException \InvalidArgumentException + * @covers ::mockObject + */ + public function testMockObjectWithInvalidArguments( + $properties, + $minProperties, + $maxProperties, + $additionalProperties, + $required + ) { + $mocker = new OpenApiDataMocker(); + $obj = $mocker->mockObject($properties, $minProperties, $maxProperties, $additionalProperties, $required); + } + + public function provideMockObjectInvalidArguments() + { + return [ + 'properties cannot be null' => [ + null, 0, 10, false, null, + ], + 'properties cannot be a string' => [ + 'foobar', 0, 10, false, null, + ], + 'minProperties is not integer' => [ + [], 3.12, null, false, null, + ], + 'minProperties is negative' => [ + [], -10, null, false, null, + ], + 'minProperties is not number' => [ + [], '1', null, false, null, + ], + 'maxProperties is not integer' => [ + [], null, 3.12, false, null, + ], + 'maxProperties is negative' => [ + [], null, -10, false, null, + ], + 'maxProperties is not number' => [ + [], null, 'foobaz', false, null, + ], + 'maxProperties less than minProperties' => [ + [], 5, 2, false, null, + ], + 'additionalProperties is not object|array|boolean' => [ + [], null, null, 'foobar', null, + ], + 'required is object, not array' => [ + [], null, null, null, new StdClass(), + ], + 'required is not array' => [ + [], null, null, null, 'foobar', + ], + 'required array with duplicates' => [ + [], null, null, null, ['username', 'username'], + ], + 'required array of non-strings' => [ + [], null, null, null, [1, 2, 3], + ], + ]; + } } {{/apiInfo}} diff --git a/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMocker.php b/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMocker.php index fe0b10f2db7..a780debbdb8 100644 --- a/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMocker.php +++ b/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMocker.php @@ -26,6 +26,7 @@ namespace OpenAPIServer\Mock; use OpenAPIServer\Mock\OpenApiDataMockerInterface as IMocker; +use StdClass; use InvalidArgumentException; /** @@ -74,6 +75,13 @@ final class OpenApiDataMocker implements IMocker $maxItems = $options['maxItems'] ?? null; $uniqueItems = $options['uniqueItems'] ?? false; return $this->mockArray($items, $minItems, $maxItems, $uniqueItems); + case IMocker::DATA_TYPE_OBJECT: + $properties = $options['properties'] ?? null; + $minProperties = $options['minProperties'] ?? 0; + $maxProperties = $options['maxProperties'] ?? null; + $additionalProperties = $options['additionalProperties'] ?? null; + $required = $options['required'] ?? null; + return $this->mockObject($properties, $minProperties, $maxProperties, $additionalProperties, $required); default: throw new InvalidArgumentException('"dataType" must be one of ' . implode(', ', [ IMocker::DATA_TYPE_INTEGER, @@ -81,6 +89,7 @@ final class OpenApiDataMocker implements IMocker IMocker::DATA_TYPE_STRING, IMocker::DATA_TYPE_BOOLEAN, IMocker::DATA_TYPE_ARRAY, + IMocker::DATA_TYPE_OBJECT, ])); } } @@ -212,10 +221,10 @@ final class OpenApiDataMocker implements IMocker * Shortcut to mock array type * Equivalent to mockData(DATA_TYPE_ARRAY); * - * @param array $items Array of described items - * @param int|null $minItems (optional) An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. - * @param int|null $maxItems (optional) An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword - * @param bool|null $uniqueItems (optional) If it has boolean value true, the instance validates successfully if all of its elements are unique + * @param object|array $items Object or assoc array of described items + * @param int|null $minItems (optional) An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. + * @param int|null $maxItems (optional) An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword + * @param bool|null $uniqueItems (optional) If it has boolean value true, the instance validates successfully if all of its elements are unique * * @throws \InvalidArgumentException when invalid arguments passed * @@ -231,8 +240,12 @@ final class OpenApiDataMocker implements IMocker $minSize = 0; $maxSize = \PHP_INT_MAX; - if (is_array($items) === false || array_key_exists('type', $items) === false) { - throw new InvalidArgumentException('"items" must be assoc array with "type" key'); + if ( + (is_array($items) === false && is_object($items) === false) + || (is_array($items) && array_key_exists('type', $items) === false) + || (is_object($items) && isset($items->type) === false) + ) { + new InvalidArgumentException('"items" must be object or assoc array with "type" key'); } if ($minItems !== null) { @@ -252,9 +265,9 @@ final class OpenApiDataMocker implements IMocker $maxSize = $maxItems; } - $dataType = $items['type']; - $dataFormat = $items['format'] ?? null; $options = $this->extractSchemaProperties($items); + $dataType = $options['type']; + $dataFormat = $options['format'] ?? null; // always genarate smallest possible array to avoid huge JSON responses $arrSize = ($maxSize < 1) ? $maxSize : max($minSize, 1); @@ -265,17 +278,104 @@ final class OpenApiDataMocker implements IMocker } /** - * @internal Extract OAS properties from array or object. + * Shortcut to mock object type. + * Equivalent to mockData(DATA_TYPE_OBJECT); * - * @param array $arr Processed array + * @param object|array $properties Object or array of described properties + * @param int|null $minProperties (optional) An object instance is valid against "minProperties" if its number of properties is greater than, or equal to, the value of this keyword. + * @param int|null $maxProperties (optional) An object instance is valid against "maxProperties" if its number of properties is less than, or equal to, the value of this keyword. + * @param bool|object|array|null $additionalProperties (optional) If "additionalProperties" is true, validation always succeeds. + * If "additionalProperties" is false, validation succeeds only if the instance is an object and all properties on the instance were covered by "properties" and/or "patternProperties". + * If "additionalProperties" is an object, validate the value as a schema to all of the properties that weren't validated by "properties" nor "patternProperties". + * @param array|null $required (optional) This array MUST have at least one element. Elements of this array must be strings, and MUST be unique. + * An object instance is valid if its property set contains all elements in this array value. + * + * @throws \InvalidArgumentException when invalid arguments passed + * + * @return object + */ + public function mockObject( + $properties, + $minProperties = 0, + $maxProperties = null, + $additionalProperties = null, + $required = null + ) { + $obj = new StdClass(); + + if (is_object($properties) === false && is_array($properties) === false) { + throw new InvalidArgumentException('The value of "properties" must be an array or object'); + } + + foreach ($properties as $propName => $propValue) { + if (is_object($propValue) === false && is_array($propValue) === false) { + throw new InvalidArgumentException('Each value of "properties" must be an array or object'); + } + } + + if ($minProperties !== null) { + if (is_integer($minProperties) === false || $minProperties < 0) { + throw new InvalidArgumentException('"minProperties" must be an integer. This integer must be greater than, or equal to, 0'); + } + } + + if ($maxProperties !== null) { + if (is_integer($maxProperties) === false || $maxProperties < 0) { + throw new InvalidArgumentException('"maxProperties" must be an integer. This integer must be greater than, or equal to, 0.'); + } + if ($maxProperties < $minProperties) { + throw new InvalidArgumentException('"maxProperties" value cannot be less than "minProperties"'); + } + } + + if ($additionalProperties !== null) { + if (is_bool($additionalProperties) === false && is_object($additionalProperties) === false && is_array($additionalProperties) === false) { + throw new InvalidArgumentException('The value of "additionalProperties" must be a boolean or object or array.'); + } + } + + if ($required !== null) { + if ( + is_array($required) === false + || count($required) > count(array_unique($required)) + ) { + throw new InvalidArgumentException('The value of "required" must be an array. Elements of this array must be unique.'); + } + foreach ($required as $requiredPropName) { + if (is_string($requiredPropName) === false) { + throw new InvalidArgumentException('Elements of "required" array must be strings'); + } + } + } + + foreach ($properties as $propName => $propValue) { + $options = $this->extractSchemaProperties($propValue); + $dataType = $options['type']; + $dataFormat = $options['dataFormat'] ?? null; + $obj->$propName = $this->mock($dataType, $dataFormat, $options); + } + + return $obj; + } + + /** + * @internal Extract OAS properties from array or object. + * @codeCoverageIgnore + * + * @param array|object $val Processed array or object * * @return array */ - private function extractSchemaProperties($arr) + private function extractSchemaProperties($val) { - $props = []; + $props = [ + 'type' => null, + 'format' => null, + ]; foreach ( [ + 'type', + 'format', 'minimum', 'maximum', 'exclusiveMinimum', @@ -296,8 +396,10 @@ final class OpenApiDataMocker implements IMocker 'example', ] as $propName ) { - if (array_key_exists($propName, $arr)) { - $props[$propName] = $arr[$propName]; + if (is_array($val) && array_key_exists($propName, $val)) { + $props[$propName] = $val[$propName]; + } elseif (is_object($val) && isset($val->$propName)) { + $props[$propName] = $val->$propName; } } return $props; @@ -305,6 +407,7 @@ final class OpenApiDataMocker implements IMocker /** * @internal + * @codeCoverageIgnore * * @return float|int */ diff --git a/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMockerInterface.php b/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMockerInterface.php index fc6e28986d7..78fd9a859a5 100644 --- a/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMockerInterface.php +++ b/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMockerInterface.php @@ -55,6 +55,9 @@ interface OpenApiDataMockerInterface /** @var string DATA_TYPE_ARRAY */ public const DATA_TYPE_ARRAY = 'array'; + /** @var string DATA_TYPE_OBJECT */ + public const DATA_TYPE_OBJECT = 'object'; + /** @var string DATA_FORMAT_INT32 Signed 32 bits */ public const DATA_FORMAT_INT32 = 'int32'; @@ -186,10 +189,10 @@ interface OpenApiDataMockerInterface * Shortcut to mock array type * Equivalent to mockData(DATA_TYPE_ARRAY); * - * @param array $items Array of described items - * @param int|null $minItems (optional) An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. - * @param int|null $maxItems (optional) An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword - * @param bool|null $uniqueItems (optional) If it has boolean value true, the instance validates successfully if all of its elements are unique + * @param object|array $items Object or assoc array of described items + * @param int|null $minItems (optional) An array instance is valid against "minItems" if its size is greater than, or equal to, the value of this keyword. + * @param int|null $maxItems (optional) An array instance is valid against "maxItems" if its size is less than, or equal to, the value of this keyword + * @param bool|null $uniqueItems (optional) If it has boolean value true, the instance validates successfully if all of its elements are unique * * @throws \InvalidArgumentException when invalid arguments passed * @@ -201,4 +204,29 @@ interface OpenApiDataMockerInterface $maxItems = null, $uniqueItems = false ); + + /** + * Shortcut to mock object type. + * Equivalent to mockData(DATA_TYPE_OBJECT); + * + * @param object|array $properties Object or array of described properties + * @param int|null $minProperties (optional) An object instance is valid against "minProperties" if its number of properties is greater than, or equal to, the value of this keyword. + * @param int|null $maxProperties (optional) An object instance is valid against "maxProperties" if its number of properties is less than, or equal to, the value of this keyword. + * @param bool|object|array|null $additionalProperties (optional) If "additionalProperties" is true, validation always succeeds. + * If "additionalProperties" is false, validation succeeds only if the instance is an object and all properties on the instance were covered by "properties" and/or "patternProperties". + * If "additionalProperties" is an object, validate the value as a schema to all of the properties that weren't validated by "properties" nor "patternProperties". + * @param array|null $required (optional) This array MUST have at least one element. Elements of this array must be strings, and MUST be unique. + * An object instance is valid if its property set contains all elements in this array value. + * + * @throws \InvalidArgumentException when invalid arguments passed + * + * @return object + */ + public function mockObject( + $properties, + $minProperties = 0, + $maxProperties = null, + $additionalProperties = null, + $required = null + ); } diff --git a/samples/server/petstore/php-slim4/test/Mock/OpenApiDataMockerTest.php b/samples/server/petstore/php-slim4/test/Mock/OpenApiDataMockerTest.php index 221e6c6bf66..2b925cddeff 100644 --- a/samples/server/petstore/php-slim4/test/Mock/OpenApiDataMockerTest.php +++ b/samples/server/petstore/php-slim4/test/Mock/OpenApiDataMockerTest.php @@ -29,6 +29,7 @@ use OpenAPIServer\Mock\OpenApiDataMocker; use OpenAPIServer\Mock\OpenApiDataMockerInterface as IMocker; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Constraint\IsType; +use StdClass; /** * OpenApiDataMockerTest Class Doc Comment @@ -47,7 +48,8 @@ class OpenApiDataMockerTest extends TestCase public function testMockCorrectArguments($dataType, $dataFormat, $options, $expectedType) { $mocker = new OpenApiDataMocker(); - $this->assertInternalType($expectedType, $mocker->mock($dataType)); + $data = $mocker->mock($dataType, $dataFormat, $options); + $this->assertInternalType($expectedType, $data); } public function provideMockCorrectArguments() @@ -57,6 +59,39 @@ class OpenApiDataMockerTest extends TestCase [IMocker::DATA_TYPE_NUMBER, null, null, IsType::TYPE_FLOAT], [IMocker::DATA_TYPE_STRING, null, null, IsType::TYPE_STRING], [IMocker::DATA_TYPE_BOOLEAN, null, null, IsType::TYPE_BOOL], + [IMocker::DATA_TYPE_ARRAY, null, [ + 'items' => [ + 'type' => IMocker::DATA_TYPE_INTEGER, + ], + ], IsType::TYPE_ARRAY], + [IMocker::DATA_TYPE_OBJECT, null, [ + 'properties' => [ + 'username' => [ + 'type' => IMocker::DATA_TYPE_INTEGER, + ], + ], + ], IsType::TYPE_OBJECT], + ]; + } + + /** + * @covers ::mock + * @dataProvider provideMockInvalidArguments + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage "dataType" must be one of integer, number, string, boolean, array, object + */ + public function testMockInvalidArguments($dataType, $dataFormat, $options) + { + $mocker = new OpenApiDataMocker(); + $data = $mocker->mock($dataType, $dataFormat, $options); + } + + public function provideMockInvalidArguments() + { + return [ + ['foobar', null, null], + [3.14, null, null], + [null, null, null], ]; } @@ -432,28 +467,54 @@ class OpenApiDataMockerTest extends TestCase $this->assertContainsOnly($expectedItemsType, $arr, true); } - $dataFormat = $items['dataFormat'] ?? null; + if (is_array($items)) { + $dataType = $items['type']; + $dataFormat = $items['dataFormat'] ?? null; - // items field numeric properties - $minimum = $items['minimum'] ?? null; - $maximum = $items['maximum'] ?? null; - $exclusiveMinimum = $items['exclusiveMinimum'] ?? null; - $exclusiveMaximum = $items['exclusiveMaximum'] ?? null; + // items field numeric properties + $minimum = $items['minimum'] ?? null; + $maximum = $items['maximum'] ?? null; + $exclusiveMinimum = $items['exclusiveMinimum'] ?? null; + $exclusiveMaximum = $items['exclusiveMaximum'] ?? null; - // items field string properties - $minLength = $items['minLength'] ?? null; - $maxLength = $items['maxLength'] ?? null; - $enum = $items['enum'] ?? null; - $pattern = $items['pattern'] ?? null; + // items field string properties + $minLength = $items['minLength'] ?? null; + $maxLength = $items['maxLength'] ?? null; + $enum = $items['enum'] ?? null; + $pattern = $items['pattern'] ?? null; - // items field array properties - $subItems = $items['items'] ?? null; - $subMinItems = $items['minItems'] ?? null; - $subMaxItems = $items['maxItems'] ?? null; - $subUniqueItems = $items['uniqueItems'] ?? null; + // items field array properties + $subItems = $items['items'] ?? null; + $subMinItems = $items['minItems'] ?? null; + $subMaxItems = $items['maxItems'] ?? null; + $subUniqueItems = $items['uniqueItems'] ?? null; + } else { + // is object + $dataType = $items->type; + $dataFormat = $items->dataFormat ?? null; + + // items field numeric properties + $minimum = $items->minimum ?? null; + $maximum = $items->maximum ?? null; + $exclusiveMinimum = $items->exclusiveMinimum ?? null; + $exclusiveMaximum = $items->exclusiveMaximum ?? null; + + // items field string properties + $minLength = $items->minLength ?? null; + $maxLength = $items->maxLength ?? null; + $enum = $items->enum ?? null; + $pattern = $items->pattern ?? null; + + // items field array properties + $subItems = $items->items ?? null; + $subMinItems = $items->minItems ?? null; + $subMaxItems = $items->maxItems ?? null; + $subUniqueItems = $items->uniqueItems ?? null; + } + foreach ($arr as $item) { - switch ($items['type']) { + switch ($dataType) { case IMocker::DATA_TYPE_INTEGER: $this->internalAssertNumber($item, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); break; @@ -478,13 +539,15 @@ class OpenApiDataMockerTest extends TestCase $intItems = ['type' => IMocker::DATA_TYPE_INTEGER, 'minimum' => 5, 'maximum' => 10]; $floatItems = ['type' => IMocker::DATA_TYPE_NUMBER, 'minimum' => -32.4, 'maximum' => 88.6, 'exclusiveMinimum' => true, 'exclusiveMaximum' => true]; $strItems = ['type' => IMocker::DATA_TYPE_STRING, 'minLength' => 20, 'maxLength' => 50]; - $boolItems = ['type' => IMocker::DATA_TYPE_BOOLEAN]; - $arrayItems = ['type' => IMocker::DATA_TYPE_ARRAY, 'items' => ['type' => IMocker::DATA_TYPE_STRING, 'minItems' => 3, 'maxItems' => 10]]; + $boolItems = (object) ['type' => IMocker::DATA_TYPE_BOOLEAN]; + $arrayItems = (object) ['type' => IMocker::DATA_TYPE_ARRAY, 'items' => ['type' => IMocker::DATA_TYPE_STRING, 'minItems' => 3, 'maxItems' => 10]]; + $objectItems = (object) ['type' => IMocker::DATA_TYPE_OBJECT, 'properties' => (object)['username' => ['type' => IMocker::DATA_TYPE_STRING]]]; $expectedInt = IsType::TYPE_INT; $expectedFloat = IsType::TYPE_FLOAT; $expectedStr = IsType::TYPE_STRING; $expectedBool = IsType::TYPE_BOOL; $expectedArray = IsType::TYPE_ARRAY; + $expectedObject = IsType::TYPE_OBJECT; return [ 'empty array' => [ @@ -523,6 +586,9 @@ class OpenApiDataMockerTest extends TestCase 'array of one array of strings' => [ $arrayItems, null, null, false, $expectedArray, 1, ], + 'array of one object' => [ + $objectItems, null, null, false, $expectedObject, 1 + ], ]; } @@ -575,4 +641,124 @@ class OpenApiDataMockerTest extends TestCase ], ]; } + + /** + * @dataProvider provideMockObjectCorrectArguments + * @covers ::mockObject + */ + public function testMockObjectWithCorrectArguments( + $properties, + $minProperties, + $maxProperties, + $additionalProperties, + $required, + $expectedKeys + ) { + $mocker = new OpenApiDataMocker(); + $obj = $mocker->mockObject( + $properties, + $minProperties, + $maxProperties, + $additionalProperties, + $required + ); + + $this->assertInternalType(IsType::TYPE_OBJECT, $obj); + $this->assertSame($expectedKeys, array_keys(get_object_vars($obj))); + } + + public function provideMockObjectCorrectArguments() + { + $additionProps = [ + 'extra' => [ + 'type' => IMocker::DATA_TYPE_STRING, + ], + ]; + return [ + 'empty object' => [ + [], 1, 10, true, null, [], + ], + 'empty object from StdClass' => [ + new StdClass(), 1, 5, false, null, [], + ], + 'object with username property' => [ + [ + 'username' => [ + 'type' => IMocker::DATA_TYPE_STRING, + ], + ], 0, 5, $additionProps, null, ['username'], + ], + 'object with foobar property' => [ + (object) [ + 'foobar' => [ + 'type' => IMocker::DATA_TYPE_INTEGER, + ], + ], 1, 1, (object) $additionProps, null, ['foobar'], + ], + ]; + } + + /** + * @dataProvider provideMockObjectInvalidArguments + * @expectedException \InvalidArgumentException + * @covers ::mockObject + */ + public function testMockObjectWithInvalidArguments( + $properties, + $minProperties, + $maxProperties, + $additionalProperties, + $required + ) { + $mocker = new OpenApiDataMocker(); + $obj = $mocker->mockObject($properties, $minProperties, $maxProperties, $additionalProperties, $required); + } + + public function provideMockObjectInvalidArguments() + { + return [ + 'properties cannot be null' => [ + null, 0, 10, false, null, + ], + 'properties cannot be a string' => [ + 'foobar', 0, 10, false, null, + ], + 'minProperties is not integer' => [ + [], 3.12, null, false, null, + ], + 'minProperties is negative' => [ + [], -10, null, false, null, + ], + 'minProperties is not number' => [ + [], '1', null, false, null, + ], + 'maxProperties is not integer' => [ + [], null, 3.12, false, null, + ], + 'maxProperties is negative' => [ + [], null, -10, false, null, + ], + 'maxProperties is not number' => [ + [], null, 'foobaz', false, null, + ], + 'maxProperties less than minProperties' => [ + [], 5, 2, false, null, + ], + 'additionalProperties is not object|array|boolean' => [ + [], null, null, 'foobar', null, + ], + 'required is object, not array' => [ + [], null, null, null, new StdClass(), + ], + 'required is not array' => [ + [], null, null, null, 'foobar', + ], + 'required array with duplicates' => [ + [], null, null, null, ['username', 'username'], + ], + 'required array of non-strings' => [ + [], null, null, null, [1, 2, 3], + ], + ]; + } }