Skip to main content

asimov_runner/programs/
matcher.rs

1// This is free and unencumbered software released into the public domain.
2
3//! Exact or approximate RDF matching through an external matcher program.
4
5use crate::{CommandExt, Executor, ExecutorError, GraphInput, GraphOutput, JsonlStream};
6use alloc::boxed::Box;
7use async_trait::async_trait;
8use derive_more::Debug;
9use std::{ffi::OsStr, process::Stdio};
10
11pub use asimov_patterns::MatcherOptions;
12
13/// A live JSONL graph stream, or an error starting the matcher.
14pub type MatcherResult = Result<JsonlStream, ExecutorError>;
15
16/// An external [matcher] that performs exact or approximate matching on RDF.
17///
18/// Output is RDF describing the matches, rather than necessarily a subset of
19/// the input dataset. Matching rules belong to the external program;
20/// this wrapper passes JSONL lines and command-line options through. Execution
21/// uses the concurrent streaming behavior described in [`crate::programs`].
22///
23/// [matcher]: https://asimov-specs.github.io/program-patterns/#matcher
24#[allow(unused)]
25#[derive(Debug)]
26pub struct Matcher {
27    executor: Executor,
28    options: MatcherOptions,
29    input: GraphInput,
30    output: GraphOutput,
31}
32
33impl Matcher {
34    /// Configures a matcher without starting it.
35    ///
36    /// Adds any configured `--input=<format>` and `--output=<format>` arguments,
37    /// followed by `options.other`. The input and output values select stdin
38    /// and stdout; stderr is captured for failure diagnostics.
39    /// Byte input is lazily adapted into JSONL batches using [`GraphInput::into_jsonl`].
40    pub fn new(
41        program: impl AsRef<OsStr>,
42        input: GraphInput,
43        output: GraphOutput,
44        options: MatcherOptions,
45    ) -> Self {
46        let mut executor = Executor::new(program);
47        executor
48            .command()
49            .option("input", options.input.as_ref())
50            .option("output", options.output.as_ref())
51            .args(&options.other)
52            .stdin(input.as_stdio())
53            .stdout(output.as_stdio())
54            .stderr(Stdio::piped());
55
56        Self {
57            executor,
58            options,
59            input: input.into_jsonl(),
60            output,
61        }
62    }
63
64    /// Starts a child and returns its live JSONL match stream.
65    ///
66    /// After successful spawning, input ownership moves into the stream, which
67    /// feeds it concurrently when polled. Subsequent executions have no graph input.
68    ///
69    /// # Errors
70    ///
71    /// Spawn failures are returned directly; input, output, wait, and exit failures are
72    /// stream items. Consume the stream to completion to check process success.
73    pub async fn execute(&mut self) -> MatcherResult {
74        self.executor
75            .execute_jsonl_with_io(&mut self.input, &mut self.output)
76            .await
77    }
78}
79
80impl asimov_patterns::Matcher<JsonlStream> for Matcher {}
81
82crate::batch::with_batching!(Matcher);
83
84crate::pipeline::stage!(
85    Matcher,
86    value,
87    value.input,
88    value.output,
89    value.options.input.as_deref(),
90    value.options.output.as_deref()
91);
92
93#[async_trait]
94impl asimov_patterns::Execute<JsonlStream> for Matcher {
95    type Error = ExecutorError;
96
97    async fn execute(&mut self) -> MatcherResult {
98        self.execute().await
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::StreamExt;
106    use alloc::vec::Vec;
107    use std::io::Cursor;
108
109    #[test]
110    fn test_options() {
111        let options = MatcherOptions::builder()
112            .input("jsonl")
113            .output("nquads")
114            .other("--exact")
115            .maybe_other(Some("--custom"))
116            .maybe_other(None::<&str>)
117            .build();
118        let mut matcher = Matcher::new(
119            "asimov-test-matcher",
120            GraphInput::Ignored,
121            GraphOutput::Captured,
122            options,
123        );
124        let args: Vec<_> = matcher.executor.command().as_std().get_args().collect();
125        assert_eq!(
126            args,
127            ["--input=jsonl", "--output=nquads", "--exact", "--custom"]
128        );
129    }
130
131    #[test]
132    fn test_default_options() {
133        let mut matcher = Matcher::new(
134            "asimov-test-matcher",
135            GraphInput::Ignored,
136            GraphOutput::Captured,
137            MatcherOptions::default(),
138        );
139        assert_eq!(matcher.executor.command().as_std().get_args().count(), 0);
140    }
141
142    #[cfg(unix)]
143    #[tokio::test]
144    async fn test_execute() {
145        let graph = b"{\"subject\":\"https://example.com/\"}\n";
146        let mut matcher = Matcher::new(
147            "cat",
148            GraphInput::AsyncRead(Box::new(Cursor::new(graph.to_vec()))),
149            GraphOutput::Captured,
150            MatcherOptions::default(),
151        );
152        let mut output = asimov_patterns::Execute::execute(&mut matcher)
153            .await
154            .unwrap();
155        let batch = output.next().await.unwrap().unwrap();
156        assert_eq!(batch.len(), 1);
157        assert_eq!(batch.lines().next().unwrap(), graph);
158        assert!(output.next().await.is_none());
159    }
160}