scala rest
Shapeless是Scala中用于通用编程的库,主要存在于生态系统中 ,但大多在幕后。 即使您不直接使用它,也可能无形地增强了项目中的某些库的功能。
为了解决日常问题,我找到了一个用无形可以解决的用例。 这篇文章并不是要解释无形的工作原理(这里有整本书 ),而是要提供它的味道。
挑战
我们需要使用ZIO和http4s编写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内容)
这是使用http4s的HttpClient.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.get
将resource
添加到uri
, parameters
是查询字符串。 现在,代表请求调用,我们有一个名为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
的通用表示OrganisationRequest
是HList
类型的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
类中。 此外,我们应该删除所有具有null
或None
值的条目,展平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 ,只需添加隐式解析的约束即可; - 隐式参数
toMap
是ToMap.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