1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
use abi_stable::std_types::{RSlice, RString};
use abi_stable::StableAbi;
use figment::providers::{Format, Yaml};
use serde::Deserialize;

/// Abstracts config deserialization from plugins, allowing differing
/// configuration for different plugin instances.
#[repr(C)]
#[derive(StableAbi)]
pub struct PluginConfigAdapter<'a> {
	key: RString,
	config_sources: RSlice<'a, __private_api::ConfigLayer>,
}

impl<'a> PluginConfigAdapter<'a> {
	/// Used internally by gravel, do not use in plugins.
	#[doc(hidden)]
	pub fn from(key: RString, config_sources: RSlice<'a, __private_api::ConfigLayer>) -> Self {
		Self { key, config_sources }
	}

	/// Build and deserialize the plugin's config into the given type.
	#[must_use]
	pub fn get<'de, T: Deserialize<'de>>(&self, default_config: &str) -> T {
		log::trace!("reading plugin config for {}", self.key);

		// layer the plugins' defaults under the provider's config section
		let figment = __private_api::create_figment(&self.config_sources)
			.focus(&format!("{}.config", self.key))
			.join(Yaml::string(default_config));

		match figment.extract() {
			Ok(config) => config,
			Err(err) => {
				log::error!("error in plugin config {}: {err}", self.key);
				std::process::exit(1);
			}
		}
	}
}

/// Used internally by gravel, do not use in plugins.
#[doc(hidden)]
pub mod __private_api {
	use abi_stable::std_types::RString;
	use abi_stable::StableAbi;
	use figment::providers::{Format, Yaml};
	use figment::Figment;

	// I'd really like to keep the app's figment and re-use it for plugins,
	// but unfortunately it can't safely pass through FFI-boundaries.
	pub fn create_figment(config_sources: &[ConfigLayer]) -> Figment {
		fn fold(figment: Figment, ConfigLayer(source, strategy): &ConfigLayer) -> Figment {
			let source = match source {
				ConfigSource::String(s) => Yaml::string(s.as_str()),
				ConfigSource::File(f) => Yaml::file(f.as_str()),
			};

			match strategy {
				MergeStrategy::Merge => figment.merge(source),
				MergeStrategy::AdMerge => figment.admerge(source),
			}
		}

		config_sources.iter().fold(Figment::new(), fold)
	}

	#[repr(C)]
	#[derive(StableAbi, Debug)]
	pub struct ConfigLayer(pub ConfigSource, pub MergeStrategy);

	#[repr(u8)]
	#[derive(StableAbi, Debug)]
	pub enum MergeStrategy {
		Merge,
		AdMerge,
	}

	#[repr(u8)]
	#[derive(StableAbi, Debug)]
	pub enum ConfigSource {
		String(RString), // TODO: refactor with RStr<'static>
		File(RString),
	}
}