Skip to content

Commit

Permalink
core and sdk: Add Hash and Show instances (#331)
Browse files Browse the repository at this point in the history
* core-common and sdk-common: Add `Hash` and `Show` instances

* Use cats-discipline to test `Hash` laws

* Add more tests

* Use `import cats.syntax.all._`
  • Loading branch information
iRevive authored Nov 1, 2023
1 parent dfb9724 commit c2c9e97
Show file tree
Hide file tree
Showing 19 changed files with 354 additions and 31 deletions.
8 changes: 7 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,10 @@ lazy val `core-trace` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
name := "otel4s-core-trace",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-effect-kernel" % CatsEffectVersion,
"org.scodec" %%% "scodec-bits" % ScodecVersion
"org.scodec" %%% "scodec-bits" % ScodecVersion,
"org.scalameta" %%% "munit-scalacheck" % MUnitVersion % Test,
"org.typelevel" %%% "cats-laws" % CatsVersion % Test,
"org.typelevel" %%% "discipline-munit" % DisciplineMUnitVersion % Test
)
)
.settings(scalafixSettings)
Expand Down Expand Up @@ -180,6 +183,9 @@ lazy val `sdk-trace` = crossProject(JVMPlatform, JSPlatform, NativePlatform)
name := "otel4s-sdk-trace",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-effect" % CatsEffectVersion,
"org.scalameta" %%% "munit-scalacheck" % MUnitVersion % Test,
"org.typelevel" %%% "cats-laws" % CatsVersion % Test,
"org.typelevel" %%% "discipline-munit" % DisciplineMUnitVersion % Test
),
)
.settings(munitDependencies)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@

package org.typelevel.otel4s

import cats.Hash
import cats.Show
import cats.implicits.showInterpolator
import cats.kernel.Hash
import cats.syntax.show._

