///|
/// What a plugin knows when the build runner calls it: the model, the plugin
/// settings and the service shape that the settings name.
pub struct PluginContext {
  model : @smithy.Model
  settings : Map[String, Json]
  service : @smithy.Shape
}

///|
/// Read the plugin settings. The settings must be an object. The setting
/// `service` must be the shape ID of a service shape in the model.
pub fn PluginContext::from_build(
  model : @smithy.Model,
  settings : Json,
) -> PluginContext raise CodegenError {
  guard settings is Object(fields) else {
    raise InvalidSettings("I expected the plugin settings to be an object.")
  }
  guard fields.get("service") is Some(String(text)) &&
    @smithy.ShapeId::parse(text) is Some(id) else {
    raise InvalidSettings("I expected a service shape ID in setting `service`.")
  }
  guard model.shapes.get(id) is Some(service) else { raise MissingService(id) }
  guard service.body is Service(..) else { raise NotAService(id) }
  { model, settings: fields, service, }
}

///|
/// The shapes that the service reaches, sorted by shape ID text.
pub fn PluginContext::closure(self : PluginContext) -> Array[@smithy.ShapeId] {
  service_closure(self.model, self.service.id)
}

///|
/// Check that every setting name is in the list. The name `service` is always
/// allowed. The error names the first unknown name in sorted order.
pub fn PluginContext::check_settings(
  self : PluginContext,
  allowed : Array[String],
) -> Unit raise CodegenError {
  let keys = self.settings.keys().to_array()
  keys.sort_by(lexical)
  for key in keys {
    if key != "service" && !allowed.contains(key) {
      raise InvalidSettings("I found unknown setting `\{key}`.")
    }
  }
}

///|
/// The text of a setting. The result is `None` when the setting is absent.
pub fn PluginContext::setting_string(
  self : PluginContext,
  name : String,
) -> String? raise CodegenError {
  match self.settings.get(name) {
    None => None
    Some(String(text)) => Some(text)
    Some(_) =>
      raise InvalidSettings("I expected a string for setting `\{name}`.")
  }
}

///|
/// Make the function that the build runner registers as a plugin. The function
/// builds the plugin context and calls `run`. A code generation error becomes
/// an invalid build configuration error with the message of the error.
pub fn build_plugin(
  run : (PluginContext) -> Map[String, String] raise CodegenError,
) -> (@smithy.Model, Json) -> Map[String, String] raise @build.BuildError {
  (model, settings) => {
    run(PluginContext::from_build(model, settings)) catch {
      error => raise @build.InvalidBuildConfig(error.message())
    }
  }
}