Skip to main content

Tegral Featureful

Tegral Featureful is a framework for defining lightweight modules (called "features") that can be installed in full applications. It is the base building block for Tegral applications.

Note that Tegral Featureful in itself is not an "application engine". It only allows you to define you own features, but not actually run full applications. This task is delegated to Tegral Web AppDSL in the case of web applications.

Package information
Package nameCatalog dependencyFull Gradle name
tegral-featurefultegralLibs.featurefulguru.zoroark.tegral:tegral-featureful:VERSION

What are features?

Features are simply objects that act upon the dependency injection environment of an application. This generally means that:

  • Simpler features will simply put() things in the environment that applications will then be able to request and consume.
  • More complex features will also add DI extensions that will automatically discover and interact with other elements within the dependency injection environment.

Creating a feature

Let's create a feature that just injects a dummy "Foo" object in the dependency injection environment. Features are defined by creating a feature object that implements the Feature interface.

class Foo {
fun bar(): String = "Bar!"
}

object FooFeature : Feature {
override val id = "acme-foo"
override val name = "ACME Foo"
override val description = "Provides a Foo instance in the DI environment"

override fun ExtensibleContextBuilderDsl.install() {
put(::Foo)
}
}

Metadata

All features have an ID, a name and a description.

  • The ID of a feature should be unique across all features. The tegral- prefix is reserved for first-party features (i.e. features created by the Tegral team that are part of the Tegral repository).
  • The name of a feature is the human-readable name of the feature.
  • The description of a feature should be a short (approx. 1 sentence) description of what the feature does.

A feature may also define a set of dependencies, which are features that will be installed together with the current feature. Defining dependencies is done by overriding the dependencies property:

override val dependencies = setOf(MyOtherFeature)

Installation

When using Tegral Web AppDSL, features can be installed using the install(...) syntax, i.e.:

fun main() {
tegral {
install(FooFeature)

// ...
}
}

Configurable features

Features can consume configuration if they need additional information to act upon the environment.

caution

Currently, only configuration via configuration files (e.g. tegral.toml) is supported with Tegral Web AppDSL. Later on, proper DSLs for configuring features in-code will be provided (similar to the way Ktor plugins work). Rule of thumb will be:

  • If it is something that users are likely to change in production, it should be in the configuration file (if a simple value) or togglable via a configuration field.
  • If it is something that developers are likely to always keep the same, whether in production or in code, it should be in code.

Configurable features use configuration sections, which are passed to a standard location in Tegral applications. For example, configurable features installed in a Tegral Web application have their sections under the [tegral.*] category.

Let's extend our previous example. Instead of "Bar!", our Foo feature will use a configurable string, or "Bar!" if no such configuration is present.

We'll first create a configuration section...

data class FooConfig(
val bar: String = "Bar!"
) {
companion object : ConfigurationSection<FooConfig>(
"foo",
// Section can be omitted entirely, in which case we'll use the default
SectionOptionality.Optional(FooConfig()),
FooConfig::class
)
}

Let's make our feature implement ConfigurableFeature instead of just Feature, and register the section there:

object FooFeature : ConfigurableFeature {
override val id = "acme-foo"
override val name = "ACME Foo"
override val description = "Provides a Foo instance in the DI environment"
override val configurationSections = listOf(FooConfig)

override fun ExtensibleContextBuilderDsl.install() {
put(::Foo)
}
}

Finally, let's modify our Foo class so that it uses our configuration section instead of the hardcoded value. This also means actually using dependency injection here, as we will be retrieving the configuration class from the DI environment.

class Foo(scope: InjectionScope) {
private val config: TegralConfig by scope()

fun bar(): String = config[FooConfig].bar
}

Optionally, you can use the wrapIn pattern to avoid calling config[...] all the time:

class Foo(scope: InjectionScope) {
private val config: TegralConfig by scope<TegralConfig>() wrapIn { it[FooConfig] }

fun bar(): String = config.bar
}
info

When defining features, you only need to define configuration sections. The application framework (e.g. Tegral Web AppDSL) will take care of defining the necessary root classes and instantiating the decoder.

Lifecycle features

Features can put services into the environment, which will be started like any other service. However, you sometimes need more control over the application's lifecycle, and need to be called back at specific points.

The LifecycleHookedFeature interface provides a few functions that can help with that, including:

  • A callback for when the configuration is successfully loaded.
  • A callback just before starting all other services.

You should almost always use services instead of lifecycle hooks, but it may be impossible to do otherwise sometimes.