Branch Framework

Branch is a zero-dependency framework for Scala 3 on Java 21+.

Why zero-dependency? For fun! Fun, and to illustrate how much you can get done with Scala without relying on bigger frameworks. Branch will not be the fastest, most performant solution, but it will (hopefully) let you get things done quickly! Think of it as the framework for your side-project, not your job.

Branch is made up of a collection of modules, each focusing on different parts:

  • Lzy - Lazy Futures or Tiny Effects?
  • Spider - A wrapper/framework around the Java HttpServer (I bet you didn't even know there was one!), as well as HttpClient helpers.
  • Piggy - A SQL framework, probably focused on PostgreSQL.
  • Friday - A Json library, because Scala doesn't already have enough.
  • Macaroni - Some re-usable helpers and meta-programming utilities.
  • Veil - .env / (Json based) Config utilities.
  • Blammo - It's better than bad, it's (Json) logging!
  • Keanu - A simple typed EventBus implementation, and a mediocre untyped ActorSystem.
  • Ursula - A slim CLI framework.

... and more to come!

A list of other things important to this framework's goals are (but not limited to):

  • Web Sockets
  • (HTML) Templating

Lzy

Lzy is somewhere between lazy Futures, and a tiny Effect System.

A Prelude

A (Scala) Future is an eager calculation - once referenced, it's going to run! This also means once it's done, we can't re-run it - only evaluate the finished state. Sometimes it's beneficial to describe some logic in a lazy fashion, where it's more of a blueprint of what to do. Calling the blueprint runs the code, whose output can be assigned to a value, but then the blueprint can be run again!

Let's compare the following Future code to Lazy code:

given ExecutionContext = LazyRuntime.executionContext
val f1: Future[Int] = Future(Random.nextInt(10))
// f1 is already running, kicked off by an implicit ExecutionContext
val f2: Future[Int] = Future(Random.nextInt(10))
// f2 is now running...
def fRandomSum: Future[Int] = for {
  a <- f1
  b <- f2
} yield (a + b)
// fRandomSum will be the same every time it's called
println(Await.result(fRandomSum, Duration.Inf))
println(Await.result(fRandomSum, Duration.Inf))
println(Await.result(fRandomSum, Duration.Inf))
val l1: Lazy[Int] = Lazy.fn(Random.nextInt(10))
// l1 is a description of what you want to do, nothing is running yet 
val l2: Lazy[Int] = Lazy.fn(Random.nextInt(10))

def lzyRandomSum: Lazy[Int] = for {
  a <- l1
  b <- l2
} yield (a + b)
// lzyRandomSum will be different each time, because the whole blueprint is evaluated on each call
println(lzyRandomSum.runSync)
println(lzyRandomSum.runSync)
println(lzyRandomSum.runSync)

This description/lazy evaluation approach lets you structure you're programs in descriptive ways, but also has the bonus of all the combinator effects you can add on. For example, let's say we want to add some ergonomic error handling to one of out lazy function and add a way to .recover a failure.

def myLazyOp(arg: Int): Lazy[Int] =
  Lazy.fn(42 / arg)

myLazyOp(0).runSync()
// -> Failure(java.lang.ArithmeticException: / by zero)

myLazyOp(0).recover(_ => Lazy.fn(0)).runSync()
// => Success(0)

The recovery doesn't have to be part of our definition of myLazyOp, and can be applied where needed, and different recovery strategies used in various places.

Other Libraries

If you like Lzy, you should check out ZIO

Spider

Oh, what a tangled web we weave when first we practice http

Spider is built on top of the built-in Java HttpServer.

JVM HttpServer

In the context of the HttpServer, you build HttpHandlers that handles an HttpExchange (receiving a request and returning a response). These handlers are registered with a root URI to the server via an HttpContext

When a HTTP request is received, the appropriate HttpContext (and handler) is located by finding the context whose path is the longest matching prefix of the request URI's path. Paths are matched literally, which means that the strings are compared case sensitively, though there is a ci string interpolator to help with case-insensitive matching.

Spider

Spider encapsulates this via RequestHandlers and ContextHandlers.

There is a RequestHandler[I,O] to extend for handling a route. You will need to use a Convserion from Array[Byte] to an Input model I, and a similar outgoing conversion for you output model O. With these conversions, the RequestHandler takes care of parsing the input streams from the HttpExchange and writing the response to the output stream. Some conversions for simple types are provided by import RequestHandler.given.

trait RequestHandler[I, O](using
                           requestDecoder: Conversion[Array[Byte], I],
                           responseEncoder: Conversion[O, Array[Byte]]
                          )

Implementing this trait, you will then need to write the function that will handle a Request[I] and return a Response[O].

Here is an example that returns the string Aloha.

import RequestHandler.given

case class GreeterGetter() extends RequestHandler[Unit, String] {
  override def handle(request: Request[Unit]): Response[String] = {
    Response(Map.empty, "Aloha")
  }
}

Request is a case class that wraps/holds some values based on the request of the HttpContext, and Response similarly models the output in a more Scala friendly way.

With all of your request handlers made, we can then use them in a Contexthandler.

trait ContextHandler(val path: String) 

The main thing to implement here is the contextRouter. The contextRouter is a PartialFunction that matches the http method/verb and request path and maps to a specific Requesthandler.

Here is an example:


case class EchoGetter(msg: String) extends RequestHandler[Unit, String] {
  override def handle(request: Request[Unit]): Response[String] = {
    Response(msg)
  }
}

val myhandler = new ContextHandler("/") {

  override val contextRouter
  : PartialFunction[(HttpVerb, Path), RequestHandler[?, ?]] = {
    case HttpVerb.GET -> >> / "some" / "path" => alohaGreeter
    case HttpVerb.GET -> >> / "some" / "path" / s"$arg" => EchoGetter(arg)
  }

}

We can then register out ContextHandler to an instance of the http server

ContextHandler.registerHandler(myhandler)(using httpServer: HttpServer)

ContextHandlers support Filters (what might typically be described as middleware, that ge process in the request/response chain), as well as the Authenticator class. These are specific to the root path the ContextHandler is bound to, and this could help determine when to group things into multiple ContextHandlers.

There is an HttpApp trait that sets up the server for you in an entry point. A quick example:

object HttpAppExample extends HttpApp {

  import RequestHandler.given

  case class GreeterGetter() extends RequestHandler[Unit, String] {
    override def handle(request: Request[Unit]): Response[String] = {
      Response("Aloha")
    }
  }

  val alohaGreeter = GreeterGetter()

  case class EchoGetter(msg: String) extends RequestHandler[Unit, String] {
    override def handle(request: Request[Unit]): Response[String] = {
      Response(msg)
    }
  }

  val myhandler = new ContextHandler("/") {

    override val filters: Seq[Filter] = Seq(
      ContextHandler.timingFilter
    )

    override val contextRouter
    : PartialFunction[(HttpVerb, Path), RequestHandler[?, ?]] = {
      case HttpVerb.GET -> >> / "some" / "path" => alohaGreeter
      case HttpVerb.GET -> >> / "some" / "path" / s"$arg" => EchoGetter(arg)
    }

  }

  ContextHandler.registerHandler(myhandler)
}

Other Libraries

If you like Spider, you should check out Tapir

Piggy

Other Libraries

If you like Piggy, you should check out Magnum

Friday

Friday is built off of a parser that is the topic of a chapter in Function Programming in Scala (2nd Ed) of parser combinators.

The library provides an AST to convert JSON to/from, as well as type-classes for Encoders, Decoders, and Codecs.

There is also an emphasis on Json AST helper methods to easily work with JSON, without having to convert to/from an explicit schema.

Great uses of this library are for encoding/decoding JSON with the Spider http server/client project, or simple JSON driven configuration files.

Working with the AST

The Json AST is described fully as

enum Json {
  case JsonNull
  case JsonBool(value: Boolean)
  case JsonNumber(value: Double)
  case JsonString(value: String)
  case JsonArray(value: IndexedSeq[Json])
  case JsonObject(value: Map[String, Json])
}

A json string can be parsed to Json, using the parse method on the Json companion object.

def parse(json: String): Either[ParseError, Json] 

Once you have a reference to a Json, you can (dangerously) access the underlying value by calling one of

  • strVal
  • numVal
  • boolVal
  • arrVal
  • objVal

These methods will throw an exception if the underlying value is not the approriate type, for example

JsonString("Some Str").strVal // Works fine
JsonString("Some Str").numVal // Throws exception

You can safely use one of the following methods to get an Option of the underlying value

  • strOpt
  • numOpt
  • boolOpt
  • arrOpt
  • objOpt

To quickly parse through possible sub-fields on a JsonObject, there is a ? extension method on both Json and Option[Json] that takes a field name as an argument

def ?(field: String): Option[Json]

With this, if we had some JSON

{
  "name": "Branch",
  "some": {
    "nested": {
      "key": "value"
    }
  }
}

We can do things like

val js: Json = ???

val maybeName: Option[Json] = js ? "name" // It's there!
val deepField: Option[Json] = js ? "some" ? "nested" ? "key" // This too!

// Not present, but doesn't throw an exception attempting to access deeper fields!
val probablyNot: Option[String] = js ? "totally" ? "not" ? "there"

Encoder, Decoders, and Codecs

Friday provides type-classes to convert Json to/from Scala models.

Decoders

For some type A, we can define a JsonEncoder[A] that can convert Json to A by providing an implementation of

  def decode(json: Json): Try[A]

For example, the following decoder can convert Json to String

given JsonDecoder[String] with {
  def decode(json: Json): Try[String] =
    Try(json.strVal)
}

Some decoders for common types like this are provided and can accessed by importing

import dev.wishingtree.branch.friday.JsonDecoder.given

Auto derivation is also supported for Product types (Sum types soon™️). We can use derives on case classes as

case class Person(name: String, age: Int)derives JsonDecoder

With the proper decoder in scope, we can decode JSON (or JSON that is still in String form) with

val personJson =
  """
    |{
    |  "name": "Mark",
    |  "age": 42
    |}
    |""".stripMargin

println {
  Json.decode[Person](personJson) // returns a Try[Person]
}

Encoders

For some type A, we can define an encoder that can convert A to Json by providing an implementation of

  def encode(a: A): Json

For example, the following encoder can convert String to Json

given JsonEncoder[String] with {
  def encode(a: String): Json = Json.JsonString(a)
}

Some encoders for common types are provided and can accessed by importing

import dev.wishingtree.branch.friday.JsonEncoder.given

Auto derivation is also supported for Product types (Sum types soon™️). We can use derives on case classes as

case class Person(name: String, age: Int)derives JsonEncoder

With the proper encoder in scope, we can use the extension method provided by the type class, or the method on the Json companion object to convert to Json

Person("Mark", 42).toJson
Json.encode(Person("Mark", 42))

Codecs

We often want to be able to go both ways, so this library provides a codec which has the ability to encode and decode.

trait JsonCodec[A] extends JsonDecoder[A], JsonEncoder[A]

This also supports auto derivation for Product types (Sum types soon™️). We can use derives on case classes as

case class Person(name: String, age: Int)derives JsonCodec

With both an encoder and decoder, there is also the JsonCodec.apply method to easily create an instance

def apply[A](using
             encoder: JsonEncoder[A],
             decoder: JsonDecoder[A]
            ): JsonCodec[A] =
  new JsonCodec[A] {
    override def decode(json: Json): Try[A] = decoder.decode(json)

    override def encode(a: A): Json = encoder.encode(a)
  }

Other Libraries

If you like Friday, you should check out uPickle

Macaroni

This module has a collection of reusable modules that could be helpful in any project.

Meta Programming

There are a couple of reusable inline helpers to summon lists of things by type, as well as some extra type helpers for Tuples.

Parser

There is a parser, which is the topic of a chapter in Function Programming in Scala (2nd Ed) of parser combinators. It is currently used to power the Friday JSON library, but is useful for any parsing application, I imagine. There is a Parsers trait, and a Reference implementation which can be used.

ResourcePool

If you need a resource pool of type R, then there is a simple trait ResourcePool[R] to extend.

The borrowing of resources is gated by a Semaphore with val poolSize: Int (defaults to 5) permits.

The pool is eagerly filled on create.

Implement def acquire: R with how to create a resource, and def release(resource: R): Unit with how to cleanly close the resource when shutting the pool down.

You can optionally over def test(resource: R): Boolean to provide a test to run after borrowing a resource, to make sure it is still healthy.

Veil

Veil is a small layer to help with configs and environment variables.

Veil can load a .env, .env.test, or .env.prod file based on the environment variable SCALA_ENV being set to DEV, TEST, or PROD. Values in this file are loaded into an in-memory map, and you can look up an env variable with Veil.get(key: String): Option[String]. If it's not present in the in-memory map, it will then search Java's System.getenv().

There is also a Config type-class that helps with loading json from files/resources, and mapping them to a case class (which presumably is used for configuration).

Blammo

This module contains some helpers around java.util.logging. So far, the main thing it has going for it is a JsonFormatter for some structured logging.

There is also a JsonConsoleLogger trait that you can mix in to your classes to get a logger that uses the formatter.

Keanu

This module currently contains a simple typed EventBus, and a (local) ActorSystem.

EventBus

Extend EventBus[T] for your type, e.g.

object IntEventBus extends EventBus[Int]

Then, you can have some implementation extend Subsciber[T] and subscribe, or pass in an anonymous implementation (e.g. IntEventBus.subscribe((msg: Int) => println(s"Got message $msg")))

Under the hood, the subscriber gets a queue of messages, and a thread that processes them, so things will be processed in order, but asynchronously.

ActorSystem

The AcotrSystem trait is implemented, and you can have an object extend it for easy singleton access. The apply method also returns a new instance as well.

let's say you have two actors:

 case class EchoActor() extends Actor {
  override def onMsg: PartialFunction[Any, Any] = {
    case any =>
      println(s"Echo: $any")
  }
}

case class SampleActor(actorSystem: ActorSystem) extends Actor {
  println("starting actor")
  var counter = 0

  override def onMsg: PartialFunction[Any, Any] = {
    case n: Int =>
      counter += n
      actorSystem.tell[EchoActor]("echo", s"Counter is now $counter")
    case "boom" => 1 / 0
    case "count" =>
      counter
    case "print" => println(s"Counter is $counter")
    case _ => println("Unhandled")
  }
}

You can register props to the actor system which capture arguments used to create actor instances. This is a bit unsafe in the sense that it takes varargs that should be supplied to create the actor, so no compiler checks at the moment.

val as = ActorSystem()
val saProps = ActorContext.props[SampleActor](as)
as.registerProp(saProps)
as.registerProp(ActorContext.props[EchoActor]())

at this point, you can send messages to the actors with the tell method on the ActorSystem:

// Helper lambda to send messages to the SampleActor named counter
val counterActor = as.tell[SampleActor]("counter", _)
counterActor(1)
counterActor(2)
counterActor(3)
counterActor(4)
counterActor("boom")
counterActor(5)
counterActor("print")

Actors are indexed based on name and type, so you can have multiple actors of the same type, but they must have unique names (and you can have actors with the same name, as long as they are different types).

If you want to shut down the ActorSystem, you can use the shutdownAwait method, which will send a PoisonPill to all actors, and attempt to wait for their queues to finish processing.

Ursula

A slim framework to make Scala CLI apps.

Anatomy of the Framework

Here is a general overview if of the pieces fit together.

How it works: UrsulaApp

You only need to make an object that extends the UrsulaApp trait, and provide a Seq[Command], which are your actions you wish to be available in your app. UrsulaApp has a final main method entrypoint, and does some processing automatically. It parses the arguments passed, and uses that to pull out the Command provided, and runs accordingly, passing on the arguments to it.

There are some built in commands provided, currently only the HelpCommand, that are also automatically injected. This means that even if you only have:

object App extends UrsulaApp {
  override val commands: Seq[Command] = Seq.empty
}

you already have a functioning cli app that has a help command that prints all the available commands accepted (as little as they are so far).

At this point, you need only implement some Commands that wrap the functionality you desire, and add them to the commands: Seq.

Commands

There is a trait Command to extend, and the essence of this the implementation of

def action(args: Seq[String]): Unit

You consolidate all of your logic you want to run in this method. Commands are meant to be a one-off calls from the main entry point, and generally not composed with other Commands, so the return type is Unit.

There are a few other items to implement, such as

val trigger: String
val description: String
val usage: String
val examples: Seq[String]

trigger is the String that should be used at the start of your CLI arguments to call that particular command. The others are simple informational strings about your command - and those are automatically used by the built-in help command to print documentation!

Two other important things to declare are

val flags: Seq[Flag[?]]
val arguments: Seq[Argument[?]]

Flags and Arguments are discussed below, but know that they are simple traits to extend that help you parse/provide values for the args passed in - and they too have some simple Strings to implement that provide auto documentation for your app. At the end of the day, you can just parse the args on your own in your ZIO logic - but usage of the Flags andArguments should hopefully simplify things for your and your apps users.

Built-In Commands

  • HelpCommand - handles the printing of documentation

Flags

Flags (trait Flag[R]) are non-positional arguments passed to the command. Flags can be generally used as either an argument flag, which expects the next element in the command arguments to be parsed as type R, or boolean flags which do not (i.e. present/not present).

Some general highlights are that it has things built in to

  • parse argument(s) that can then be used in you Command
  • declare conflicts with other flags
  • declare requirements of other flags
  • provide defaults, of ENV variables to be used

Arguments

Arguments (trait Argument[R]) are positional arguments passed to the command, and are to be parsed to type R

Some general highlights are that you can encode the parsing logic.