Scala:在匹配语句后返回基数 class 时访问 case class 字段

Scala: accessing case class fields when returning a base class after a match statement

我目前正在尝试创建一个 class 和子 classes 来封装数据管道的各种配置方面(在 json 中提供);我也在非常学习 Scala。我正在使用 Play 框架库来解析 json 字符串输入 - https://www.playframework.com/documentation/2.8.x/ScalaJson

我有一些代码目前可以工作,但感觉有几个方面不对,而且这不是正确的方法。

应用程序的工作流需要获取一个 json 字符串,根据各种相似但略有不同的结构对其进行解析和验证,然后使下游处理的其他部分可以访问这些值(例如,如果文件type 是一个 csv 文件,因此设置一些配置,如果它是一个 json 文件,则改为执行此操作);但需要注意的是,这是一个动态过程。在我看来,这似乎是 case classes 的完美用例,但我觉得我误解了它们的用法。

所以我有一个密封的(为了确保所有匹配都是已知的)抽象class FileConfig,目前有两个subclasses,DelimitedConfig 和JsonConfig。 DelimitedConfig class 还使用了一个额外的 case class DelimitedFileTypeDetails,这基本上是目前两者之间的主要区别,但随着我的继续,还会添加其他偏差;并且我还有所有三个 classes 的伴随对象,以便利用播放框架格式方法:

sealed abstract class FileConfig

case class DelimitedFileTypeDetails (delimiter: String, hasColumnHeaders: Boolean, rowsToSkip: Int)

object DelimitedFileTypeDetails {
    implicit val format = Json.format[DelimitedFileTypeDetails]
}

case class DelimitedConfig (deltaLakeDatabricksMountPoint: String,
    restrictedDeltaLakeDatabricksMountPoint: String,
    notebookToRun: String,
    rowHashExclusionColumns: List[String],
    deltaDatabase: String,
    mergeKeySQLClause: String,
    mergeUpdateFilterSQLClause: String,
    fileToMerge: String,
    containsPIIData: Boolean,
    piiEntityNames: List[String],
    transformsClass: String,
    transformsMethod: String,
    extractStartTime: String,
    fileType: String,
    fileTypeDetails: DelimitedFileTypeDetails
    ) extends FileConfig {
      require(fileType == "Delimited", "fileType must be 'Delimited' for class DelimitedConfig")
    }

object DelimitedConfig {
  implicit val format = Json.format[DelimitedConfig]
}

case class JsonConfig (deltaLakeDatabricksMountPoint: String,
    restrictedDeltaLakeDatabricksMountPoint: String,
    notebookToRun: String,
    rowHashExclusionColumns: List[String],
    deltaDatabase: String,
    mergeKeySQLClause: String,
    mergeUpdateFilterSQLClause: String,
    fileToMerge: String,
    containsPIIData: Boolean,
    piiEntityNames: List[String],
    transformsClass: String,
    transformsMethod: String,
    extractStartTime: String,
    fileType: String) extends FileConfig {
      require(fileType == "Json", "fileType must be 'Json' for class JsonConfig")
    }

object JsonConfig {
  implicit val format = Json.format[JsonConfig]
}

object FileConfig {
  implicit val format = Json.format[FileConfig]
}

在这些之后,我还有一个函数可以使用 play 框架的验证方法对任何提供的 json 字符串进行匹配;这将是第一个问题 - 这感觉真的很笨拙,有没有办法修改对象的嵌套评估?本质上它检查它是否满足第一个 subclass,如果不满足(JsError 是结果)然后检查下一个,依此类推:

object FileConfigParser {

  /**
    * Parse the provided json to ensure it matches the specified format.
    *
    * @param jsonString - a string representation of the json
    * @return - FileConfig of the parsed json
    */
  def parseFileConfig(jsonString: String): FileConfig = {

    val json = Json.parse(jsonString)

    json.validate[DelimitedConfig] match {
      case s: JsSuccess[DelimitedConfig] => s.get.asInstanceOf[DelimitedConfig]
      case e: JsError => {
        json.validate[JsonConfig] match {
          case s: JsSuccess[JsonConfig] => s.get.asInstanceOf[JsonConfig]
          case e: JsError => {
            throw new Error("Invalid json provided")
          }
        }
      }
    }
  }
}

一切正常,当我运行函数时,验证成功;然而,由于 parseFileConfig 函数的结果,我无法访问 class 字段,直到我 运行 在我的主要对象中进行进一步匹配:-

val jsonString = """{
      "sourceDatabricksMountPoint": "/mnt/rawlayer/myData/",
      "deltaLakeDatabricksMountPoint": "/mnt/delta/",
      "restrictedDeltaLakeDatabricksMountPoint": "/mnt/sensitive_delta/",
      "notebookToRun":"/Ingestion/LoadDeltaLake",
      "rowHashExclusionColumns": ["RowLastUpdatedTime"],
      "deltaDatabase": "myDb",
      "restrictedDeltaDatabase": "NICE_WFM_RESTRICTED",
      "deltaTableName": "myTable",
      "mergeKeySQLClause": "s.date=t.date AND s.ID=t.ID",
      "mergeUpdateFilterSQLClause":"s.RowHash <> t.RowHash",
      "fileToMerge": "myFile.txt",
      "fileType":"Delimited",
      "fileTypeDetails": {"delimiter": "|",
                          "hasColumnHeaders": true,
                          "rowsToSkip": 1
                              },
      "containsPIIData": true,
      "piiEntityNames": ["name", "surname"],
      "transformsMethod": "convertDateTimeColumns",
      "transformsClass": "com.example.transforms.CustomTransform",
      "extractStartTime":"2020-12-10T00:00:00.000000"
      }"""

    val fileConfig: FileConfig = parseFileConfig(jsonString)

    println(fileConfig.getClass())

    // if you uncomment this it doesn't work
    // println(fileConfig.fileType)

    // but this does work
    val fileType = fileConfig match {
      case d: DelimitedConfig => d.fileType
      case j: JsonConfig => j.fileType
    }

    println(fileType)

必须 运行 与主代码中的每个字段进行匹配感觉有点乏味,有没有办法在 class 中执行此操作,或者让字段可从class?或者....我只是做错了这一切,还有更好的方法吗?

提前致谢,

马特

如果每个 FileConfig 都有一个给定类型的成员(例如 fileType),您可以将该成员放入 FileConfig:

sealed abstract class FileConfig {
  def fileType: String
}

然后在 类 扩展 FileConfig 的各种情况下,你会得到 override val fileType: String.

也就是说,我会重新考虑让 fileType 成为 case class 中的一个字段。如果每个 DelimitedConfig 都将 fileType 设为 "Delimited",那么将其设为 def 然后定义一个 Format[FileConfig] 可能是有意义的,它会查看 fileType 字段在反序列化时注入到 JSON 中。我已经有一段时间没有使用 Play JSON,所以我不太记得那里的技术。