From 67fa01d8e1299f9267ad35d7479e160574f456aa Mon Sep 17 00:00:00 2001 From: adamw Date: Mon, 23 Sep 2024 08:59:49 +0200 Subject: [PATCH] Release 0.4.0 --- README.md | 4 +- generated-doc/out/basics/start-here.md | 4 +- generated-doc/out/channels/io.md | 59 +++++------ generated-doc/out/index.md | 3 +- generated-doc/out/io.md | 140 ------------------------- generated-doc/out/kafka.md | 52 ++++----- generated-doc/out/mdc-logback.md | 2 +- generated-doc/out/oxapp.md | 18 ++-- 8 files changed, 64 insertions(+), 218 deletions(-) delete mode 100644 generated-doc/out/io.md diff --git a/README.md b/README.md index 87328d0e..8a017c8f 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,13 @@ the project! To test Ox, use the following dependency, using either [sbt](https://www.scala-sbt.org): ```scala -"com.softwaremill.ox" %% "core" % "0.3.9" +"com.softwaremill.ox" %% "core" % "0.4.0" ``` Or [scala-cli](https://scala-cli.virtuslab.org): ```scala -//> using dep "com.softwaremill.ox::core:0.3.9" +//> using dep "com.softwaremill.ox::core:0.4.0" ``` Documentation is available at [https://ox.softwaremill.com](https://ox.softwaremill.com), ScalaDocs can be browsed at [https://javadoc.io](https://www.javadoc.io/doc/com.softwaremill.ox). diff --git a/generated-doc/out/basics/start-here.md b/generated-doc/out/basics/start-here.md index 2953dcb0..26a9c572 100644 --- a/generated-doc/out/basics/start-here.md +++ b/generated-doc/out/basics/start-here.md @@ -4,10 +4,10 @@ ```scala // sbt dependency -"com.softwaremill.ox" %% "core" % "0.3.9" +"com.softwaremill.ox" %% "core" % "0.4.0" // scala-cli dependency -//> using dep com.softwaremill.ox::core:0.3.9 +//> using dep com.softwaremill.ox::core:0.4.0 ``` ## Scope of the Ox project diff --git a/generated-doc/out/channels/io.md b/generated-doc/out/channels/io.md index 5896588f..21b212f8 100644 --- a/generated-doc/out/channels/io.md +++ b/generated-doc/out/channels/io.md @@ -2,8 +2,6 @@ Ox allows creating a `Source` which reads from a file or `InpuStream`, as well as directing an existing source into a file or an `OutputStream`. These methods work only with a `Source[Chunk[Byte]]`. Ox takes care of closing files/streams after processing and on errors. -All I/O operations require the [IO capability](../io.md). - ## InputStream and OutputStream ### Source.fromInputStream @@ -12,18 +10,17 @@ An `InputStream` can be converted to a `Source[Chunk[Byte]]`: ```scala import ox.channels.Source -import ox.{IO, supervised} +import ox.supervised import java.io.ByteArrayInputStream import java.io.InputStream val inputStream: InputStream = new ByteArrayInputStream("some input".getBytes) supervised { - IO.unsafe: - Source - .fromInputStream(inputStream) // Source[Chunk[Byte]] - .decodeStringUtf8 - .map(_.toUpperCase) - .foreach(println) // "SOME INPUT" + Source + .fromInputStream(inputStream) // Source[Chunk[Byte]] + .decodeStringUtf8 + .map(_.toUpperCase) + .foreach(println) // "SOME INPUT" } ``` @@ -32,18 +29,17 @@ You can define a custom chunk size instead of using the default: ```scala import ox.channels.Source -import ox.{IO, supervised} +import ox.supervised import java.io.ByteArrayInputStream import java.io.InputStream val inputStream: InputStream = new ByteArrayInputStream("some input".getBytes) supervised { - IO.unsafe: - Source - .fromInputStream(inputStream, chunkSize = 4) // Source[Chunk[Byte]] - .decodeStringUtf8 - .map(_.toUpperCase) - .foreach(println) // "SOME", " INPUT" + Source + .fromInputStream(inputStream, chunkSize = 4) // Source[Chunk[Byte]] + .decodeStringUtf8 + .map(_.toUpperCase) + .foreach(println) // "SOME", " INPUT" } ``` @@ -53,16 +49,15 @@ A `Source[Chunk[Byte]]` can be directed to write to an `OutputStream`: ```scala import ox.channels.Source -import ox.{IO, supervised} +import ox.supervised import java.io.ByteArrayOutputStream val outputStream = new ByteArrayOutputStream() supervised { val source = Source.fromIterable(List("text1,", "text2")) - IO.unsafe: - source - .encodeUtf8 - .toOutputStream(outputStream) + source + .encodeUtf8 + .toOutputStream(outputStream) } outputStream.toString // "TEXT1,TEXT2" ``` @@ -75,16 +70,15 @@ You can obtain a `Source` of byte chunks read from a file for a given path: ```scala import ox.channels.Source -import ox.{IO, supervised} +import ox.supervised import java.nio.file.Paths supervised { - IO.unsafe: - Source - .fromFile(Paths.get("/path/to/my/file.txt")) - .linesUtf8 - .map(_.toUpperCase) - .toList // List("FILE_LINE1", "FILE_LINE2") + Source + .fromFile(Paths.get("/path/to/my/file.txt")) + .linesUtf8 + .map(_.toUpperCase) + .toList // List("FILE_LINE1", "FILE_LINE2") } ``` @@ -96,14 +90,13 @@ A `Source[Chunk[Byte]]` can be written to a file under a given path: ```scala import ox.channels.Source -import ox.{IO, supervised} +import ox.supervised import java.nio.file.Paths supervised { val source = Source.fromIterable(List("text1,", "text2")) - IO.unsafe: - source - .encodeUtf8 - .toFile(Paths.get("/path/to/my/target/file.txt")) + source + .encodeUtf8 + .toFile(Paths.get("/path/to/my/target/file.txt")) } ``` diff --git a/generated-doc/out/index.md b/generated-doc/out/index.md index 8a55eb53..694d1632 100644 --- a/generated-doc/out/index.md +++ b/generated-doc/out/index.md @@ -45,10 +45,9 @@ In addition to this documentation, ScalaDocs can be browsed at [https://javadoc. .. toctree:: :maxdepth: 2 - :caption: Resiliency, I/O & utilities + :caption: Resiliency & utilities oxapp - io retries repeat scheduled diff --git a/generated-doc/out/io.md b/generated-doc/out/io.md deleted file mode 100644 index 3012ed8c..00000000 --- a/generated-doc/out/io.md +++ /dev/null @@ -1,140 +0,0 @@ -# I/O - -Ox includes the `IO` capability, which is designed to be part of the signature of any method, which performs I/O -either directly or indirectly. The goal is for method signatures to be truthful, and specify the possible side effects, -failure modes and timing in a reasonably precise and practical way. For example: - -```scala -import ox.IO - -def readFromFile(path: String)(using IO): String = ??? -def writeToFile(path: String, content: String)(using IO): Unit = ??? -def transform(path: String)(f: String => String)(using IO): Unit = - writeToFile(path, f(readFromFile(path))) -``` - -In other words, the presence of a `using IO` parameter indicates that the method might: - -* have side effects: write to a file, send a network request, read from a database, etc. -* take a non-trivial amount of time to complete due to blocking, data transfer, etc. -* throw an exception (unless the exceptions are handled, and e.g. transformed into - [application errors](basics/error-handling.md)) - -Quite importantly, the **absence** of `using IO` specifies that the method has **no** I/O side effects (however, it -might still block the thread, e.g. when using a channel, or have other side effects, such as throwing exceptions, -accessing the current time, or generating a random number). Compiler assists in checking this property, but only to a -certain degree - it's possible to cheat! - -The `IO` capability can be introduced using `IO.unsafe`. Ideally, this method should only be used at the edges of your -application (e.g. in the `main` method), or when integrating with third-party libraries. Otherwise, the capability -should be passed as an implicit parameter. Such an ideal scenario might not possible, but there's still value in `IO` -tracking: by looking up the usages of `IO.unsafe` it's possible to quickly find the "roots" where the `IO` capability -is introduced. For example: - -```scala -import ox.IO - -def sendHTTPRequest(body: String)(using IO): String = ??? - -@main def run(): Unit = - IO.unsafe: - sendHTTPRequest("Hello, world!") -``` - -For testing purposes, instead of using `IO.unsafe`, there's a special import which grants the capability within the -scope of the import. By having different mechanisms for introducing `IO` in production and test code, test usages don't -pollute the search results, when verifying `IO.unsafe` usages (which should be as limited as possible). For example: - -```scala -import ox.IO -import ox.IO.globalForTesting.given - -def myMethod()(using IO): Unit = ??? - -def testMyMethod(): Unit = myMethod() -``` - -Take care not to capture the capability e.g. using constructors (unless you are sure such usage is safe), as this might -circumvent the tracking of I/O operations. Similarly, the capability might be captured by lambdas, which might later be -used when the IO capability is not in scope. In future Scala and Ox releases, these problems should be detected at -compile-time using the upcoming capture checker. - -## The requireIO compiler plugin - -Ox provides a compiler plugin, which verifies at compile-time that the `IO` capability is present when invoking any -methods from the JDK or Java libraries that specify to throw an IO-related exception, such as `java.io.IOException`. - -To use the plugin, add the following settings to your sbt configuration: - -```scala -autoCompilerPlugins := true -addCompilerPlugin("com.softwaremill.ox" %% "plugin" % "0.3.9") -``` - -For scala-cli: - -```scala -//> using plugin com.softwaremill.ox:::plugin:0.3.9 -``` - -With the plugin enabled, the following code won't compile: - -```scala -import java.io.InputStream - -object Test: - def test(): Unit = - val is: InputStream = ??? - is.read() - -/* -[error] -- Error: Test.scala:8:11 -[error] 8 | is.read() -[error] | ^^^^^^^^^ -[error] |The `java.io.InputStream.read` method throws an `java.io.IOException`, -[error] |but the `ox.IO` capability is not available in the implicit scope. -[error] | -[error] |Try adding a `using IO` clause to the enclosing method. - */ -``` - -You can think of the plugin as a way to translate between the effect system that is part of Java - checked exceptions - -and the `IO` effect specified by Ox. Note that only usages of Java methods which have the proper `throws` clauses will -be checked (or of Scala methods, which have the `@throws` annotation). - -```{note} -If you are using a Scala library that uses Java's I/O under the covers, such usages can't (and won't) be -checked by the plugin. The scope of the plugin is currently limited to the JDK and Java libraries only. -``` - -### Other I/O exceptions - -In some cases, libraries wrap I/O exceptions in their own types. It's possible to configure the plugin to require the -`IO` capability for such exceptions as well. In order to do so, you need to pass the fully qualified names of these -exceptions as a compiler plugin option, each class in a separate option. For example, in sbt: - -```scala -Compile / scalacOptions += "-P:requireIO:com.example.MyIOException" -``` - -In a scala-cli directive: - -```bash -//> using option -P:requireIO:com.example.MyIOException -``` - -Currently, by default the plugin checks for the following exceptions: - -* `java.io.IOException` -* `java.sql.SQLException` - -## Potential benefits of tracking methods that perform I/O - -Tracking which methods perform I/O using the `IO` capability has the only benefit of giving you method signatures, -which carry more information. In other words: more type safety. The specific benefits might include: - -* better code readability (what does this method do? -* local reasoning (does this method perform I/O?) -* safer refactoring (adding I/O to a previously pure method triggers errors in the compiler, you need to consciously add the capability) -* documentation through types (an IO method can take a longer time, have side-effects) -* possible failure modes (an IO method might throw an exception) diff --git a/generated-doc/out/kafka.md b/generated-doc/out/kafka.md index ccb78d3d..1995c321 100644 --- a/generated-doc/out/kafka.md +++ b/generated-doc/out/kafka.md @@ -3,7 +3,7 @@ Dependency: ```scala -"com.softwaremill.ox" %% "kafka" % "0.3.9" +"com.softwaremill.ox" %% "kafka" % "0.4.0" ``` `Source`s which read from a Kafka topic, mapping stages and drains which publish to Kafka topics are available through @@ -11,24 +11,21 @@ the `KafkaSource`, `KafkaStage` and `KafkaDrain` objects. In all cases either a `KafkaProducer` / `KafkaConsumer` is needed, or `ProducerSettings` / `ConsumerSetttings` need to be provided with the bootstrap servers, consumer group id, key / value serializers, etc. -All Kafka I/O operations require the [IO capability](io.md). - To read from a Kafka topic, use: ```scala import ox.channels.ChannelClosed import ox.kafka.{ConsumerSettings, KafkaSource, ReceivedMessage} import ox.kafka.ConsumerSettings.AutoOffsetReset -import ox.{IO, supervised} +import ox.supervised supervised { val settings = ConsumerSettings.default("my_group").bootstrapServers("localhost:9092").autoOffsetReset(AutoOffsetReset.Earliest) val topic = "my_topic" - IO.unsafe: - val source = KafkaSource.subscribe(settings, topic) - - source.receive(): ReceivedMessage[String, String] | ChannelClosed + val source = KafkaSource.subscribe(settings, topic) + + source.receive(): ReceivedMessage[String, String] | ChannelClosed } ``` @@ -37,16 +34,15 @@ To publish data to a Kafka topic: ```scala import ox.channels.Source import ox.kafka.{ProducerSettings, KafkaDrain} -import ox.{IO, pipe, supervised} +import ox.{pipe, supervised} import org.apache.kafka.clients.producer.ProducerRecord supervised { val settings = ProducerSettings.default.bootstrapServers("localhost:9092") - IO.unsafe: - Source - .fromIterable(List("a", "b", "c")) - .mapAsView(msg => ProducerRecord[String, String]("my_topic", msg)) - .pipe(KafkaDrain.publish(settings)) + Source + .fromIterable(List("a", "b", "c")) + .mapAsView(msg => ProducerRecord[String, String]("my_topic", msg)) + .pipe(KafkaDrain.publish(settings)) } ``` @@ -71,7 +67,7 @@ computed. For example: ```scala import ox.kafka.{ConsumerSettings, KafkaDrain, KafkaSource, ProducerSettings, SendPacket} import ox.kafka.ConsumerSettings.AutoOffsetReset -import ox.{IO, pipe, supervised} +import ox.{pipe, supervised} import org.apache.kafka.clients.producer.ProducerRecord supervised { @@ -80,12 +76,11 @@ supervised { val sourceTopic = "source_topic" val destTopic = "dest_topic" - IO.unsafe: - KafkaSource - .subscribe(consumerSettings, sourceTopic) - .map(in => (in.value.toLong * 2, in)) - .map((value, original) => SendPacket(ProducerRecord[String, String](destTopic, value.toString), original)) - .pipe(KafkaDrain.publishAndCommit(producerSettings)) + KafkaSource + .subscribe(consumerSettings, sourceTopic) + .map(in => (in.value.toLong * 2, in)) + .map((value, original) => SendPacket(ProducerRecord[String, String](destTopic, value.toString), original)) + .pipe(KafkaDrain.publishAndCommit(producerSettings)) } ``` @@ -97,17 +92,16 @@ To publish data as a mapping stage: import ox.channels.Source import ox.kafka.ProducerSettings import ox.kafka.KafkaStage.* -import ox.{IO, supervised} +import ox.supervised import org.apache.kafka.clients.producer.{ProducerRecord, RecordMetadata} supervised { val settings = ProducerSettings.default.bootstrapServers("localhost:9092") - IO.unsafe: - val metadatas: Source[RecordMetadata] = Source - .fromIterable(List("a", "b", "c")) - .mapAsView(msg => ProducerRecord[String, String]("my_topic", msg)) - .mapPublish(settings) - - // process the metadatas source further + val metadatas: Source[RecordMetadata] = Source + .fromIterable(List("a", "b", "c")) + .mapAsView(msg => ProducerRecord[String, String]("my_topic", msg)) + .mapPublish(settings) + + // process the metadatas source further } ``` diff --git a/generated-doc/out/mdc-logback.md b/generated-doc/out/mdc-logback.md index a5a6fe81..df05ae79 100644 --- a/generated-doc/out/mdc-logback.md +++ b/generated-doc/out/mdc-logback.md @@ -3,7 +3,7 @@ Dependency: ```scala -"com.softwaremill.ox" %% "mdc-logback" % "0.3.9" +"com.softwaremill.ox" %% "mdc-logback" % "0.4.0" ``` Ox provides support for setting inheritable MDC (mapped diagnostic context) values, when using the [Logback](https://logback.qos.ch) diff --git a/generated-doc/out/oxapp.md b/generated-doc/out/oxapp.md index 54768b61..7b6e41d1 100644 --- a/generated-doc/out/oxapp.md +++ b/generated-doc/out/oxapp.md @@ -1,8 +1,8 @@ # OxApp To properly handle application interruption and clean shutdown, Ox provides a way to define application entry points -using `OxApp` trait. The application's main `run` function is then executed on a virtual thread, with a root `Ox` and -`IO` capabilities provided. +using `OxApp` trait. The application's main `run` function is then executed on a virtual thread, with a root `Ox` +capability provided. Here's an example: @@ -11,7 +11,7 @@ import ox.* import scala.concurrent.duration.* object MyApp extends OxApp: - def run(args: Vector[String])(using Ox, IO): ExitCode = + def run(args: Vector[String])(using Ox): ExitCode = forkUser { sleep(500.millis) println("Fork finished!") @@ -30,14 +30,14 @@ In the code below, the resource is released when the application is interrupted: import ox.* object MyApp extends OxApp: - def run(args: Vector[String])(using Ox, IO): ExitCode = + def run(args: Vector[String])(using Ox): ExitCode = releaseAfterScope: println("Releasing ...") println("Waiting ...") never ``` -The `run` function receives command line arguments as a `Vector` of `String`s, a given `Ox` and `IO` capabilities and +The `run` function receives command line arguments as a `Vector` of `String`s, a given `Ox` capability and has to return an `ox.ExitCode` value which translates to the exit code returned from the program. `ox.ExitCode` is defined as: @@ -48,14 +48,14 @@ enum ExitCode(val code: Int): ``` There's also a simplified variant of `OxApp` for situations where you don't care about command line arguments. -The `run` function doesn't take any arguments beyond the root `Ox` and `IO` capabilities, expects no `ExitCode` and will +The `run` function doesn't take any arguments beyond the root `Ox` capability, expects no `ExitCode` and will handle any exceptions thrown by printing a stack trace and returning an exit code of `1`: ```scala import ox.* object MyApp extends OxApp.Simple: - def run(using Ox, IO): Unit = println("All done!") + def run(using Ox): Unit = println("All done!") ``` `OxApp` has also a variant that integrates with [either](basics/error-handling.md#boundary-break-for-eithers) @@ -80,7 +80,7 @@ object MyApp extends OxApp.WithEitherErrors[MyAppError]: case ComputationError(_) => ExitCode.Failure(23) } - def run(args: Vector[String])(using Ox, EitherError[MyAppError], IO): ExitCode = + def run(args: Vector[String])(using Ox, EitherError[MyAppError]): ExitCode = doWork().ok() // will end the scope with MyAppError as `doWork` returns a Left ExitCode.Success ``` @@ -106,7 +106,7 @@ object MyApp extends OxApp: interruptedExitCode = ExitCode.Failure(130) ) - def run(args: Vector[String])(using Ox, IO): ExitCode = + def run(args: Vector[String])(using Ox): ExitCode = sleep(60.seconds) ExitCode.Success ```