1use std::path::Path;
5
6use serde::Deserialize;
7
8use crate::error::MifRhError;
9
10#[derive(Debug, Clone, Deserialize)]
16pub struct TopicConfig {
17 pub id: String,
19 #[serde(default)]
21 pub ontologies: Vec<String>,
22}
23
24#[derive(Debug, Clone, Deserialize)]
26pub struct HarnessConfig {
27 #[serde(default)]
29 pub topics: Vec<TopicConfig>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct TopicBinding {
36 pub id: String,
38 pub pinned_version: Option<String>,
40}
41
42impl HarnessConfig {
43 pub fn load(path: &Path) -> Result<Self, MifRhError> {
50 if !path.exists() {
51 return Err(MifRhError::ConfigMissing {
52 path: path.display().to_string(),
53 });
54 }
55 let contents = std::fs::read_to_string(path).map_err(|source| MifRhError::Io {
56 path: path.display().to_string(),
57 source,
58 })?;
59 serde_json::from_str(&contents).map_err(|source| MifRhError::Json {
60 path: path.display().to_string(),
61 source,
62 })
63 }
64
65 #[must_use]
69 pub fn topic_bindings(&self, topic: &str) -> Vec<TopicBinding> {
70 self.topics
71 .iter()
72 .find(|t| t.id == topic)
73 .map(|t| t.ontologies.iter().map(|s| parse_binding(s)).collect())
74 .unwrap_or_default()
75 }
76}
77
78fn parse_binding(binding: &str) -> TopicBinding {
79 binding.split_once('@').map_or_else(
80 || TopicBinding {
81 id: binding.to_string(),
82 pinned_version: None,
83 },
84 |(id, version)| TopicBinding {
85 id: id.to_string(),
86 pinned_version: Some(version.to_string()),
87 },
88 )
89}
90
91#[cfg(test)]
92mod tests {
93 use std::io::Write as _;
94
95 use super::{HarnessConfig, TopicBinding};
96
97 #[test]
98 fn parses_bare_and_version_pinned_bindings() {
99 let mut file = tempfile::NamedTempFile::new().unwrap();
100 file.write_all(
101 br#"{"topics":[
102 {"id":"edu","ontologies":["edu-fixture"]},
103 {"id":"eng","ontologies":["software-engineering@0.5.0"]},
104 {"id":"bare","ontologies":[]}
105 ]}"#,
106 )
107 .unwrap();
108
109 let config = HarnessConfig::load(file.path()).unwrap();
110 assert_eq!(
111 config.topic_bindings("edu"),
112 [TopicBinding {
113 id: "edu-fixture".to_string(),
114 pinned_version: None
115 }]
116 );
117 assert_eq!(
118 config.topic_bindings("eng"),
119 [TopicBinding {
120 id: "software-engineering".to_string(),
121 pinned_version: Some("0.5.0".to_string())
122 }]
123 );
124 assert_eq!(config.topic_bindings("bare"), []);
125 }
126
127 #[test]
128 fn unconfigured_topic_binds_nothing_rather_than_erroring() {
129 let mut file = tempfile::NamedTempFile::new().unwrap();
130 file.write_all(br#"{"topics":[]}"#).unwrap();
131 let config = HarnessConfig::load(file.path()).unwrap();
132 assert_eq!(config.topic_bindings("unknown"), []);
133 }
134
135 #[test]
136 fn reports_missing_config() {
137 let error = HarnessConfig::load(std::path::Path::new("/nonexistent/harness.config.json"))
138 .unwrap_err();
139 assert!(matches!(error, super::MifRhError::ConfigMissing { .. }));
140 }
141}