Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Provide access to akka-http request/response attributes #1961

Merged
merged 1 commit into from
Jul 29, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Provide access to akka-http request/response attributes
Fixes #859

This adds an `attribute`/`getAttribute` method to `Metadata`, and connects it
to the underlying request/response that the metadata has been sourced from.

I haven't implemented passing attributes on from user created metadata, I'm
not sure if there's a use case for that.
  • Loading branch information
jroper committed Jul 19, 2024
commit 63d74270c713968d071797e0bf4f4af46d298bba
Original file line number Diff line number Diff line change
@@ -156,7 +156,7 @@ public class @{serviceName}HandlerFactory {
private static CompletionStage<akka.http.javadsl.model.HttpResponse> handle(akka.http.javadsl.model.HttpRequest request, String method, @serviceName implementation, Materializer mat, akka.japi.Function<ActorSystem, akka.japi.Function<Throwable, Trailers>> eHandler, ClassicActorSystemProvider system) {
return GrpcMarshalling.negotiated(request, (reader, writer) -> {
final CompletionStage<akka.http.javadsl.model.HttpResponse> response;
@{if(powerApis) { "Metadata metadata = MetadataBuilder.fromHeaders(request.getHeaders());" } else { "" }}
@{if(powerApis) { "Metadata metadata = MetadataBuilder.fromHttpMessage(request);" } else { "" }}
switch(method) {
@for(method <- service.methods) {
case "@method.grpcName":
Original file line number Diff line number Diff line change
@@ -119,7 +119,7 @@ object @{serviceName}Handler {
(method match {
@for(method <- service.methods) {
case "@method.grpcName" =>
@{if(powerApis) { "val metadata = MetadataBuilder.fromHeaders(request.headers)" } else { "" }}
@{if(powerApis) { "val metadata = MetadataBuilder.fromHttpMessage(request)" } else { "" }}
@{method.unmarshal}(request.entity)(@method.deserializer.name, mat, reader)
.@{if(method.outputStreaming) { "map" } else { "flatMap" }}(implementation.@{method.nameSafe}(_@{if(powerApis) { ", metadata" } else { "" }}))
.map(e => @{method.marshal}(e, eHandler)(@method.serializer.name, writer, system))
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# not for user extension
ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.grpc.javadsl.Metadata.getAttribute")
ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.grpc.scaladsl.Metadata.akka$grpc$scaladsl$Metadata$_setter_$rawHttpMessage_=")
ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.grpc.scaladsl.Metadata.rawHttpMessage")
ProblemFilters.exclude[ReversedMissingMethodProblem]("akka.grpc.scaladsl.Metadata.attribute")
Original file line number Diff line number Diff line change
@@ -303,10 +303,10 @@ object AkkaHttpClientUtils {
.mapMaterializedValue(_ =>
Future.successful(new GrpcResponseMetadata() {
override def headers: akka.grpc.scaladsl.Metadata =
new HeaderMetadataImpl(response.headers)
new HttpMessageMetadataImpl(response)

override def getHeaders(): akka.grpc.javadsl.Metadata =
new JavaMetadataImpl(new HeaderMetadataImpl(response.headers))
new JavaMetadataImpl(new HttpMessageMetadataImpl(response))

override def trailers: Future[akka.grpc.scaladsl.Metadata] =
trailerPromise.future.map(new HeaderMetadataImpl(_))
22 changes: 21 additions & 1 deletion runtime/src/main/scala/akka/grpc/internal/MetadataImpl.scala
Original file line number Diff line number Diff line change
@@ -9,7 +9,8 @@ import scala.collection.JavaConverters._
import scala.collection.immutable
import scala.compat.java8.OptionConverters._
import akka.annotation.InternalApi
import akka.http.scaladsl.model.HttpHeader
import akka.http.scaladsl.model.{ AttributeKey, HttpHeader, HttpMessage }
import akka.http.javadsl.{ model => jm }
import akka.japi.Pair
import akka.util.ByteString
import akka.grpc.scaladsl.{ BytesEntry, Metadata, MetadataEntry, MetadataStatus, StringEntry }
@@ -107,6 +108,8 @@ class GrpcMetadataImpl(delegate: io.grpc.Metadata) extends Metadata {
delegate.keys.iterator.asScala.flatMap(key => getEntries(key).map(entry => (key, entry))).toList
}

override def attribute[T](key: AttributeKey[T]): Option[T] = None

override def toString: String =
MetadataImpl.niceStringRep(map)

@@ -149,6 +152,8 @@ class EntryMetadataImpl(entries: List[(String, MetadataEntry)] = Nil) extends Me

override def toString: String =
MetadataImpl.niceStringRep(asMap)

override def attribute[T](key: AttributeKey[T]): Option[T] = None
}

/**
@@ -182,6 +187,8 @@ class HeaderMetadataImpl(headers: immutable.Seq[HttpHeader] = immutable.Seq.empt
override def asList: List[(String, MetadataEntry)] =
headers.map(toKeyEntry).toList

override def attribute[T](key: AttributeKey[T]): Option[T] = None

override def toString: String =
MetadataImpl.niceStringRep(asMap)

@@ -199,6 +206,16 @@ class HeaderMetadataImpl(headers: immutable.Seq[HttpHeader] = immutable.Seq.empt
}
}

/**
* This class wraps an HttpMessage with the Metadata interface.
*
* @param message The HTTP message to wrap.
*/
class HttpMessageMetadataImpl(message: HttpMessage) extends HeaderMetadataImpl(message.headers) {
override def attribute[T](key: AttributeKey[T]): Option[T] = message.attribute(key)
override private[grpc] val rawHttpMessage = Some(message)
}

/**
* This class wraps a scaladsl.Metadata instance with the javadsl.Metadata interface.
*
@@ -226,6 +243,9 @@ class JavaMetadataImpl(val delegate: Metadata) extends javadsl.Metadata with jav
override def asScala: Metadata =
delegate

override def getAttribute[T](key: jm.AttributeKey[T]): Optional[T] =
delegate.rawHttpMessage.flatMap(_.attribute(key)).asJava

override def toString: String =
delegate.toString

11 changes: 10 additions & 1 deletion runtime/src/main/scala/akka/grpc/javadsl/Metadata.scala
Original file line number Diff line number Diff line change
@@ -4,11 +4,12 @@

package akka.grpc.javadsl

import java.util.{ List => JList, Map => JMap, Optional }
import java.util.{ Optional, List => JList, Map => JMap }
import akka.annotation.{ ApiMayChange, DoNotInherit }
import akka.util.ByteString
import akka.japi.Pair
import akka.grpc.scaladsl
import akka.http.javadsl.model.AttributeKey

/**
* Immutable representation of the metadata in a call
@@ -47,6 +48,14 @@ trait Metadata {
* @return Returns the scaladsl.Metadata interface for this instance.
*/
def asScala: scaladsl.Metadata

/**
* Get an attribute from the underlying akka-http message associated with this metadata.
*
* Will return `None` if this metadata is not associated with an akka-http request or response, for example,
* if using the netty client support.
*/
def getAttribute[T](key: AttributeKey[T]): Optional[T]
}

/**
21 changes: 18 additions & 3 deletions runtime/src/main/scala/akka/grpc/javadsl/MetadataBuilder.scala
Original file line number Diff line number Diff line change
@@ -5,12 +5,11 @@
package akka.grpc.javadsl

import java.lang.{ Iterable => jIterable }

import akka.annotation.ApiMayChange

import scala.collection.JavaConverters._
import akka.http.javadsl.model.HttpHeader
import akka.http.scaladsl.model.{ HttpHeader => sHttpHeader }
import akka.http.javadsl.model.{ HttpHeader, HttpMessage }
import akka.http.scaladsl.model.{ HttpHeader => sHttpHeader, HttpMessage => sHttpMessage }
import akka.http.scaladsl.model.headers.RawHeader
import akka.util.ByteString
import akka.grpc.scaladsl
@@ -69,6 +68,22 @@ object MetadataBuilder {
def fromHeaders(headers: jIterable[HttpHeader]): Metadata =
new JavaMetadataImpl(scaladsl.MetadataBuilder.fromHeaders(headers.asScala.map(asScala).toList))

/**
* Constructs a Metadata instance from an HTTP message.
*
* @param message The HTTP message.
* @return The metadata instance.
*/
def fromHttpMessage(message: HttpMessage): Metadata = {
message match {
// All HTTP messages should be scaladsl HttpMessages
case s: sHttpMessage => new JavaMetadataImpl(scaladsl.MetadataBuilder.fromHttpMessage(s))
// If not, just pass the headers
case _ => fromHeaders(message.getHeaders)
}

}

/**
* Converts from a javadsl.HttpHeader to a scaladsl.HttpHeader.
* @param header A Java HTTP header.
10 changes: 10 additions & 0 deletions runtime/src/main/scala/akka/grpc/scaladsl/Metadata.scala
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@
package akka.grpc.scaladsl

import akka.annotation.{ ApiMayChange, DoNotInherit, InternalApi }
import akka.http.scaladsl.model.{ AttributeKey, HttpMessage }
import akka.util.ByteString
import com.google.protobuf.any

@@ -21,6 +22,7 @@ import com.google.protobuf.any
*/
@InternalApi
private[grpc] val raw: Option[io.grpc.Metadata] = None
private[grpc] val rawHttpMessage: Option[HttpMessage] = None

/**
* @return The text header value for `key` if one exists, if the same key has multiple values the last occurrence
@@ -45,6 +47,14 @@ import com.google.protobuf.any
*/
@ApiMayChange
def asList: List[(String, MetadataEntry)]

/**
* Get an attribute from the underlying akka-http message associated with this metadata.
*
* Will return `None` if this metadata is not associated with an akka-http request or response, for example,
* if using the netty client support.
*/
def attribute[T](key: AttributeKey[T]): Option[T]
}

/**
14 changes: 12 additions & 2 deletions runtime/src/main/scala/akka/grpc/scaladsl/MetadataBuilder.scala
Original file line number Diff line number Diff line change
@@ -6,9 +6,9 @@ package akka.grpc.scaladsl

import scala.collection.immutable
import akka.annotation.{ ApiMayChange, DoNotInherit }
import akka.http.scaladsl.model.HttpHeader
import akka.http.scaladsl.model.{ HttpHeader, HttpMessage }
import akka.util.ByteString
import akka.grpc.internal.{ EntryMetadataImpl, HeaderMetadataImpl, MetadataImpl }
import akka.grpc.internal.{ EntryMetadataImpl, HeaderMetadataImpl, HttpMessageMetadataImpl, MetadataImpl }

/**
* This class provides an interface for constructing immutable Metadata instances.
@@ -78,4 +78,14 @@ object MetadataBuilder {
*/
def fromHeaders(headers: immutable.Seq[HttpHeader]): Metadata =
new HeaderMetadataImpl(headers)

/**
* Creates a Metadata instance from an HTTP message.
*
* @param message The message
* @return The new Metadata instance.
*/
def fromHttpMessage(message: HttpMessage): Metadata =
new HttpMessageMetadataImpl(message)

}
24 changes: 23 additions & 1 deletion runtime/src/test/scala/akka/grpc/internal/MetadataImplSpec.scala
Original file line number Diff line number Diff line change
@@ -4,7 +4,8 @@

package akka.grpc.internal

import akka.grpc.scaladsl.{ BytesEntry, Metadata, StringEntry }
import akka.grpc.scaladsl.{ BytesEntry, Metadata, MetadataBuilder, StringEntry }
import akka.http.scaladsl.model.AttributeKey
import akka.http.scaladsl.model.headers.RawHeader
import akka.util.ByteString
import org.scalatest.concurrent.ScalaFutures
@@ -72,6 +73,27 @@ class MetadataImplSpec extends AnyWordSpec with Matchers with ScalaFutures {
testMetadata(new HeaderMetadataImpl(headers))
}

"HttpMessageMetadataImpl" should {
val headers = TEXT_ENTRIES.collect {
case (k, v) => RawHeader(k, v)
} ++ BINARY_ENTRIES.collect {
case (k, v) => RawHeader(k, MetadataImpl.encodeBinaryHeader(v))
} ++ DUPE_TEXT_VALUES.map(v => RawHeader(DUPE_TEXT_KEY, v)) ++ DUPE_BINARY_VALUES.map(v =>
RawHeader(DUPE_BINARY_KEY, MetadataImpl.encodeBinaryHeader(v)))

testMetadata(new HeaderMetadataImpl(headers))

"grant access HttpRequest attributes" in {
case class MyAttribute(value: Int)
val myAttributeKey = AttributeKey("my-attribute", classOf[MyAttribute])
val value = MyAttribute(10)
val request = akka.http.scaladsl.model.HttpRequest().withAttributes(Map(myAttributeKey -> value))
val metadata = MetadataBuilder.fromHttpMessage(request)

metadata.attribute(myAttributeKey) should be(Some(value))
}
}

def testMetadata(m: Metadata): Unit = {
"return expected text values" in {
TEXT_ENTRIES.foreach {