更新2:
得到了一个解决方案我现在非常满意,使用更通用的
Extractor[Subject, Selector, Result]
来描述从Selector
中提取一些Subject
以获得Result
,然后创建一个zio-json
的能力Json.Obj
——具体实现吧。人体工程学可以改进一点,但总的来说我认为它工作得很好。然而,有两个警告,我不确定如何消除,并且确信在某些情况下可能会导致问题。它们发生在上面链接的 scastie 的第 22 行:
the type test for Tuple.Head[Sel] cannot be checked at runtime because it refers to an abstract type member or type parameter
the type test for Tuple.Tail[Sel] cannot be checked at runtime because it refers to an abstract type member or type parameter
我针对此类错误的一般策略通常是添加
ClassTag
,但这在这种情况下似乎没有帮助。虽然我通常了解擦除,但我仍然不确定如何解决此警告。
更新:
通过更新的 scastie 更接近(我认为),采用更无形的方法,但现在在调用站点遇到错误并出现问题:
Recursive value $t needs type
不确定它指的是什么递归?
原文:
这更多的是出于教学目的,但我正在尝试为 zio-json 编写一个元组提取器
Json.Obj
(尽管我认为这些概念不仅适用于这个库)。目标是传递一个Json.Obj
,一个代表调用者有兴趣从该对象中提取的键的Strings
元组,以及一个与代表调用者想要转换的类型的键元组长度相同的元组类型相应的值,例如:
def [Keys <: Tuple, Values <: Tuple](json: Json.Obj, keys: Keys)(using
Tuple.Union[Keys] <: String, Tuple.Size[Keys] =:= Tuple.Size[Values]): V
这是一个scastie,我尝试用
unapply
实际做这样的事情,但在一些事情上遇到了麻烦,主要是确保JsonEncoder
元组中的每种类型都存在Values
(原样) json.as[V]
功能所需)。
我有一个更简单版本的工作解决方案,其中所有值都假设为字符串,并且输入和输出为
Seq
,因此无法验证键的数量是否与提取的值的数量相匹配:
case class ZioJsonExtractor(keys: String*):
def unapplySeq(json: Json.Obj): Option[Seq[String]] =
keys.foldLeft(Option(List.empty[String])): (vals, key) =>
for
vs <- vals
v <- json.get(key).flatMap(_.asString)
yield v :: vs
.map(_.reverse)
可以使用类似的东西
val jsonExtractor = ZioJsonExtractor("desired", "keys")
val json: Json.Obj = ...
json match
case jsonExtractor(extracted, values) => // here `extracted` and `values` are both Strings
是否可以做更类型安全的版本?有什么想法如何继续吗?
好吧,这是一个非常接近最终版本的版本,但仍然愿意听取改进外观/感觉/可用性的想法:
import zio.json.*
import zio.json.ast.Json
import scala.reflect.ClassTag
// describes the general ability to extract some `Selector` from a `Subject` to obtain a `Result`
trait Extractor[Subject, Selector, Result]:
def extract(from: Subject, select: Selector): Option[Result]
// givens for a `Tuple`s
object Extractor:
given tuple1Extract[Sub, Sel, R](using e: Extractor[Sub, Sel, R]): Extractor[Sub, Sel *: EmptyTuple, R *: EmptyTuple] with
override def extract(from: Sub, sel: Sel *: EmptyTuple): Option[R *: EmptyTuple] =
sel match
case s *: EmptyTuple => e.extract(from, s).map(_ *: EmptyTuple)
// HS, TS and their `ClassTag`s are needed to get rid of the "... cannot be checked at runtime..." warnings, though would love to hear cleaner ways to achieve that if possible
given tupleExtract[Sub, Sel <: NonEmptyTuple, R <: NonEmptyTuple, HS, TS <: Tuple](using
eh: Extractor[Sub, HS, Tuple.Head[R]],
et: Extractor[Sub, TS, Tuple.Tail[R]],
concat: HS *: TS =:= Sel,
cths: ClassTag[HS], ctts: ClassTag[TS],
): Extractor[Sub, Sel, R] with
override def extract(from: Sub, sel: Sel): Option[R] =
sel match
case (s: HS) *: (rest: TS) =>
for
value <- eh.extract(from, s)
values <- et.extract(from, rest)
yield (value *: values).asInstanceOf[R]
// `zio-json`-specifc `Extractor`s
case class ZioJsonExtractor[K, V](keys: K):
def unapply(json: Json.Obj)(using e: Extractor[Json.Obj, K, V]): Option[V] =
e.extract(json, keys)
object ZioJsonExtractor:
given baseExtract[A](using JsonDecoder[A]): Extractor[Json.Obj, String, A] with
override def extract(obj: Json.Obj, key: String): Option[A] =
for
vAst <- obj.get(key)
v <- vAst.as[A].toOption
yield v
// convenience class
class ZioJson[V]:
def apply[K](keys: K): ZioJsonExtractor[K, V] = ZioJsonExtractor(keys)
// test data
case class User(name: String)
case class Amount(count: Int)
import ZioJsonExtractor.given
given userJsonSchema: JsonCodec[User] = DeriveJsonCodec.gen
given amountJsonSchema: JsonCodec[Amount] = DeriveJsonCodec.gen
@main
def tryIt(): Unit =
val json = Json.Obj(
"user" -> Json.Obj("name" -> Json.Str("Bobson Dugnutt")),
"amount" -> Json.Obj("count" -> Json.Num(42)),
"notMatched" -> Json.Obj("not" -> Json.Str("interested")),
"some" -> Json.Str("string"),
)
val userAndAmount: ZioJsonExtractor[(String, String), (User, Amount)] = ZioJson[(User, Amount)].apply("user", "amount")
val amountAndUser = ZioJson[(Amount, User)].apply("amount", "user")
val someStr = ZioJson[String].apply("some")
json match
case userAndAmount(u, a) =>
println(s"extracted $u and $a!")
case _ =>
println("no match")
json match
case amountAndUser(a, u) =>
println(s"extracted $a and $u!")
case _ =>
println("no match")
json match
case someStr(str) =>
println(s"extracted $str!")
case _ =>
println("no match")