diff --git a/Annotations/AnnotationGenerator.php b/Annotations/AnnotationGenerator.php index ee04cc3..b3103f8 100644 --- a/Annotations/AnnotationGenerator.php +++ b/Annotations/AnnotationGenerator.php @@ -99,6 +99,11 @@ class AnnotationGenerator */ protected $allowLocalRequests; + /** + * @var array|null + */ + protected $parameterExamples; + public function __construct( DocumentationGenerator $generator, ?PathResolver $pathResolver = null, @@ -110,6 +115,7 @@ public function __construct( $this->artifactWriter = $artifactWriter ?? new ArtifactWriter(); $this->missingImportantDataWarnings = []; $this->allowLocalRequests = $allowLocalRequests; + $this->parameterExamples = null; $this->currentPluginDir = Manager::getInstance()::getPluginDirectory('OpenApiDocs'); } @@ -455,7 +461,7 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa // Sometimes, doc-block can wrap type hinting with parenthesis. Remove them. $type = trim($type, '()'); // If the signature type is array, but the type hinting provides more, use that instead - if ($type === 'array' && strpos($docType, '[]') !== false && strpos($docType, '|') === false) { + if ($type === 'array' && $this->hasSpecificArrayShape($docType) && strpos($docType, '|') === false) { $type = $docType; } $typesMap = []; @@ -468,6 +474,7 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa $typeHints = array_diff($typeHints, ['bool']); } + $isRequired = !key_exists('default', $paramMetadata) || $paramMetadata['default'] instanceof NoDefaultValue; $allTypeHintsAreStringLiterals = $this->areAllTypeHintsStringLiterals($typeHints); $enumValues = []; if ($allTypeHintsAreStringLiterals) { @@ -478,17 +485,13 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa } else { foreach ($typeHints as $typePart) { $typePart = trim($typePart, ' ()'); - $normalisedType = $this->getOpenApiTypeFromPhpType($typePart); + $normalisedType = $this->hasSpecificArrayShape($typePart) ? 'array' : $this->getOpenApiTypeFromPhpType($typePart); // If the type is array, check if there's a subType - $subType = null; - if ($normalisedType === 'array' && $typePart !== 'array' && strpos($typePart, '[]') !== false) { - $subType = substr($typePart, 0, strpos($typePart, '[]')); - } + $subType = $this->getArraySubTypeFromPhpType($typePart, $normalisedType); $typesMap[$normalisedType] = $subType !== null ? $this->getOpenApiTypeFromPhpType($subType) : $subType; } } - $isRequired = !key_exists('default', $paramMetadata) || $paramMetadata['default'] instanceof NoDefaultValue; $description = $paramDocInfo['description'] ?? ''; if (empty($description)) { $this->addMissingImportantDataWarning($methodName, $paramName, 'Description is not specified in comment block.'); @@ -506,11 +509,20 @@ public function buildParameterAnnotationData(string $methodName, string $paramNa $example = trim($example, '"'); } + if ($isRequired && $example === '') { + $configExample = $this->getParameterExampleFromConfig($paramName, $type, $typesMap); + if ($configExample !== null) { + $example = $configExample; + } + } + // Clean up the descriptions a little more like removing linebreaks and escaping double-quotes $description = $this->normaliseDescriptionText($description); $default = $paramMetadata['default'] ?? null; - if (!is_string($default)) { + if ($default === null) { + $default = NoDefaultValue::class; + } elseif (!is_string($default)) { $default = json_encode($default); } @@ -555,6 +567,138 @@ protected function areAllTypeHintsStringLiterals(array $typeHints): bool return true; } + protected function hasSpecificArrayShape(string $type): bool + { + return strpos($type, '[]') !== false || preg_match('/^(array|list)<.+>$/', trim($type)) === 1; + } + + protected function getArraySubTypeFromPhpType(string $typePart, string $normalisedType): ?string + { + if ($normalisedType !== 'array' || $typePart === 'array') { + return null; + } + + if (strpos($typePart, '[]') !== false) { + return substr($typePart, 0, strpos($typePart, '[]')); + } + + if (preg_match('/^(array|list)<(.+)>$/', trim($typePart), $matches) !== 1) { + return null; + } + + $genericParts = array_map('trim', explode(',', $matches[2], 2)); + if (count($genericParts) === 1) { + return $genericParts[0]; + } + + return $genericParts[1]; + } + + /** + * Load and return the configured parameter examples. + * + * @return array + */ + protected function getParameterExamplesConfig(): array + { + if ($this->parameterExamples !== null) { + return $this->parameterExamples; + } + + $configPath = $this->currentPluginDir . '/config/ParameterExamples.php'; + if (!is_file($configPath)) { + $this->parameterExamples = []; + return $this->parameterExamples; + } + + $config = require $configPath; + $this->parameterExamples = is_array($config) ? $config : []; + + return $this->parameterExamples; + } + + /** + * Return a config-backed example string if the configured example is intentionally simple enough to support. + */ + protected function getParameterExampleFromConfig(string $paramName, string $type, array $typesMap): ?string + { + $config = $this->getParameterExamplesConfig(); + $keysToTry = [ + $paramName . ':' . $type, + $paramName . ':' . preg_replace('/\s+/', '', $type), + ]; + + $configValue = null; + $foundConfigValue = false; + foreach (array_unique($keysToTry) as $key) { + if (!array_key_exists($key, $config)) { + continue; + } + + $configValue = $config[$key]; + $foundConfigValue = true; + break; + } + + if (!$foundConfigValue) { + return null; + } + + return $this->normaliseConfiguredParameterExample($configValue, $typesMap); + } + + /** + * Convert supported scalar/basic-array config values into the string form used by schema generation. + */ + protected function normaliseConfiguredParameterExample($example, array $typesMap = []): ?string + { + if (is_bool($example)) { + return $example ? 'true' : 'false'; + } + + if (is_int($example) || is_float($example) || is_string($example)) { + return strval($example); + } + + if ( + !is_array($example) + || !$this->isBasicExampleArray($example) + || !$this->supportsBasicArrayExample($typesMap) + ) { + return null; + } + + $encoded = json_encode(array_values($example)); + + return is_string($encoded) ? $encoded : null; + } + + /** + * Only use array config examples when the emitted schema includes an array shape. + */ + protected function supportsBasicArrayExample(array $typesMap): bool + { + return array_key_exists('array', $typesMap); + } + + /** + * Only support flat indexed arrays of scalar values for now. + */ + protected function isBasicExampleArray(array $example): bool + { + if (array_values($example) !== $example) { + return false; + } + + foreach ($example as $item) { + if (!is_bool($item) && !is_int($item) && !is_float($item) && !is_string($item)) { + return false; + } + } + + return true; + } + /** * Take description text and normalise it. This includes trimming surrounding whitespace, removing newlines and * escaping double-quote characters. @@ -1863,7 +2007,7 @@ public function buildSchemaObjectArray(string $type, string $subType = '', strin */ public function wrapStringWithQuotes(string $string, string $type, string $quoteCharacter = '"'): string { - if (in_array($type, ['integer', 'boolean', 'array'])) { + if (in_array($type, ['integer', 'number', 'boolean', 'array'])) { return $string; } @@ -1972,12 +2116,17 @@ public function compileOperationLines(string $path, string $opId, string $plugin $paramMap[] = 'description="' . $param['description'] . '"'; } $exampleString = $param['example']; - if (in_array('array', array_keys($param['types']))) { + $useParameterLevelExample = $this->shouldUseParameterLevelExample($param['types'], $exampleString); + if (in_array('array', array_keys($param['types'])) && !$useParameterLevelExample) { // The annotation expects example objects and not arrays, so replace [] with {} $exampleString = str_replace(['[', ']'], ['{', '}'], $exampleString); // Escape quotes differently for the annotation examples $exampleString = str_replace('\"', '""', $exampleString); } + if ($useParameterLevelExample) { + $paramMap[] = 'example="' . $this->normaliseDescriptionText($exampleString) . '"'; + $exampleString = ''; + } $paramMap[] = $this->buildSchemaObjectArrays( $param['types'], strval($param['default']), @@ -2019,4 +2168,18 @@ public function compileOperationLines(string $path, string $opId, string $plugin $this->removeTrailingCommaFromLastLine($lines); return $lines; } + + /** + * Use a parameter-level string example for scalar/array unions so Swagger UI can show a concrete query value. + * + * @param array $typesMap + */ + protected function shouldUseParameterLevelExample(array $typesMap, string $example): bool + { + if (count($typesMap) <= 1 || !array_key_exists('array', $typesMap) || $example === '') { + return false; + } + + return is_array(json_decode($example, true)); + } } diff --git a/config/ParameterExamples.php b/config/ParameterExamples.php new file mode 100644 index 0000000..2a4452f --- /dev/null +++ b/config/ParameterExamples.php @@ -0,0 +1,246 @@ + 1, + 'period:\'day\'|\'week\'|\'month\'|\'year\'|\'range\'' => 'day', + 'period:"day"|"week"|"month"|"year"|"range"' => 'day', + 'date:string' => 'yesterday', + 'idSite:int|string' => 1, + 'apiModule:string' => 'VisitsSummary', + 'apiAction:string' => 'get', + 'period:string' => 'day', + 'urls:string[]' => [ + 'https://example.org', + 'https://example.org/pricing', + ], + 'pluginName:string' => 'Goals', + 'segmentName:string' => 'New Zealand visitors', + 'idSite:int' => 1, + 'idExperiment:int' => 1, + 'successMetric:string' => 'conversion_rate', + 'name:string' => 'Pricing page signup test', + 'hypothesis:string' => 'A shorter signup flow will improve conversions.', + 'description:string' => 'Compare the pricing page signup experience for New Zealand traffic.', + 'variations:array>' => [ + [ + 'name' => 'Original pricing page', + 'percentage' => 50, + ], + [ + 'name' => 'Short signup form', + 'percentage' => 50, + ], + ], + 'includedTargets:array>' => [ + [ + 'attribute' => 'url', + 'type' => 'equals', + 'value' => 'https://example.org/pricing', + ], + ], + 'successMetrics:array>' => [ + [ + 'metric' => 'conversion_rate', + ], + ], + 'confidenceThreshold:int|float|string' => 95, + 'mdeRelative:int' => 10, + 'percentageParticipants:int' => 100, + 'statuses:string|array' => [ + 'running', + 'finished', + ], + 'pageUrl:string' => 'https://example.org/pricing', + 'pageName:string' => 'Pricing', + 'downloadUrl:string' => 'https://example.org/files/brochure.pdf', + 'outlinkUrl:string' => 'https://partner.example.com/signup', + 'idExport:int' => 7, + 'type:string' => 'html', + 'parameters:string' => 'module=API&method=VisitsSummary.get&idSite=1&period=day&date=2026-05-01&format=html', + 'note:string' => 'Investigate conversion spike from NZ campaign traffic.', + 'idNote:int' => 12, + 'idSite:string' => '1', + 'idSubtable:int' => 2, + 'displayDateRange:string' => '2026-05-01,2026-05-07', + 'cohorts:string' => '2026-05-01,2026-05-07', + 'idFailure:int|string' => 3, + 'idLogCrashes:int[]|int|string' => [101, 102], + 'idLogCrash:int' => 101, + 'idAlert:int' => 1, + 'subPeriodN:int|string' => 1, + 'idSites:string|array' => [ + 1, + 2, + ], + 'period:\'day\'|\'week\'|\'month\'' => 'day', + 'period:"day"|"week"|"month"' => 'day', + 'emailMe:bool' => true, + 'additionalEmails:list' => [ + 'alice@example.org', + ], + 'phoneNumbers:list' => [ + '+64211234567', + ], + 'metric:string' => 'nb_visits', + 'metricCondition:string' => 'nb_conversions', + 'metricValue:float|int|string' => 10, + 'comparedTo:int' => 1, + 'reportUniqueId:string' => 'VisitsSummary_get', + 'idDimension:int' => 1, + 'scope:\'visit\'|\'action\'' => 'action', + 'scope:"visit"|"action"' => 'action', + 'active:bool|int' => true, + 'idCustomReport:int' => 1, + 'reportType:string' => 'table', + 'metricIds:string[]' => [ + 'nb_visits', + 'nb_conversions', + 'conversion_rate', + ], + 'idSubtable:int|string|false' => 1, + 'login:string' => 'alice', + 'idDashboard:int' => 1, + 'copyToUser:string' => 'alice', + 'featureName:string' => 'FormAnalytics', + 'question:string' => 'Which pricing page sections get the most engagement?', + 'idForm:int' => 1, + 'lastMinutes:int' => 30, + 'stepPosition:int' => 1, + 'idFunnel:int' => 1, + 'idGoal:int|string' => 1, + 'isActivated:bool|int|string' => true, + 'funnelName:string' => 'Pricing Signup Funnel', + 'steps:array>' => [ + [ + 'name' => 'Pricing Page', + 'url' => 'https://example.org/pricing', + ], + [ + 'name' => 'Signup Page', + 'url' => 'https://partner.example.com/signup', + ], + ], + 'url:string' => 'https://example.org/pricing', + 'idGoal:int' => 1, + 'matchAttribute:string' => 'url', + 'pattern:string' => 'https://example.org/pricing', + 'patternType:string' => 'equals', + 'matchPageRules:array' => [ + [ + 'attribute' => 'url', + 'type' => 'equals', + 'value' => 'https://example.org/pricing', + ], + ], + 'idSiteHsr:int' => 1, + 'idLogHsr:int' => 101, + 'idVisit:int' => 1001, + 'heatmapType:int' => 1, + 'deviceType:int' => 1, + 'languageCode:string' => 'en', + 'use12HourClock:bool' => true, + 'idSite:int|int[]' => 1, + 'licenseKey:string' => 'hsr-demo-license', + 'provider:string' => 'google', + 'phoneNumber:string' => '+64211234567', + 'verificationCode:string' => '123456', + 'delegatedManagement:bool' => true, + 'isEnabled:bool|int|string' => true, + 'memberOf:string' => 'cn=matomo-users,ou=groups,dc=example,dc=org', + 'filter:string' => '(objectClass=person)', + 'initialIdSite:int' => 1, + 'clientId:string' => '0123456789abcdef0123456789abcdef', + 'grantTypes:string[]' => ['authorization_code'], + 'scope:string' => 'matomo:read', + 'active:string' => '1', + 'plugin:string' => 'HeatmapSessionRecording', + 'format:string' => 'html', + 'visits:array' => [ + [ + 'idVisit' => 12345, + 'idSite' => 1, + ], + ], + 'segment:string' => 'countryCode==NZ', + 'idSites:int|string|int[]' => [ + 1, + 2, + ], + 'sourceIdSites:array' => [ + 1, + 2, + ], + 'sourceIdSites:integer|(integer|array)' => [ + 1, + 2, + ], + 'sourceIdSites:integer | (integer | array)' => [ + 1, + 2, + ], + 'timezone:string' => 'Pacific/Auckland', + 'currency:string' => 'NZD', + 'period:\'day\'|\'week\'|\'month\'|\'never\'' => 'never', + 'period:"day"|"week"|"month"|"never"' => 'never', + 'hour:int' => 9, + 'reportFormat:string' => 'pdf', + 'reports:list' => [ + 'VisitsSummary.get', + 'Goals.get', + ], + 'parameters:array' => [ + 'module' => 'VisitsSummary', + 'action' => 'get', + ], + 'idReport:int' => 1, + 'idSite:int|null' => 1, + 'idSegment:int' => 1, + 'definition:string' => 'countryCode==NZ', + 'permission:string' => 'view', + 'siteName:string' => 'Example NZ Site', + 'urls:string[]|string' => [ + 'https://example.org', + 'https://example.org/pricing', + ], + 'ipRange:string' => '203.0.113.0/24', + 'excludedIps:string' => '203.0.113.10,203.0.113.11', + 'searchKeywordParameters:string' => 'q,query,keyword', + 'searchCategoryParameters:string' => 'category,cat', + 'excludedUserAgents:string' => 'HeadlessChrome,Googlebot', + 'excludedReferrers:string' => 'partner.example.com,internal.example.org', + 'enabled:bool' => true, + 'defaultCurrency:string' => 'NZD', + 'defaultTimezone:string' => 'Pacific/Auckland', + 'exclusionType:string' => 'ip', + 'oldGroupName:string' => 'Marketing Team', + 'newGroupName:string' => 'Growth Team', + 'idContext:string' => 'web', + 'idContainer:string' => 'GTMNZ123', + 'environment:string' => 'live', + 'idContainerVersion:int' => 3, + 'idTag:int' => 2, + 'idTrigger:int' => 3, + 'idVariable:int' => 4, + 'context:string' => 'web', + 'exportedContainerVersion:string' => 'live-v3', + 'id:string' => 'pricing-signup-goal', + 'pageTitle:string' => 'Pricing', + 'actionName:string' => 'Pricing Page', + 'actionType:\'url\'|\'title\'' => 'url', + 'actionType:"url"|"title"' => 'url', + 'userLogin:string' => 'alice', + 'providerId:string' => 'google', + 'interactionPosition:string' => 'header', + 'preferenceName:string' => 'reportFormat', + 'preferenceValue:mixed' => 'pdf', + 'access:string' => 'view', + 'userEmail:string' => 'alice@example.org', + 'password:string' => 'correct-horse-battery-staple', + 'email:string' => 'alice@example.org', + 'hasSuperUserAccess:bool|int|string' => true, + 'access:string|list' => ['view'], + 'idSites:string|int|int[]' => [1, 2], + 'capabilities:string|string[]' => ['read:reports'], + 'idSites:int|int[]|string' => [1, 2], + 'passwordConfirmation:string' => 'correct-horse-battery-staple', +]; diff --git a/tests/Resources/MockAnnotationGenerator.php b/tests/Resources/MockAnnotationGenerator.php index b56f7b1..23e4c6e 100644 --- a/tests/Resources/MockAnnotationGenerator.php +++ b/tests/Resources/MockAnnotationGenerator.php @@ -90,4 +90,24 @@ public function determineResponses(array $rules, string $plugin, string $method, { return parent::determineResponses($rules, $plugin, $method, $reflectionMethod, $paramsData); } + + public function normaliseConfiguredParameterExample($example, array $typesMap = []): ?string + { + return parent::normaliseConfiguredParameterExample($example, $typesMap); + } + + public function isBasicExampleArray(array $example): bool + { + return parent::isBasicExampleArray($example); + } + + public function supportsBasicArrayExample(array $typesMap): bool + { + return parent::supportsBasicArrayExample($typesMap); + } + + public function shouldUseParameterLevelExample(array $typesMap, string $example): bool + { + return parent::shouldUseParameterLevelExample($typesMap, $example); + } } diff --git a/tests/Unit/AnnotationGeneratorTest.php b/tests/Unit/AnnotationGeneratorTest.php index d91dffd..77c841a 100644 --- a/tests/Unit/AnnotationGeneratorTest.php +++ b/tests/Unit/AnnotationGeneratorTest.php @@ -544,6 +544,16 @@ public function getTestDataForBuildParameterAnnotationData(): iterable 'default' => 'false', 'example' => '', ]]; + yield 'should not include null as a default value' => ['someParam', [ + 'default' => null, + ], [], [ + 'name' => 'someParam', + 'types' => ['string' => null], + 'description' => '', + 'required' => 'false', + 'default' => 'Piwik\API\NoDefaultValue', + 'example' => '', + ]]; yield 'should still count empty string as a default value' => ['someParam', [ 'default' => '', ], [], [ @@ -688,7 +698,7 @@ public function getTestDataForBuildParameterAnnotationData(): iterable 'description' => '', 'required' => 'true', 'default' => 'Piwik\API\NoDefaultValue', - 'example' => '', + 'example' => 'day', 'enum' => ['day', 'week', 'month'], ]]; yield 'should extract enum values when docInfo uses double-quoted string literals' => ['format', [], [ @@ -724,6 +734,26 @@ public function getTestDataForBuildParameterAnnotationData(): iterable 'default' => 'Piwik\API\NoDefaultValue', 'example' => '', ]]; + yield 'should allow union with generic string array type' => ['statuses', [], [ + 'type' => 'string|array', + ], [ + 'name' => 'statuses', + 'types' => ['string' => null, 'array' => 'string'], + 'description' => '', + 'required' => 'true', + 'default' => 'Piwik\API\NoDefaultValue', + 'example' => '["running","finished"]', + ]]; + yield 'should determine subtype for list syntax' => ['someParam', [], [ + 'type' => 'list', + ], [ + 'name' => 'someParam', + 'types' => ['array' => 'string'], + 'description' => '', + 'required' => 'true', + 'default' => 'Piwik\API\NoDefaultValue', + 'example' => '', + ]]; yield 'should allow multiple types when metadata type is bool and doc type is piped' => ['someParam', [ 'type' => 'bool', ], [ @@ -821,6 +851,53 @@ public function getTestDataForBuildParameterAnnotationData(): iterable 'default' => 'Piwik\API\NoDefaultValue', 'example' => '[{"key1":"value1","key2":"value2"},{"key3":"value3","key4":"value4"}]', ]]; + yield 'should use config example when docblock example is absent' => ['idSite', [ + 'type' => 'string', + ], [ + 'type' => 'int|string', + ], [ + 'name' => 'idSite', + 'types' => ['integer' => null, 'string' => null], + 'description' => '', + 'required' => 'true', + 'default' => 'Piwik\API\NoDefaultValue', + 'example' => '1', + ]]; + yield 'should not use config example when parameter is optional' => ['statuses', [ + 'default' => [], + ], [ + 'type' => 'string|array', + ], [ + 'name' => 'statuses', + 'types' => ['string' => null, 'array' => 'string'], + 'description' => '', + 'required' => 'false', + 'default' => '[]', + 'example' => '', + ]]; + yield 'should prefer docblock example over config example' => ['idSite', [ + 'type' => 'string', + ], [ + 'type' => 'int|string', + 'description' => 'Some test description. [@example=99]', + ], [ + 'name' => 'idSite', + 'types' => ['integer' => null, 'string' => null], + 'description' => 'Some test description.', + 'required' => 'true', + 'default' => 'Piwik\API\NoDefaultValue', + 'example' => '99', + ]]; + yield 'should preserve integer and integer array union type' => ['idSites', [], [ + 'type' => 'int|int[]', + ], [ + 'name' => 'idSites', + 'types' => ['integer' => null, 'array' => 'integer'], + 'description' => '', + 'required' => 'true', + 'default' => 'Piwik\API\NoDefaultValue', + 'example' => '', + ]]; } public function testDetermineParameters(): void @@ -1220,6 +1297,28 @@ public function testBuildSchemaObjectArrayIgnoresEnumForNonStringTypes(): void $this->assertEquals($expectedWithoutEnum, $this->annotationGenerator->buildSchemaObjectArray('integer', '', NoDefaultValue::class, '1', ['1', '2'])); } + public function testNormaliseConfiguredParameterExampleSupportsOnlySimpleValues(): void + { + $annotationGenerator = new MockAnnotationGenerator(new DocumentationGenerator()); + + $this->assertSame('true', $annotationGenerator->normaliseConfiguredParameterExample(true)); + $this->assertSame('1.5', $annotationGenerator->normaliseConfiguredParameterExample(1.5)); + $this->assertSame('["one","two"]', $annotationGenerator->normaliseConfiguredParameterExample(['one', 'two'], ['array' => 'string'])); + $this->assertNull($annotationGenerator->normaliseConfiguredParameterExample(['one', 'two'], ['string' => null])); + $this->assertSame('["one","two"]', $annotationGenerator->normaliseConfiguredParameterExample(['one', 'two'], ['array' => 'string', 'string' => null])); + $this->assertNull($annotationGenerator->normaliseConfiguredParameterExample(['key' => 'value'], ['array' => 'string'])); + $this->assertNull($annotationGenerator->normaliseConfiguredParameterExample([['nested']], ['array' => 'string'])); + } + + public function testShouldUseParameterLevelExampleForScalarArrayUnions(): void + { + $annotationGenerator = new MockAnnotationGenerator(new DocumentationGenerator()); + + $this->assertTrue($annotationGenerator->shouldUseParameterLevelExample(['string' => null, 'array' => 'string'], '["one","two"]')); + $this->assertFalse($annotationGenerator->shouldUseParameterLevelExample(['array' => 'string'], '["one","two"]')); + $this->assertFalse($annotationGenerator->shouldUseParameterLevelExample(['string' => null, 'array' => 'string'], 'one')); + } + /** * @dataProvider getTestDataForWrapStringWithQuotes * @@ -1243,6 +1342,7 @@ public function getTestDataForWrapStringWithQuotes(): iterable yield 'should be empty quoted string if everything is empty' => ['', '', null, '""']; yield 'should be empty quoted string if string type and empty value' => ['', 'string', null, '""']; yield 'should be empty string if integer type and empty value' => ['', 'integer', null, '']; + yield 'should be empty string if number type and empty value' => ['', 'number', null, '']; yield 'should be empty string if boolean type and empty value' => ['', 'boolean', null, '']; yield 'should be empty string if array type and empty value' => ['', 'array', null, '']; yield 'should be empty quoted string if string type and quoted empty string value' => ['""', 'string', null, '""']; @@ -1253,6 +1353,7 @@ public function getTestDataForWrapStringWithQuotes(): iterable yield 'should be quoted string if no type and string value' => ['test', '', null, '"test"']; yield 'should be quoted string if string type and string value' => ['test', 'string', null, '"test"']; yield 'should be integer string if integer type and integer string value' => ['12', 'integer', null, '12']; + yield 'should be number string if number type and float string value' => ['1.5', 'number', null, '1.5']; yield 'should be boolean string if boolean type and boolean string value' => ['true', 'boolean', null, 'true']; yield 'should be array string if array type and array string value' => ['[]', 'array', null, '[]']; yield 'should be use the custom quote character when provided even when not quote' => ['test', 'string', '|', "|test|"];