From d8626c273522701a4c9270010d67104ad80a9f30 Mon Sep 17 00:00:00 2001
From: Christopher Davis <cdavis9999@gmail.com>
Date: Mon, 1 Feb 2021 08:37:20 -0600
Subject: [PATCH 1/4] Introduce a Trait to Build OpenAPI Discriminators

See https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/

This is the adapter layer that will be included in the various model
describers. The creation of the discriminator and the `oneOf` values is
a little finicky and I wanted it to be tested and centralized.
---
 .../ApplyOpenApiDiscriminatorTrait.php        | 60 +++++++++++++
 .../ApplyOpenApiDiscriminatorTraitTest.php    | 88 +++++++++++++++++++
 2 files changed, 148 insertions(+)
 create mode 100644 ModelDescriber/ApplyOpenApiDiscriminatorTrait.php
 create mode 100644 Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php

diff --git a/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php b/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php
new file mode 100644
index 0000000..dd551fd
--- /dev/null
+++ b/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php
@@ -0,0 +1,60 @@
+<?php
+
+/*
+ * This file is part of the NelmioApiDocBundle package.
+ *
+ * (c) Nelmio
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Nelmio\ApiDocBundle\ModelDescriber;
+
+use Nelmio\ApiDocBundle\Model\Model;
+use Nelmio\ApiDocBundle\Model\ModelRegistry;
+use OpenApi\Annotations as OA;
+use Symfony\Component\PropertyInfo\Type;
+
+/**
+ * Contains helper methods that add `discriminator` and `oneOf` values to
+ * Open API schemas to support poly morphism.
+ *
+ * @see https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/
+ * @internal
+ */
+trait ApplyOpenApiDiscriminatorTrait
+{
+    /**
+     * @param Model $model the model that's being described, This is used to pass groups and config
+     *        down to the children models in `oneOf`
+     * @param OA\Schema $schema The Open API schema to which `oneOf` and `discriminator` properties
+     *        will be added
+     * @param string $discriminatorProperty The property that determine which model will be unsierailized
+     * @param array<string, string> $typeMap the map of $discriminatorProperty values to their
+     *        types
+     */
+    protected function applyOpenApiDiscriminator(
+        Model $model,
+        OA\Schema $schema,
+        ModelRegistry $modelRegistry,
+        string $discriminatorProperty,
+        array $typeMap
+    ) : void {
+        $schema->oneOf = [];
+        $schema->discriminator = new OA\Discriminator([]);
+        $schema->discriminator->propertyName = $discriminatorProperty;
+        $schema->discriminator->mapping = [];
+        foreach ($typeMap as $propertyValue => $className) {
+            $oneOfSchema = new OA\Schema([]);
+            $oneOfSchema->ref = $modelRegistry->register(new Model(
+                new Type(Type::BUILTIN_TYPE_OBJECT, false, $className),
+                $model->getGroups(),
+                $model->getOptions()
+            ));
+            $schema->oneOf[] = $oneOfSchema;
+            $schema->discriminator->mapping[$propertyValue] = clone $oneOfSchema;
+
+        }
+    }
+}
diff --git a/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php b/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php
new file mode 100644
index 0000000..0644a8b
--- /dev/null
+++ b/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php
@@ -0,0 +1,88 @@
+<?php
+
+/*
+ * This file is part of the NelmioApiDocBundle package.
+ *
+ * (c) Nelmio
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Nelmio\ApiDocBundle\Tests\ModelDescriber;
+
+use Doctrine\Common\Annotations\AnnotationReader;
+use Nelmio\ApiDocBundle\Model\Model;
+use Nelmio\ApiDocBundle\Model\ModelRegistry;
+use Nelmio\ApiDocBundle\ModelDescriber\ApplyOpenApiDiscriminatorTrait;
+use OpenApi\Annotations as OA;
+use Symfony\Component\PropertyInfo\Type;
+use PHPUnit\Framework\TestCase;
+
+class ApplyOpenApiDiscriminatorTraitTest extends TestCase
+{
+    use ApplyOpenApiDiscriminatorTrait;
+
+    const GROUPS = ['test'];
+    const OPTIONS = ['test' => 123];
+
+    private $schema;
+
+    private $model;
+
+    public function testApplyAddsDiscriminatorProperty()
+    {
+        $this->applyOpenApiDiscriminator($this->model, $this->schema, $this->modelRegistry, 'type', [
+            'one' => 'FirstType',
+            'two' => 'SecondType',
+        ]);
+
+        $this->assertInstanceOf(OA\Discriminator::class, $this->schema->discriminator);
+        $this->assertSame('type', $this->schema->discriminator->propertyName);
+        $this->assertArrayHasKey('one', $this->schema->discriminator->mapping);
+        $this->assertSame(
+            $this->modelRegistry->register($this->createModel('FirstType')),
+            $this->schema->discriminator->mapping['one']->ref
+        );
+        $this->assertArrayHasKey('two', $this->schema->discriminator->mapping);
+        $this->assertSame(
+            $this->modelRegistry->register($this->createModel('SecondType')),
+            $this->schema->discriminator->mapping['two']->ref
+        );
+    }
+
+    public function testApplyAddsOneOfFieldToSchema()
+    {
+        $this->applyOpenApiDiscriminator($this->model, $this->schema, $this->modelRegistry, 'type', [
+            'one' => 'FirstType',
+            'two' => 'SecondType',
+        ]);
+
+        $this->assertNotSame(OA\UNDEFINED, $this->schema->oneOf);
+        $this->assertCount(2, $this->schema->oneOf);
+        $this->assertSame(
+            $this->modelRegistry->register($this->createModel('FirstType')),
+            $this->schema->oneOf[0]->ref
+        );
+        $this->assertSame(
+            $this->modelRegistry->register($this->createModel('SecondType')),
+            $this->schema->oneOf[1]->ref
+        );
+    }
+
+    protected function setUp() : void
+    {
+        $this->schema = new OA\Schema([]);
+        $this->model = $this->createModel(__CLASS__);
+        $this->modelRegistry = new ModelRegistry([], new OA\OpenApi([]));
+    }
+
+    private function createModel(string $className) : Model
+    {
+        return new Model(
+            new Type(Type::BUILTIN_TYPE_OBJECT, false, $className),
+            self::GROUPS,
+            self::OPTIONS
+        );
+    }
+}

From 9299c0e52ea6a45bcd94961dd2170234d142ea5f Mon Sep 17 00:00:00 2001
From: Christopher Davis <cdavis9999@gmail.com>
Date: Mon, 1 Feb 2021 08:56:31 -0600
Subject: [PATCH 2/4] Support OpenAPI Polymorphism in ObjectModelDescriber

This is the default "symfony support" class, so seems like the right
place.
---
 ModelDescriber/ObjectModelDescriber.php       | 13 +++++++++
 Tests/Functional/Controller/ApiController.php | 10 +++++++
 .../Entity/SymfonyDiscriminator.php           | 28 +++++++++++++++++++
 .../Entity/SymfonyDiscriminatorOne.php        | 20 +++++++++++++
 .../Entity/SymfonyDiscriminatorTwo.php        | 20 +++++++++++++
 Tests/Functional/FunctionalTest.php           | 20 +++++++++++++
 6 files changed, 111 insertions(+)
 create mode 100644 Tests/Functional/Entity/SymfonyDiscriminator.php
 create mode 100644 Tests/Functional/Entity/SymfonyDiscriminatorOne.php
 create mode 100644 Tests/Functional/Entity/SymfonyDiscriminatorTwo.php

diff --git a/ModelDescriber/ObjectModelDescriber.php b/ModelDescriber/ObjectModelDescriber.php
index fc86439..893ae96 100644
--- a/ModelDescriber/ObjectModelDescriber.php
+++ b/ModelDescriber/ObjectModelDescriber.php
@@ -22,11 +22,13 @@ use Nelmio\ApiDocBundle\PropertyDescriber\PropertyDescriberInterface;
 use OpenApi\Annotations as OA;
 use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
 use Symfony\Component\PropertyInfo\Type;
+use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
 use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
 
 class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwareInterface
 {
     use ModelRegistryAwareTrait;
+    use ApplyOpenApiDiscriminatorTrait;
 
     /** @var PropertyInfoExtractorInterface */
     private $propertyInfo;
