Skip to main content

asimov_patterns/programs/
adapter.rs

1// This is free and unencumbered software released into the public domain.
2
3//! Dataset queries: the adapter marker trait and output-format options.
4
5use crate::Execute;
6use alloc::{string::String, vec::Vec};
7use bon::Builder;
8
9/// An RDF dataset proxy that evaluates a SPARQL query and produces RDF.
10///
11/// The program defines the dataset and supported SPARQL features. `CONSTRUCT`
12/// and `DESCRIBE` yield graph results; `SELECT` and `ASK` need a documented
13/// mapping of their results to RDF. The pattern does not define SPARQL Update.
14///
15/// # Command-line contract
16///
17/// `PROGRAM [OPTIONS] [QUERY-FILE]`
18///
19/// The query file defaults to `-` (stdin); RDF is written to stdout. Output
20/// serialization is selected by [`AdapterOptions::output`] and defaults to
21/// `jsonl`. Unsupported query forms must cause failure rather than a non-RDF
22/// result mislabeled as RDF.
23///
24/// `T` represents the implementation's result, not necessarily a parsed graph.
25/// See [`crate::programs`] for shared options and links to concrete execution
26/// behavior, and the
27/// [adapter specification][spec] for the external program's requirements.
28///
29/// [spec]: https://asimov-specs.github.io/program-patterns/#adapter
30pub trait Adapter<T>: Execute<T> {}
31
32/// Output-format selection and additional arguments for an [`Adapter`].
33///
34/// `Default` leaves the format unset and additional arguments empty. The
35/// process wrapper omits unset options, delegating defaults to the program;
36/// building this value does not parse the query or validate format support.
37///
38/// # Examples
39///
40/// ```rust
41/// use asimov_patterns::AdapterOptions;
42///
43/// let options = AdapterOptions::builder()
44///     .output("jsonl")
45///     .build();
46/// ```
47#[derive(Clone, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, Builder)]
48#[builder(derive(Debug), on(String, into))]
49pub struct AdapterOptions {
50    /// Additional arguments, in order, including an optional query-file operand.
51    ///
52    /// The process wrapper appends these after `--output`. Each string is one
53    /// literal argument, without shell expansion; see [`crate::programs`].
54    #[builder(field)]
55    pub other: Vec<String>,
56
57    /// RDF serialization passed as `--output=FORMAT` (`-o` in the CLI).
58    ///
59    /// `None` omits the option; the specified program default is `jsonl`.
60    /// This selects a format, not an output filename or capture policy.
61    pub output: Option<String>,
62}
63
64impl<S: adapter_options_builder::State> AdapterOptionsBuilder<S> {
65    /// Appends one literal argument to [`AdapterOptions::other`].
66    ///
67    /// Calls accumulate in order. A separate option value needs its own call;
68    /// for example, `.other("--name").other("value with spaces")`.
69    pub fn other(mut self, flag: impl Into<String>) -> Self {
70        self.other.push(flag.into());
71        self
72    }
73
74    /// Appends a present argument to [`AdapterOptions::other`]; `None` adds nothing.
75    pub fn maybe_other(mut self, flag: Option<impl Into<String>>) -> Self {
76        if let Some(flag) = flag {
77            self.other.push(flag.into());
78        }
79        self
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn optional_arguments_preserve_order_and_boundaries() {
89        let options = AdapterOptions::builder()
90            .other("--dataset")
91            .maybe_other(Some("value with spaces"))
92            .maybe_other(None::<&str>)
93            .other("query.rq")
94            .build();
95        assert_eq!(
96            options.other,
97            ["--dataset", "value with spaces", "query.rq"]
98        );
99    }
100}