Skip to content

Commit

Permalink
Merge pull request #391 from iRevive/sdk-common/context
Browse files Browse the repository at this point in the history
sdk-common: add `Context`
  • Loading branch information
iRevive authored Dec 12, 2023
2 parents b93b50f + 875cf64 commit 9999b5f
Show file tree
Hide file tree
Showing 3 changed files with 266 additions and 0 deletions.
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)
}
}

}

0 comments on commit 9999b5f

Please sign in to comment.