diff --git a/build.sbt b/build.sbt index 1c8fdf4..7da0ada 100644 --- a/build.sbt +++ b/build.sbt @@ -90,7 +90,15 @@ lazy val rootProject = (project in file(".")) .aggregate(projectAggregates: _*) lazy val core = (projectMatrix in file("core")) - .settings(name := "core") + .settings( + name := "core", + libraryDependencies ++= { + CrossVersion.partialVersion(scalaVersion.value) match { + case Some((3, _)) => Nil + case _ => Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided) + } + } + ) .jvmPlatform( scalaVersions = scala2 ++ scala3, settings = commonJvmSettings diff --git a/core/src/main/scala-2/sttp/attributes/AttributeKeyMacro.scala b/core/src/main/scala-2/sttp/attributes/AttributeKeyMacro.scala new file mode 100644 index 0000000..2d91879 --- /dev/null +++ b/core/src/main/scala-2/sttp/attributes/AttributeKeyMacro.scala @@ -0,0 +1,12 @@ +package sttp.attributes + +import scala.reflect.macros.blackbox + +private[attributes] object AttributeKeyMacro { + def apply[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[AttributeKey[T]] = { + import c.universe._ + c.Expr[AttributeKey[T]]( + q"new _root_.sttp.tapir.AttributeKey(${c.universe.show(implicitly[c.WeakTypeTag[T]].tpe)})" + ) + } +} diff --git a/core/src/main/scala-2/sttp/attributes/AttributeKeyMacros.scala b/core/src/main/scala-2/sttp/attributes/AttributeKeyMacros.scala new file mode 100644 index 0000000..2a27b4e --- /dev/null +++ b/core/src/main/scala-2/sttp/attributes/AttributeKeyMacros.scala @@ -0,0 +1,7 @@ +package sttp.attributes + +trait AttributeKeyMacros { + + /** Create an attribute key which contains the full type name of the given type, as inferred by the macro. */ + def apply[T]: AttributeKey[T] = macro AttributeKeyMacro[T] +} diff --git a/core/src/main/scala-3/sttp/attributes/AttributeKeyMacros.scala b/core/src/main/scala-3/sttp/attributes/AttributeKeyMacros.scala new file mode 100644 index 0000000..4490ea6 --- /dev/null +++ b/core/src/main/scala-3/sttp/attributes/AttributeKeyMacros.scala @@ -0,0 +1,18 @@ +package sttp.attributes + +import scala.quoted.* + +trait AttributeKeyMacros { + + /** Create an attribute key which contains the full type name of the given type, as inferred by the macro. */ + inline def apply[T]: AttributeKey[T] = ${ AttributeKeyMacros[T] } +} + +private[attributes] object AttributeKeyMacros { + def apply[T: Type](using q: Quotes): Expr[AttributeKey[T]] = { + import quotes.reflect.* + val t = TypeRepr.of[T] + + '{ new AttributeKey[T](${ Expr(t.show) }) } + } +} diff --git a/core/src/main/scala/sttp/attributes/AttributeKey.scala b/core/src/main/scala/sttp/attributes/AttributeKey.scala new file mode 100644 index 0000000..b461249 --- /dev/null +++ b/core/src/main/scala/sttp/attributes/AttributeKey.scala @@ -0,0 +1,20 @@ +package sttp.attributes + +/** A key to be used in an [[AttributeMap]]. The key typically stores the full type name of the value of the attribute. + * Keys can be conveniently created using the `AttributeKey[T]` macro in the companion object. + * + * @param typeName + * The fully qualified name of `T`. + * @tparam T + * Type of the value of the attribute. + */ +class AttributeKey[T](val typeName: String) { + override def equals(other: Any): Boolean = other match { + case that: AttributeKey[_] => typeName == that.typeName + case _ => false + } + + override def hashCode(): Int = typeName.hashCode +} + +object AttributeKey extends AttributeKeyMacros diff --git a/core/src/main/scala/sttp/attributes/AttributeMap.scala b/core/src/main/scala/sttp/attributes/AttributeMap.scala new file mode 100644 index 0000000..94ef72b --- /dev/null +++ b/core/src/main/scala/sttp/attributes/AttributeMap.scala @@ -0,0 +1,24 @@ +package sttp.attributes + +/** A type-safe map from [[AttributeKey]] to values of types determined by the key. + * + * An attribute is arbitrary data that is attached to some top-level sttp-defined objects. The data is not interpreted + * by the libraries in any way, and is passed as-is. However, built-in or user-provided extensions and integrations + * might use the attributes to provide their functionality. + * + * Typically, you'll add attributes using an `.attribute` method on the sttp-defined object. Each attribute should + * correspond to a type. The type should be defined by the extension, which uses the attribute, and make it available + * for import. + */ +case class AttributeMap(private val storage: Map[String, Any]) { + def get[T](k: AttributeKey[T]): Option[T] = storage.get(k.typeName).asInstanceOf[Option[T]] + def put[T](k: AttributeKey[T], v: T): AttributeMap = copy(storage = storage + (k.typeName -> v)) + def remove[T](k: AttributeKey[T]): AttributeMap = copy(storage = storage - k.typeName) + + def isEmpty: Boolean = storage.isEmpty + def nonEmpty: Boolean = storage.nonEmpty +} + +object AttributeMap { + val Empty: AttributeMap = AttributeMap(Map.empty) +}