@@ -71,6 +73,17 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
         $annotationsReader = new AnnotationsReader($this->doctrineReader, $this->modelRegistry, $this->mediaTypes);
         $annotationsReader->updateDefinition($reflClass, $schema);
 
+        $discriminatorMap = $this->doctrineReader->getClassAnnotation($reflClass, DiscriminatorMap::class);
+        if ($discriminatorMap && $schema->discriminator === OA\UNDEFINED) {
+            $this->applyOpenApiDiscriminator(
+                $model,
+                $schema,
+                $this->modelRegistry,
+                $discriminatorMap->getTypeProperty(),
+                $discriminatorMap->getMapping()
+            );
+        }
+
         $propertyInfoProperties = $this->propertyInfo->getProperties($class, $context);
 
         if (null === $propertyInfoProperties) {
diff --git a/Tests/Functional/Controller/ApiController.php b/Tests/Functional/Controller/ApiController.php
index 4988efd..554b1eb 100644
--- a/Tests/Functional/Controller/ApiController.php
+++ b/Tests/Functional/Controller/ApiController.php
@@ -18,6 +18,7 @@ use Nelmio\ApiDocBundle\Annotation\Security;
 use Nelmio\ApiDocBundle\Tests\Functional\Entity\Article;
 use Nelmio\ApiDocBundle\Tests\Functional\Entity\CompoundEntity;
 use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyConstraints;
+use Nelmio\ApiDocBundle\Tests\Functional\Entity\SymfonyDiscriminator;
 use Nelmio\ApiDocBundle\Tests\Functional\Entity\User;
 use Nelmio\ApiDocBundle\Tests\Functional\Form\DummyType;
 use Nelmio\ApiDocBundle\Tests\Functional\Form\UserType;
@@ -221,4 +222,13 @@ class ApiController
     public function compoundEntityAction()
     {
     }
+
+    /**
+     * @Route("/discriminator-mapping", methods={"GET", "POST"})
+     *
+     * @OA\Response(response=200, description="Worked well!", @Model(type=SymfonyDiscriminator::class))
+     */
+    public function discriminatorMappingAction()
+    {
+    }
 }
