sofie_cli/new_command/
menu.rs1use std::path::PathBuf;
2
3use sofie_model::naming::path_parameters;
4use sofie_model::{App, AreaContribution, KnownArea, Method, Module, Route};
5
6use super::edits::area_type;
7use super::{Edits, checked_name, find_module};
8use crate::gen_command::GenError;
9use crate::source_edits::{add_call, read};
10
11pub struct NewMenuEntry {
12 pub module: String,
13 pub area: String,
14 pub handler: String,
15 pub label: String,
16}
17
18pub(super) fn new_menu_entry(
19 edits: &mut Edits,
20 app: &App,
21 entry: &NewMenuEntry,
22) -> Result<(), GenError> {
23 let module = find_module(app, &entry.module)?;
24 let contribution = contribution(module, &entry.area)?;
25 let area = app.area(&entry.area).ok_or_else(|| {
26 GenError::Usage(format!(
27 "the app has no area `{}`; declare it in a contract first",
28 entry.area
29 ))
30 })?;
31 let handler = checked_name(&entry.handler)?;
32 page(contribution, handler)?;
33 let label = entry.label.trim();
34 if label.is_empty() {
35 return Err(GenError::Usage(
36 "a menu entry needs a label, as `Orders`".to_owned(),
37 ));
38 }
39 let contract = module.contract.as_ref().ok_or_else(|| {
40 GenError::Usage(format!(
41 "module `{}` has no contract, whose generated `routes` a menu entry links to; add \
42 `contract/` as `sofie new module` writes it",
43 module.krate.crate_name
44 ))
45 })?;
46 let route = format!("routes::{}::{handler}()", contribution.area);
47 write_entry(
48 edits,
49 module,
50 contribution,
51 &area,
52 &route,
53 &format!("entry({label:?}, {}::{route})", contract.crate_name),
54 )?;
55 edits.depend(&module.krate, contract)?;
56 edits.enable_framework_feature(&module.krate, "menu")
57}
58
59fn contribution<'app>(
60 module: &'app Module,
61 area: &str,
62) -> Result<&'app AreaContribution, GenError> {
63 module
64 .areas
65 .iter()
66 .find(|contribution| contribution.area == area)
67 .ok_or_else(|| {
68 let known: Vec<&str> = module
69 .areas
70 .iter()
71 .map(|contribution| contribution.area.as_str())
72 .collect();
73 GenError::Usage(format!(
74 "module `{}` routes nothing in area `{area}`; its areas: {}",
75 module.krate.crate_name,
76 known.join(", ")
77 ))
78 })
79}
80
81fn page(contribution: &AreaContribution, handler: &str) -> Result<(), GenError> {
83 let area = &contribution.area;
84 let routed: Vec<&Route> = contribution
85 .routes
86 .iter()
87 .filter(|route| route.handler.last().is_some_and(|last| last == handler))
88 .collect();
89 let Some(page) = routed.iter().find(|route| route.method == Method::Get) else {
90 return Err(match routed.first() {
91 Some(write) => GenError::Usage(format!(
92 "`{handler}` answers {} {} in area `{area}`, which is no page: a menu entry \
93 links to a `get` route",
94 write.method.name(),
95 write.path
96 )),
97 None => GenError::Usage(format!(
98 "area `{area}` routes no handler `{handler}`; its pages: {}",
99 page_handlers(contribution).join(", ")
100 )),
101 });
102 };
103 let parameters = path_parameters(&page.path);
104 if !parameters.is_empty() {
105 return Err(GenError::Usage(format!(
106 "a menu entry links to a page of the area without parameters; GET {} takes `{}`",
107 page.path,
108 parameters.join("`, `")
109 )));
110 }
111 Ok(())
112}
113
114fn page_handlers(contribution: &AreaContribution) -> Vec<&str> {
115 contribution
116 .routes
117 .iter()
118 .filter(|route| route.method == Method::Get && path_parameters(&route.path).is_empty())
119 .filter_map(|route| route.handler.last().map(String::as_str))
120 .collect()
121}
122
123fn write_entry(
124 edits: &mut Edits,
125 module: &Module,
126 contribution: &AreaContribution,
127 area: &KnownArea,
128 route: &str,
129 call: &str,
130) -> Result<(), GenError> {
131 let file = menu_file(contribution);
132 if file.is_file() {
133 if read(&file)?.contains(route) {
134 return Err(GenError::Usage(format!(
135 "{} has an entry for `{route}` already",
136 file.display()
137 )));
138 }
139 return edits.edit_rust(&file, |text| {
140 add_call(text, "menu", call).ok_or_else(|| {
141 GenError::Usage(format!(
142 "{} has no `pub fn menu(menu: &mut Menu<…>) {{ … }}` to add the entry to",
143 file.display()
144 ))
145 })
146 });
147 }
148 let (area_import, area_type) = area_type(area);
149 edits.create_rust(
150 &file,
151 &format!(
152 "{area_import}use sofie::prelude::*;\n\n\
153 pub fn menu(menu: &mut Menu<{area_type}>) {{\n menu.{call};\n}}\n"
154 ),
155 )?;
156 edits.declare(&module.krate, &["areas", &contribution.area, "menu"])
157}
158
159fn menu_file(contribution: &AreaContribution) -> PathBuf {
160 contribution.menu.as_ref().map_or_else(
161 || contribution.routes_file.with_file_name("menu.rs"),
162 |menu| menu.file.clone(),
163 )
164}