scala rest_在Scala中使用Shapeless进行编程以编写REST API

scala rest

Shapeless是Scala中用于通用编程的库,主要存在于生态系统中 ,但大多在幕后。 即使您不直接使用它,也可能无形地增强了项目中的某些库的功能。

为了解决日常问题,我找到了一个用无形可以解决的用例。 这篇文章并不是要解释无形的工作原理(这里有整本书 ),而是要提供它的味道。

挑战

我们需要使用ZIOhttp4s编写REST API使用者。 这是服务定义:

import io.circe. Decoder
import zio.{ Has , RIO , Task }

object HttpClient  {
  type HttpClient  = Has [ Service ]

  trait Service  {
    protected final val rootUrl = "http://localhost:8080"

    def get [ T ](uri: String , parameters: Map [ String , String ])
              ( implicit d: Decoder [ T ]): Task [ List [ T ]]
  }

  def get [ T ](resource: String , parameters: Map [ String , String ])
            ( implicit d: Decoder [ T ]): RIO [ HttpClient , List [ T ]] =
    RIO .accessM[ HttpClient ](_.get.get[ T ](resource, parameters))
}

如果您不熟悉ZIO(您应该很棒),那么您需要知道的是:

  • 每个获取请求都返回一个List Task
  • Service外部的get函数只是访问效果环境的助手(ZIO内容)

在此处了解有关ZIO模块和层的更多信息

这是使用http4sHttpClient.Service实现:

import io.circe. Decoder
import org.http4s. Uri
import org.http4s.circe. CirceEntityCodec .circeEntityDecoder
import org.http4s.client. Client
import org.http4s.client.dsl. Http4sClientDsl
import zio._
import zio.interop.catz._

class Http4sClient ( client: Client [ Task ] )
  extends HttpClient . Service with Http4sClientDsl [ Task ] {

  def get [ T ](resource: String , parameters: Map [ String , String ])
            ( implicit d: Decoder [ T ]): Task [ List [ T ]] = {
    val uri = Uri (path = rootUrl + resource)
      .withQueryParams(parameters)

    client
      .expect[ List [ T ]](uri.toString())
      .foldM( IO .fail(_), ZIO .succeed(_))
  }
}

Http4sClient.getresource添加到uriparameters是查询字符串。 现在,代表请求调用,我们有一个名为OrganisationRequest的案例类:

case class OrganisationRequest ( code: Option [ String ],
                               description: Option [ String ],
                               page: Integer = 1 )

问题

使用客户端(通过get帮助程序)很简单,除了一个细节:

import HttpClient .get

def organisations (request: OrganisationRequest ):
  get[ Organisation ]( "/organisations" , ???)

我们需要将request转换为Map[String, String] ,这是一个简单的任务。 但是,有许多“请求”对象,将toMap方法写入其中的每个对象都是一种Java式的解决方案。 挑战在于:我们如何构建这种通用转换?

剧透:无形。

有点无形

本节是对无形工作原理的理解,因此当我们到达那里时,解决方案将更有意义。 Shapeless可以创建一个异构列表 (或HList )作为案例类的通用表示形式。 让我们使用Generic来做:

scala>import shapeless._

scala> val org = OrganisationRequest ( Some ( "acme" ), None , 5 )
org: OrganisationRequest = OrganisationRequest ( Some (org), None , 5 )

scala> val gen = Generic [ OrganisationRequest ]
gen: shapeless. Generic [ OrganisationRequest ]
  { type Repr  =
    Option [ String ]
    :: Option [ String ]
    :: Integer
    :: shapeless. HNil } = anon$macro$ 4 $ 1 @ 48 f146f2

scala> gen.to(org)
res8: gen. Repr = Some (acme) :: None :: 5 :: HNil

的通用表示OrganisationRequestHList类型的Option[String] :: Option[String] :: Int :: HNil 。 我们有值,但是我们需要Map的字段名称。 我们需要LabelledGeneric而不是Generic