diff --git a/Tests/Functional/Entity/SymfonyDiscriminator.php b/Tests/Functional/Entity/SymfonyDiscriminator.php
new file mode 100644
index 0000000..cd4e562
--- /dev/null
+++ b/Tests/Functional/Entity/SymfonyDiscriminator.php
@@ -0,0 +1,28 @@
+<?php
+
+/*
+ * This file is part of the NelmioApiDocBundle package.
+ *
+ * (c) Nelmio
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
+
+use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
+
+/**
+ * @DiscriminatorMap(typeProperty="type", mapping={
+ *      "one": SymfonyDiscriminatorOne::class,
+ *      "two": SymfonyDiscriminatorTwo::class,
+ * })
+ */
+abstract class SymfonyDiscriminator
+{
+    /**
+     * @var string
+     */
+    public $type;
+}
diff --git a/Tests/Functional/Entity/SymfonyDiscriminatorOne.php b/Tests/Functional/Entity/SymfonyDiscriminatorOne.php
new file mode 100644
index 0000000..6b969ef
--- /dev/null
+++ b/Tests/Functional/Entity/SymfonyDiscriminatorOne.php
@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * This file is part of the NelmioApiDocBundle package.
+ *
+ * (c) Nelmio
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
+
+class SymfonyDiscriminatorOne extends SymfonyDiscriminator
+{
+    /**
+     * @var string
+     */
+    public $one;
+}
diff --git a/Tests/Functional/Entity/SymfonyDiscriminatorTwo.php b/Tests/Functional/Entity/SymfonyDiscriminatorTwo.php
new file mode 100644
index 0000000..9e6b9eb
--- /dev/null
+++ b/Tests/Functional/Entity/SymfonyDiscriminatorTwo.php
@@ -0,0 +1,20 @@
+<?php
+
+/*
+ * This file is part of the NelmioApiDocBundle package.
+ *
+ * (c) Nelmio
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Nelmio\ApiDocBundle\Tests\Functional\Entity;
+
+class SymfonyDiscriminatorTwo extends SymfonyDiscriminator
+{
+    /**
+     * @var string
+     */
+    public $two;
+}
diff --git a/Tests/Functional/FunctionalTest.php b/Tests/Functional/FunctionalTest.php
index 914a032..b7ba209 100644
--- a/Tests/Functional/FunctionalTest.php
+++ b/Tests/Functional/FunctionalTest.php
@@ -498,4 +498,24 @@ class FunctionalTest extends WebTestCase
         $this->assertNotHasProperty('protectedField', $model);
         $this->assertNotHasProperty('protected', $model);
     }
