Some reorganization and thinking a lot about data propagation.

This commit is contained in:
Pat Garrity 2024-05-12 22:29:42 -05:00
parent c1e7e594e8
commit e3c0a4260b
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
12 changed files with 129 additions and 112 deletions

View file

@ -47,7 +47,7 @@ lazy val testSettings = Seq(
lazy val `gs-log` = project lazy val `gs-log` = project
.in(file(".")) .in(file("."))
.aggregate(api) .aggregate(data, api)
.settings(noPublishSettings) .settings(noPublishSettings)
.settings(name := s"${gsProjectName.value}-v${semVerMajor.value}") .settings(name := s"${gsProjectName.value}-v${semVerMajor.value}")
@ -56,6 +56,9 @@ lazy val data = project
.settings(sharedSettings) .settings(sharedSettings)
.settings(testSettings) .settings(testSettings)
.settings(name := s"${gsProjectName.value}-data-v${semVerMajor.value}") .settings(name := s"${gsProjectName.value}-data-v${semVerMajor.value}")
.settings(libraryDependencies ++= Seq(
Deps.Cats.Core
))
lazy val api = project lazy val api = project
.in(file("modules/api")) .in(file("modules/api"))

View file

@ -1,5 +1,9 @@
package gs.log.v0.api 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 * Interface for emitting logs. This and [[Log]] are the primary types that
* users of this library will interact with. * users of this library will interact with.
@ -86,7 +90,9 @@ object Logger:
given CanEqual[Name, Name] = CanEqual.derived 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 end Name

View file

@ -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.*

View file

@ -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

View file

@ -1,7 +1,4 @@
package gs.log.v0.api package gs.log.v0.data
import gs.log.v0.data.LogData
import gs.log.v0.data.LogMessage
import java.time.Instant import java.time.Instant
import scala.collection.mutable.{Map => MutMap} import scala.collection.mutable.{Map => MutMap}
@ -127,11 +124,11 @@ object Log:
/** /**
* Instantiate a new log with the given message. * Instantiate a new log with the given message.
* *
* @param tags The message. * @param logMessage The message.
* @return New [[Log]] instance. * @return New [[Log]] instance.
*/ */
def message(msg: LogMessage): Log = def message(logMessage: LogMessage): Log =
new Log().message(msg) new Log().message(logMessage)
/** /**
* Instantiate a new log with the given exception. * Instantiate a new log with the given exception.
@ -155,7 +152,7 @@ object Log:
case class Metadata( case class Metadata(
level: LogLevel, level: LogLevel,
timestamp: Instant, timestamp: Instant,
owner: Logger.Name owner: String
) )
end Log end Log

View file

@ -34,7 +34,7 @@ object LogData:
* by callers - [[Safe]] is used to ensure that all data is explicitly * by callers - [[Safe]] is used to ensure that all data is explicitly
* denoted. * denoted.
*/ */
sealed trait Value: trait Value:
type A type A
/** @return /** @return
@ -42,33 +42,73 @@ object LogData:
*/ */
def data: A def data: A
/**
* Provide standard capture support for strings.
*
* @param data The string data.
*/
case class Str(data: String) extends Value: case class Str(data: String) extends Value:
override type A = String override type A = String
/**
* Provide standard capture support for Boolean values.
*
* @param data The Boolean data.
*/
case class Bool(data: Boolean) extends Value: case class Bool(data: Boolean) extends Value:
override type A = Boolean override type A = Boolean
/**
* Provide standard capture support for 32-bit integers.
*
* @param data The integer data.
*/
case class Int32(data: Int) extends Value: case class Int32(data: Int) extends Value:
override type A = Int 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: case class Int64(data: Long) extends Value:
override type A = Long 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: case class Float32(data: Float) extends Value:
override type A = Float 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: case class Float64(data: Double) extends Value:
override type A = Double override type A = Double
/**
* Provide standard capture support for dates.
*
* @param data The date data.
*/
case class Date(data: LocalDate) extends Value: case class Date(data: LocalDate) extends Value:
override type A = LocalDate override type A = LocalDate
/**
* Provide standard capture support for instants.
*
* @param data The instant data.
*/
case class Instant(data: java.time.Instant) extends Value: case class Instant(data: java.time.Instant) extends Value:
override type A = java.time.Instant override type A = java.time.Instant
// TODO: IS THIS APPROPRIATE? trait Generic[AA, Codec[_]] extends Value:
case class Generic[AA](data: AA) extends Value:
override type A = AA override type A = AA
def codec: Codec[A]
/** Indicates data (in terms of [[Value]]) that has been explicitly marked as /** Indicates data (in terms of [[Value]]) that has been explicitly marked as
* _safe_ to log in some form. * _safe_ to log in some form.
@ -177,7 +217,7 @@ object LogData:
* The value that is safe to log in clear text. * The value that is safe to log in clear text.
*/ */
def logClearText[A: Loggable](data: A): ClearText = 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 /** 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 * encrypted before being output. In this use case, the data is valuable
@ -201,7 +241,7 @@ object LogData:
* The value to encrypt. * The value to encrypt.
*/ */
def logEncrypted[A: Loggable](data: A): RequiresEncryption = 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 /** 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 * hashed before being output. In this use case, the data is valuable to
@ -227,7 +267,7 @@ object LogData:
* The value to hash. * The value to hash.
*/ */
def logHashed[A: Loggable](data: A): RequiresSecureHash = def logHashed[A: Loggable](data: A): RequiresSecureHash =
RequiresSecureHash(Loggable[A].renderForLogs(data)) RequiresSecureHash(Loggable[A].capture(data))
trait Syntax: trait Syntax:

View file

@ -1,4 +1,4 @@
package gs.log.v0.api package gs.log.v0.data
/** Enumerates all supported log levels, which control logging verbosity. /** Enumerates all supported log levels, which control logging verbosity.
* *

View file

@ -1,5 +1,7 @@
package gs.log.v0.data package gs.log.v0.data
import cats.Show
/** Opaque type (String) for representing messages that should be logged. This /** Opaque type (String) for representing messages that should be logged. This
* type can _only_ be instantiated using the `log` string interpolator, which * type can _only_ be instantiated using the `log` string interpolator, which
* allows it to be (relatively) "safe" in that users _must_ explicitly declare * 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 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) extension (logMessage: LogMessage)
/** Express this [[LogMessage]] as a string. /** Express this [[LogMessage]] as a string.
* *
* @return * @return
* The string rendition of this [[LogMessage]]. * 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]]. /** String interpolator for instantiating a [[LogMessage]].
* *
* @param args * @param args
* All interpolation variables. Must be [[LogData.ClearText]] * All interpolation variables. Must be [[LogMessage.ClearTextString]]
* instances. * instances.
* @return * @return
* The new [[LogMessage]]. * The new [[LogMessage]].
*/ */
def log(args: LogData.ClearText*): LogMessage = def log(args: ClearTextString*): LogMessage =
sc.s(args.map(d => R.render(d.value))*) sc.s(args.map(cts => cts.data)*)
end LogMessage end LogMessage

View file

@ -7,13 +7,13 @@ import java.time.LocalDate
*/ */
trait Loggable[A]: trait Loggable[A]:
/** /**
* Render the given data as some primitive supported by `gs-log`. Note that * Capture the given data as some primitive supported by `gs-log` for
* this type class does **not** support structured types. * 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. * @return The [[LogData.Value]] housing the output primitive.
*/ */
def renderForLogs(data: A): LogData.Value def capture(data: A): LogData.Value
object Loggable: object Loggable:

View file

@ -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.*

View file

@ -1,33 +1,28 @@
package gs.log.v0.data package gs.log.v0.data
import gs.log.v0.syntax.*
import cats.syntax.all.*
class LogMessageTests extends munit.FunSuite: class LogMessageTests extends munit.FunSuite:
import LogMessageTests.Syntax.*
test("should instantiate a log message from a string literal") { test("should instantiate a log message from a string literal") {
val msg = log"1 2 3 4" 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") { test("should support equality checks") {
val msg = log"1 2 3 4" val msg = log"1 2 3 4"
val alt = log"1 2 3 4" val alt = log"1 2 3 4"
assert(msg == alt) assertEquals(msg, alt)
assert(msg.str() == alt.str()) assertEquals(msg.show, alt.show)
assert(msg.toString() == "1 2 3 4") assertEquals(msg.toString(), "1 2 3 4")
assert(msg.hashCode() == "1 2 3 4".hashCode()) assertEquals(msg.hashCode(), "1 2 3 4".hashCode())
} }
test("should accept variables safe to log in clear text") { test("should accept variables safe to log in clear text") {
val x = "foo".logClearText() val x = "foo".clearTextString()
val y = 10.logClearText() val y = 10.clearTextString()
val msg: LogMessage = log"x = $x, y = $y" val msg: LogMessage = log"x = $x, y = $y"
val expected = s"x = ${x.value.renderBytes()}, y = ${y.value.renderBytes()}" val expected = s"x = ${x.data}, y = ${y.data}"
assert(msg.str() == expected) assertEquals(msg.show, expected)
} }
object LogMessageTests:
private object Syntax extends LogMessage.Builder
end LogMessageTests

View file

@ -1 +1 @@
sbt.version=1.9.9 sbt.version=1.10.0