scala>val lgen = LabelledGeneric [ OrganisationRequest ]
lgen: shapeless. LabelledGeneric [ OrganisationRequest ]
  { type Repr  =
    Option [ String ] with shapeless.labelled. KeyTag [ Symbol with shapeless.tag. Tagged [ String ( "code" )], Option [ String ]]
    :: Option [ String ] with shapeless.labelled. KeyTag [ Symbol with shapeless.tag. Tagged [ String ( "description" )], Option [ String ]]
    :: Integer with shapeless.labelled. KeyTag [ Symbol with shapeless.tag. Tagged [ String ( "page" )], Integer ]
    :: shapeless. HNil } = shapeless. LabelledGeneric $$anon$ 1 @ 55 f78c67

如您所见,使用LabelledGeneric也可以保留有关字段名称的信息。

解决方案

幸运的是,我们不需要自己操作LabelledGeneric ,shapeless为我们提供了很多有用的类型类,可以在shapless.ops包中找到shapless.ops 。 我们将使用ToMap构建解决方案:

scala>import shapeless.ops.product. ToMap

scala> val toMap = ToMap [ OrganisationRequest ]
toMap: shapeless.ops.product. ToMap [ OrganisationRequest ]
  { type K  = Symbol
    with shapeless.tag. Tagged [_ >: String ( "page" )
    with String ( "description" )
    with String ( "code" ) <: String ];
  type V  = java.io. Serializable } =
    shapeless.ops.product$ ToMap $$anon$ 5 @ 3 bccd311

scala> val map = toMap(org)
map: toMap. Out = Map ( 'page -> 5 ,
                     'description -> None ,
                     'code -> Some (acme))

我们可以使用无形语法使其变得更好:

scala>import shapeless.syntax.std.product._

scala> val map = org.toMap[ Symbol , Any ]
map: Map [ Symbol , Any ] = Map ( 'page -> 5 ,
                           'description -> None ,
                           'code -> Some (acme))

对于最终解决方案,让我们创建一个implicit class ,以便将parameters方法添加到我们的request类中。 此外,我们应该删除所有具有nullNone值的条目,展平Options并将键和值转换为String

import shapeless.ops.product. ToMap
import shapeless.syntax.std.product._

implicit class RequestOps [ A <: Product ]( val a: A )  {
  def parameters ( implicit toMap: ToMap . Aux [ A , Symbol , Any ]): Map [ String , String ] =
    a.toMap[ Symbol , Any ]
      .filter {
        case (_, v: Option [ Any ]) => v.isDefined
        case (_, v) => v != null
      }
      .map {
        case (k, v: Option [ Any ]) => k.name -> v.get.toString
        case (k, v) => k.name -> v.toString
      }
}

这里有一些评论:

  • A <: Product需要放置到位,以便我们可以使用shapeless.ops.product所有案例类都实现Product ,只需添加隐式解析的约束即可;
  • 隐式参数toMapToMap.Aux而不是ToMap 。 长话短说,无形的定义了Aux别名,以使其内部的某些复杂性更易于阅读和使用。 只要在这里相信我;)

最后,这为我们带来了一个优雅的解决方案:

import HttpClient .get
import RequestOps

def organisations (request: OrganisationRequest ):
  get[ Organisation ]( "/organisations" , request.parameters)

结论

即使乍看之下,无形的看上去几乎是神奇的,
致力于让自己更好地理解它,我发现它可能非常
实际上有用。 无形提供了广泛的
可以以各种方式使用的类型类,并花费时间
了解他们的工作方式是一项非常有趣的练习,可以改善
与类型类,派生和带来清晰度相关的技能
一些使用无形工作(例如circe)的流行图书馆如何。

我听说添加过多的变形会适当影响
项目的编译时间。 如果您愿意,我想了解更多
直接使用Shapeless的经验,请分享评论。

最初于 2020年4月6日 发布在 https://juliano-alves.com 上。

翻译自: https://hackernoon.com/using-shapeless-for-programming-in-scala-to-write-a-rest-api-kv8a32ez

scala rest

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值