+
+    public function testModelsWithDiscriminatorMapAreLoadedWithOpenApiPolymorphism()
+    {
+        $model = $this->getModel('SymfonyDiscriminator');
+
+        $this->assertInstanceOf(OA\Discriminator::class, $model->discriminator);
+        $this->assertSame('type', $model->discriminator->propertyName);
+        $this->assertCount(2, $model->discriminator->mapping);
+        $this->assertArrayHasKey('one', $model->discriminator->mapping);
+        $this->assertArrayHasKey('two', $model->discriminator->mapping);
+        $this->assertNotSame(OA\UNDEFINED, $model->oneOf);
+        $this->assertCount(2, $model->oneOf);
+    }
+
+    public function testDiscriminatorMapLoadsChildrenModels()
+    {
+        // get model does its own assertions
+        $this->getModel('SymfonyDiscriminatorOne');
+        $this->getModel('SymfonyDiscriminatorTwo');
+    }
 }

From ac7e29da2112a7110f0b4166b442964a9c72c6e6 Mon Sep 17 00:00:00 2001
From: Christopher Davis <cdavis9999@gmail.com>
Date: Mon, 1 Feb 2021 09:50:15 -0600
Subject: [PATCH 3/4] Fix CS

---
 .../ApplyOpenApiDiscriminatorTrait.php         | 18 +++++++++---------
 ModelDescriber/ObjectModelDescriber.php        |  2 +-
 .../ApplyOpenApiDiscriminatorTraitTest.php     |  7 +++----
 3 files changed, 13 insertions(+), 14 deletions(-)

diff --git a/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php b/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php
index dd551fd..d3e5fe9 100644
--- a/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php
+++ b/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php
@@ -21,18 +21,19 @@ use Symfony\Component\PropertyInfo\Type;
  * Open API schemas to support poly morphism.
  *
  * @see https://swagger.io/docs/specification/data-models/inheritance-and-polymorphism/
+ *
  * @internal
  */
 trait ApplyOpenApiDiscriminatorTrait
 {
     /**
-     * @param Model $model the model that's being described, This is used to pass groups and config
-     *        down to the children models in `oneOf`
-     * @param OA\Schema $schema The Open API schema to which `oneOf` and `discriminator` properties
-     *        will be added
-     * @param string $discriminatorProperty The property that determine which model will be unsierailized
-     * @param array<string, string> $typeMap the map of $discriminatorProperty values to their
-     *        types
+     * @param Model                 $model                 the model that's being described, This is used to pass groups and config
+     *                                                     down to the children models in `oneOf`
+     * @param OA\Schema             $schema                The Open API schema to which `oneOf` and `discriminator` properties
+     *                                                     will be added
+     * @param string                $discriminatorProperty The property that determine which model will be unsierailized
+     * @param array<string, string> $typeMap               the map of $discriminatorProperty values to their
+     *                                                     types
      */
     protected function applyOpenApiDiscriminator(
         Model $model,
@@ -40,7 +41,7 @@ trait ApplyOpenApiDiscriminatorTrait
         ModelRegistry $modelRegistry,
         string $discriminatorProperty,
         array $typeMap
-    ) : void {
+    ): void {
         $schema->oneOf = [];
         $schema->discriminator = new OA\Discriminator([]);
         $schema->discriminator->propertyName = $discriminatorProperty;
@@ -54,7 +55,6 @@ trait ApplyOpenApiDiscriminatorTrait
             ));
             $schema->oneOf[] = $oneOfSchema;
             $schema->discriminator->mapping[$propertyValue] = clone $oneOfSchema;
-
         }
     }
 }
