用于 Zio-Json 对象的 Scala 3 类型提取器

问题描述 投票:0回答:1

更新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

是否可以做更类型安全的版本?有什么想法如何继续吗?

scala tuples type-level-computation zio-json
1个回答
0
投票

好吧,这是一个非常接近最终版本的版本,但仍然愿意听取改进外观/感觉/可用性的想法:

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")

斯卡斯蒂

© www.soinside.com 2019 - 2024. All rights reserved.