Some reorganization and thinking a lot about data propagation.
This commit is contained in:
parent
c1e7e594e8
commit
e3c0a4260b
12 changed files with 129 additions and 112 deletions
|
@ -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"))
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.*
|
|
@ -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
|
|
@ -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
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package gs.log.v0.api
|
||||
package gs.log.v0.data
|
||||
|
||||
/** Enumerates all supported log levels, which control logging verbosity.
|
||||
*
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
10
modules/data/src/main/scala/gs/log/v0/syntax/package.scala
Normal file
10
modules/data/src/main/scala/gs/log/v0/syntax/package.scala
Normal 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.*
|
|
@ -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
|
||||
|
|
|
@ -1 +1 @@
|
|||
sbt.version=1.9.9
|
||||
sbt.version=1.10.0
|
||||
|
|
Loading…
Add table
Reference in a new issue