Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[data] introduce Environment and improve KyoException #1057

Merged
merged 9 commits into from
Jan 28, 2025
7 changes: 1 addition & 6 deletions kyo-core/jvm/src/main/scala/kyo/StreamCompression.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,7 @@ import scala.annotation.tailrec

object StreamCompression:

final class StreamCompressionException(reason: String | Throwable)(using Frame) extends KyoException(
message = reason match
case string: String => Text(string)
case _: Throwable => null,
cause = reason
)
final class StreamCompressionException(cause: Text | Throwable)(using Frame) extends KyoException("", cause)

enum CompressionLevel(val value: Int) derives CanEqual:
case Default extends CompressionLevel(-1)
Expand Down
56 changes: 46 additions & 10 deletions kyo-data/shared/src/main/scala/kyo/KyoException.scala
Original file line number Diff line number Diff line change
@@ -1,34 +1,70 @@
package kyo

import KyoException.maxMessageLength
import kyo.*
import kyo.Ansi.*
import kyo.internal.Environment
import scala.util.control.NoStackTrace

/** Kyo's base exception class that provides enhanced error reporting and context.
*
* The exception's behavior changes based on the environment:
* - In development: Provides detailed, ANSI-colored error messages with context
* - In production: Returns minimal error messages for cleaner logs
*
* @param message
* The primary error message
* @param cause
* Either a Text explanation or a Throwable that caused this exception
* @param frame
* Implicit Frame providing context about where the exception was created
*/
class KyoException private[kyo] (
message: Text | Null = null,
cause: Text | Throwable | Null = null
message: Text = "",
cause: Text | Throwable = ""
)(using val frame: Frame) extends Exception(
Option(message) match
case Some(message) => message.toString;
case _ => null,
message.toString,
cause match
case cause: Throwable => cause;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙀 is the ; the cause of the trouble? I'm so sorry 😿

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's harmless :)

case cause: Throwable => cause
case _ => null
) with NoStackTrace:

/** Returns the underlying cause of this exception. Overriding this method is important for performance since the method is synchronized
* in the super classes.
*
* @return
* The Throwable cause if one was provided, null otherwise
*/
override def getCause(): Throwable =
cause match
case cause: Throwable => cause
case _ => null

/** Formats and returns the exception message.
*
* In production mode, returns only the cause message if present, or null if no cause message was provided.
*
* @return
* The formatted exception message
*/
override def getMessage(): String =
val detail =
cause match
case _: Throwable => Absent
case cause: Text @unchecked => Maybe(cause)
case cause: Text @unchecked => Maybe(cause.take(maxMessageLength))

val msg = frame.render(("⚠️ KyoException".red.bold :: Maybe(message).toList ::: detail.toList).map(_.show)*)
s"\n$msg\n"
if Environment.isDevelopment then
val msg = frame.render(
("⚠️ KyoException".red.bold :: Maybe(message).toList ::: detail.toList)
.map(_.show)*
)
s"\n$msg\n"
else
detail.map(_.toString).getOrElse(null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about .fold("")(_.take(1000).toString)?

end if
end getMessage

end KyoException

object KyoException:
/** Maximum length for error messages to prevent excessive output. */
val maxMessageLength = 1000
37 changes: 37 additions & 0 deletions kyo-data/shared/src/main/scala/kyo/internal/Environment.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package kyo.internal

/** Environment detection utility.
*
* Provides functionality to detect whether the JVM is running in a development environment. This information is used to control the
* verbosity of error messages and debugging information in Kyo's implementations.
*
* The development mode can be controlled in two ways:
* 1. Explicitly via the system property "-Dkyo.development=true"
* 2. Automatically by detecting SBT in the classpath
*/
private[kyo] object Environment:

/** Determines if the execution is a development environment.
*
* This method checks the following conditions in order:
*
* 1. If system property "kyo.development" exists:
* - Returns true if the property value is "true" (case insensitive)
* - Returns false if the property value is anything else
* 2. If the property doesn't exist:
* - Returns true if SBT is detected in the classpath (contains "org.scala-sbt")
* - Returns false otherwise
*
* @return
* true if running in a development environment, false otherwise
*/
val isDevelopment: Boolean = inferIsDevelopment()

private[internal] def inferIsDevelopment(): Boolean =
sys.props.get("kyo.development").map(_.toLowerCase) match
case Some("true") => true
case Some(_) => false
case None =>
sys.props.get("java.class.path").exists(_.contains("org.scala-sbt"))

end Environment
25 changes: 25 additions & 0 deletions kyo-data/shared/src/test/scala/kyo/internal/EnvironmentTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package kyo.internal

import org.scalatest.freespec.AnyFreeSpec
import org.scalatest.matchers.should.Matchers

class EnvironmentTest extends AnyFreeSpec with Matchers:

"infers as development" - {
"if system property is enabled, which should be the case in this test execution" in {
System.setProperty("kyo.development", "true")
assert(Environment.inferIsDevelopment())
}
}

"doesn't infer as development" - {
"if system property is false" in {
System.setProperty("kyo.development", "false")
assert(!Environment.inferIsDevelopment())
}
"if system property is invalid" in {
System.setProperty("kyo.development", "blah")
assert(!Environment.inferIsDevelopment())
}
}
end EnvironmentTest
Loading