Skip to content

Commit

Permalink
Move from tags to attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
adamw committed Dec 10, 2024
1 parent 941ccf4 commit 10f15d0
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 56 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ val zio2InteropRsVersion = "2.0.2"

val oxVersion = "0.5.1"
val sttpModelVersion = "1.7.11"
val sttpSharedVersion = "1.4.0"
val sttpSharedVersion = "1.4.1"

val logback = "ch.qos.logback" % "logback-classic" % "1.5.12"

Expand Down
5 changes: 3 additions & 2 deletions core/src/main/scala/sttp/client4/SpecifyAuthScheme.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ package sttp.client4
import sttp.client4.internal.DigestAuthenticator
import sttp.client4.internal.Utf8
import java.util.Base64
import sttp.attributes.AttributeKey

class SpecifyAuthScheme[+R <: PartialRequestBuilder[R, _]](
hn: String,
req: R,
digestTag: String
digestAttributeKey: AttributeKey[DigestAuthenticator.DigestAuthData]
) {
def basic(user: String, password: String): R = {
val c = new String(Base64.getEncoder.encode(s"$user:$password".getBytes(Utf8)), Utf8)
Expand All @@ -21,5 +22,5 @@ class SpecifyAuthScheme[+R <: PartialRequestBuilder[R, _]](
req.header(hn, s"Bearer $token")

def digest(user: String, password: String): R =
req.tag(digestTag, DigestAuthenticator.DigestAuthData(user, password))
req.attribute(digestAttributeKey, DigestAuthenticator.DigestAuthData(user, password))
}
3 changes: 2 additions & 1 deletion core/src/main/scala/sttp/client4/SttpApi.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import sttp.capabilities.Streams
import sttp.capabilities.Effect
import sttp.client4.wrappers.FollowRedirectsBackend
import sttp.client4.logging.LoggingOptions
import sttp.attributes.AttributeMap

trait SttpApi extends SttpExtensions with UriInterpolator {
val DefaultReadTimeout: Duration = 1.minute
Expand All @@ -34,7 +35,7 @@ trait SttpApi extends SttpExtensions with UriInterpolator {
httpVersion = None,
loggingOptions = LoggingOptions()
),
Map()
AttributeMap.Empty
)

/** A starting request, with the following modification comparing to [[emptyRequest]]: `Accept-Encoding` is set to
Expand Down
57 changes: 33 additions & 24 deletions core/src/main/scala/sttp/client4/request.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import sttp.client4.internal.{ToCurlConverter, ToRfc2616Converter}
import sttp.shared.Identity

import scala.collection.immutable.Seq
import sttp.attributes.AttributeMap

/** A generic description of an HTTP request, along with a description of how the response body should be handled.
*
Expand Down Expand Up @@ -67,8 +68,8 @@ trait GenericRequest[+T, -R] extends RequestBuilder[GenericRequest[T, R]] with R
* @param response
* Description of how the response body should be handled. Needs to be specified upfront so that the response is
* always consumed and hence there are no requirements on client code to consume it.
* @param tags
* Request-specific tags which can be used by backends for logging, metrics, etc. Empty by default.
* @param attributes
* Request-specific attributes which can be used by backends for logging, metrics, etc. Empty by default.
* @tparam T
* The target type, to which the response body should be read.
*/
Expand All @@ -79,7 +80,7 @@ case class Request[T](
headers: Seq[Header],
response: ResponseAs[T],
options: RequestOptions,
tags: Map[String, Any]
attributes: AttributeMap
) extends GenericRequest[T, Any]
with RequestBuilder[Request[T]] {

Expand All @@ -88,11 +89,19 @@ case class Request[T](
override def method(method: Method, uri: Uri): Request[T] = copy(uri = uri, method = method)
override def withHeaders(headers: Seq[Header]): Request[T] = copy(headers = headers)
override def withOptions(options: RequestOptions): Request[T] = copy(options = options)
override def withTags(tags: Map[String, Any]): Request[T] = copy(tags = tags)
override def withAttributes(attributes: AttributeMap): Request[T] = copy(attributes = attributes)
override protected def copyWithBody(body: BasicBody): Request[T] = copy(body = body)

def multipartStreamBody[S](ps: Seq[Part[BodyPart[S]]]): StreamRequest[T, S] =
StreamRequest(method, uri, MultipartStreamBody(ps), headers, StreamResponseAs(response.delegate), options, tags)
StreamRequest(
method,
uri,
MultipartStreamBody(ps),
headers,
StreamResponseAs(response.delegate),
options,
attributes
)

def multipartStreamBody[S](p1: Part[BodyPart[S]], ps: Part[BodyPart[S]]*): StreamRequest[T, S] =
StreamRequest(
Expand All @@ -102,11 +111,11 @@ case class Request[T](
headers,
StreamResponseAs(response.delegate),
options,
tags
attributes
)

def streamBody[S](s: Streams[S])(b: s.BinaryStream): StreamRequest[T, S] =
StreamRequest(method, uri, StreamBody(s)(b), headers, StreamResponseAs(response.delegate), options, tags)
StreamRequest(method, uri, StreamBody(s)(b), headers, StreamResponseAs(response.delegate), options, attributes)

/** Specifies the target type to which the response body should be read. Note that this replaces any previous
* specifications, which also includes any previous `mapResponse` invocations.
Expand All @@ -117,19 +126,19 @@ case class Request[T](

/** Specifies that this is a WebSocket request. A [[WebSocketBackend]] will be required to send this request. */
def response[F[_], T2](ra: WebSocketResponseAs[F, T2]): WebSocketRequest[F, T2] =
WebSocketRequest(method, uri, body, headers, ra, options, tags)
WebSocketRequest(method, uri, body, headers, ra, options, attributes)

/** Specifies that the response body should be processed using a non-blocking, asynchronous stream, as witnessed by
* the `S` capability. A [[StreamBackend]] will be required to send this request.
*/
def response[T2, S](ra: StreamResponseAs[T2, S]): StreamRequest[T2, S] =
StreamRequest(method, uri, body, headers, ra, options, tags)
StreamRequest(method, uri, body, headers, ra, options, attributes)

/** Specifies that this is a WebSocket request, and the WebSocket will be processed using a non-blocking, asynchronous
* stream, as witnessed by the `S` capability. A [[WebSocketStreamBackend]] will be required to send this request.
*/
def response[T2, S](ra: WebSocketStreamResponseAs[T2, S]): WebSocketStreamRequest[T2, S] =
WebSocketStreamRequest(method, uri, body, headers, ra, options, tags)
WebSocketStreamRequest(method, uri, body, headers, ra, options, attributes)

/** Sends the request, using the given backend.
*
Expand Down Expand Up @@ -182,8 +191,8 @@ object Request {
* @param response
* Description of how the response body should be handled. Needs to be specified upfront so that the response is
* always consumed and hence there are no requirements on client code to consume it.
* @param tags
* Request-specific tags which can be used by backends for logging, metrics, etc. Empty by default.
* @param attributes
* Request-specific attributes which can be used by backends for logging, metrics, etc. Empty by default.
* @tparam T
* The target type, to which the response body should be read. If the response body is streamed, this might be the
* value obtained by processing the entire stream.
Expand All @@ -197,7 +206,7 @@ final case class StreamRequest[T, R](
headers: Seq[Header],
response: StreamResponseAs[T, R],
options: RequestOptions,
tags: Map[String, Any]
attributes: AttributeMap
) extends GenericRequest[T, R]
with RequestBuilder[StreamRequest[T, R]] {

Expand All @@ -206,7 +215,7 @@ final case class StreamRequest[T, R](
override def method(method: Method, uri: Uri): StreamRequest[T, R] = copy(method = method, uri = uri)
override def withHeaders(headers: Seq[Header]): StreamRequest[T, R] = copy(headers = headers)
override def withOptions(options: RequestOptions): StreamRequest[T, R] = copy(options = options)
override def withTags(tags: Map[String, Any]): StreamRequest[T, R] = copy(tags = tags)
override def withAttributes(attributes: AttributeMap): StreamRequest[T, R] = copy(attributes = attributes)
override protected def copyWithBody(body: BasicBody): StreamRequest[T, R] = copy(body = body)

/** Specifies the target type to which the response body should be read. Note that this replaces any previous
Expand All @@ -229,7 +238,7 @@ final case class StreamRequest[T, R](
headers,
WebSocketStreamResponseAs[T2, Effect[F] with R](ra.delegate),
options,
tags
attributes
)

def mapResponse[T2](f: T => T2): StreamRequest[T2, R] = copy(response = response.map(f))
Expand Down Expand Up @@ -263,8 +272,8 @@ final case class StreamRequest[T, R](
* @param response
* Description of how the WebSocket should be handled. Needs to be specified upfront so that the response is always
* consumed and hence there are no requirements on client code to consume it.
* @param tags
* Request-specific tags which can be used by backends for logging, metrics, etc. Empty by default.
* @param attributes
* Request-specific attributes which can be used by backends for logging, metrics, etc. Empty by default.
* @tparam F
* The effect type used to process the WebSocket. Might include asynchronous computations (e.g.
* [[scala.concurrent.Future]]), pure effect descriptions (`IO`), or synchronous computations ([[Identity]]).
Expand All @@ -279,7 +288,7 @@ final case class WebSocketRequest[F[_], T](
headers: Seq[Header],
response: WebSocketResponseAs[F, T],
options: RequestOptions,
tags: Map[String, Any]
attributes: AttributeMap
) extends GenericRequest[T, WebSockets with Effect[F]]
with RequestBuilder[WebSocketRequest[F, T]] {

Expand All @@ -288,7 +297,7 @@ final case class WebSocketRequest[F[_], T](
override def method(method: Method, uri: Uri): WebSocketRequest[F, T] = copy(method = method, uri = uri)
override def withHeaders(headers: Seq[Header]): WebSocketRequest[F, T] = copy(headers = headers)
override def withOptions(options: RequestOptions): WebSocketRequest[F, T] = copy(options = options)
override def withTags(tags: Map[String, Any]): WebSocketRequest[F, T] = copy(tags = tags)
override def withAttributes(attributes: AttributeMap): WebSocketRequest[F, T] = copy(attributes = attributes)
override protected def copyWithBody(body: BasicBody): WebSocketRequest[F, T] = copy(body = body)

def streamBody[S](s: Streams[S])(b: s.BinaryStream): WebSocketStreamRequest[T, Effect[F] with S] =
Expand All @@ -299,7 +308,7 @@ final case class WebSocketRequest[F[_], T](
headers,
WebSocketStreamResponseAs[T, Effect[F] with S](response.delegate),
options,
tags
attributes
)

def mapResponse[T2](f: T => T2): WebSocketRequest[F, T2] = copy(response = response.map(f))
Expand Down Expand Up @@ -343,8 +352,8 @@ final case class WebSocketRequest[F[_], T](
* @param response
* Description of how the WebSocket should be handled. Needs to be specified upfront so that the response is always
* consumed and hence there are no requirements on client code to consume it.
* @param tags
* Request-specific tags which can be used by backends for logging, metrics, etc. Empty by default.
* @param attributes
* Request-specific attributes which can be used by backends for logging, metrics, etc. Empty by default.
* @tparam T
* The target type, to which the response body should be read. If the WebSocket interactions are described entirely
* by the response description, this might be `Unit`. Otherwise, this can be an `S` stream of frames or mapped
Expand All @@ -359,7 +368,7 @@ final case class WebSocketStreamRequest[T, S](
headers: Seq[Header],
response: WebSocketStreamResponseAs[T, S],
options: RequestOptions,
tags: Map[String, Any]
attributes: AttributeMap
) extends GenericRequest[T, S with WebSockets]
with RequestBuilder[WebSocketStreamRequest[T, S]] {

Expand All @@ -368,7 +377,7 @@ final case class WebSocketStreamRequest[T, S](
override def method(method: Method, uri: Uri): WebSocketStreamRequest[T, S] = copy(method = method, uri = uri)
override def withHeaders(headers: Seq[Header]): WebSocketStreamRequest[T, S] = copy(headers = headers)
override def withOptions(options: RequestOptions): WebSocketStreamRequest[T, S] = copy(options = options)
override def withTags(tags: Map[String, Any]): WebSocketStreamRequest[T, S] = copy(tags = tags)
override def withAttributes(attributes: AttributeMap): WebSocketStreamRequest[T, S] = copy(attributes = attributes)
override protected def copyWithBody(body: BasicBody): WebSocketStreamRequest[T, S] = copy(body = body)

def mapResponse[T2](f: T => T2): WebSocketStreamRequest[T2, S] = copy(response = response.map(f))
Expand Down
45 changes: 30 additions & 15 deletions core/src/main/scala/sttp/client4/requestBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import java.io.InputStream
import java.nio.ByteBuffer
import scala.concurrent.duration.Duration
import scala.collection.immutable.Seq
import sttp.attributes.AttributeKey
import sttp.attributes.AttributeMap

/** The builder methods of requests or partial requests of type `PR`.
*
Expand All @@ -43,8 +45,8 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R]
def response: ResponseAsDelegate[_, _]
def options: RequestOptions

/** Request-specific tags which can be used by backends for logging, metrics, etc. Empty by default. */
def tags: Map[String, Any]
/** Request-specific attributes which can be used by backends for logging, metrics, etc. Empty by default. */
def attributes: AttributeMap

/** Set the method & uri to the given ones. */
def method(method: Method, uri: Uri): R
Expand All @@ -55,8 +57,8 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R]
/** Replace all options with the given ones. */
def withOptions(options: RequestOptions): PR

/** Replace all tags with the given ones. */
def withTags(tags: Map[String, Any]): PR
/** Replace attributes with the given ones. */
def withAttributes(attributes: AttributeMap): PR

protected def copyWithBody(body: BasicBody): PR

Expand Down Expand Up @@ -121,10 +123,21 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R]
*/
def headers(hs: Header*): PR = hs.foldLeft(this)(_.header(_))

/** Allows specifying basic, token, bearer (in the `Authorization` header) or digest authentication for this request.
*/
def auth: SpecifyAuthScheme[PR] =
new SpecifyAuthScheme[PR](HeaderNames.Authorization, this, DigestAuthenticationBackend.DigestAuthTag)
new SpecifyAuthScheme[PR](HeaderNames.Authorization, this, DigestAuthenticationBackend.DigestAuthAttributeKey)

/** Allows specifying basic, token, bearer (in the `Proxy-Authorization` header) or digest proxy authentication for
* this request.
*/
def proxyAuth: SpecifyAuthScheme[PR] =
new SpecifyAuthScheme[PR](HeaderNames.ProxyAuthorization, this, DigestAuthenticationBackend.ProxyDigestAuthTag)
new SpecifyAuthScheme[PR](
HeaderNames.ProxyAuthorization,
this,
DigestAuthenticationBackend.ProxyDigestAuthAttributeKey
)

def acceptEncoding(encoding: String): PR = header(HeaderNames.AcceptEncoding, encoding)

/** Adds the given cookie. Any previously defined cookies are left intact. */
Expand Down Expand Up @@ -288,10 +301,6 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R]
*/
def redirectToGet(r: Boolean): PR = withOptions(options.copy(redirectToGet = r))

def tag(k: String, v: Any): PR = withTags(tags + (k -> v))

def tag(k: String): Option[Any] = tags.get(k)

/** Disables auto-decompression of response bodies which are received with supported `Content-Encoding headers. */
def disableAutoDecompression: PR = withOptions(options.copy(disableAutoDecompression = true))

Expand Down Expand Up @@ -341,6 +350,12 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R]
*/
def loggingOptions: LoggingOptions = options.loggingOptions

/** Reads a per-request attribute for the given key, if present. */
def attribute[T](k: AttributeKey[T]): Option[T] = attributes.get(k)

/** Sets a per-request attribute for the given key, with the given value. */
def attribute[T](k: AttributeKey[T], v: T): PR = withAttributes(attributes.put(k, v))

def show(
includeBody: Boolean = true,
includeHeaders: Boolean = true,
Expand All @@ -359,8 +374,8 @@ trait PartialRequestBuilder[+PR <: PartialRequestBuilder[PR, R], +R]
* @param response
* Description of how the response body should be handled. Needs to be specified upfront so that the response is
* always consumed and hence there are no requirements on client code to consume it.
* @param tags
* Request-specific tags which can be used by backends for logging, metrics, etc. Empty by default.
* @param attributes
* Request-specific attributes which can be used by backends for logging, metrics, etc. Empty by default.
* @tparam T
* The target type, to which the response body should be read.
*/
Expand All @@ -369,16 +384,16 @@ final case class PartialRequest[T](
headers: Seq[Header],
response: ResponseAs[T],
options: RequestOptions,
tags: Map[String, Any]
attributes: AttributeMap
) extends PartialRequestBuilder[PartialRequest[T], Request[T]] {

override def showBasic: String = "(no method & uri set)"

override def method(method: Method, uri: Uri): Request[T] =
Request(method, uri, body, headers, response, options, tags)
Request(method, uri, body, headers, response, options, attributes)
override def withHeaders(headers: Seq[Header]): PartialRequest[T] = copy(headers = headers)
override def withOptions(options: RequestOptions): PartialRequest[T] = copy(options = options)
override def withTags(tags: Map[String, Any]): PartialRequest[T] = copy(tags = tags)
override def withAttributes(attributes: AttributeMap): PartialRequest[T] = copy(attributes = attributes)
override protected def copyWithBody(body: BasicBody): PartialRequest[T] = copy(body = body)
def response[T2](ra: ResponseAs[T2]): PartialRequest[T2] = copy(response = ra)
}
Expand Down
Loading

0 comments on commit 10f15d0

Please sign in to comment.