Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,13 @@ lazy val `oxygen-zio`: CrossProject =
zio.organization %%% zio.streams % zio.coreVersion,
),
)
// `JavaCommandService` relies on `java.lang.ProcessBuilder`, which Scala.js does not provide.
// JS falls back to `UnimplementedCommandService` (see the `.js` CommandServicePlatformSpecificImpl),
// so exclude the JVM/Native-only implementation from the JS source set.
.jsSettings(
Compile / unmanagedSources / excludeFilter :=
(Compile / unmanagedSources / excludeFilter).value || "JavaCommandService.scala",
)
.dependsOn(
`oxygen-schema` % testAndCompile,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package oxygen.core.typeclass

import java.time.{Duration, LocalDate, LocalDateTime, LocalTime}
import oxygen.core.TypeTag
import oxygen.core.{Text, TypeTag}
import oxygen.core.syntax.either.*
import oxygen.core.syntax.option.*
import oxygen.core.syntax.string.*
Expand Down Expand Up @@ -268,7 +268,7 @@ object StringDecoder extends StringDecoderLowPriority.LowPriority1 {
lastSuccessfulValue: Any,
message: Option[String],
hint: Option[Hint],
) {
) extends oxygen.core.error.Error {

private lazy val cameFromString: Boolean = prevTypeInfo == TypeTag[String]

Expand All @@ -290,7 +290,7 @@ object StringDecoder extends StringDecoderLowPriority.LowPriority1 {
case (None, Some(hint)) => s"$transformHintStr ~ $hint"
case (None, None) => transformHintStr

override def toString: String = showDetailed
override def errorMessage: Text = Text.fromString(showDetailed)

}

Expand Down
11 changes: 4 additions & 7 deletions modules/general/json/src/main/scala/oxygen/json/JsonError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@ package oxygen.json

import oxygen.predef.core.*

final case class JsonError(path: List[JsonError.Path], cause: JsonError.Cause) extends Throwable {
final case class JsonError(path: List[JsonError.Path], cause: JsonError.Cause) extends Error {

def inField(name: String): JsonError = JsonError(JsonError.Path.Field(name) :: path, cause)
def atIndex(index: Int): JsonError = JsonError(JsonError.Path.Index(index) :: path, cause)

override def getMessage: String =
path match
case head :: tail => s"${head.showFirst}${tail.map(_.showNonFirst).mkString} : ${cause.show}"
case Nil => cause.show

override def toString: String = getMessage
override def errorMessage: Text = path match
case head :: tail => str"${head.showFirst}${tail.map(_.showNonFirst).mkString} : ${cause.show}"
case Nil => Text.fromString(cause.show)

}
object JsonError {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package oxygen.zio.system

trait CommandServicePlatformSpecificImpl { self: CommandServicePlatformSpecific =>

// TODO (KR) : implement for real
override val default: CommandService = UnimplementedCommandService

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package oxygen.zio.system

trait CommandServicePlatformSpecificImpl { self: CommandServicePlatformSpecific =>

override val default: CommandService = JavaCommandService

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package oxygen.zio.system

trait CommandServicePlatformSpecificImpl { self: CommandServicePlatformSpecific =>

override val default: CommandService = JavaCommandService

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package oxygen.zio.error

import oxygen.predef.core.*
import oxygen.zio.system.*

sealed trait CommandError extends Error {

val command: BuiltCommand

protected final def failedToExecuteCommand: Text =
str"Failed to execute command [ ${command.showCommand(false)} ]"

}
object CommandError {

final case class ExecutionFailure(command: BuiltCommand, cause: Error) extends CommandError {
override def errorMessage: Text = failedToExecuteCommand
override def causes: ArraySeq[Error] = ArraySeq(cause)
}

final case class NonZeroExit(command: BuiltCommand, exit: Int, stdOut: Option[String], stdErr: Option[String]) extends CommandError {
override def errorMessage: Text = str"$failedToExecuteCommand: Non-zero exit code ($exit)"
override def causes: ArraySeq[Error] = ArraySeq.from(stdErr.map(Error(_)))
}

final case class DecodingFailure(command: BuiltCommand, error: Error, stdOut: Option[String], stdErr: Option[String]) extends CommandError {
override def errorMessage: Text = str"$failedToExecuteCommand: Decoding failure"
override def causes: ArraySeq[Error] = stdErr match
case Some(stdErr) => ArraySeq(error, Error(stdErr))
case None => ArraySeq(error)
}

final case class Unimplemented(command: BuiltCommand) extends CommandError {
override def errorMessage: Text = str"$failedToExecuteCommand: Operation not supported on this platform"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package oxygen.zio.system

import oxygen.predef.core.*
import oxygen.zio.error.CommandError
import oxygen.zio.syntax.log.*
import zio.*

trait CommandService {

def executeSync(
command: BuiltCommand,
stdIn: CommandInputSource,
trim: Boolean,
)(using Trace): IO[CommandError, (stdOut: String, stdErr: String, exitCode: Int)]

def executeSyncStreamErr(
command: BuiltCommand,
stdIn: CommandInputSource,
stdErr: CommandOutputSource,
trim: Boolean,
)(using Trace): IO[CommandError, (stdOut: String, exitCode: Int)]

def executeCode(
command: BuiltCommand,
stdIn: CommandInputSource,
stdOut: CommandOutputSource,
stdErr: CommandOutputSource,
)(using Trace): IO[CommandError, Int]

/////// ///////////////////////////////////////////////////////////////

final def executeCodeSuccess(
command: BuiltCommand,
stdIn: CommandInputSource,
stdOut: CommandOutputSource,
stdErr: CommandOutputSource,
)(using Trace): IO[CommandError, Unit] =
executeCode(
command = command,
stdIn = stdIn,
stdOut = stdOut,
stdErr = stdErr,
).flatMap {
case 0 => ZIO.unit
case exitCode => ZIO.fail { CommandError.NonZeroExit(command, exitCode, None, None) }
}
final def executeStringSuccess(
command: BuiltCommand,
stdIn: CommandInputSource,
stdErr: CommandOutputSource,
trim: Boolean,
)(using Trace): IO[CommandError, String] =
stdErr match {
case CommandOutputSource.Empty =>
executeSyncDecodeWith(command = command, stdIn = stdIn, stdErrOnSuccess = CommandOutputSource.Empty, trim = trim) { _.asRight }
case _ =>
executeSyncStreamErr(
command = command,
stdIn = stdIn,
stdErr = stdErr,
trim = trim,
).flatMap {
case (exitCode = 0, stdOut = stdOut) => ZIO.succeed { stdOut }
case (exitCode = exitCode, stdOut = stdOut) => ZIO.fail { CommandError.NonZeroExit(command, exitCode, stdOut.someWhen(_.nonEmpty), None) }
}
}

final def executeSyncDecodeWith[A](
command: BuiltCommand,
stdIn: CommandInputSource,
stdErrOnSuccess: CommandOutputSource,
trim: Boolean,
)(
decode: String => Either[Error, A],
)(using Trace): IO[CommandError, A] =
for {

rawRes <- executeSync(command = command, stdIn = stdIn, trim = trim)
optStdOutString: Option[String] = rawRes.stdOut.someWhen(_.nonEmpty)
optStdErrString: Option[String] = rawRes.stdErr.someWhen(_.nonEmpty)

_ <- ZIO.fail { CommandError.NonZeroExit(command, rawRes.exitCode, optStdOutString, optStdErrString) }.unlessDiscard { rawRes.exitCode == 0 }
decodedRes <- decode(rawRes.stdOut) match
case Right(value) => ZIO.succeed { value }
case Left(error) => ZIO.fail { CommandError.DecodingFailure(command, error, optStdOutString, optStdErrString) }

_ <- ZIO.foreachDiscard(optStdErrString) { stdErrString =>
stdErrOnSuccess match {
case CommandOutputSource.Empty => ZIO.unit
case CommandOutputSource.PipeStdOut => Console.printLine { stdErrString }.orDie
case CommandOutputSource.PipeStdErr => Console.printLineError { stdErrString }.orDie
case CommandOutputSource.File(path) => path.write(stdErrString).mapError { CommandError.ExecutionFailure(command, _) }
case CommandOutputSource.Log(logLevel, showCommand: ShowCommand.NonEmpty) => ZIO.logAtLevel(logLevel)(stdErrString, Cause.Empty) @@ showCommand.toAspect(command)
case CommandOutputSource.Log(logLevel, ShowCommand.Empty) => ZIO.logAtLevel(logLevel)(stdErrString, Cause.Empty)
}
}

} yield decodedRes

}
object CommandService extends CommandServicePlatformSpecific, CommandServicePlatformSpecificImpl {

def apply[R, E, A](f: CommandService => ZIO[R, E, A]): ZIO[R, E, A] = current.get.flatMap(f)

val current: FiberRef[CommandService] = Unsafe.unsafely { FiberRef.unsafe.make { default } }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package oxygen.zio.system

trait CommandServicePlatformSpecific {

val default: CommandService

}
Loading
Loading