Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

sdk-common: add Context #391

Merged
merged 1 commit into from
Dec 12, 2023
Merged
Show file tree
Hide file tree
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
/*
* Copyright 2023 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.otel4s
package sdk.context

import cats.Hash
import cats.Show
import cats.effect.kernel.Unique

/** A type-safe immutable storage.
*
* @see
* [[https://opentelemetry.io/docs/specs/otel/context]]
*/
sealed trait Context {

/** Retrieves the value associated with the given `key` from the context, if
* such a value exists.
*/
def get[A](key: Context.Key[A]): Option[A]

/** Creates a copy of this context with the given `value` associated with the
* given `key`.
*/
def updated[A](key: Context.Key[A], value: A): Context

override final def toString: String = Show[Context].show(this)
}

object Context {
private val Empty: Context = new MapContext(Map.empty)

/** A key for use with a [[Context]].
*
* @param name
* the name of the key
*
* @tparam A
* the type of the value that can be associated with this key
*/
final class Key[A] private (
val name: String,
private[context] val unique: Unique.Token
) extends context.Key[A] {

override def hashCode(): Int = Hash[Key[A]].hash(this)

override def equals(obj: Any): Boolean =
obj match {
case other: Key[A @unchecked] => Hash[Key[A]].eqv(this, other)
case _ => false
}

override def toString: String = Show[Key[A]].show(this)
}

object Key {

/** Creates a unique key with the given '''debug''' name.
*
* '''Keys may have the same debug name but they aren't equal:'''
* {{{
* for {
* key1 <- Key.unique[IO, Int]("key")
* key2 <- Key.unique[IO, Int]("key")
* } yield key1 == key2 // false
* }}}
*
* @param name
* the '''debug''' name of the key
*
* @tparam A
* the type of the value that can be associated with this key
*/
def unique[F[_]: Unique, A](name: String): F[Key[A]] =
Unique[F].applicative.map(Unique[F].unique)(u => new Key(name, u))

implicit def keyHash[A]: Hash[Key[A]] = Hash.by(_.unique)

implicit def keyShow[A]: Show[Key[A]] = Show(k => s"Key(${k.name})")

implicit def keyProvider[F[_]: Unique]: context.Key.Provider[F, Key] =
new context.Key.Provider[F, Key] {
def uniqueKey[A](name: String): F[Key[A]] = unique(name)
}
}

/** The empty [[Context]].
*/
def root: Context = Empty

implicit val contextShow: Show[Context] = Show { case ctx: MapContext =>
ctx.storage
.map { case (key, value) => s"${key.name}=$value" }
.mkString("Context{", ", ", "}")
}

implicit object Contextual extends context.Contextual[Context] {
type Key[A] = Context.Key[A]

def get[A](ctx: Context)(key: Key[A]): Option[A] =
ctx.get(key)

def updated[A](ctx: Context)(key: Key[A], value: A): Context =
ctx.updated(key, value)

def root: Context = Context.root
}

private[context] final class MapContext(
private[context] val storage: Map[Context.Key[_], Any]
) extends Context {
def get[A](key: Key[A]): Option[A] =
storage.get(key).map(_.asInstanceOf[A])

def updated[A](key: Key[A], value: A): Context =
new MapContext(storage.updated(key, value))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2023 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.otel4s.sdk.context

import cats.Show
import cats.effect.SyncIO
import cats.kernel.laws.discipline.HashTests
import munit.DisciplineSuite
import org.scalacheck.Arbitrary
import org.scalacheck.Cogen
import org.scalacheck.Gen
import org.scalacheck.Prop

class ContextKeySuite extends DisciplineSuite {

private def keyGen[A]: Gen[Context.Key[A]] =
for {
name <- Gen.alphaNumStr
} yield Context.Key.unique[SyncIO, A](name).unsafeRunSync()

private implicit val contextKeyArb: Arbitrary[Context.Key[String]] =
Arbitrary(keyGen)

private implicit val contextKeyCogen: Cogen[Context.Key[String]] =
Cogen[Int].contramap(v => v.unique.hashCode)

checkAll("Context.Key.HashLaws", HashTests[Context.Key[String]].hash)

test("Show[Context.Key[_]]") {
Prop.forAll(keyGen[String]) { key =>
assertEquals(Show[Context.Key[String]].show(key), s"Key(${key.name})")
assertEquals(Show[Context.Key[String]].show(key), key.toString)
}
}

test("keys with the same name must be unique") {
val key1 = Context.Key.unique[SyncIO, String]("key").unsafeRunSync()
val key2 = Context.Key.unique[SyncIO, String]("key").unsafeRunSync()

assertNotEquals(key1, key2)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2023 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.otel4s.sdk.context

import cats.Show
import cats.effect.SyncIO
import munit.DisciplineSuite
import org.scalacheck.Gen
import org.scalacheck.Prop

class ContextSuite extends DisciplineSuite {

private def keyGen[A]: Gen[Context.Key[A]] =
for {
name <- Gen.alphaNumStr
} yield Context.Key.unique[SyncIO, A](name).unsafeRunSync()

private val contextGen: Gen[Context] =
for {
stringKey <- keyGen[String]
string <- Gen.alphaNumStr
intKey <- keyGen[Int]
int <- Gen.posNum[Int]
doubleKey <- keyGen[Double]
double <- Gen.double
longKey <- keyGen[Long]
long <- Gen.long
} yield Context.root
.updated(stringKey, string)
.updated(intKey, int)
.updated(doubleKey, double)
.updated(longKey, long)

test("get values from the context") {
Prop.forAll(keyGen[String], Gen.alphaNumStr) { case (key, value) =>
val ctx = Context.root.updated(key, value)
assertEquals(ctx.get(key), Some(value))
}
}

test("override values in the context") {
Prop.forAll(keyGen[String], Gen.alphaNumStr, Gen.alphaNumStr) {
case (key, value1, value2) =>
val ctx = Context.root.updated(key, value1).updated(key, value2)
assertEquals(ctx.get(key), Some(value2))
}
}

test("Show[Context]") {
Prop.forAll(contextGen) { ctx =>
val expected = ctx match {
case m: Context.MapContext =>
m.storage
.map { case (key, value) => s"${key.name}=$value" }
.mkString("Context{", ", ", "}")
}
assertEquals(Show[Context].show(ctx), expected)
assertEquals(Show[Context].show(ctx), ctx.toString)
}
}

}