Plugin API
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
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 ofResourceType.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:
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 valuesfailValidation()
: Terminate app execution by calling this invalidateDependentArguments
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 aList<String>
. Should you need to perform validation, you canfailValidation
in the provided optional lambda.String.makeList()
: Split a comma-separated list of stringsjsonStringToStringMap(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 aList<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.
Last updated