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}