diff --git a/build.sbt b/build.sbt index c265431..bc88edd 100644 --- a/build.sbt +++ b/build.sbt @@ -47,7 +47,7 @@ lazy val testSettings = Seq( lazy val `gs-log` = project .in(file(".")) - .aggregate(api) + .aggregate(data, api) .settings(noPublishSettings) .settings(name := s"${gsProjectName.value}-v${semVerMajor.value}") @@ -56,6 +56,9 @@ lazy val data = project .settings(sharedSettings) .settings(testSettings) .settings(name := s"${gsProjectName.value}-data-v${semVerMajor.value}") + .settings(libraryDependencies ++= Seq( + Deps.Cats.Core + )) lazy val api = project .in(file("modules/api")) diff --git a/modules/api/src/main/scala/gs/log/v0/api/Logger.scala b/modules/api/src/main/scala/gs/log/v0/api/Logger.scala index 17019d7..2be3d77 100644 --- a/modules/api/src/main/scala/gs/log/v0/api/Logger.scala +++ b/modules/api/src/main/scala/gs/log/v0/api/Logger.scala @@ -1,5 +1,9 @@ package gs.log.v0.api +import gs.log.v0.data.Log +import gs.log.v0.data.LogLevel +import cats.Show + /** * Interface for emitting logs. This and [[Log]] are the primary types that * users of this library will interact with. @@ -86,7 +90,9 @@ object Logger: given CanEqual[Name, Name] = CanEqual.derived - extension (name: Name) def str(): String = name + given Show[Name] = new Show[Name] { + override def show(t: Name): String = t + } end Name diff --git a/modules/api/src/main/scala/gs/log/v0/syntax/package.scala b/modules/api/src/main/scala/gs/log/v0/syntax/package.scala deleted file mode 100644 index c258e69..0000000 --- a/modules/api/src/main/scala/gs/log/v0/syntax/package.scala +++ /dev/null @@ -1,10 +0,0 @@ -package gs.log.v0.syntax - -import gs.log.v0.data.LogMessage -import gs.log.v0.data.LogData - -private object LogMessageBuilder extends LogMessage.Builder -private object LogDataSyntax extends LogData.Syntax - -export LogDataSyntax.* -export LogMessageBuilder.* diff --git a/modules/data/src/main/scala/gs/log/v0/data/DataRenderer.scala b/modules/data/src/main/scala/gs/log/v0/data/DataRenderer.scala deleted file mode 100644 index 3fc51f7..0000000 --- a/modules/data/src/main/scala/gs/log/v0/data/DataRenderer.scala +++ /dev/null @@ -1,57 +0,0 @@ -package gs.log.v0.data - -import scala.reflect.ClassTag -import java.time.LocalDate -import java.time.Instant - -trait DataRenderer: - def render[A](data: A)(using CT: ClassTag[A]): String - -object DataRenderer: - - abstract class SingleTypeRenderer[A]: - def render(data: A): String - - object Defaults: - - final class StringRenderer extends SingleTypeRenderer[String]: - override def render(data: String): String = data - - final class BooleanRenderer extends SingleTypeRenderer[Boolean]: - override def render(data: Boolean): String = data.toString() - - final class IntRenderer extends SingleTypeRenderer[Int]: - override def render(data: Int): String = data.toString() - - final class LongRenderer extends SingleTypeRenderer[Long]: - override def render(data: Long): String = data.toString() - - final class FloatRenderer extends SingleTypeRenderer[Float]: - override def render(data: Float): String = data.toString() - - final class DoubleRenderer extends SingleTypeRenderer[Double]: - override def render(data: Double): String = data.toString() - - final class DateRenderer extends SingleTypeRenderer[LocalDate]: - override def render(data: LocalDate): String = data.toString() - - final class InstantRenderer extends SingleTypeRenderer[Instant]: - override def render(data: Instant): String = data.toString() - - end Defaults - - class RegistryRenderer( - private val registry: Map[Class[?], SingleTypeRenderer[?]] - ): - - def render[A](data: A)(using CT: ClassTag[A]): String = - getRendererFor[A].render(data) - - private def getRendererFor[A](using CT: ClassTag[A]): SingleTypeRenderer[A] = - Option(registry.get(CT.runtimeClass)) match - case Some(r) => r.asInstanceOf[SingleTypeRenderer[A]] - case None => throw new IllegalArgumentException("No renderer in registry!") - - end RegistryRenderer - -end DataRenderer diff --git a/modules/api/src/main/scala/gs/log/v0/api/Log.scala b/modules/data/src/main/scala/gs/log/v0/data/Log.scala similarity index 95% rename from modules/api/src/main/scala/gs/log/v0/api/Log.scala rename to modules/data/src/main/scala/gs/log/v0/data/Log.scala index d51b986..ebf29db 100644 --- a/modules/api/src/main/scala/gs/log/v0/api/Log.scala +++ b/modules/data/src/main/scala/gs/log/v0/data/Log.scala @@ -1,7 +1,4 @@ -package gs.log.v0.api - -import gs.log.v0.data.LogData -import gs.log.v0.data.LogMessage +package gs.log.v0.data import java.time.Instant import scala.collection.mutable.{Map => MutMap} @@ -127,11 +124,11 @@ object Log: /** * Instantiate a new log with the given message. * - * @param tags The message. + * @param logMessage The message. * @return New [[Log]] instance. */ - def message(msg: LogMessage): Log = - new Log().message(msg) + def message(logMessage: LogMessage): Log = + new Log().message(logMessage) /** * Instantiate a new log with the given exception. @@ -155,7 +152,7 @@ object Log: case class Metadata( level: LogLevel, timestamp: Instant, - owner: Logger.Name + owner: String ) end Log diff --git a/modules/data/src/main/scala/gs/log/v0/data/LogData.scala b/modules/data/src/main/scala/gs/log/v0/data/LogData.scala index 9536d23..bf86d77 100644 --- a/modules/data/src/main/scala/gs/log/v0/data/LogData.scala +++ b/modules/data/src/main/scala/gs/log/v0/data/LogData.scala @@ -34,7 +34,7 @@ object LogData: * by callers - [[Safe]] is used to ensure that all data is explicitly * denoted. */ - sealed trait Value: + trait Value: type A /** @return @@ -42,33 +42,73 @@ object LogData: */ def data: A + /** + * Provide standard capture support for strings. + * + * @param data The string data. + */ case class Str(data: String) extends Value: override type A = String + /** + * Provide standard capture support for Boolean values. + * + * @param data The Boolean data. + */ case class Bool(data: Boolean) extends Value: override type A = Boolean + /** + * Provide standard capture support for 32-bit integers. + * + * @param data The integer data. + */ case class Int32(data: Int) extends Value: override type A = Int + /** + * Provide standard capture support for 64-bit integers. + * + * @param data The long integer data. + */ case class Int64(data: Long) extends Value: override type A = Long + /** + * Provide standard capture support for 32-bit floating point numbers. + * + * @param data The floating point data. + */ case class Float32(data: Float) extends Value: override type A = Float + /** + * Provide standard capture support for 64-bit floating point numbers. + * + * @param data The double-precision floating point data. + */ case class Float64(data: Double) extends Value: override type A = Double + /** + * Provide standard capture support for dates. + * + * @param data The date data. + */ case class Date(data: LocalDate) extends Value: override type A = LocalDate + /** + * Provide standard capture support for instants. + * + * @param data The instant data. + */ case class Instant(data: java.time.Instant) extends Value: override type A = java.time.Instant - // TODO: IS THIS APPROPRIATE? - case class Generic[AA](data: AA) extends Value: + trait Generic[AA, Codec[_]] extends Value: override type A = AA + def codec: Codec[A] /** Indicates data (in terms of [[Value]]) that has been explicitly marked as * _safe_ to log in some form. @@ -177,7 +217,7 @@ object LogData: * The value that is safe to log in clear text. */ def logClearText[A: Loggable](data: A): ClearText = - ClearText(Loggable[A].renderForLogs(data)) + ClearText(Loggable[A].capture(data)) /** The specified data is _not_ safe to log in clear text, and must be * encrypted before being output. In this use case, the data is valuable @@ -201,7 +241,7 @@ object LogData: * The value to encrypt. */ def logEncrypted[A: Loggable](data: A): RequiresEncryption = - RequiresEncryption(Loggable[A].renderForLogs(data)) + RequiresEncryption(Loggable[A].capture(data)) /** The contianed data is _not_ safe to log in clear text, and must be * hashed before being output. In this use case, the data is valuable to @@ -227,7 +267,7 @@ object LogData: * The value to hash. */ def logHashed[A: Loggable](data: A): RequiresSecureHash = - RequiresSecureHash(Loggable[A].renderForLogs(data)) + RequiresSecureHash(Loggable[A].capture(data)) trait Syntax: diff --git a/modules/api/src/main/scala/gs/log/v0/api/LogLevel.scala b/modules/data/src/main/scala/gs/log/v0/data/LogLevel.scala similarity index 98% rename from modules/api/src/main/scala/gs/log/v0/api/LogLevel.scala rename to modules/data/src/main/scala/gs/log/v0/data/LogLevel.scala index 7c3fccc..229e1d2 100644 --- a/modules/api/src/main/scala/gs/log/v0/api/LogLevel.scala +++ b/modules/data/src/main/scala/gs/log/v0/data/LogLevel.scala @@ -1,4 +1,4 @@ -package gs.log.v0.api +package gs.log.v0.data /** Enumerates all supported log levels, which control logging verbosity. * diff --git a/modules/data/src/main/scala/gs/log/v0/data/LogMessage.scala b/modules/data/src/main/scala/gs/log/v0/data/LogMessage.scala index bf25c71..4b29926 100644 --- a/modules/data/src/main/scala/gs/log/v0/data/LogMessage.scala +++ b/modules/data/src/main/scala/gs/log/v0/data/LogMessage.scala @@ -1,5 +1,7 @@ package gs.log.v0.data +import cats.Show + /** Opaque type (String) for representing messages that should be logged. This * type can _only_ be instantiated using the `log` string interpolator, which * allows it to be (relatively) "safe" in that users _must_ explicitly declare @@ -17,27 +19,58 @@ object LogMessage: given CanEqual[LogMessage, LogMessage] = CanEqual.derived + given Show[LogMessage] = new Show[LogMessage] { + override def show(t: LogMessage): String = t + } + + /** + * Represents a string that is explicitly safe to show in clear text. + * + * @param data The clear text data. + */ + case class ClearTextString(data: String) + + object ClearTextString: + + given CanEqual[ClearTextString, ClearTextString] = CanEqual.derived + + given Show[ClearTextString] = new Show[ClearTextString] { + override def show(t: ClearTextString): String = t.data + } + + end ClearTextString + extension (logMessage: LogMessage) /** Express this [[LogMessage]] as a string. * * @return * The string rendition of this [[LogMessage]]. */ - def str(): String = logMessage + def unwrap(): String = logMessage - trait Builder: + trait Syntax: - extension (sc: StringContext)(using R: DataRenderer) + extension [A: Show](data: A) + /** + * Explicitly mark this data as _safe_ to render in clear text. Produce a + * string representation. + * + * @return The clear text string representation of this data. + */ + def clearTextString(): ClearTextString = + ClearTextString(Show[A].show(data)) + + extension (sc: StringContext) /** String interpolator for instantiating a [[LogMessage]]. * * @param args - * All interpolation variables. Must be [[LogData.ClearText]] + * All interpolation variables. Must be [[LogMessage.ClearTextString]] * instances. * @return * The new [[LogMessage]]. */ - def log(args: LogData.ClearText*): LogMessage = - sc.s(args.map(d => R.render(d.value))*) + def log(args: ClearTextString*): LogMessage = + sc.s(args.map(cts => cts.data)*) end LogMessage diff --git a/modules/data/src/main/scala/gs/log/v0/data/Loggable.scala b/modules/data/src/main/scala/gs/log/v0/data/Loggable.scala index 39cede4..e0e0d60 100644 --- a/modules/data/src/main/scala/gs/log/v0/data/Loggable.scala +++ b/modules/data/src/main/scala/gs/log/v0/data/Loggable.scala @@ -7,13 +7,13 @@ import java.time.LocalDate */ trait Loggable[A]: /** - * Render the given data as some primitive supported by `gs-log`. Note that - * this type class does **not** support structured types. + * Capture the given data as some primitive supported by `gs-log` for + * propagating data for future rendition. * - * @param data The data to render. + * @param data The data to capture. * @return The [[LogData.Value]] housing the output primitive. */ - def renderForLogs(data: A): LogData.Value + def capture(data: A): LogData.Value object Loggable: diff --git a/modules/data/src/main/scala/gs/log/v0/syntax/package.scala b/modules/data/src/main/scala/gs/log/v0/syntax/package.scala new file mode 100644 index 0000000..f3fb337 --- /dev/null +++ b/modules/data/src/main/scala/gs/log/v0/syntax/package.scala @@ -0,0 +1,10 @@ +package gs.log.v0.syntax + +import gs.log.v0.data.LogMessage +import gs.log.v0.data.LogData + +private object LogMessageSyntax extends LogMessage.Syntax +private object LogDataSyntax extends LogData.Syntax + +export LogDataSyntax.* +export LogMessageSyntax.* diff --git a/modules/data/src/test/scala/gs/log/v0/data/LogMessageTests.scala b/modules/data/src/test/scala/gs/log/v0/data/LogMessageTests.scala index 4099efd..bf7f1ab 100644 --- a/modules/data/src/test/scala/gs/log/v0/data/LogMessageTests.scala +++ b/modules/data/src/test/scala/gs/log/v0/data/LogMessageTests.scala @@ -1,33 +1,28 @@ package gs.log.v0.data +import gs.log.v0.syntax.* +import cats.syntax.all.* + class LogMessageTests extends munit.FunSuite: - import LogMessageTests.Syntax.* - test("should instantiate a log message from a string literal") { val msg = log"1 2 3 4" - assert(msg.str() == "1 2 3 4") + assertEquals(msg.show, "1 2 3 4") } test("should support equality checks") { val msg = log"1 2 3 4" val alt = log"1 2 3 4" - assert(msg == alt) - assert(msg.str() == alt.str()) - assert(msg.toString() == "1 2 3 4") - assert(msg.hashCode() == "1 2 3 4".hashCode()) + assertEquals(msg, alt) + assertEquals(msg.show, alt.show) + assertEquals(msg.toString(), "1 2 3 4") + assertEquals(msg.hashCode(), "1 2 3 4".hashCode()) } test("should accept variables safe to log in clear text") { - val x = "foo".logClearText() - val y = 10.logClearText() + val x = "foo".clearTextString() + val y = 10.clearTextString() val msg: LogMessage = log"x = $x, y = $y" - val expected = s"x = ${x.value.renderBytes()}, y = ${y.value.renderBytes()}" - assert(msg.str() == expected) + val expected = s"x = ${x.data}, y = ${y.data}" + assertEquals(msg.show, expected) } - -object LogMessageTests: - - private object Syntax extends LogMessage.Builder - -end LogMessageTests diff --git a/project/build.properties b/project/build.properties index 04267b1..081fdbb 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.9 +sbt.version=1.10.0