distage-framework

Roles

A “Role” is an entrypoint for a specific application hosted in a larger software suite. Bundling multiple roles in a single .jar file can simplify deployment and operations.

distage-framework module contains the distage Role API:

libraryDependencies += "io.7mind.izumi" %% "distage-framework" % "0.10.2-M6"

With default RoleAppLauncherImpl, roles to launch are specified on the command-line: ./launcher role1 role2 role3. Only the components required by the specified roles will be created, everything else will be pruned. (see: GC)

Two roles are bundled by default: Help and ConfigWriter.

Further reading: Roles: a viable alternative to Microservices

Typesafe Config

distage-extension-config library allows summoning case classes and sealed traits from typesafe-config configuration

To use it, add distage-extension-config library:

libraryDependencies += "io.7mind.izumi" %% "distage-extension-config" % "0.10.2-M6"

Add a configuration file in HOCON format:

# resources/application.conf
conf {
    name = "John"
    age = 33
    other = true
}

Parse it into case classes and summon into your object graph:

import distage.{DIKey, GCMode, ModuleDef, Id, Injector}
import distage.config.{AppConfigModule, ConfigModuleDef}
import com.typesafe.config.ConfigFactory

final case class Conf(name: String, age: Int)

final case class OtherConf(other: Boolean)

final class ConfigPrinter(conf: Conf, otherConf: OtherConf @Id("other")) {
  def print() = {
    println(s"name: ${conf.name}, age: ${conf.age}, other: ${otherConf.other}")
  }
}

// load
val config = ConfigFactory.defaultApplication()
// config: com.typesafe.config.Config = Config(SimpleConfigObject({"conf":{"age":33,"name":"John","other":true}}))

// declare paths to parse
val configModule = new ConfigModuleDef {
  makeConfig[Conf]("conf")
  makeConfig[OtherConf]("conf").named("other")
}
// configModule: AnyRef with ConfigModuleDef = Module(make[{type.repl.Session::repl.Session.App0::repl.Session.App0.Conf}].from(call(izumi.distage.config.ConfigModuleDef$$Lambda$18415/0x000000084454c840@1802d864(izumi.distage.config.model.AppConfig): repl.Session::repl.Session.App0::repl.Session.App0.Conf)).tagged(Set(ConfTag(conf))) ((distage-framework.md:37)), make[{type.repl.Session::repl.Session.App0::repl.Session.App0.OtherConf@other}].from(call(izumi.distage.config.ConfigModuleDef$$Lambda$18415/0x000000084454c840@4e7c0bec(izumi.distage.config.model.AppConfig): repl.Session::repl.Session.App0::repl.Session.App0.OtherConf)).tagged(Set(ConfTag(conf))) ((distage-framework.md:38)))

// add config itself to the graph
val appConfigModule = AppConfigModule(config)
// appConfigModule: AppConfigModule = Module(make[{type.izumi.distage.config.model.AppConfig}].from(value(AppConfig(Config(SimpleConfigObject({"conf":{"age":33,"name":"John","other":true}}))): izumi.distage.config.model.AppConfig)) ((ConfigModuleDef.scala:20)))

val appModule = new ModuleDef {
  make[ConfigPrinter]
}
// appModule: AnyRef with ModuleDef = Module(make[{type.repl.Session::repl.Session.App0::repl.Session.App0.ConfigPrinter}].from(call(<function1>(repl.Session::repl.Session.App0::repl.Session.App0.Conf, repl.Session::repl.Session.App0::repl.Session.App0.OtherConf): repl.Session::repl.Session.App0::repl.Session.App0.ConfigPrinter)) ((distage-framework.md:46)))

Injector().produceGet[ConfigPrinter](
  Seq(appModule, configModule, appConfigModule).merge
).use {
  configPrinter =>
    configPrinter.print()
} 
// name: John, age: 33, other: true
// res1: izumi.fundamentals.platform.functional.package.Identity[Unit] = ()

Automatic derivation of config codecs is based on circe-config & circe-derivation. Circe codecs for a type will be reused if they exist.

Plugins

distage-extension-plugins module adds classpath discovery for modules that inherit a marker trait PluginBase. Plugins enable extreme late-binding; e.g. they allow a program to extend itself at launch time with new Plugin classes on the classpath. Plugins are compatible with compile-time checks as long as they’re defined in a separate module.

To use plugins, first add the distage-extension-plugins library:

