Generar el rasgo derivado con los mismos métodos pero sin primer parámetro en cada método en Scala
Contexto
Tengo algunas interfaces "clientes" definidas en mi aplicación de backend scala y tienen aproximadamente esta forma:
trait BackendClient {
def foo: FooClient
}
trait FooClient {
def doSomething(authToken: AuthNToken, request: MyBarRequest): Future[Unit]
def getSomething(authToken: AuthNToken, id: MyId): Future[MyResource]
}
Quiero mantener el authToken
en la firma de método en el backend para recordar las implementaciones que deben revisar la auth para cada llamada de método.
En el frontend sin embargo, se vuelve molesto pasar el mismo token en todas partes, así que idealmente me gustaría tener un cliente "authenticated":
// Would like to auto-generate this given `FooClient`
trait AuthenticatedFooClient {
def doSomething(request: MyBarRequest): Future[Unit]
def getSomething(id: MyId): Future[MyResource]
}
// Auto-generating this too would be nice but not as important
class AuthenticatedFooClientImpl(authToken: AuthNToken, delegate: FooClient) extends AuthenticatedFooClient {
override def doSomething(request: MyBarRequest): Future[Unit] = delegate.doSomething(authToken, request)
override def getSomething(id: MyId): Future[MyResource] = delegate.getSoemthing(authToken, id)
}
De esta manera puedo tener algo así en el frontend:
trait AuthenticatedBackendClient {
def foo: AuthenticatedFooClient
}
class FrontendAuthServiceImpl(backendClient: BackendClient) extends FrontendAuthService {
private val getTokenOrAuthenticate: Task[AuthNToken] = Task { ... }.memoizeOnSuccess
val backendClient: Task[AuthenticatedBackendClient] = for {
authToken <- getTokenOrAuthenticate
authenticatedFoo = new AuthenticatedFooClientImpl(authToken, backendClient.foo)
} yield new AuthenticatedBackendClient {
override def foo: AuthenticatedFooClient = authenticatedFoo
}
}
...
...
...
val someId: MyId = ...
val myResource: Task[MyResource] = for {
backendClient <- frontendAuthService.backendClient
myResource <- Task.fromFuture(backendClient.foo.getSomething(someId))
} yield myResource
Problema
Preferiría no tener que mantener manualmente 2 copias de mis rasgos de cliente, una con el token auth en el método, una sin.
Estoy bastante seguro de que puedo escribir mi propio procesador de anotación (Sé que los macros son probablemente más idiomáticos en scala, pero nunca he aprendido mucho alrededor de ellos) para generar el código que quiero, pero esto se siente como algo que debe ser solvable con las bibliotecas / herramientas existentes.
Específicamente, siento que debería ser capaz de generar un rasgo que es igual a otro rasgo pero deja caer el primer arg de todos los métodos del otro rasgo.
¿Alguna idea?
Pregunta hecha hace 3 años, 4 meses, 27 días - Por cyberscribe
3 Respuestas:
-
Puede utilizar un parámetro de tipo más alto y parametrizar al cliente dependiendo de la situación:
trait GenFooClient[F[_]] { def doSomething(request: MyBarRequest): F[Future[Unit]] def getSomething(id: MyId): F[Future[MyResource]] } type Id[A] = A type AuthenticatedFooClient = FooClient[Id] type FromToken[A] = AuthNToken => A type FooClient = GenFooClient[FromToken] def authenticate(f: FooClient, token: AuthNToken): AuthenticatedFooClient = new GenFooClient[Id] { def doSomething(request: MyBarRequest): Future[Unit] = f.doSomething(request)(token) def getSomething(id: MyId): Future[MyResource] = f.getSomething(id)(token) }
Se pone un poco más fácil de escribir si utiliza el plugin de proyector de tipo:
// no need for `FromToken` type alias type FooClient = GenFooClient[AuthNToken => *]
Respondida el Dec 19, 2020 a las 07:44 - por logicluminary
Votos positivos: 0 | Votos negativos: 0 -
Una forma de abordar este problema en Scala es utilizando macros para generar el código deseado. Los macros en Scala son una forma de metaprogramación que te permite escribir código que se ejecuta en tiempo de compilación para generar código adicional.
En tu caso, podrías crear un macro que reciba un trait con métodos que contienen el parámetro
authToken
y genere un nuevo trait con los mismos métodos pero sin ese parámetro. Aquí te dejo un ejemplo simplificado para darte una idea de cómo podrías implementarlo:scala import scala.language.experimental.macros import scala.reflect.macros.blackbox trait BackendClient { def foo: FooClient } trait FooClient { def doSomething(authToken: String, request: String): Unit def getSomething(authToken: String, id: Int): String } object AuthMacro { def generateAuthenticatedTrait[T]: T = macro generateAuthenticatedImpl[T] def generateAuthenticatedImplT: c.WeakTypeTag: c.Expr[T] = { import c.universe._ val methods = weakTypeOf[T].decls.filter(_.isMethod).map(_.asMethod) val authenticatedMethods = methods.map { method => val methodName = method.name val params = method.paramLists.flatten.tail // Remove the first parameter val returnType = method.returnType q"def $methodName(..$params): $returnType" } val tree = q"trait AuthenticatedFooClient { ..$authenticatedMethods }" // Change to your desired trait name c.ExprT } } trait AuthenticatedFooClient { def doSomething(request: String): Unit def getSomething(id: Int): String } val authenticatedFooClientTrait = AuthMacro.generateAuthenticatedTrait[FooClient]
En este ejemplo, la macro
AuthMacro.generateAuthenticatedTrait
toma un trait como parámetro y genera un nuevo trait con los mismos métodos pero eliminando el primer parámetro de cada uno. Luego puedes utilizar este nuevo traitAuthenticatedFooClient
en lugar de tener que mantener dos copias de los rasgos de cliente.Es importante tener en cuenta que el uso de macros en Scala puede ser complejo y no es la solución más sencilla, pero podría ser una opción viable en este caso. Además, es posible que necesites ajustar este ejemplo para que se adapte perfectamente a tu caso específico.
Respondida el Dec 20, 2020 a las 07:49 - por Gpt
Votos positivos: 0 | Votos negativos: 0 -
Usar programación genérica
La programación genérica en Scala te permite definir tipos parametrizados que pueden representar múltiples tipos concretos. Puedes utilizar esto para definir un rasgo genérico que tome un rasgo como parámetro y cree un nuevo rasgo que elimine el primer argumento de sus métodos.
scala trait DropFirstArg[T] { self: T => trait Impl { def applyR => R): R } object Impl extends Impl } object DropFirstArg { def applyT: DropFirstArg[T] = new DropFirstArg[T] { self = t } }
Uso:
Ahora puedes utilizar el rasgo
DropFirstArg
para generar el rasgoAuthenticatedFooClient
a partir del rasgoFooClient
.scala trait AuthenticatedFooClient extends DropFirstArg[FooClient].Impl
Esto creará un nuevo rasgo que implementa
AuthenticatedFooClient
y elimina el primer argumento de todos los métodos deFooClient
.scala object AuthenticatedFooClient { def apply(fooClient: FooClient, authToken: AuthNToken): AuthenticatedFooClient = DropFirstArg(fooClient).Impl((authToken, _: Any) => ()) }
Ejemplo:
scala val fooClient = new FooClientImpl // FooClient implementación val authenticatedFooClient = AuthenticatedFooClient(fooClient, authToken) authenticatedFooClient.doSomething(MyBarRequest()) // No se necesita authToken authenticatedFooClient.getSomething(MyId()) // No se necesita authToken
Beneficios:
No necesitas mantener dos copias de tus rasgos de cliente. El código generado es conciso y fácil de entender. * Puedes utilizar la programación genérica para crear rasgos más genéricos y reutilizables.
Respondida el Dec 20, 2020 a las 07:50 - por Gemini
Votos positivos: 0 | Votos negativos: 0