/** Represents the key-value attribute.
*
Expand Down Expand Up @@ -76,4 +76,8 @@ String, Boolean, Long, Double, List[String], List[Boolean], List[Long], List[Dou
implicit def hashAttribute[T: Hash]: Hash[Attribute[T]] =
Hash.by(a => (a.key, a.value))

implicit val hashAttributeExistential: Hash[Attribute[_]] = {
implicit val hashAny: Hash[Any] = Hash.fromUniversalHashCode
Hash.by(a => (a.key, a.value))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,7 @@ object AttributeKey {
implicit def attributeKeyShow[A]: Show[AttributeKey[A]] =
Show.show(key => show"${key.`type`}(${key.name})")

implicit val attributeKeyExistentialHash: Hash[AttributeKey[_]] =
Hash.by(key => (key.name, key.`type`))

}
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ object AttributeType {
implicit def attributeTypeShow[A]: Show[AttributeType[A]] =
Show.fromToString

implicit val attributeTypeExistentialHash: Hash[AttributeType[_]] =
Hash.fromUniversalHashCode

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
package org.typelevel.otel4s
package trace

import cats.Hash
import cats.Show

/** Type of [[Span]]. Can be used to specify additional relationships between
* spans in addition to a parent/child relationship.
*/
Expand Down Expand Up @@ -48,4 +51,7 @@ object SpanKind {
* relationship between producer and consumer spans.
*/
case object Consumer extends SpanKind

implicit val spanKindHash: Hash[SpanKind] = Hash.fromUniversalHashCode
implicit val spanKindShow: Show[SpanKind] = Show.fromToString
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@

package org.typelevel.otel4s.trace

import cats.Hash
import cats.Show

/** The set of canonical status codes
*/
sealed trait Status extends Product with Serializable
Expand All @@ -33,4 +36,7 @@ object Status {
/** The operation contains an error. */
case object Error extends Status

implicit val statusHash: Hash[Status] = Hash.fromUniversalHashCode
implicit val statusShow: Show[Status] = Show.fromToString

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2022 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.trace

import cats.Show
import cats.kernel.laws.discipline.HashTests
import munit._
import org.scalacheck.Arbitrary
import org.scalacheck.Cogen
import org.scalacheck.Gen
import org.scalacheck.Prop

class SpanKindSuite extends DisciplineSuite {

private val spanKindGen: Gen[SpanKind] =
Gen.oneOf(
SpanKind.Internal,
SpanKind.Server,
SpanKind.Client,
SpanKind.Producer,
SpanKind.Consumer
)

private implicit val spanKindArbitrary: Arbitrary[SpanKind] =
Arbitrary(spanKindGen)

private implicit val spanKindCogen: Cogen[SpanKind] =
Cogen[String].contramap(_.toString)

checkAll("SpanKind.HashLaws", HashTests[SpanKind].hash)

property("Show[SpanKind]") {
Prop.forAll(spanKindGen) { spanKind =>
val expected = spanKind match {
case SpanKind.Internal => "Internal"
case SpanKind.Server => "Server"
case SpanKind.Client => "Client"
case SpanKind.Producer => "Producer"
case SpanKind.Consumer => "Consumer"
}

assertEquals(Show[SpanKind].show(spanKind), expected)
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2022 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.trace

import cats.Show
import cats.kernel.laws.discipline.HashTests
import munit._
import org.scalacheck.Arbitrary
import org.scalacheck.Cogen
import org.scalacheck.Gen
import org.scalacheck.Prop

class StatusSuite extends DisciplineSuite {

private val statusGen: Gen[Status] =
Gen.oneOf(Status.Unset, Status.Ok, Status.Error)

private implicit val statusArbitrary: Arbitrary[Status] =
Arbitrary(statusGen)

private implicit val statusCogen: Cogen[Status] =
Cogen[String].contramap(_.toString)

checkAll("Status.HashLaws", HashTests[Status].hash)

property("Show[Status]") {
Prop.forAll(statusGen) { status =>
val expected = status match {
case Status.Unset => "Unset"
case Status.Ok => "Ok"
case Status.Error => "Error"
}

assertEquals(Show[Status].show(status), expected)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,26 @@

package org.typelevel.otel4s.trace

import cats.Show
import cats.kernel.laws.discipline.HashTests
import munit._
import org.scalacheck.Arbitrary
import org.scalacheck.Cogen
import org.scalacheck.Gen
import org.scalacheck.Prop

class TraceFlagsSuite extends FunSuite {
class TraceFlagsSuite extends DisciplineSuite {

private val traceFlagsGen: Gen[TraceFlags] =
Gen.chooseNum(0, 255).map(byte => TraceFlags.fromByte(byte.toByte))

private implicit val traceFlagsArbitrary: Arbitrary[TraceFlags] =
Arbitrary(traceFlagsGen)

private implicit val traceFlagsCogen: Cogen[TraceFlags] =
Cogen[Byte].contramap(_.toByte)

checkAll("TraceFlags.HashLaws", HashTests[TraceFlags].hash)

test("default instances") {
assertEquals(TraceFlags.Default.toHex, "00")
Expand Down Expand Up @@ -51,4 +68,10 @@ class TraceFlagsSuite extends FunSuite {
assertEquals(TraceFlags.fromHex("zxc"), None)
}

property("Show[TraceFlags]") {
Prop.forAll(traceFlagsGen) { traceFlags =>
assertEquals(Show[TraceFlags].show(traceFlags), traceFlags.toHex)
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package org.typelevel.otel4s.sdk

import cats.Applicative
import cats.Hash
import cats.Monad
import cats.Monoid
import cats.Show
Expand Down Expand Up @@ -59,6 +60,17 @@ final class Attributes private (
def toMap: Map[AttributeKey[_], Attribute[_]] = m
def toList: List[Attribute[_]] = m.values.toList

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

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

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

object Attributes {
Expand All @@ -79,6 +91,9 @@ object Attributes {
.mkString("Attributes(", ", ", ")")
}

implicit val hashAttributes: Hash[Attributes] =
Hash.by(_.m)

implicit val monoidAttributes: Monoid[Attributes] =
new Monoid[Attributes] {
def empty: Attributes = Attributes.Empty
Expand Down
16 changes: 10 additions & 6 deletions sdk/common/src/main/scala/org/typelevel/otel4s/sdk/Resource.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,9 @@

package org.typelevel.otel4s.sdk

import cats.Hash
import cats.Show
import cats.implicits.catsSyntaxEitherId
import cats.implicits.catsSyntaxOptionId
import cats.implicits.catsSyntaxSemigroup
import cats.implicits.showInterpolator
import cats.syntax.all._
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.sdk.Resource.ResourceInitiationError
import org.typelevel.otel4s.semconv.resource.attributes.ResourceAttributes._
Expand Down Expand Up @@ -61,7 +59,7 @@ final case class Resource(

schemaUrlOptEither.map(
Resource(
other.attributes |+| attributes,
attributes |+| other.attributes,
_
)
)
Expand All @@ -76,6 +74,9 @@ final case class Resource(
throw _,
identity
)

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

object Resource {
Expand Down Expand Up @@ -119,6 +120,9 @@ object Resource {
val Default: Resource = TelemetrySdk.mergeIntoUnsafe(Mandatory)

implicit val showResource: Show[Resource] =
r => show"Resource(${r.attributes}, ${r.schemaUrl})"
r => show"Resource{attributes=${r.attributes}, schemaUrl=${r.schemaUrl}}"

implicit val hashResource: Hash[Resource] =
Hash.by(r => (r.attributes, r.schemaUrl))

}
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,14 @@
package org.typelevel.otel4s.sdk

import org.scalacheck.Arbitrary
import org.scalacheck.Cogen
import org.scalacheck.Gen
import org.scalacheck.Gen.listOf
import org.scalacheck.Gen.nonEmptyListOf
import org.scalacheck.rng.Seed
import org.typelevel.otel4s.Attribute
import org.typelevel.otel4s.AttributeKey
import org.typelevel.otel4s.AttributeType

object arbitrary extends ArbitraryInstances
trait ArbitraryInstances {
Expand Down Expand Up @@ -90,4 +94,45 @@ trait ArbitraryInstances {
schemaUrl <- Gen.option(nonEmptyString)
} yield Resource(attrs, schemaUrl))

implicit val attributeTypeCogen: Cogen[AttributeType[_]] =
Cogen[String].contramap(_.toString)

implicit def attributeKeyCogen[A]: Cogen[AttributeKey[A]] =
Cogen[(String, String)].contramap[AttributeKey[A]] { attribute =>
(attribute.name, attribute.`type`.toString)
}

implicit def attributeCogen[A: Cogen]: Cogen[Attribute[A]] =
Cogen[(AttributeKey[A], A)].contramap(a => (a.key, a.value))

implicit val attributeExistentialCogen: Cogen[Attribute[_]] =
Cogen { (seed, attr) =>
def primitive[A: Cogen](seed: Seed): Seed =
Cogen[A].perturb(seed, attr.value.asInstanceOf[A])

def list[A: Cogen](seed: Seed): Seed =
Cogen[List[A]].perturb(seed, attr.value.asInstanceOf[List[A]])

val valueCogen: Seed => Seed = attr.key.`type` match {
case AttributeType.Boolean => primitive[Boolean]
case AttributeType.Double => primitive[Double]
case AttributeType.String => primitive[String]
case AttributeType.Long => primitive[Long]
case AttributeType.BooleanList => list[Boolean]
case AttributeType.DoubleList => list[Double]
case AttributeType.StringList => list[String]
case AttributeType.LongList => list[Long]
}

valueCogen(attributeKeyCogen.perturb(seed, attr.key))
}

implicit val attributesCogen: Cogen[Attributes] =
Cogen[List[Attribute[_]]].contramap(_.toList)

implicit val resourceCogen: Cogen[Resource] =
Cogen[(Attributes, Option[String])].contramap { r =>
(r.attributes, r.schemaUrl)
}

}
Loading

0 comments on commit c2c9e97

Please sign in to comment.