shuyi_cli/contract/
modules.rs1use 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#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
23#[serde(deny_unknown_fields)]
24pub struct ModulesInput {}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
28#[serde(deny_unknown_fields)]
29pub struct ModulesOutput {
30 pub engine: String,
32 pub concerns: Vec<Concern>,
35 pub modules: Vec<ModuleEntry>,
37 pub on: Vec<String>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
43#[serde(deny_unknown_fields)]
44pub struct ModuleEntry {
45 pub name: String,
47 pub feature: String,
49 pub on: bool,
51 pub info: ModuleInfo,
53}
54
55pub 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
63pub 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
98pub(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
107pub 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
116pub fn text(output: &ModulesOutput) -> String {
118 let mut out = format!("modules: the catalog of {}\n", output.engine);
119 for concern in &output.concerns {
120 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;