Issues with collection of Dynamically typed service classes & objects
<p>We are building data structure migration system that should be capable of migrating from any version of a case class to the latest one; as well as from the latest version to any one of the previous ones.
The concept is as follows:</p>

<p>We have a structure of case classes and migration classes</p>

<pre><code>v1/SchemaV1 &lt;= Case class V1
v2/SchemaV2 &lt;= Case class V2
v2/MigrationV2 &lt;= Migration Class from 1-&gt;2 and back
v3/SchemaV3 &lt;= Case class V3
v3/MigrationV3 &lt;= Migration Class from 2-&gt;3 and back
...
</code></pre>

<p>Where:</p>

<ul>
<li>case classes can potentially be anything, however they are implementing the same interface <code>Schema</code></li>
<li>Migration classes implement Migration trait and must implement 'up' and 'down' methods that simply convert one object into another.</li>
</ul>

<p>Currently Migration trait looks like this:</p>

<pre><code>trait TypedMigration[F &lt;: Schema, T &lt;: Schema] {

  type FromSchema = F
  type ToSchema = T

  /**
    * This method is responsible for migrating one version of a schema 'F'(From) to another 'T' (To)
    *
    * @param from
    * @return
    */
  def up(from: FromSchema): ToSchema

  /**
    * This method is responsible for migrating one version of a schema down one level
    *
    * @param from
    * @return
    */
  def down(from: ToSchema): FromSchema
}
</code></pre>

<p>The idea here, that every implementation of migration class will have <code>from</code> and <code>to</code> types enforced.</p>

<p>As an example:</p>

<pre><code>class V2Migration extends TypedMigration[v1.SchemaV1, v2.SchemaV2] {
   ...
}
</code></pre>

<p>How, there is a service class the responsible for managing this whole migration process. This class has two methods that handles migrations 'up' &amp; 'down' respectively</p>

<p>As an example:</p>

<pre><code>protected[versioning] def doMigrateUp(schema: Schema): ChecklistSchema = {

    // load all required migration classes
    val migrations = loadMigrationObjects[Schema, Schema](resolveSchemaVersion(schema), targetVersion)

    var migratedSchema = schema
    // apply all migrations up
    migrations.foreach { migration =&gt;
      migratedSchema = migration.up(migratedSchema)
    }

    migratedSchema.asInstanceOf[ChecklistSchema]
  }
</code></pre>

<p>Where:</p>

<ul>
<li>`loadMigrationObjects' method returns a list of migration objects in order</li>
<li><code>ChecklistSchema</code> is a type alias to the latest version. So I can safely cast to it at the end.</li>
</ul>

<p>All good till now...</p>

<p>The problem for me begins in the 'doMigrateDown` method:</p>

<pre><code>protected[versioning] def doMigrateDown[S &lt;: Schema](schema: ChecklistSchema, targetVersion: Int): S = {
    val migrations = loadMigrationObjects[Schema, ChecklistSchema](resolveSchemaVersion(schema), targetVersion)

    var migratedSchema = schema
    // apply all migrations down
    migrations.foreach { migration =&gt;
      migratedSchema = migration.down(migratedSchema)
    }

    migratedSchema.asInstanceOf[S]
  }
</code></pre>

<p>Here, there are two problems:</p>

<ol>
<li><p>I am getting compilation error:
<a href="https://i.stack.imgur.com/S9hWG.png" rel="nofollow noreferrer"><img src="https://i.stack.imgur.com/S9hWG.png" alt="enter image description here"></a></p></li>
<li><p>I am not sure if this casting can actually work.</p></li>
</ol>

<p>I this point I am totally stuck...</p>

<p>For the reference, this is how we build the list of migration objects:</p>

<pre><code>protected[versioning] def loadMigrationObjects[F &lt;: Schema, T &lt;: Schema](currentVersion: Int, targetVersion: Int): List[TypedMigration[F, T]] = {

    val migrationsRange = if (currentVersion &lt;= targetVersion) {
      currentVersion + 1 to targetVersion
    } else {
      currentVersion to targetVersion + 1 by -1
    }

    val migrations = List[TypedMigration[F, T]]()

    migrationsRange.foldLeft(migrations) { case (acc, versionNumber) =&gt;
      Try {

        val migrationClassName = MigrationClassNameFormat.format(versionNumber, versionNumber)
        Class.forName(migrationClassName).newInstance().asInstanceOf[TypedMigration[F, T]]

      } match {
        case Success(m) =&gt; acc :+ m
        case Failure(e: ClassNotFoundException) =&gt;
          logger.info("migration class was not found", e)
          acc
        case Failure(e) =&gt; throw e
      }
    }

  }
</code></pre>