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))]
49#[cfg_attr(feature = "clap", derive(clap::Args))]
50pub struct AdapterOptions {
51 /// Additional arguments, in order, including an optional query-file operand.
52 ///
53 /// The process wrapper appends these after `--output`. Each string is one
54 /// literal argument, without shell expansion; see [`crate::programs`].
55 #[builder(field)]
56 pub other: Vec<String>,
57
58 /// RDF serialization passed as `--output=FORMAT` (`-o` in the CLI).
59 ///
60 /// `None` omits the option; the specified program default is `jsonl`.
61 /// This selects a format, not an output filename or capture policy.
62 pub output: Option<String>,
63}
64
65impl<S: adapter_options_builder::State> AdapterOptionsBuilder<S> {
66 /// Appends one literal argument to [`AdapterOptions::other`].
67 ///
68 /// Calls accumulate in order. A separate option value needs its own call;
69 /// for example, `.other("--name").other("value with spaces")`.
70 pub fn other(mut self, flag: impl Into<String>) -> Self {
71 self.other.push(flag.into());
72 self
73 }
74
75 /// Appends a present argument to [`AdapterOptions::other`]; `None` adds nothing.
76 pub fn maybe_other(mut self, flag: Option<impl Into<String>>) -> Self {
77 if let Some(flag) = flag {
78 self.other.push(flag.into());
79 }
80 self
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 #[test]
89 fn optional_arguments_preserve_order_and_boundaries() {
90 let options = AdapterOptions::builder()
91 .other("--dataset")
92 .maybe_other(Some("value with spaces"))
93 .maybe_other(None::<&str>)
94 .other("query.rq")
95 .build();
96 assert_eq!(
97 options.other,
98 ["--dataset", "value with spaces", "query.rq"]
99 );
100 }
101}