I would like to suggest another way of solving this problem.
For what I understand of you model, what you really need is to identify which remote storage you have to execute the correct open
logic.
Thus, you can just provide implicit evidence of that, for example like this:
sealed trait StorageTag extends Product with Serializable
implicit case object Gcs extends StorageTag
implicit case object S3 extends StorageTag
sealed trait StorageFile[T <: StorageTag] extends Product with Serializable {
def bucket: String
def path: String
}
final case class GcsFile(bucket: String, path: String) extends StorageFile[Gcs.type]
final case class S3File(bucket: String, path: String) extends StorageFile[S3.type]
sealed trait StorageConfig[T <: StorageTag] extends Product with Serializable {
def keyPath: String
}
final case class GcsConfig(keyPath: String) extends StorageConfig[Gcs.type]
final case class S3Config(keyPath: String) extends StorageConfig[S3.type]
def open[T <: StorageTag](storageFile: StorageFile[T], storageConfig: StorageConfig[T])
(implicit tag: T):String = tag match {
case S3 =>
s"S3 -> bucket: '${storageFile.bucket}', path: '${storageFile.path}' | config keyPath: '${storageConfig.keyPath}'"
case Gcs =>
s"Gcs -> bucket: '${storageFile.bucket}', path: '${storageFile.path}' | config keyPath: '${storageConfig.keyPath}'"
}
Now, you can call the method this way
open(S3File(bucket = "bucket", path = "path"), S3Config(keyPath = "keyPath"))
// res0: String = "S3 -> bucket: 'bucket', path: 'path' | config keyPath: 'keyPath'"
open(GcsFile(bucket = "bucket", path = "path"), GcsConfig(keyPath = "keyPath"))
// res1: String = "Gcs -> bucket: 'bucket', path: 'path' | config keyPath: 'keyPath'"
open(S3File(bucket = "bucket", path = "path"), GcsConfig(keyPath = "keyPath"))
// Compile time error!
Note that this approach would only work if all StorageFiles
and StorageConfigs
have the same properties.
If that is not the case you can try something like the following:
However, note that this code is not completely typesafe, and may be tricked
sealed trait StorageTag extends Product with Serializable
implicit case object Gcs extends StorageTag
implicit case object S3 extends StorageTag
sealed trait StorageFile[T <: StorageTag] extends Product with Serializable
final case class GcsFile(bucket: String, path: String, id: Int) extends StorageFile[Gcs.type]
final case class S3File(bucket: String, path: String) extends StorageFile[S3.type]
sealed trait StorageConfig[T <: StorageTag] extends Product with Serializable
final case class GcsConfig(keyPath: String, name: String) extends StorageConfig[Gcs.type]
final case class S3Config(keyPath: String) extends StorageConfig[S3.type]
def open[T <: StorageTag](storageFile: StorageFile[T], storageConfig: StorageConfig[T])
(implicit tag: T): String = tag match {
case S3 =>
// These lines are not checked in compile-time, you can put GcsFile instead, and it will compile and fail at run-time!!!
val S3File(bucket, path) = storageFile
val S3Config(keyPath) = storageConfig
s"S3 -> bucket: '${bucket}', path: '${path}' | config keyPath: '${keyPath}'"
case Gcs =>
val GcsFile(bucket, path, id) = storageFile
val GcsConfig(keyPath, name) = storageConfig
s"Gcs -> bucket: '${bucket}', path: '${path}', id: $id | config keyPath: '${keyPath}', name: 'name'"
}
open(S3File(bucket = "bucket", path = "path"), S3Config(keyPath = "keyPath"))
// res0: String = "S3 -> bucket: 'bucket', path: 'path' | config keyPath: 'keyPath'"
open(GcsFile(bucket = "bucket", path = "path", id = 0), GcsConfig(keyPath = "keyPath", name = "name"))
// res1: String = "Gcs -> bucket: 'bucket', path: 'path', id: 0 | config keyPath: 'keyPath', name: 'name'"
open(S3File(bucket = "bucket", path = "path"), GcsConfig(keyPath = "keyPath", name = "name"))
// Compile time error!
open(
GcsFile(bucket = "bucket", path = "path", id = 0).asInstanceOf[StorageFile[StorageTag]],
GcsConfig(keyPath = "keyPath", name = "name").asInstanceOf[StorageConfig[StorageTag]]
)(S3.asInstanceOf[StorageTag])
// Runtime error!!!!!!!