1use crate::app::App;
12use bevy_ecs::prelude::Resource;
13
14pub trait BackendEntry: Copy + std::fmt::Debug + Send + Sync + 'static {
20 const KIND: &'static str;
22
23 fn name(&self) -> &'static str;
25
26 fn priority(&self) -> i32;
28}
29
30#[derive(Resource, Clone, Debug)]
32pub struct Backends<B: BackendEntry> {
33 entries: Vec<B>,
34}
35
36impl<B: BackendEntry> Default for Backends<B> {
37 fn default() -> Self {
38 Self {
39 entries: Vec::new(),
40 }
41 }
42}
43
44impl<B: BackendEntry> Backends<B> {
45 pub fn register(&mut self, backend: B) {
47 self.entries.retain(|b| b.name() != backend.name());
48 self.entries.push(backend);
49 }
50
51 pub fn get(&self, name: &str) -> Option<&B> {
53 self.entries.iter().find(|b| b.name() == name)
54 }
55
56 pub fn by_priority(&self) -> Vec<B> {
59 let mut list = self.entries.clone();
60 list.sort_by(|a, b| b.priority().cmp(&a.priority()).then(a.name().cmp(b.name())));
61 list
62 }
63
64 pub fn preferred(&self) -> Option<B> {
67 self.by_priority().into_iter().next()
68 }
69
70 pub fn names(&self) -> Vec<&'static str> {
72 let mut names: Vec<&'static str> = self.entries.iter().map(|b| b.name()).collect();
73 names.sort_unstable();
74 names
75 }
76
77 pub fn select(&self, choice: Option<&str>) -> Result<Vec<B>, String> {
81 match choice {
82 Some(name) => self.get(name).map(|b| vec![*b]).ok_or_else(|| {
83 format!(
84 "the app asks for the '{name}' {} backend, and this build carries {}",
85 B::KIND,
86 describe(&self.names())
87 )
88 }),
89 None if self.entries.is_empty() => Err(missing::<B>()),
90 None => Ok(self.by_priority()),
91 }
92 }
93}
94
95pub fn missing<B: BackendEntry>() -> String {
97 format!("this build carries no {} backend", B::KIND)
98}
99
100fn describe(names: &[&str]) -> String {
102 match names {
103 [] => "none".to_string(),
104 [one] => format!("only '{one}'"),
105 many => {
106 let quoted: Vec<String> = many.iter().map(|n| format!("'{n}'")).collect();
107 let (last, rest) = quoted.split_last().expect("at least two");
108 format!("{} and {last}", rest.join(", "))
109 }
110 }
111}
112
113pub fn register_backend<B: BackendEntry>(app: &mut App, backend: B) {
116 app.world
117 .get_resource_or_insert_with(Backends::<B>::default)
118 .register(backend);
119}
120
121pub fn registered<B: BackendEntry>(app: &App) -> Backends<B> {
123 app.world
124 .get_resource::<Backends<B>>()
125 .cloned()
126 .unwrap_or_default()
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[derive(Clone, Copy, Debug)]
134 struct Entry {
135 name: &'static str,
136 priority: i32,
137 }
138
139 impl BackendEntry for Entry {
140 const KIND: &'static str = "test";
141 fn name(&self) -> &'static str {
142 self.name
143 }
144 fn priority(&self) -> i32 {
145 self.priority
146 }
147 }
148
149 fn entry(name: &'static str, priority: i32) -> Entry {
150 Entry { name, priority }
151 }
152
153 #[test]
154 fn a_name_selects_one_backend_and_none_selects_all_by_priority() {
155 let mut backends = Backends::default();
156 backends.register(entry("slow", 0));
157 backends.register(entry("fast", 10));
158 backends.register(entry("also-slow", 0));
159
160 let named = backends.select(Some("slow")).expect("registered");
161 assert_eq!(named.len(), 1);
162 assert_eq!(named[0].name, "slow");
163
164 let order: Vec<&str> = backends
165 .select(None)
166 .expect("some")
167 .iter()
168 .map(|b| b.name)
169 .collect();
170 assert_eq!(order, ["fast", "also-slow", "slow"]);
171 assert_eq!(backends.preferred().map(|b| b.name), Some("fast"));
172 }
173
174 #[test]
175 fn asking_for_a_backend_the_build_lacks_says_what_it_has() {
176 let mut backends = Backends::default();
177 let none = backends.select(Some("cpu")).expect_err("empty");
178 assert!(
179 none.contains("'cpu' test backend") && none.contains("none"),
180 "{none}"
181 );
182 assert_eq!(
183 backends.select(None).expect_err("empty"),
184 "this build carries no test backend"
185 );
186 assert!(backends.preferred().is_none());
187
188 backends.register(entry("gpu", 10));
189 let one = backends.select(Some("cpu")).expect_err("missing");
190 assert!(one.contains("only 'gpu'"), "{one}");
191
192 backends.register(entry("other", 0));
193 backends.register(entry("third", 0));
194 let many = backends.select(Some("cpu")).expect_err("missing");
195 assert!(many.contains("'gpu', 'other' and 'third'"), "{many}");
196 }
197
198 #[test]
199 fn registering_a_name_again_replaces_it() {
200 let mut app = App::new();
201 register_backend(&mut app, entry("gpu", 1));
202 register_backend(&mut app, entry("gpu", 5));
203 let backends = registered::<Entry>(&app);
204 assert_eq!(backends.names(), ["gpu"]);
205 assert_eq!(backends.get("gpu").map(|b| b.priority), Some(5));
206 }
207}