libraryDependencies += "io.7mind.izumi" %% "distage-extension-plugins" % "0.10.2-M6"

Create a module extending the PluginDef trait instead of ModuleDef:

// package com.example.petstore

import distage._
import distage.plugins._

object PetStorePlugin extends PluginDef {
  make[PetRepository]
  make[PetStoreService]
  make[PetStoreController]
}

Collect all PluginDefs in a package:

val pluginConfig = PluginConfig.cached(
  packagesEnabled = Seq("com.example.petstore") // packages to scan
)
// pluginConfig: PluginConfig = PluginConfig(List(com.example.petstore),List(),true,false,List(),List())

val appModules = PluginLoader().load(pluginConfig)
// appModules: Seq[PluginBase] = List(Module(make[{type.com.example.petstore.PetRepository}].from(call(com.example.petstore.PetStorePlugin$$$Lambda$18468/0x00000008445a5040@477ad836(): com.example.petstore.PetRepository)) ((PetStorePlugin.scala:6)), make[{type.com.example.petstore.PetStoreService}].from(call(com.example.petstore.PetStorePlugin$$$Lambda$18469/0x00000008445a4840@22ed8bca(): com.example.petstore.PetStoreService)) ((PetStorePlugin.scala:7)), make[{type.com.example.petstore.PetStoreController}].from(call(com.example.petstore.PetStorePlugin$$$Lambda$18470/0x00000008445a3840@30943f3d(): com.example.petstore.PetStoreController)) ((PetStorePlugin.scala:8))))

Execute collected modules as usual:

// combine all modules into one

val appModule = appModules.merge
// appModule: PluginBase = Module(make[{type.com.example.petstore.PetRepository}].from(call(com.example.petstore.PetStorePlugin$$$Lambda$18468/0x00000008445a5040@477ad836(): com.example.petstore.PetRepository)) ((PetStorePlugin.scala:6)), make[{type.com.example.petstore.PetStoreService}].from(call(com.example.petstore.PetStorePlugin$$$Lambda$18469/0x00000008445a4840@22ed8bca(): com.example.petstore.PetStoreService)) ((PetStorePlugin.scala:7)), make[{type.com.example.petstore.PetStoreController}].from(call(com.example.petstore.PetStorePlugin$$$Lambda$18470/0x00000008445a3840@30943f3d(): com.example.petstore.PetStoreController)) ((PetStorePlugin.scala:8)))

// launch

Injector()
  .produceGet[PetStoreController](appModule)
  .use(_.run())
// PetStoreController: running!
// res3: izumi.fundamentals.platform.functional.package.Identity[Unit] = ()

Compile-time checks

An experimental compile-time verification API is available in the distage-framework module.

To use it add distage-framework library:

libraryDependencies += "io.7mind.izumi" %% "distage-framework" % "0.10.2-M6"

Only plugins defined in a different module can be checked at compile-time, test scope counts as a different module.

Example:

In main scope:

// package com.example

import distage.DIKey
import distage.StandardAxis.Env
import distage.config.ConfigModuleDef
import distage.plugins.PluginDef
import izumi.distage.staticinjector.plugins.ModuleRequirements

final case class HostPort(host: String, port: Int)

final case class Config(hostPort: HostPort)

final class Service(conf: Config, otherService: OtherService)
final class OtherService

// error: OtherService is not bound here, even though Service depends on it
final class AppPlugin extends PluginDef with ConfigModuleDef {
  tag(Env.Prod)
  
  make[Service]
  makeConfig[Config]("config")
}

// Declare OtherService as an external dependency
final class AppRequirements extends ModuleRequirements(
  // If we remove this line, compilation will rightfully break
  Set(DIKey.get[OtherService])
)

In config:

// src/main/resources/application.conf
config {
  host = localhost
  port = 8080
}

In test scope:

// package com.example.test

import com.example._
import org.scalatest.wordspec.AnyWordSpec
import izumi.distage.staticinjector.plugins.StaticPluginChecker

final class AppPluginTest extends AnyWordSpec {
  "App plugin will work (if OtherService is provided later)" in {
    StaticPluginChecker.checkWithConfig[AppPlugin, AppRequirements]("env:prod", ".*.application.conf")   
  }
}

checkWithConfig will run at compile-time whenever AppPluginTest is recompiled.

Note: Since version 0.10.0, configuration files are no longer checked for correctness by the compile-time checker, see: https://github.com/7mind/izumi/issues/763