diff --git a/ModelDescriber/ObjectModelDescriber.php b/ModelDescriber/ObjectModelDescriber.php
index 893ae96..5260da8 100644
--- a/ModelDescriber/ObjectModelDescriber.php
+++ b/ModelDescriber/ObjectModelDescriber.php
@@ -74,7 +74,7 @@ class ObjectModelDescriber implements ModelDescriberInterface, ModelRegistryAwar
         $annotationsReader->updateDefinition($reflClass, $schema);
 
         $discriminatorMap = $this->doctrineReader->getClassAnnotation($reflClass, DiscriminatorMap::class);
-        if ($discriminatorMap && $schema->discriminator === OA\UNDEFINED) {
+        if ($discriminatorMap && OA\UNDEFINED === $schema->discriminator) {
             $this->applyOpenApiDiscriminator(
                 $model,
                 $schema,
diff --git a/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php b/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php
index 0644a8b..9ad9c8a 100644
--- a/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php
+++ b/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php
@@ -11,13 +11,12 @@
 
 namespace Nelmio\ApiDocBundle\Tests\ModelDescriber;
 
-use Doctrine\Common\Annotations\AnnotationReader;
 use Nelmio\ApiDocBundle\Model\Model;
 use Nelmio\ApiDocBundle\Model\ModelRegistry;
 use Nelmio\ApiDocBundle\ModelDescriber\ApplyOpenApiDiscriminatorTrait;
 use OpenApi\Annotations as OA;
-use Symfony\Component\PropertyInfo\Type;
 use PHPUnit\Framework\TestCase;
+use Symfony\Component\PropertyInfo\Type;
 
 class ApplyOpenApiDiscriminatorTraitTest extends TestCase
 {
@@ -70,14 +69,14 @@ class ApplyOpenApiDiscriminatorTraitTest extends TestCase
         );
     }
 
-    protected function setUp() : void
+    protected function setUp(): void
     {
         $this->schema = new OA\Schema([]);
         $this->model = $this->createModel(__CLASS__);
         $this->modelRegistry = new ModelRegistry([], new OA\OpenApi([]));
     }
 
-    private function createModel(string $className) : Model
+    private function createModel(string $className): Model
     {
         return new Model(
             new Type(Type::BUILTIN_TYPE_OBJECT, false, $className),

From 87004fc4286e4ed8d46073632f7e1b84b4939cce Mon Sep 17 00:00:00 2001
From: Christopher Davis <cdavis9999@gmail.com>
Date: Mon, 8 Feb 2021 15:39:14 -0600
Subject: [PATCH 4/4] Don't Use `ref` in Discriminator `mapping`

Instead just include the schema ref directly per the documentation.
---
 ModelDescriber/ApplyOpenApiDiscriminatorTrait.php           | 2 +-
 Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php b/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php
index d3e5fe9..3c9446d 100644
--- a/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php
+++ b/ModelDescriber/ApplyOpenApiDiscriminatorTrait.php
@@ -54,7 +54,7 @@ trait ApplyOpenApiDiscriminatorTrait
                 $model->getOptions()
             ));
             $schema->oneOf[] = $oneOfSchema;
-            $schema->discriminator->mapping[$propertyValue] = clone $oneOfSchema;
+            $schema->discriminator->mapping[$propertyValue] = $oneOfSchema->ref;
         }
     }
 }
diff --git a/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php b/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php
index 9ad9c8a..51df5e4 100644
--- a/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php
+++ b/Tests/ModelDescriber/ApplyOpenApiDiscriminatorTraitTest.php
@@ -41,12 +41,12 @@ class ApplyOpenApiDiscriminatorTraitTest extends TestCase
         $this->assertArrayHasKey('one', $this->schema->discriminator->mapping);
         $this->assertSame(
             $this->modelRegistry->register($this->createModel('FirstType')),
-            $this->schema->discriminator->mapping['one']->ref
+            $this->schema->discriminator->mapping['one']
         );
         $this->assertArrayHasKey('two', $this->schema->discriminator->mapping);
         $this->assertSame(
             $this->modelRegistry->register($this->createModel('SecondType')),
-            $this->schema->discriminator->mapping['two']->ref
+            $this->schema->discriminator->mapping['two']
         );
     }