Skip to content

Commit

Permalink
Enum cleanup (#1381)
Browse files Browse the repository at this point in the history
Simplifies enum expansion rules and adds documentation for it.
  • Loading branch information
DerManoMann authored Jan 13, 2023
1 parent 6fea22f commit ee81477
Show file tree
Hide file tree
Showing 44 changed files with 518 additions and 399 deletions.
2 changes: 1 addition & 1 deletion Examples/using-refs/using-refs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ components:
stockLevel:
$ref: '#/components/schemas/StockLevel'
StockLevel:
type: integer
type: string
enum:
- AVAILABLE
- SOLD_OUT
Expand Down
179 changes: 178 additions & 1 deletion docs/guide/common-techniques.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ relevant source code as appropriate.
`swagger-php` will scan your project and merge all meta-data into one` @OA\OpenApi` annotation.

::: warning
As of `swagger-php` v4 all annotations or attributes must be associated with
As of `swagger-php` v4 all annotations or attributes must be associated with
a structural element (`class`, `method`, `parameter` or `enum`)
:::

Expand Down Expand Up @@ -188,3 +188,180 @@ info:
- version: "2.1"
level: fullapi
```

## Enums

[PHP 8.1 enums](https://www.php.net/manual/en/language.enumerations.basics.php) are supported in two main use cases.

### Enum cases as value

Enum cases can be used as value in an `enum` list just like a `string`, `integer` or any other primitive type.

**Basic enum:**

```php
use OpenApi\Attributes as OAT;
enum Suit
{
case Hearts;
case Diamonds;
case Clubs;
case Spades;
}
class Model
{
#[OAT\Property(enum: [Suit::Hearts, Suit::Diamonds])]
protected array $someSuits;
}
```

**Results in:**

```yaml
openapi: 3.0.0
components:
schemas:
Model:
properties:
someSuits:
type: array
enum:
- Hearts
- Diamonds
type: object
```

**Backed enums**

If the enum is a backed enum, the case backing value is used instead of the name.

## Enum schemas

The simples way of using enums is to annotate them as `Schema`. This allows you to reference them like any other schema in your spec.

```php
use OpenApi\Attributes as OAT;
#[OAT\Schema()]
enum Colour
{
case GREEN;
case BLUE;
case RED;
}
#[OAT\Schema()]
class Product
{
#[OAT\Property()]
public Colour $colour;
}
```

**Results in:**

```yaml
openapi: 3.0.0
components:
schemas:
Colour:
type: string
enum:
- GREEN
- BLUE
- RED
Product:
properties:
colour:
$ref: '#/components/schemas/Colour'
type: object
```

**Backed enums**

For backed enums there exist two rules that determine whether the name or backing value is used:
1. If no schema type is given, the enum name is used.
2. If a schema type is given, and it matches the backing type, the enum backing value is used.

**Using the name of a backed enum:**

```php
use OpenApi\Attributes as OAT;
#[OAT\Schema()]
enum Colour: int
{
case GREEN = 1;
case BLUE = 2;
case RED = 3;
}
#[OAT\Schema()]
class Product
{
#[OAT\Property()]
public Colour $colour;
}
```

**Results in:**

```yaml
openapi: 3.0.0
components:
schemas:
Colour:
type: string
enum:
- GREEN
- BLUE
- RED
Product:
properties:
colour:
$ref: '#/components/schemas/Colour'
type: object
```

**Using the backing value:**

```php
use OpenApi\Attributes as OAT;
#[OAT\Schema(type: 'integer')]
enum Colour: int
{
case GREEN = 1;
case BLUE = 2;
case RED = 3;
}
#[OAT\Schema()]
class Product
{
#[OAT\Property()]
public Colour $colour;
}
```

**Results in:**

```yaml
openapi: 3.0.0
components:
schemas:
Colour:
type: integer
enum:
- 1
- 2
- 3
Product:
properties:
colour:
$ref: '#/components/schemas/Colour'
type: object
```
6 changes: 3 additions & 3 deletions docs/reference/processors.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ Look at all (direct) traits for a schema and:
- inherit from the trait if it has a schema (allOf).
## [ExpandEnums](https://github.com/zircote/swagger-php/tree/master/src/Processors/ExpandEnums.php)

Look at all enums with a schema and:
- set the name `schema`
- set `enum` values.
Expands PHP enums.

Determines `schema`, `enum` and `type`.
## [AugmentSchemas](https://github.com/zircote/swagger-php/tree/master/src/Processors/AugmentSchemas.php)

Use the Schema context to extract useful information and inject that into the annotation.
Expand Down
7 changes: 7 additions & 0 deletions src/Processors/Concerns/TypesTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,11 @@ public function mapNativeType(OA\Schema $schema, string $type): bool

return true;
}

public function native2spec(string $type): string
{
$mapped = array_key_exists($type, self::$NATIVE_TYPE_MAP) ? self::$NATIVE_TYPE_MAP[$type] : $type;

return is_array($mapped) ? $mapped[0] : $mapped;
}
}
52 changes: 25 additions & 27 deletions src/Processors/ExpandEnums.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@
use OpenApi\Generator;

/**
* Look at all enums with a schema and:
* - set the name `schema`
* - set `enum` values.
* Expands PHP enums.
*
* Determines `schema`, `enum` and `type`.
*/
class ExpandEnums
{
Expand All @@ -30,39 +30,37 @@ public function __invoke(Analysis $analysis)
$this->expandSchemaEnum($analysis);
}

private function expandContextEnum(Analysis $analysis): void
protected function expandContextEnum(Analysis $analysis): void
{
/** @var OA\Schema[] $schemas */
$schemas = $analysis->getAnnotationsOfType([OA\Schema::class, OAT\Schema::class], true);

foreach ($schemas as $schema) {
if ($schema->_context->is('enum')) {
$source = $schema->_context->enum;
$re = new \ReflectionEnum($schema->_context->fullyQualifiedName($source));
$re = new \ReflectionEnum($schema->_context->fullyQualifiedName($schema->_context->enum));
$schema->schema = !Generator::isDefault($schema->schema) ? $schema->schema : $re->getShortName();
$type = 'string';
$schemaType = 'string';
if ($re->isBacked() && ($backingType = $re->getBackingType()) && method_exists($backingType, 'getName')) {
if (Generator::isDefault($schema->type)) {
$type = $backingType->getName();
} else {
$type = $schema->type;
$schemaType = $schema->type;
}

$schemaType = $schema->type;
$enumType = null;
if ($re->isBacked() && ($backingType = $re->getBackingType()) && $backingType instanceof \ReflectionNamedType) {
$enumType = $backingType->getName();
}
$schema->enum = array_map(function ($case) use ($re, $schemaType, $type) {
if ($re->isBacked() && $type === $schemaType) {
return $case->getBackingValue();
}

return $case->name;
// no (or invalid) schema type means name
$useName = Generator::isDefault($schemaType) || ($enumType && $this->native2spec($enumType) != $schemaType);

$schema->enum = array_map(function ($case) use ($useName) {
return $useName ? $case->name : $case->getBackingValue();
}, $re->getCases());
$this->mapNativeType($schema, $type);

$schema->type = $useName ? 'string' : $enumType;

$this->mapNativeType($schema, $schemaType);
}
}
}

private function expandSchemaEnum(Analysis $analysis): void
protected function expandSchemaEnum(Analysis $analysis): void
{
/** @var OA\Schema[] $schemas */
$schemas = $analysis->getAnnotationsOfType([
Expand All @@ -78,21 +76,21 @@ private function expandSchemaEnum(Analysis $analysis): void
}

if (is_string($schema->enum)) {
// might be enum class
// might be enum class-string
if (is_a($schema->enum, \UnitEnum::class, true)) {
$source = $schema->enum::cases();
$cases = $schema->enum::cases();
} else {
throw new \InvalidArgumentException("Unexpected enum value, requires specifying the Enum class string: $schema->enum");
}
} elseif (is_array($schema->enum)) {
// might be array of enum, string, int, etc...
$source = $schema->enum;
// might be an array of \UnitEnum::class, string, int, etc...
$cases = $schema->enum;
} else {
throw new \InvalidArgumentException('Unexpected enum value, requires Enum class string or array');
}

$enums = [];
foreach ($source as $enum) {
foreach ($cases as $enum) {
if (is_a($enum, \UnitEnum::class)) {
$enums[] = $enum->value ?? $enum->name;
} else {
Expand Down
Loading

0 comments on commit ee81477

Please sign in to comment.