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.

Add distage-framework library for Roles API:

sbt
libraryDependencies += "io.7mind.izumi" %% "distage-framework" % "1.0.0-SNAPSHOT"
Maven
<dependency>
  <groupId>io.7mind.izumi</groupId>
  <artifactId>distage-framework_2.13</artifactId>
  <version>1.0.0-SNAPSHOT</version>
</dependency>
Gradle
dependencies {
  compile group: 'io.7mind.izumi', name: 'distage-framework_2.13', version: '1.0.0-SNAPSHOT'
}

With default RoleAppMain, roles to launch are specified on the command-line:

./launcher :rolename1 :rolename2`

Only the components required by the specified roles will be created, everything else will be pruned. (see: Dependency Pruning)

BundledRolesModule contains two example roles:

  • Help - prints help message when launched ./launcher :help
  • ConfigWriter - writes reference config into files, split by roles (includes only parts of the config used by the application)

Further reading: - Roles: a viable alternative to Microservices and Monoliths

Typesafe Config

distage-extension-config library allows parsing case classes and sealed traits from typesafe-config configuration files.

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

sbt
libraryDependencies += "io.7mind.izumi" %% "distage-extension-config" % "1.0.0-SNAPSHOT"
Maven
<dependency>
  <groupId>io.7mind.izumi</groupId>
  <artifactId>distage-extension-config_2.13</artifactId>
  <version>1.0.0-SNAPSHOT</version>
</dependency>
Gradle
dependencies {
  compile group: 'io.7mind.izumi', name: 'distage-extension-config_2.13', version: '1.0.0-SNAPSHOT'
}

Use helper functions in ConfigModuleDef, makeConfig and make[_].fromConfig to parse the bound AppConfig instance into case classes, sealed traits or literals:

import distage.{Id, Injector}
import distage.config.{AppConfig, 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}")
  }
}

val module = new ConfigModuleDef {
  make[ConfigPrinter]

  // declare paths to parse
  makeConfig[Conf]("conf")
  makeConfig[OtherConf]("conf").named("other")

  // add config instance
  make[AppConfig].from(AppConfig(
    ConfigFactory.parseString(
      """conf {
        |  name = "John"
        |  age = 33
        |  other = true
        |}""".stripMargin
    )
  ))
}
// module: AnyRef with ConfigModuleDef = 
// make[{type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.OtherConf@other}].from(call(ƒ:izumi.distage.config.codec.DIConfigReader$$Lambda$21203/319031496@15fd4efd(izumi.distage.config.model.AppConfig): repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.OtherConf)).tagged(Set(ConfTag(conf))) ((distage-framework.md:46))
// make[{type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.Conf}].from(call(ƒ:izumi.distage.config.codec.DIConfigReader$$Lambda$21203/319031496@268c50ac(izumi.distage.config.model.AppConfig): repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.Conf)).tagged(Set(ConfTag(conf))) ((distage-framework.md:45))
// make[{type.izumi.distage.config.model.AppConfig}].from(call(ƒ:<function0>(): izumi.distage.config.model.AppConfig)) ((distage-framework.md:49))
// make[{type.repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.ConfigPrinter}].from(call(π:Class(repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.Conf, repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.OtherConf): repl.MdocSession::repl.MdocSession.App0::repl.MdocSession.App0.ConfigPrinter)) ((distage-framework.md:42))

Injector().produceRun(module) {
  configPrinter: 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 pureconfig-magnolia.

When a given type already has custom pureconfig.ConfigReader instances in implicit scope, they will be used, otherwise they will be derived automatically.

You don’t have to explicitly make[AppConfig] in distage-testkit’s tests and in distage-framework’s Roles, unless you want to override default behavior.

By default, tests and roles will try to read the configurations from resources with the following names, in order:

  • ${roleName}.conf
  • ${roleName}-reference.conf
  • ${roleName}-reference-dev.conf
  • application.conf
  • application-reference.conf
  • application-reference-dev.conf
  • common.conf
  • common-reference.conf
  • common-reference-dev.conf

Where distage-testkit uses TestConfig#configBaseName instead of roleName.

Explicit config files passed to the role launcher -c file.conf the command-line flag have a higher priority than resource configs.

Role-specific configs on the command-line, passed via -c file.conf option after a :roleName argument, in turn have a higher priority than the explicit config files passed before the first :role argument.

Example:

  ./launcher -c global.conf :role1 -c role1.conf :role2 -c role2.conf

Here configs will be loaded in the following order, with higher priority earlier and earlier configs overriding the values in later configs:

  • explicits: role1.conf, role2.conf, global.conf,
  • resources: role1[-reference,-dev].conf, role2[-reference,-dev].conf, ,application[-reference,-dev].conf, common[-reference,-dev].conf

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:

sbt
libraryDependencies += "io.7mind.izumi" %% "distage-extension-plugins" % "1.0.0-SNAPSHOT"
Maven
<dependency>
  <groupId>io.7mind.izumi</groupId>
  <artifactId>distage-extension-plugins_2.13</artifactId>
  <version>1.0.0-SNAPSHOT</version>
</dependency>
Gradle
dependencies {
  compile group: 'io.7mind.izumi', name: 'distage-extension-plugins_2.13', version: '1.0.0-SNAPSHOT'
}

Create a module extending the PluginDef trait instead of ModuleDef:

package com.example.petstore

import distage.Injector
import distage.plugins.{PluginConfig, PluginDef, PluginLoader}

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

Collect all the PluginDef classes and objects in a package:

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

val appModules = PluginLoader().load(pluginConfig)
// appModules: izumi.distage.plugins.load.LoadedPlugins = LoadedPlugins(List(
// make[{type.com.example.petstore.PetStoreController}].from(call(π:Class(): com.example.petstore.PetStoreController)) ((PetStorePlugin.scala:8))
// make[{type.com.example.petstore.PetStoreService}].from(call(π:Class(): com.example.petstore.PetStoreService)) ((PetStorePlugin.scala:7))
// make[{type.com.example.petstore.PetRepository}].from(call(π:Class(): com.example.petstore.PetRepository)) ((PetStorePlugin.scala:6))),List(),List())

Wire the collected modules as usual:

// combine all modules into one

val appModule = appModules.result.merge
// appModule: izumi.distage.plugins.PluginBase = 
// make[{type.com.example.petstore.PetRepository}].from(call(π:Class(): com.example.petstore.PetRepository)) ((PetStorePlugin.scala:6))
// make[{type.com.example.petstore.PetStoreService}].from(call(π:Class(): com.example.petstore.PetStoreService)) ((PetStorePlugin.scala:7))
// make[{type.com.example.petstore.PetStoreController}].from(call(π:Class(): com.example.petstore.PetStoreController)) ((PetStorePlugin.scala:8))

// launch

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

Compile-time checks

WIP on current 1.0.0-M1. Version1.0.0 will have finalized API.