use std::fmt;
use jsonschema::error::ValidationErrorKind;
use onetaskgraph_plugin_api::{SecretResolver, SourceError, SourceName, SourcePlugin, TaskSource};
use serde_json::Value;
use crate::config::{Config, ConfigError, SourceConfig};
use crate::plan::SourceFailure;
pub struct ResolvedSource {
name: SourceName,
source: Box<dyn TaskSource>,
}
impl ResolvedSource {
#[must_use]
pub fn adopt(name: SourceName, source: Box<dyn TaskSource>) -> Self {
Self { name, source }
}
#[must_use]
pub fn name(&self) -> &SourceName {
&self.name
}
#[must_use]
pub fn kind(&self) -> &str {
self.source.kind()
}
#[must_use]
pub fn source(&self) -> &dyn TaskSource {
self.source.as_ref()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct UnavailableSource {
name: SourceName,
kind: &'static str,
error: SourceError,
}
impl UnavailableSource {
#[must_use]
pub fn name(&self) -> &SourceName {
&self.name
}
#[must_use]
pub fn kind(&self) -> &str {
self.kind
}
#[must_use]
pub fn error(&self) -> &SourceError {
&self.error
}
#[must_use]
pub fn failure(&self) -> SourceFailure {
SourceFailure {
source: self.name.clone(),
error: self.error.clone(),
}
}
}
impl fmt::Debug for ResolvedSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ResolvedSource")
.field("name", &self.name)
.field("kind", &self.kind())
.finish_non_exhaustive()
}
}
pub fn validate_sources(config: &Config) -> Result<(), ConfigError> {
for (name, source) in config.sources() {
checked_plugin(name, source)?;
}
Ok(())
}
pub fn resolve(
config: &Config,
secrets: &dyn SecretResolver,
) -> Result<Vec<ResolvedSource>, ConfigError> {
validate_sources(config)?;
let (built, unavailable) = resolve_available(config, secrets);
match unavailable.first() {
None => Ok(built),
Some(failed) => Err(ConfigError::setting(
format!("sources.{}", failed.name()),
failed.error().to_string(),
format!(
"correct that source's configuration, or remove it — `onetaskgraph \
config show` reports every setting under `sources.{}` and the layer it \
came from.",
failed.name()
),
)),
}
}
#[must_use]
pub fn resolve_available(
config: &Config,
secrets: &dyn SecretResolver,
) -> (Vec<ResolvedSource>, Vec<UnavailableSource>) {
let mut built = Vec::new();
let mut unavailable = Vec::new();
for (name, source) in config.sources() {
let plugin = source.plugin().plugin();
match plugin.build(name, source.config(), secrets) {
Ok(source) => built.push(ResolvedSource::adopt(name.clone(), source)),
Err(error) => unavailable.push(UnavailableSource {
name: name.clone(),
kind: plugin.kind(),
error,
}),
}
}
(built, unavailable)
}
fn checked_plugin(
name: &SourceName,
source: &SourceConfig,
) -> Result<Box<dyn SourcePlugin>, ConfigError> {
let plugin = source.plugin().plugin();
check_block(name, source.config(), plugin.as_ref())?;
Ok(plugin)
}
fn check_block(
name: &SourceName,
block: &Value,
plugin: &dyn SourcePlugin,
) -> Result<(), ConfigError> {
let schema = plugin.config_schema();
let validator = jsonschema::validator_for(schema.as_value()).map_err(|error| {
ConfigError::setting(
format!("sources.{name}.plugin"),
format!(
"the `{}` plugin declares a configuration schema this build cannot \
compile: {error}",
plugin.kind()
),
"that is a defect in this binary rather than in your configuration — please \
report it, naming the plugin above. Removing that source lets the rest of \
this configuration run in the meantime.",
)
})?;
let Some(problem) = validator.iter_errors(block).next() else {
return Ok(());
};
if schema.as_value().get("properties").is_none()
&& let Some(fields) = block.as_object()
&& let Some(first) = fields.keys().next()
{
return Err(ConfigError::setting(
format!("sources.{name}.config.{first}"),
format!(
"the `{}` plugin declares no configuration fields, so its `config:` block \
must be empty or absent; this one sets {}",
plugin.kind(),
fields.keys().cloned().collect::<Vec<_>>().join(", ")
),
format!(
"remove those fields — `onetaskgraph schema` prints what this plugin \
accepts under `plugin_config.{}`.",
plugin.kind()
),
));
}
let pointer = problem.instance_path().to_string().replace('/', ".");
let unexpected = match problem.kind() {
ValidationErrorKind::AdditionalProperties { unexpected } => unexpected.first(),
_ => None,
};
let key = match unexpected {
Some(field) => format!("sources.{name}.config{pointer}.{field}"),
None => format!("sources.{name}.config{pointer}"),
};
Err(ConfigError::setting(
key,
problem.to_string(),
format!(
"check that field against the `{}` plugin's schema — `onetaskgraph schema` \
prints it under `plugin_config.{}`.",
plugin.kind(),
plugin.kind()
),
))
}