Scala бесформенного набора текста [Symbol, String] с классами падежей - PullRequest
4 голосов
/ 17 июня 2019

Я читаю параметры запроса и преобразовываю их в Map[Symbol, String].Я хотел бы добавить некоторую безопасность типов к этим параметрам запроса через набор классов case.

Эти классы дел будут разными в зависимости от входящего http-запроса, поэтому для этого необходимо поддерживать разные классы дел.

Если параметры входящего запроса не соответствуют определенному case class the Parser должен вернуть None.

Я попытался использовать бесформенный для реализации универсального парсера.Это работает, если все параметры имеют тип String.Но мне нужно поддерживать любой тип параметра запроса.

Я пытался включить неявную логику преобразования, описанную в этом посте, но не смог заставить ее работать.https://meta.plasm.us/posts/2015/11/08/type-classes-and-generic-derivation/ (от нового к бесформенному)

Существующий Parser (без преобразования строки в тип):

class Parser[A] {
  def from[R <: HList]
  (m: Map[Symbol, String])
  (implicit
   gen: LabelledGeneric.Aux[A, R],
   fromMap: FromMap[R]
  ): Option[A] = fromMap(m).map(gen.from)
}

object Parser {
  def to[A]: Parser[A] = new Parser[A]
}

Тесты, описывающие проблему:

class ParserSpec extends FlatSpec with Matchers {
  private val sampleName: String = "Bob"
  private val sampleVersion: Int = 1

  //Partial Solution
  case class QueryParams(name: String, version: String)

  //Full Solution (not working)
  case class QueryParams2(name: String, version: Int)

  "A Parser" should "parse query parameters from a map with only string values" in {
    val mapOfQueryParams = Map('name -> sampleName, 'version -> sampleVersion.toString)
    val result = Parser.to[QueryParams].from(mapOfQueryParams)

    result shouldBe 'defined
    result.get.name shouldEqual sampleName
    result.get.version shouldEqual sampleVersion.toString
  }
  it should "parse query parameters from a map with any type of value" in {
    val mapOfQueryParams = Map('name -> sampleName, 'version -> sampleVersion.toString)
    val result = Parser.to[QueryParams2].from(mapOfQueryParams)

    //result is not defined as it's not able to convert a string to integer
    result shouldBe 'defined
    result.get.name shouldEqual sampleName
    result.get.version shouldEqual sampleVersion
  }
}

1 Ответ

4 голосов
/ 18 июня 2019

FromMap использует shapeless.Typeable для преобразования значений в ожидаемый тип. Поэтому самый простой способ заставить ваш код работать, это определить экземпляр Typeable для преобразования из String в Int (и дополнительные Typeable экземпляры для любого типа значения, который появляется в ваших классах case):

implicit val stringToInt: Typeable[Int] = new Typeable[Int] {
  override def cast(t: Any): Option[Int] = t match {
    case t: String => Try(t.toInt).toOption
    case _ => Typeable.intTypeable.cast(t)
  }

  override def describe: String = "Int from String"
}

Однако это не преднамеренное использование Typeable, которое предназначено для подтверждения того, что переменная с типом Any уже является экземпляром ожидаемого типа без какого-либо преобразования. Другими словами, он предназначен для безопасной реализации типа asInstanceOf, который также может обойти удаление типа.


Для правильности вы можете определить свой собственный класс типов ReadFromMap, который использует ваш собственный класс типов Read для преобразования из String s в ожидаемые типы. Вот простая реализация класса типов Read (при условии Scala 2.12):

import scala.util.Try

trait Read[T] {
  def apply(string: String): Option[T]
}

object Read {
  implicit val readString: Read[String] = Some(_)
  implicit val readInt: Read[Int] = s => Try(s.toInt).toOption
  // Add more implicits for other types in your case classes
}

И вы можете скопировать и адаптировать реализацию FromMap для использования этого Read класса типов:

import shapeless._
import shapeless.labelled._

trait ReadFromMap[R <: HList] extends Serializable {
  def apply(map: Map[Symbol, String]): Option[R]
}

object ReadFromMap {
  implicit def hnil: ReadFromMap[HNil] = _ => Some(HNil)

  implicit def hlist[K <: Symbol, V, T <: HList](implicit
    keyWitness: Witness.Aux[K],
    readValue: Read[V],
    readRest: ReadFromMap[T]
  ): ReadFromMap[FieldType[K, V] :: T] = map => for {
    value <- map.get(keyWitness.value)
    converted <- readValue(value)
    rest <- readRest(map)
  } yield field[K](converted) :: rest
}

Тогда просто используйте этот новый класс типов в вашем Parser:

class Parser[A] {
  def from[R <: HList]
  (m: Map[Symbol, String])
  (implicit
    gen: LabelledGeneric.Aux[A, R],
    fromMap: ReadFromMap[R]
  ): Option[A] = fromMap(m).map(gen.from)
}
...