Plugin API

BabelFSH has been recently refactored to utilize a real modular build with self-contained plugins. This is not yet documented.

The base plugin API is intentionally very simple. Every plugin is identified by a plugin ID (lower-case, non-numeric with only hyphens allowed), and specifies its command line arguments using a command line parser library.

Plugins are (currently) included at compile time, with no lazy loading from JAR files currently being supported. If there is a need for that functionality, someone would have to figure out library loading and plugin registratio in Kotlin 😉.

BabelfshTerminologyPlugin

The base class for the plugin API is the abstract class BabelfshTerminologyPlugin:

abstract class BabelfshTerminologyPlugin<ResourceContainerClass : ResourceContainer<*, *>, ResourceContentContainerClass : BabelfshConversionResult, ArgType : PluginArguments>(
    val pluginId: String,
    val babelfshContext: BabelfshContext,
    val resourceType: ResourceType,
    val attribution: String?,
    val extraHelp: String?,
    val shortHelp: String,
    val argumentConstructor: (ArgParser, Path?) -> ArgType
) {

    private val logger = LoggerFactory.getLogger(javaClass)

    fun parseArgs(pluginCommentData: PluginCommentData, resource: TsResourceData): ArgType {
        return mainBody(programName = "BabelFSH $resourceType plugin '$pluginId'", columns = getTerminalWidth()) {
            // body of parseArgs
        }
    }
    
    private fun ArgParser.parseIntoArgType(workingDirectory: Path?) = this.parseInto { argumentConstructor(this, workingDirectory) }

    abstract fun produceContent(
        args: PluginArguments, resource: ResourceContainerClass
    ): List<ResourceContentContainerClass>

    protected fun failConversion(s: String): Nothing {
        throw ConversionFailedException(s)
    }

    fun getHelp() {
        mainBody(columns = getTerminalWidth()) {
            val helpParser = ArgParser(
                args = arrayOf("--help"), helpFormatter = BabelfshHelpFormatter(this)
            )
            helpParser.parseIntoArgType(null)
        }

    }
}

