Skip to main content

shuyi_cli/contract/
modules.rs

1//! `modules`: the catalog by concern (ADR-0009 ยง6), as the engine the
2//! game builds with has it, and which modules the game has on.
3//!
4//! It reads the engine checkout the game's `shuyi` dependency names
5//! ([`catalog::engine_root`]): its concerns (`docs/modules.ron`), every
6//! answer with who gives it and its status, and the module crates it
7//! builds, each with its metadata. A module is on when the game's
8//! `Cargo.toml` turns on its feature of the `shuyi` crate.
9
10use std::fmt::Write as _;
11use std::fs;
12use std::path::Path;
13
14use schemars::JsonSchema;
15use serde::{Deserialize, Serialize};
16
17use super::{Answer, Op, codes};
18use crate::catalog::{self, Catalog, Concern, ModuleInfo};
19use crate::project::Project;
20
21/// What `modules` lists: everything (nothing to choose yet).
22#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
23#[serde(deny_unknown_fields)]
24pub struct ModulesInput {}
25
26/// The catalog, and the game's modules.
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
28#[serde(deny_unknown_fields)]
29pub struct ModulesOutput {
30    /// The engine checkout read, `/`-separated.
31    pub engine: String,
32    /// The concerns, each with its answers, who gives each and its
33    /// status.
34    pub concerns: Vec<Concern>,
35    /// The modules the engine builds, by name.
36    pub modules: Vec<ModuleEntry>,
37    /// The modules the game has on, by name, in its `Cargo.toml`'s order.
38    pub on: Vec<String>,
39}
40
41/// A module the engine builds.
42#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
43#[serde(deny_unknown_fields)]
44pub struct ModuleEntry {
45    /// Its crate (`shuyi-physics`).
46    pub name: String,
47    /// Its feature of the `shuyi` crate (`physics`).
48    pub feature: String,
49    /// Whether the game has it on.
50    pub on: bool,
51    /// Its metadata: what it answers, serves and sits beside.
52    pub info: ModuleInfo,
53}
54
55/// `modules` on the game of `project`, in the engine it builds with.
56pub fn modules(project: &Project, input: &ModulesInput) -> Answer<ModulesOutput> {
57    match catalog::engine_root(&project.root).and_then(|root| Catalog::read(&root)) {
58        Ok(catalog) => modules_in(&project.root, &catalog, input),
59        Err(e) => Answer::failed(Op::Modules, codes::ENGINE_DEPENDENCY, e),
60    }
61}
62
63/// `modules` on the game at `game`, with the engine's `catalog`.
64pub fn modules_in(game: &Path, catalog: &Catalog, _input: &ModulesInput) -> Answer<ModulesOutput> {
65    let features = match on_in(game) {
66        Ok(features) => features,
67        Err((code, e)) => return Answer::failed(Op::Modules, code, e),
68    };
69    let mut limits =
70        vec!["only the game's root Cargo.toml was read for the modules it has on".to_owned()];
71    for feature in &features {
72        if catalog.module(feature).is_none() {
73            limits.push(format!(
74                "the game turns on the feature `{feature}` of `shuyi`, which no module of its \
75                 engine has"
76            ));
77        }
78    }
79    let on: Vec<String> = features.iter().map(|f| catalog::module_of(f)).collect();
80    let output = ModulesOutput {
81        engine: catalog.root.to_string_lossy().replace('\\', "/"),
82        concerns: catalog.concerns.concerns.clone(),
83        modules: catalog
84            .modules
85            .iter()
86            .map(|module| ModuleEntry {
87                name: module.name.clone(),
88                feature: module.feature().to_owned(),
89                on: on.contains(&module.name),
90                info: module.info.clone(),
91            })
92            .collect(),
93        on,
94    };
95    Answer::new(Op::Modules, true, output, limits)
96}
97
98/// The features of `shuyi` the game at `game` turns on, or the problem
99/// code and why they can't be read.
100pub(crate) fn on_in(game: &Path) -> Result<Vec<String>, (codes::Code, String)> {
101    let path = game.join("Cargo.toml");
102    let text =
103        fs::read_to_string(&path).map_err(|e| (codes::FILE, format!("{}: {e}", path.display())))?;
104    catalog::features_on(&text).map_err(|e| (codes::ENGINE_DEPENDENCY, e))
105}
106
107/// `modules`' usage.
108pub fn usage(command: &str) -> String {
109    format!(
110        "usage: {command} modules [--json]\n\n\
111         The engine's catalog by concern: each answer, who gives it and its status, the\n\
112         modules the engine builds, and the ones this game has on."
113    )
114}
115
116/// What `modules` lists, for people.
117pub fn text(output: &ModulesOutput) -> String {
118    let mut out = format!("modules: the catalog of {}\n", output.engine);
119    for concern in &output.concerns {
120        // Writing to a `String` can't fail.
121        let _ = writeln!(out, "{}", concern.name);
122        for answer in &concern.answers {
123            let mark = if output.on.contains(&answer.by) {
124                " (on)"
125            } else {
126                ""
127            };
128            let _ = writeln!(
129                out,
130                "  {}: {}, {}{mark}",
131                answer.name,
132                answer.by,
133                answer.status.name()
134            );
135        }
136    }
137    if output.modules.is_empty() {
138        out.push_str("the engine builds no module yet\n");
139    }
140    for module in &output.modules {
141        let on = if module.on { "on" } else { "off" };
142        let _ = writeln!(out, "{} ({on}): feature `{}`", module.name, module.feature);
143    }
144    out
145}
146
147#[cfg(test)]
148mod tests;