1use std::any::{Any, TypeId};
3use std::collections::{BTreeMap, HashMap};
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result, ensure};
7use serde::{Deserialize, Serialize};
8
9use crate::engine::Contract;
10
11#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
12pub struct InputOperation {
13 pub name: String,
14 pub kind: String,
15}
16
17#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
18pub struct InputSummary {
19 pub format: String,
21 pub title: String,
22 pub version: Option<String>,
23 pub types: Vec<String>,
24 pub operations: Vec<InputOperation>,
25}
26
27#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
28pub struct InputDiagnostic {
29 pub code: String,
30 pub message: String,
31}
32
33pub struct InputContract {
37 pub summary: InputSummary,
38 pub diagnostics: Vec<InputDiagnostic>,
39 contracts: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
40}
41
42impl InputContract {
43 pub fn new(summary: InputSummary) -> Self {
44 Self {
45 summary,
46 diagnostics: Vec::new(),
47 contracts: HashMap::new(),
48 }
49 }
50
51 pub fn publish<C: Contract>(&mut self, contract: C) -> Result<()> {
52 ensure!(
53 !self.contracts.contains_key(&TypeId::of::<C>()),
54 "input contract {} was already published",
55 C::NAME
56 );
57 self.contracts.insert(TypeId::of::<C>(), Box::new(contract));
58 Ok(())
59 }
60
61 pub fn take<C: Contract>(&mut self) -> Result<C> {
62 self.contracts
63 .remove(&TypeId::of::<C>())
64 .and_then(|contract| contract.downcast::<C>().ok())
65 .map(|contract| *contract)
66 .with_context(|| {
67 format!(
68 "input provider did not publish required contract {}",
69 C::NAME
70 )
71 })
72 }
73
74 pub fn get<C: Contract>(&self) -> Result<&C> {
75 self.contracts
76 .get(&TypeId::of::<C>())
77 .and_then(|contract| contract.downcast_ref())
78 .with_context(|| {
79 format!(
80 "input provider did not publish required contract {}",
81 C::NAME
82 )
83 })
84 }
85}
86
87#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
89#[serde(default, deny_unknown_fields)]
90pub struct InputOptions {
91 pub operation_files: Vec<PathBuf>,
92 pub import_roots: Vec<PathBuf>,
93 pub broker: Option<serde_json::Value>,
94 pub workflow_sources: BTreeMap<String, PathBuf>,
95}
96
97pub trait InputPlugin: Send + Sync {
98 fn id(&self) -> &str;
100 fn format(&self) -> &str;
102 fn load(&self, path: &Path) -> Result<InputContract>;
103 fn load_with_options(&self, path: &Path, options: &InputOptions) -> Result<InputContract> {
104 ensure!(
105 options == &InputOptions::default(),
106 "input provider {} does not support these options",
107 self.id()
108 );
109 self.load(path)
110 }
111}
112
113#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
114pub struct InputPluginInfo {
115 pub provider: String,
116 pub format: String,
117}
118
119pub struct LoadedInput {
120 pub provider: String,
121 pub source: PathBuf,
122 pub contract: InputContract,
123}
124
125#[derive(Default)]
126pub struct InputRegistry {
127 plugins: BTreeMap<String, Box<dyn InputPlugin>>,
128}
129
130fn valid_identifier(value: &str) -> bool {
131 value.as_bytes().first().is_some_and(u8::is_ascii_lowercase)
132 && value.bytes().all(|byte| {
133 byte.is_ascii_lowercase() || byte.is_ascii_digit() || b"._-".contains(&byte)
134 })
135}
136
137impl InputRegistry {
138 pub fn new() -> Self {
139 Self::default()
140 }
141
142 pub fn register<P: InputPlugin + 'static>(&mut self, plugin: P) -> Result<()> {
143 ensure!(
144 valid_identifier(plugin.id()),
145 "invalid input provider identifier {:?}",
146 plugin.id()
147 );
148 ensure!(
149 valid_identifier(plugin.format()),
150 "invalid input format identifier {:?}",
151 plugin.format()
152 );
153 ensure!(
154 !self.plugins.contains_key(plugin.id()),
155 "duplicate input provider {:?}",
156 plugin.id()
157 );
158 self.plugins
159 .insert(plugin.id().to_owned(), Box::new(plugin));
160 Ok(())
161 }
162
163 pub fn plugins(&self) -> Vec<InputPluginInfo> {
164 self.plugins
165 .values()
166 .map(|plugin| InputPluginInfo {
167 provider: plugin.id().to_owned(),
168 format: plugin.format().to_owned(),
169 })
170 .collect()
171 }
172
173 pub fn load(&self, format: &str, provider: Option<&str>, path: &Path) -> Result<LoadedInput> {
176 self.load_with_options(format, provider, path, &InputOptions::default())
177 }
178 pub fn load_with_options(
179 &self,
180 format: &str,
181 provider: Option<&str>,
182 path: &Path,
183 options: &InputOptions,
184 ) -> Result<LoadedInput> {
185 let plugin = if let Some(id) = provider {
186 let plugin = self
187 .plugins
188 .get(id)
189 .with_context(|| format!("unknown input provider {id:?}"))?;
190 ensure!(
191 plugin.format() == format,
192 "input provider {id:?} reads {}, not {format}",
193 plugin.format()
194 );
195 plugin.as_ref()
196 } else {
197 let matching: Vec<_> = self
198 .plugins
199 .values()
200 .filter(|plugin| plugin.format() == format)
201 .collect();
202 ensure!(
203 !matching.is_empty(),
204 "no input provider registered for format {format:?}"
205 );
206 ensure!(
207 matching.len() == 1,
208 "multiple input providers for {format:?}: {}; select --provider explicitly",
209 matching
210 .iter()
211 .map(|plugin| plugin.id())
212 .collect::<Vec<_>>()
213 .join(", ")
214 );
215 matching[0].as_ref()
216 };
217 let contract = plugin.load_with_options(path, options).with_context(|| {
218 format!(
219 "input provider {} failed reading {}",
220 plugin.id(),
221 path.display()
222 )
223 })?;
224 ensure!(
225 contract.summary.format == format,
226 "input provider {} published format {:?}, expected {format:?}",
227 plugin.id(),
228 contract.summary.format
229 );
230 Ok(LoadedInput {
231 provider: plugin.id().to_owned(),
232 source: path.to_path_buf(),
233 contract,
234 })
235 }
236}
237
238impl Contract for crate::AdaptedApi {
239 const NAME: &'static str = "poolster.http-api";
240}
241
242pub struct InputProvider<C: Contract> {
245 registry: std::sync::Arc<InputRegistry>,
246 format: String,
247 provider: Option<String>,
248 source: PathBuf,
249 options: InputOptions,
250 meta: crate::engine::Meta,
251 marker: std::marker::PhantomData<fn() -> C>,
252}
253
254impl<C: Contract> InputProvider<C> {
255 pub fn new(
256 registry: std::sync::Arc<InputRegistry>,
257 format: impl Into<String>,
258 source: impl Into<PathBuf>,
259 ) -> Self {
260 Self {
261 registry,
262 format: format.into(),
263 source: source.into(),
264 provider: None,
265 options: InputOptions::default(),
266 meta: crate::engine::Meta::new(),
267 marker: std::marker::PhantomData,
268 }
269 }
270 pub fn using(mut self, provider: impl Into<String>) -> Self {
271 self.provider = Some(provider.into());
272 self
273 }
274 pub fn with_options(mut self, options: InputOptions) -> Self {
275 self.options = options;
276 self
277 }
278 pub fn handle(&self) -> crate::engine::Handle<C> {
279 self.meta.handle()
280 }
281}
282
283impl<C: Contract, L: crate::engine::Language> crate::engine::Plugin<L> for InputProvider<C> {
284 fn supports_native_input(&self) -> bool {
285 true
286 }
287 fn kind(&self) -> &'static str {
288 "input-provider"
289 }
290 fn meta(&self) -> &crate::engine::Meta {
291 &self.meta
292 }
293 fn provides(&self) -> Vec<crate::engine::Provision> {
294 vec![crate::engine::Provision::of::<C>()]
295 }
296 fn generate(&self, cx: &mut crate::engine::PluginContext<'_, L>) -> Result<()> {
297 let mut input = self
298 .registry
299 .load_with_options(
300 &self.format,
301 self.provider.as_deref(),
302 &self.source,
303 &self.options,
304 )?
305 .contract;
306 cx.publish(input.take::<C>()?)
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313 struct Native(String);
314 impl Contract for Native {
315 const NAME: &'static str = "example.native";
316 }
317 #[derive(Debug)]
318 struct Missing;
319 impl Contract for Missing {
320 const NAME: &'static str = "example.missing";
321 }
322 struct Provider(&'static str);
323 impl InputPlugin for Provider {
324 fn id(&self) -> &str {
325 self.0
326 }
327 fn format(&self) -> &str {
328 "custom"
329 }
330 fn load(&self, path: &Path) -> Result<InputContract> {
331 let mut input = InputContract::new(InputSummary {
332 format: "custom".into(),
333 title: "Custom".into(),
334 version: None,
335 types: vec![],
336 operations: vec![],
337 });
338 input.publish(Native(std::fs::read_to_string(path)?))?;
339 Ok(input)
340 }
341 }
342
343 #[test]
344 fn community_provider_publishes_native_contract_without_core_enum_changes() {
345 let file = tempfile::NamedTempFile::new().unwrap();
346 std::fs::write(file.path(), "native contract").unwrap();
347 let mut registry = InputRegistry::new();
348 registry.register(Provider("custom.parser")).unwrap();
349 let input = registry.load("custom", None, file.path()).unwrap();
350 assert_eq!(input.contract.get::<Native>().unwrap().0, "native contract");
351 assert_eq!(input.provider, "custom.parser");
352 assert!(
353 input
354 .contract
355 .get::<Missing>()
356 .unwrap_err()
357 .to_string()
358 .contains("example.missing")
359 );
360 }
361
362 #[test]
363 fn replacements_need_explicit_selection_and_duplicate_ids_fail() {
364 let file = tempfile::NamedTempFile::new().unwrap();
365 let mut registry = InputRegistry::new();
366 registry.register(Provider("custom.first")).unwrap();
367 registry.register(Provider("custom.second")).unwrap();
368 assert!(
369 registry
370 .load("custom", None, file.path())
371 .err()
372 .unwrap()
373 .to_string()
374 .contains("multiple input providers")
375 );
376 assert!(
377 registry
378 .load("custom", Some("custom.second"), file.path())
379 .is_ok()
380 );
381 assert!(registry.register(Provider("custom.first")).is_err());
382 assert!(
383 registry
384 .load("other", Some("custom.first"), file.path())
385 .is_err()
386 );
387 assert!(registry.load("other", None, file.path()).is_err());
388 assert!(
389 registry
390 .load("custom", Some("unknown"), file.path())
391 .is_err()
392 );
393 }
394
395 #[test]
396 fn registry_rejects_invalid_ids_and_false_format_claims() {
397 struct Wrong;
398 impl InputPlugin for Wrong {
399 fn id(&self) -> &str {
400 "custom.wrong"
401 }
402 fn format(&self) -> &str {
403 "custom"
404 }
405 fn load(&self, _: &Path) -> Result<InputContract> {
406 Ok(InputContract::new(InputSummary {
407 format: "other".into(),
408 title: String::new(),
409 version: None,
410 types: vec![],
411 operations: vec![],
412 }))
413 }
414 }
415 let mut registry = InputRegistry::new();
416 assert!(registry.register(Provider("../invalid")).is_err());
417 registry.register(Wrong).unwrap();
418 assert!(
419 registry
420 .load("custom", None, Path::new("unused"))
421 .err()
422 .unwrap()
423 .to_string()
424 .contains("published format")
425 );
426 }
427
428 #[test]
429 fn duplicate_publications_preserve_first_contract() {
430 let mut input = InputContract::new(InputSummary {
431 format: "custom".into(),
432 title: "Custom".into(),
433 version: None,
434 types: vec![],
435 operations: vec![],
436 });
437 input.publish(Native("first".into())).unwrap();
438 assert!(input.publish(Native("second".into())).is_err());
439 assert_eq!(input.get::<Native>().unwrap().0, "first");
440 }
441}