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 c2e6883b9696..80d6cd2e9db8 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 @@ -76,12 +76,19 @@ final class OpenApiDataMocker implements IMocker return $this->mockString($dataFormat, $minLength, $maxLength); case IMocker::DATA_TYPE_BOOLEAN: return $this->mockBoolean(); + case IMocker::DATA_TYPE_ARRAY: + $items = $options['items'] ?? null; + $minItems = $options['minItems'] ?? 0; + $maxItems = $options['maxItems'] ?? null; + $uniqueItems = $options['uniqueItems'] ?? false; + return $this->mockArray($items, $minItems, $maxItems, $uniqueItems); default: throw new InvalidArgumentException('"dataType" must be one of ' . implode(', ', [ IMocker::DATA_TYPE_INTEGER, IMocker::DATA_TYPE_NUMBER, IMocker::DATA_TYPE_STRING, IMocker::DATA_TYPE_BOOLEAN, + IMocker::DATA_TYPE_ARRAY, ])); } } @@ -209,6 +216,101 @@ final class OpenApiDataMocker implements IMocker return (bool) mt_rand(0, 1); } + /** + * 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 + * + * @throws \InvalidArgumentException when invalid arguments passed + * + * @return array + */ + public function mockArray( + $items, + $minItems = 0, + $maxItems = null, + $uniqueItems = false + ) { + $arr = []; + $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 ($minItems !== null) { + if (is_integer($minItems) === false || $minItems < 0) { + throw new InvalidArgumentException('"mitItems" must be an integer. This integer must be greater than, or equal to, 0'); + } + $minSize = $minItems; + } + + if ($maxItems !== null) { + if (is_integer($maxItems) === false || $maxItems < 0) { + throw new InvalidArgumentException('"maxItems" must be an integer. This integer must be greater than, or equal to, 0.'); + } + if ($maxItems < $minItems) { + throw new InvalidArgumentException('"maxItems" value cannot be less than "minItems"'); + } + $maxSize = $maxItems; + } + + $dataType = $items['type']; + $dataFormat = $items['format'] ?? null; + $options = $this->extractSchemaProperties($items); + + // always genarate smallest possible array to avoid huge JSON responses + $arrSize = ($maxSize < 1) ? $maxSize : max($minSize, 1); + while (count($arr) < $arrSize) { + $arr[] = $this->mock($dataType, $dataFormat, $options); + } + return $arr; + } + + /** + * @internal Extract OAS properties from array or object. + * + * @param array $arr Processed array + * + * @return array + */ + private function extractSchemaProperties($arr) + { + $props = []; + foreach ( + [ + 'minimum', + 'maximum', + 'exclusiveMinimum', + 'exclusiveMaximum', + 'minLength', + 'maxLength', + 'pattern', + 'enum', + 'items', + 'minItems', + 'maxItems', + 'uniqueItems', + 'properties', + 'minProperties', + 'maxProperties', + 'additionalProperties', + 'required', + 'example', + ] as $propName + ) { + if (array_key_exists($propName, $arr)) { + $props[$propName] = $arr[$propName]; + } + } + return $props; + } + /** * @internal * 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 399b48fe1a5c..e94d55f1afd9 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 @@ -60,6 +60,9 @@ interface {{interfaceNamePrefix}}OpenApiDataMocker{{interfaceNameSuffix}} /** @var string DATA_TYPE_FILE */ public const DATA_TYPE_FILE = 'file'; + /** @var string DATA_TYPE_ARRAY */ + public const DATA_TYPE_ARRAY = 'array'; + /** @var string DATA_FORMAT_INT32 Signed 32 bits */ public const DATA_FORMAT_INT32 = 'int32'; @@ -186,5 +189,25 @@ interface {{interfaceNamePrefix}}OpenApiDataMocker{{interfaceNameSuffix}} * @return bool */ public function mockBoolean(); + + /** + * 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 + * + * @throws \InvalidArgumentException when invalid arguments passed + * + * @return array + */ + public function mockArray( + $items, + $minItems = 0, + $maxItems = null, + $uniqueItems = false + ); } {{/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 1ad9bc9601b7..eef16fffd935 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 @@ -416,5 +416,172 @@ class OpenApiDataMockerTest extends TestCase $this->assertContains($str, $enum); } } + + /** + * @dataProvider provideMockArrayCorrectArguments + * @covers ::mockArray + */ + public function testMockArrayFlattenWithCorrectArguments( + $items, + $minItems, + $maxItems, + $uniqueItems, + $expectedItemsType = null, + $expectedArraySize = null + ) { + $mocker = new OpenApiDataMocker(); + $arr = $mocker->mockArray($items, $minItems, $maxItems, $uniqueItems); + + $this->assertIsArray($arr); + if ($expectedArraySize !== null) { + $this->assertCount($expectedArraySize, $arr); + } + if ($expectedItemsType && $expectedArraySize > 0) { + $this->assertContainsOnly($expectedItemsType, $arr, true); + } + + $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']) { + case IMocker::DATA_TYPE_INTEGER: + $this->internalAssertNumber($item, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + break; + case IMocker::DATA_TYPE_NUMBER: + $this->internalAssertNumber($item, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + break; + case IMocker::DATA_TYPE_STRING: + $this->internalAssertString($item, $minLength, $maxLength); + break; + case IMocker::DATA_TYPE_BOOLEAN: + $this->assertInternalType(IsType::TYPE_BOOL, $item); + break; + case IMocker::DATA_TYPE_ARRAY: + $this->testMockArrayFlattenWithCorrectArguments($subItems, $subMinItems, $subMaxItems, $subUniqueItems); + break; + } + } + } + + public function provideMockArrayCorrectArguments() + { + $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]]; + $expectedInt = IsType::TYPE_INT; + $expectedFloat = IsType::TYPE_FLOAT; + $expectedStr = IsType::TYPE_STRING; + $expectedBool = IsType::TYPE_BOOL; + $expectedArray = IsType::TYPE_ARRAY; + + return [ + 'empty array' => [ + $strItems, null, 0, false, null, 0, + ], + 'empty array, limit zero' => [ + $strItems, 0, 0, false, null, 0, + ], + 'array of one string as default size' => [ + $strItems, null, null, false, $expectedStr, 1, + ], + 'array of one string, limit one' => [ + $strItems, 1, 1, false, $expectedStr, 1, + ], + 'array of two strings' => [ + $strItems, 2, null, false, $expectedStr, 2, + ], + 'array of five strings, limit ten' => [ + $strItems, 5, 10, false, $expectedStr, 5, + ], + 'array of five strings, limit five' => [ + $strItems, 5, 5, false, $expectedStr, 5, + ], + 'array of one string, limit five' => [ + $strItems, null, 5, false, $expectedStr, 1, + ], + 'array of one integer' => [ + $intItems, null, null, false, $expectedInt, 1, + ], + 'array of one float' => [ + $floatItems, null, null, false, $expectedFloat, 1, + ], + 'array of one boolean' => [ + $boolItems, null, null, false, $expectedBool, 1, + ], + 'array of one array of strings' => [ + $arrayItems, null, null, false, $expectedArray, 1, + ], + ]; + } + + /** + * @dataProvider provideMockArrayInvalidArguments + * @expectedException \InvalidArgumentException + * @covers ::mockArray + */ + public function testMockArrayWithInvalidArguments( + $items, + $minItems, + $maxItems, + $uniqueItems + ) { + $mocker = new OpenApiDataMocker(); + $arr = $mocker->mockArray($items, $minItems, $maxItems, $uniqueItems); + } + + public function provideMockArrayInvalidArguments() + { + $intItems = ['type' => IMocker::DATA_TYPE_INTEGER]; + + return [ + 'items is nor array' => [ + 'foobar', null, null, false, + ], + 'items doesnt have "type" key' => [ + ['foobar' => 'foobaz'], null, null, false, + ], + 'minItems is not integer' => [ + $intItems, 3.12, null, false, + ], + 'minItems is negative' => [ + $intItems, -10, null, false, + ], + 'minItems is not number' => [ + $intItems, '1', null, false, + ], + 'maxItems is not integer' => [ + $intItems, null, 3.12, false, + ], + 'maxItems is negative' => [ + $intItems, null, -10, false, + ], + 'maxItems is not number' => [ + $intItems, null, 'foobaz', false, + ], + 'maxItems less than minItems' => [ + $intItems, 5, 2, false, + ], + ]; + } } {{/apiInfo}} diff --git a/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMocker.php b/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMocker.php index 190a08e1d650..fe0b10f2db70 100644 --- a/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMocker.php +++ b/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMocker.php @@ -68,12 +68,19 @@ final class OpenApiDataMocker implements IMocker return $this->mockString($dataFormat, $minLength, $maxLength); case IMocker::DATA_TYPE_BOOLEAN: return $this->mockBoolean(); + case IMocker::DATA_TYPE_ARRAY: + $items = $options['items'] ?? null; + $minItems = $options['minItems'] ?? 0; + $maxItems = $options['maxItems'] ?? null; + $uniqueItems = $options['uniqueItems'] ?? false; + return $this->mockArray($items, $minItems, $maxItems, $uniqueItems); default: throw new InvalidArgumentException('"dataType" must be one of ' . implode(', ', [ IMocker::DATA_TYPE_INTEGER, IMocker::DATA_TYPE_NUMBER, IMocker::DATA_TYPE_STRING, IMocker::DATA_TYPE_BOOLEAN, + IMocker::DATA_TYPE_ARRAY, ])); } } @@ -201,6 +208,101 @@ final class OpenApiDataMocker implements IMocker return (bool) mt_rand(0, 1); } + /** + * 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 + * + * @throws \InvalidArgumentException when invalid arguments passed + * + * @return array + */ + public function mockArray( + $items, + $minItems = 0, + $maxItems = null, + $uniqueItems = false + ) { + $arr = []; + $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 ($minItems !== null) { + if (is_integer($minItems) === false || $minItems < 0) { + throw new InvalidArgumentException('"mitItems" must be an integer. This integer must be greater than, or equal to, 0'); + } + $minSize = $minItems; + } + + if ($maxItems !== null) { + if (is_integer($maxItems) === false || $maxItems < 0) { + throw new InvalidArgumentException('"maxItems" must be an integer. This integer must be greater than, or equal to, 0.'); + } + if ($maxItems < $minItems) { + throw new InvalidArgumentException('"maxItems" value cannot be less than "minItems"'); + } + $maxSize = $maxItems; + } + + $dataType = $items['type']; + $dataFormat = $items['format'] ?? null; + $options = $this->extractSchemaProperties($items); + + // always genarate smallest possible array to avoid huge JSON responses + $arrSize = ($maxSize < 1) ? $maxSize : max($minSize, 1); + while (count($arr) < $arrSize) { + $arr[] = $this->mock($dataType, $dataFormat, $options); + } + return $arr; + } + + /** + * @internal Extract OAS properties from array or object. + * + * @param array $arr Processed array + * + * @return array + */ + private function extractSchemaProperties($arr) + { + $props = []; + foreach ( + [ + 'minimum', + 'maximum', + 'exclusiveMinimum', + 'exclusiveMaximum', + 'minLength', + 'maxLength', + 'pattern', + 'enum', + 'items', + 'minItems', + 'maxItems', + 'uniqueItems', + 'properties', + 'minProperties', + 'maxProperties', + 'additionalProperties', + 'required', + 'example', + ] as $propName + ) { + if (array_key_exists($propName, $arr)) { + $props[$propName] = $arr[$propName]; + } + } + return $props; + } + /** * @internal * diff --git a/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMockerInterface.php b/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMockerInterface.php index b9329ead1dce..fc6e28986d72 100644 --- a/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMockerInterface.php +++ b/samples/server/petstore/php-slim4/lib/Mock/OpenApiDataMockerInterface.php @@ -52,6 +52,9 @@ interface OpenApiDataMockerInterface /** @var string DATA_TYPE_FILE */ public const DATA_TYPE_FILE = 'file'; + /** @var string DATA_TYPE_ARRAY */ + public const DATA_TYPE_ARRAY = 'array'; + /** @var string DATA_FORMAT_INT32 Signed 32 bits */ public const DATA_FORMAT_INT32 = 'int32'; @@ -178,4 +181,24 @@ interface OpenApiDataMockerInterface * @return bool */ public function mockBoolean(); + + /** + * 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 + * + * @throws \InvalidArgumentException when invalid arguments passed + * + * @return array + */ + public function mockArray( + $items, + $minItems = 0, + $maxItems = null, + $uniqueItems = false + ); } diff --git a/samples/server/petstore/php-slim4/test/Mock/OpenApiDataMockerTest.php b/samples/server/petstore/php-slim4/test/Mock/OpenApiDataMockerTest.php index 2681522cb2aa..221e6c6bf662 100644 --- a/samples/server/petstore/php-slim4/test/Mock/OpenApiDataMockerTest.php +++ b/samples/server/petstore/php-slim4/test/Mock/OpenApiDataMockerTest.php @@ -408,4 +408,171 @@ class OpenApiDataMockerTest extends TestCase $this->assertContains($str, $enum); } } + + /** + * @dataProvider provideMockArrayCorrectArguments + * @covers ::mockArray + */ + public function testMockArrayFlattenWithCorrectArguments( + $items, + $minItems, + $maxItems, + $uniqueItems, + $expectedItemsType = null, + $expectedArraySize = null + ) { + $mocker = new OpenApiDataMocker(); + $arr = $mocker->mockArray($items, $minItems, $maxItems, $uniqueItems); + + $this->assertIsArray($arr); + if ($expectedArraySize !== null) { + $this->assertCount($expectedArraySize, $arr); + } + if ($expectedItemsType && $expectedArraySize > 0) { + $this->assertContainsOnly($expectedItemsType, $arr, true); + } + + $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']) { + case IMocker::DATA_TYPE_INTEGER: + $this->internalAssertNumber($item, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + break; + case IMocker::DATA_TYPE_NUMBER: + $this->internalAssertNumber($item, $minimum, $maximum, $exclusiveMinimum, $exclusiveMaximum); + break; + case IMocker::DATA_TYPE_STRING: + $this->internalAssertString($item, $minLength, $maxLength); + break; + case IMocker::DATA_TYPE_BOOLEAN: + $this->assertInternalType(IsType::TYPE_BOOL, $item); + break; + case IMocker::DATA_TYPE_ARRAY: + $this->testMockArrayFlattenWithCorrectArguments($subItems, $subMinItems, $subMaxItems, $subUniqueItems); + break; + } + } + } + + public function provideMockArrayCorrectArguments() + { + $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]]; + $expectedInt = IsType::TYPE_INT; + $expectedFloat = IsType::TYPE_FLOAT; + $expectedStr = IsType::TYPE_STRING; + $expectedBool = IsType::TYPE_BOOL; + $expectedArray = IsType::TYPE_ARRAY; + + return [ + 'empty array' => [ + $strItems, null, 0, false, null, 0, + ], + 'empty array, limit zero' => [ + $strItems, 0, 0, false, null, 0, + ], + 'array of one string as default size' => [ + $strItems, null, null, false, $expectedStr, 1, + ], + 'array of one string, limit one' => [ + $strItems, 1, 1, false, $expectedStr, 1, + ], + 'array of two strings' => [ + $strItems, 2, null, false, $expectedStr, 2, + ], + 'array of five strings, limit ten' => [ + $strItems, 5, 10, false, $expectedStr, 5, + ], + 'array of five strings, limit five' => [ + $strItems, 5, 5, false, $expectedStr, 5, + ], + 'array of one string, limit five' => [ + $strItems, null, 5, false, $expectedStr, 1, + ], + 'array of one integer' => [ + $intItems, null, null, false, $expectedInt, 1, + ], + 'array of one float' => [ + $floatItems, null, null, false, $expectedFloat, 1, + ], + 'array of one boolean' => [ + $boolItems, null, null, false, $expectedBool, 1, + ], + 'array of one array of strings' => [ + $arrayItems, null, null, false, $expectedArray, 1, + ], + ]; + } + + /** + * @dataProvider provideMockArrayInvalidArguments + * @expectedException \InvalidArgumentException + * @covers ::mockArray + */ + public function testMockArrayWithInvalidArguments( + $items, + $minItems, + $maxItems, + $uniqueItems + ) { + $mocker = new OpenApiDataMocker(); + $arr = $mocker->mockArray($items, $minItems, $maxItems, $uniqueItems); + } + + public function provideMockArrayInvalidArguments() + { + $intItems = ['type' => IMocker::DATA_TYPE_INTEGER]; + + return [ + 'items is nor array' => [ + 'foobar', null, null, false, + ], + 'items doesnt have "type" key' => [ + ['foobar' => 'foobaz'], null, null, false, + ], + 'minItems is not integer' => [ + $intItems, 3.12, null, false, + ], + 'minItems is negative' => [ + $intItems, -10, null, false, + ], + 'minItems is not number' => [ + $intItems, '1', null, false, + ], + 'maxItems is not integer' => [ + $intItems, null, 3.12, false, + ], + 'maxItems is negative' => [ + $intItems, null, -10, false, + ], + 'maxItems is not number' => [ + $intItems, null, 'foobaz', false, + ], + 'maxItems less than minItems' => [ + $intItems, 5, 2, false, + ], + ]; + } }