密封特征和动态案例对象

时间:2018-12-25 22:33:44

标签: scala

我有一些枚举实现为密封特征和案例对象。我之所以喜欢使用ADT方法,是因为它没有详尽的警告,并且主要是因为我们想避免类型擦除。像这样

 sealed abstract class Maker(val value: String) extends Product with Serializable {
    override def toString = value
  }

  object Maker {
    case object ChryslerMaker extends Vendor("Chrysler")
    case object ToyotaMaker extends Vendor("Toyota")
    case object NissanMaker extends Vendor("Nissan")
    case object GMMaker extends Vendor("General Motors")
    case object UnknownMaker extends Vendor("")

    val tipos = List(ChryslerMaker, ToyotaMaker, NissanMaker,GMMaker, UnknownMaker)
    private val fromStringMap: Map[String, Maker] = tipos.map(s => s.toString -> s).toMap

    def apply(key: String): Option[Maker] = fromStringMap.get(key)
  }

到目前为止,它运行良好,现在我们正在考虑向其他程序员提供对我们代码的访问权限,以允许他们在现场进行配置。我看到两个潜在的问题: 1)人们搞砸了,写着类似的东西:

case object ChryslerMaker extends Vendor("Nissan")

人们忘记更新Tipos

我一直在研究使用配置文件(JSON或csv)来提供这些值并像处理其他许多元素一样读取它们,但是我发现的所有答案都依赖于宏,并且似乎极其依赖使用的scala版本(对我们来说是2.12)。

我想找到的是: 1a)(首选)一种从字符串列表中动态创建案例对象的方法,以确保对象名称与其持有的值一致 1b)(可接受),如果这证明在测试阶段很难获得对象和值的方法 2)检查列表中元素的数量是否与创建的案例对象的数量相匹配。

我忘了提一下,我只是简要地列举了枚举,但我不希望包括其他库,除非我真正了解其优缺点(如果您认为,现在我不确定与ADT方法相比枚举有多大这是最好的方法,可以将我引向进行有效的讨论)

谢谢!

1 个答案:

答案 0 :(得分:2)

我想到的一个想法是创建一个SBT SourceGenerator task
这将读取输入的JSON,CSV,XML或任何文件,这是您的项目的一部分,并将创建一个scala文件。

// ----- File: project/VendorsGenerator.scala -----
import sbt.Keys._
import sbt._

/**
 * An SBT task that generates a managed source file with all Scalastyle inspections.
 */
object VendorsGenerator {
  // For demonstration, I will use this plain List[String] to generate the code,
  // you may change the code to read a file instead.
  // Or maybe this will be good enough.
  final val vendors: List[String] =
    List(
      "Chrysler",
      "Toyota",
      ...
      "Unknow"
    )

  val generatorTask = Def.task {
    // Make the 'tipos' List, which contains all vendors.
    val tipos =
      vendors
        .map(vendorName => s"${vendorName}Vendor")
        .mkString("val tipos: List[Vendor] = List(", ",", ")")

    // Make a case object for each vendor.
    val vendorObjects = vendors.map { vendorName =>
      s"""case object ${vendorName}Vendor extends Vendor { override final val value: String = "${vendorName}" }"""
    }

    // Fill the code template.
    val code =
      List(
        List(
          "package vendors",
          "sealed trait Vendor extends Product with Serializable {",
          "def value: String",
          "override final def toString: String = value",
          "}",
          "object Vendors extends (String => Option[Vendor]) {"
        ),
        vendorObjects,
        List(
          tipos,
          "private final val fromStringMap: Map[String, Vendor] = tipos.map(v => v.toString -> v).toMap",
          "override def apply(key: String): Option[Vendor] = fromStringMap.get(key.toLowerCase)",
          "}"
        )
      ).flatten

    // Save the new file to the managed sources dir.
    val vendorsFile = (sourceManaged in Compile).value / "vendors.scala"
    IO.writeLines(vendorsFile, code)
    Seq(vendorsFile)
  }
}

现在,您可以激活源生成器了。
每次在编译步骤之前运行此任务。

// ----- File: build.sbt -----
sourceGenerators in Compile += VendorsGenerator.generatorTask.taskValue

请注意,我建议您这样做,因为我之前做过,而且没有任何宏或元编程经验。
此外,请注意,此示例在Strings中传递了很多内容,这使得代码有点难以理解和维护。

顺便说一句,我没有使用enumeratum,但快速浏览一下似乎是解决此问题的最佳方法

编辑

  

我准备好要读取HOCON文件并生成匹配代码的代码。现在我的问题是将scala文件放在项目目录中的位置,以及在哪里生成文件。我有点困惑,因为似乎有多个步骤:1)编译我的scala生成器,2)运行生成器,以及3)编译并生成项目。是这样吗?

您的生成器不是您的项目代码的一部分,而是您的元项目(我知道这听起来很令人困惑,您可以阅读this来理解它)-这样,您将生成器放在根级别(位于指定{strong> sbt版本 的project文件的文件夹中)的build.properties文件夹内
如果您的生成器需要某些依赖项(我确定它确实可以读取 HOCON ,则可以将其放在build.sbt内的project文件中文件夹。
如果您打算将单元测试添加到生成器,则可以在元项目中创建一个完整的scala项目(您可以看一下开源项目的project folder(是的,我知道,再次使我感到困惑)(我在此工作以作参考)-我的个人建议是,除了测试生成器本身之外,还应该测试生成的文件,或者两者都更好。

生成的文件将自动放置在src_managed文件夹(位于target内)中,因此将从源代码版本控制中将其忽略)
里面的路径只是 order ,因为src_managed文件夹中的所有内容在编译时默认都包括在内。

val vendorsFile = (sourceManaged in Compile).value / "vendors.scala" // Path to the file to write.`

为了访问源代码上生成的文件中定义的值,您只需要将包添加到生成的文件中,然后从您的代码中将该包中的值导入(与任何普通文件一样)

您无需担心与编译顺序有关的任何事情,如果您将源代码生成器包含在build.sbt文件中, SBT 将自动处理所有事情。

sourceGenerators in Compile += VendorsGenerator.generatorTask.taskValue // Activate the source generator.

SBT 将在每次需要编译时重新运行您的生成器。

  

“顺便说一句,我在导入中得到“未找到:对象sbt”。

如果该项目位于元项目空间内,则默认情况下它将找到sbt包,不用担心。