Skip to main content

yog_cli/
serve.rs

1//! `yog serve` — the CLI as a service for tools (Yog-IDLE): frames of
2//! binary Yog Data (`yog_data::io`) on stdin and stdout, each request a
3//! [`Request`] and each answer a [`Reply`]. Every format is read, written
4//! and converted here, so a tool needn't be rebuilt when a format changes —
5//! only this binary. Answers hold plain values: a typed one (`@item:x`) is
6//! sent as its string.
7//!
8//! Methods: the service's own (`data`), a mod's resources (`media`) and
9//! the designers' (`design::service`).
10
11mod check;
12mod data;
13mod error;
14mod media;
15
16pub use error::{Problem, ServiceError};
17
18use yog_data::Schema;
19
20/// What a tool asks: a method, and its parameters.
21#[derive(Debug, Clone, Schema)]
22pub struct Request {
23    pub method: String,
24    #[data(default)]
25    pub params: yog_data::Value,
26}
27
28/// The answer: the method's result, or what went wrong.
29pub type Reply = Result<yog_data::Value, ServiceError>;
30
31/// Serves requests until stdin ends.
32pub fn run() -> Result<(), String> {
33    let mut input = std::io::stdin().lock();
34    let mut out = std::io::stdout().lock();
35    loop {
36        let reply = match yog_data::io::read_frame_bytes(&mut input) {
37            Ok(None) => return Ok(()),
38            Ok(Some(bytes)) => match yog_data::read_bytes::<Request>(&bytes) {
39                Ok(request) => answer(&request),
40                Err(e) => Err(format!("not a request: {e:?}").into()),
41            },
42            Err(e) => return Err(e.to_string()),
43        };
44        yog_data::io::write_frame(&mut out, &reply).map_err(|e| e.to_string())?;
45    }
46}
47
48/// `request` answered, in plain values.
49pub fn answer(request: &Request) -> Reply {
50    let (method, params) = (request.method.as_str(), &request.params);
51    let reply = data::method(method, params)
52        .or_else(|| media::method(method, params))
53        // The designers: drawing, pickers' choices, edits of a mod's files.
54        .or_else(|| crate::design::service::dispatch(method, params))
55        .unwrap_or_else(|| Err(format!("no method `{method}`").into()));
56    reply
57        .map(|v| yog_data::typed::flatten(&v))
58        .map_err(|mut e| {
59            e.file = e.file.take().or_else(|| file_of(params));
60            e
61        })
62}
63
64/// The file a request is about: its `path`, or its arguments'.
65fn file_of(params: &yog_data::Value) -> Option<String> {
66    let path = |v: &yog_data::Value| {
67        v.get(yog_ids::serve_param::PATH)
68            .and_then(yog_data::Value::as_str)
69            .map(str::to_string)
70    };
71    path(params).or_else(|| params.get(yog_ids::serve_param::ARGS).and_then(path))
72}
73
74#[cfg(test)]
75mod tests;