Constructor Arguments

  • pluginId: The unique Plugin ID, which must conform to /[a-z-]+/ .

  • babelfshContext: A data class that provides the FHIR context objects, the working directory, the current release version, and some specialized settings. All plugins need access to this object.

  • resourceType one of ResourceType.CodeSystem, ResourceType.ValueSet or ResourceType.ConceptMap .

  • attribution: If the plugin needs to credit the work of others, do leave a note here.

  • extraHelp /shortHelp: all plugins need to declare a short help string. If needed, you can also declare a longer help string, to explain the intention of this plugin.

  • argumentConstructor : A bit of unavoidable boilerplate to instantiate the respective plugin argument class. ALWAYS Looks like { parser, workingDirectory -> PluginTypeName(parser, workingDirectory} . Due to Java Type Erasure, this is sadly unavoidable: at run time, the resource factory doesn't know the type of the plugin arguments.

Functions

  • produceContent : This is the main substance of any plugin. In this method, you would take in the arguments, BabelfshContext and the already-generated resource skeleton (should you need to read attributes from it), and return a list of data items (for CS: concepts, for CM: groups, for VS: ??)

  • failConversion: Should you need to abort execution, you can terminate with a clear message here.

Argument Classes

All plugins need to declare their arguments using classes that inherit PluginArguments. This class takes the argument partser and working directory path as constructor arguments, and then declares arguments using delegate functions. The API of the PluginArguments class is quite simple, there is a single public method that's pre-defined to be a no-op:

  • validateDependentArguments(): should you need to validate relationships between your arguments, e.g., if one argument requires another, or is exclusive with another, you can do this by overriding this method.

All arguments are declared using delegates, i.e. using by parser.x , where x can (generally) be one of:

Delegate
Return Type

flagging

Boolean

if provided, the flag will be true

storing<T>

T

T is generally inferred by the Kotlin compiler, but normally T. If you provide a lambda (parser.storing("-e", "--example", help="Foo") { toInt() }, the string value will be converted.

adding<T>

T

mapping<T>

T

Provide a list of Pair (using the "--fast" to Mode.FAST syntax) and you will limit the options the user has.

Otherwise, the abstract PluginArguments provides some helper functions that you might find useful:

  • String.preprocess() : Remove quotes and whitespace from argument values

  • failValidation() : Terminate app execution by calling this in validateDependentArguments or other validators.

  • String.convertToAbsolutePath() : if you need to read a file in your plugin, this converts the string argument to a Path, while validating that the file exists and is readable.

  • jsonArrayToStringList(string: String, validation: ((String) -> Unit)?) : Convert a JSON array that's provided as a String to a List<String> . Should you need to perform validation, you can failValidation in the provided optional lambda.

  • String.makeList() : Split a comma-separated list of strings

  • jsonStringToStringMap(s: String, validation: ((Map<String, String>) -> Unit)? = null): Decode a JSON object to a string-string-map

  • <T> decodeJsonArray : De-serialize a typed (serializable) list of JSON objects to a List<T>.

Example Argument Class

class EdqmCsPluginArgs(parser: ArgParser, workingDirectory: Path?) : PluginArguments(parser, workingDirectory) {
        val usernameEnvironmentVariable: String by parser.storing(
            "-u", "--username-env",
            help = "The environment variable containing the username for the EDQM API. " +
                    "It is intentionally not possible to provide the username in the FSH source file directly to avoid leaking your username!"
        ) {
            preprocess()
        }

        val apiKeyEnvironmentVariable: String by parser.storing(
            "-k",
            "--api-key-env",
            help = "The environment variable containing the API key for the EDQM API. " +
                    "It is intentionally not possible to provide the API key in the FSH source file directly to avoid leaking your key!"
        ) {
            preprocess()
        }

        val designationLanguages: List<String> by parser.storing(
            "-l",
            "--language",
            help = "The language(s) to be used for designations. The special code 'all' can be used to generate designations in all languages."
        ) {
            makeList().map { it.lowercase() }
        }.default(listOf("de", "en"))

        val mode: EdqmCsPluginMode by parser.mapping(
            "--mode-class-codes" to EdqmCsPluginMode.CLASS_CODES,
            "--mode-standard-terms" to EdqmCsPluginMode.STANDARD_TERMS,
            help = "The mode for the CodeSystem plugin, either generate the class code system, or the code system with the actual terms."
        )

        val classPropertyCode: String by parser.storing(
            "--class-property",
            help = "The code for the property that contains the class of the concept; default 'class'; required property"
        ) {
            preprocess()
        }.default("class")

        val statusProperty: String by parser.storing(
            "--status-property",
            help = "The code for the property that contains the status of the concept; default 'status'; required property"
        ) {
            preprocess()
        }.default("status")

        val classAsString: Boolean by parser.mapping(
            mapOf(
                "--class-as-string" to true,
                "--class-as-coding" to false
            ),
            help = "Whether to use the class as a code or as a coding. Default is 'true' (code)."
        ).default(true)

        val classPropertySystem: String by parser.storing(
            "--class-system",
            help = "The system for the property that contains the class of the concept, needed if --class-as-coding is provided; default 'http://standardterms.edqm.eu/classes'"
        ) {
            preprocess()
        }.default("http://standardterms.edqm.eu/classes")

        // some fields truncated for brevity

        val versionProperty: String? by parser.storing(
            "--version-property", help = "The code for the property that contains the version of the concept; optional property"
        ) {
            preprocess()
        }.default(null)

        override fun validateDependentArguments() {
            if (!classAsString && classPropertySystem.isBlank()) {
                failValidation("If the class is used as a coding, the system must be provided.")
            }
            if (designationLanguages.contains("all") && designationLanguages.size > 1) {
                failValidation("The special code 'all' must be the only language selected for designations.")
            }
        }
    }

This argument class, from the EDQM Plugin, uses a number of optional and required arguments, makeList, as well as the validateDependentArguments function. The plugin is actually special in that it reads some arguments from the environment rather than the command line to make sure that credentials don't leak in FSH code, and it is not possible to directly provide the respective arguments in the command line.

For the implementation of your command line, please be referred to the documentation of the command line library, xenomachina/kotlin-argparser on GitHub!

Last updated