1use std::cell::RefCell;
4use std::rc::Rc;
5
6use ravenrs_app::{
7 Application, ApplicationBuilder, AttachmentStore, BusinessObject, MemoryDatabase, ModelError, ModelStore,
8 Module, ObjectSpace,
9};
10use ravenrs_app_i18n::{Catalog, Language, Texts};
11use ravenrs_widgets::{Alert, AnyView, Labels, Localized, Tone};
12
13use crate::editors::EditorRegistry;
14use crate::session::{SpaceFactory, Words};
15use crate::{AppShell, SystemModule};
16
17pub(crate) type TextPacks = Vec<(String, String)>;
19
20pub(crate) fn texts_of(language: Language, packs: &[(String, String)]) -> Result<Texts, ModelError> {
22 let mut catalog = Catalog::builtin();
23 for (code, json) in packs {
24 catalog.merge_json(code, json).map_err(|error| ModelError(error.0))?;
25 }
26 Ok(catalog.texts(language))
27}
28
29type Seed = Box<dyn FnOnce(&mut dyn ObjectSpace) -> Result<(), String>>;
30type Storage = Box<dyn FnOnce(&Application) -> SpaceFactory>;
31type SessionStep = Box<dyn FnOnce(&crate::Session)>;
32
33#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
60pub struct App {
61 builder: ApplicationBuilder,
62 language: Language,
63 texts: TextPacks,
64 modules: Vec<Box<dyn FnOnce(ApplicationBuilder) -> ApplicationBuilder>>,
65 editors: EditorRegistry,
66 seed: Option<Seed>,
67 storage: Option<Storage>,
68 kept: Option<Rc<dyn ravenrs_app::Storage>>,
69 list_editors: Vec<(String, crate::list_view::ListEditorFactory)>,
70 attachment_store: Option<Rc<dyn AttachmentStore>>,
71 model_store: Option<Rc<dyn ModelStore>>,
72 steps: Vec<SessionStep>,
73 administrator: Option<String>,
74 security: Option<(Rc<ravenrs_app::SecurityContext>, ravenrs_app::SpaceMaker)>,
75 actions: Vec<ActionMaker>,
76}
77
78type ActionMaker = Rc<dyn Fn() -> ravenrs_app::Action>;
79
80struct OwnActions(Vec<ActionMaker>);
82
83impl Module for OwnActions {
84 fn name(&self) -> &str {
85 "OwnActions"
86 }
87
88 fn view_controllers(&self) -> Vec<Box<dyn ravenrs_app::ViewController>> {
89 vec![Box::new(OwnActionsController(self.0.iter().map(|make| make()).collect()))]
90 }
91}
92
93struct OwnActionsController(Vec<ravenrs_app::Action>);
94
95impl ravenrs_app::ViewController for OwnActionsController {
96 fn id(&self) -> &str {
97 "OwnActions"
98 }
99
100 fn actions(&self) -> Vec<ravenrs_app::Action> {
101 self.0.clone()
102 }
103}
104
105impl std::fmt::Debug for App {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 f.debug_struct("App").field("language", &self.language).finish_non_exhaustive()
108 }
109}
110
111#[cfg_attr(feature = "live", ravenrs_live::expose)]
112impl App {
113 pub fn new(title: impl Into<String>) -> Self {
115 Self {
116 builder: Application::builder(title),
117 language: Language::default(),
118 texts: Vec::new(),
119 modules: Vec::new(),
120 editors: EditorRegistry::default(),
121 seed: None,
122 storage: None,
123 kept: None,
124 list_editors: Vec::new(),
125 attachment_store: None,
126 model_store: None,
127 steps: Vec::new(),
128 administrator: None,
129 security: None,
130 actions: Vec::new(),
131 }
132 }
133
134 pub fn administrator_settings(mut self, difference: impl Into<String>) -> Self {
137 self.administrator = Some(difference.into());
138 self
139 }
140
141 pub fn records_of(
143 mut self,
144 entity: impl Into<String>,
145 source: impl Fn() -> Vec<ravenrs_app::Object> + 'static,
146 ) -> Self {
147 let entity = entity.into();
148 self.steps.push(Box::new(move |session| session.use_records(entity, source)));
149 self
150 }
151
152 pub fn collection_of(
154 mut self,
155 entity: impl Into<String>,
156 field: impl Into<String>,
157 source: impl Fn(&ravenrs_app::Object) -> Vec<ravenrs_app::Object> + 'static,
158 ) -> Self {
159 let (entity, field) = (entity.into(), field.into());
160 self.steps.push(Box::new(move |session| session.use_collection(entity, field, source)));
161 self
162 }
163
164 pub fn language(mut self, language: Language) -> Self {
166 self.language = language;
167 self
168 }
169
170 pub fn texts(mut self, language: impl Into<String>, json: impl Into<String>) -> Self {
175 self.texts.push((language.into(), json.into()));
176 self
177 }
178
179 pub fn entity<T: BusinessObject>(mut self) -> Self {
181 self.builder = self.builder.entity_of::<T>();
182 self
183 }
184
185 pub fn entity_info(mut self, entity: ravenrs_app::EntityInfo) -> Self {
187 self.builder = self.builder.entity(entity);
188 self
189 }
190
191 pub fn action(mut self, make: impl Fn() -> ravenrs_app::Action + 'static) -> Self {
220 self.actions.push(Rc::new(make));
221 self
222 }
223
224 pub fn module(mut self, module: impl Module + 'static) -> Self {
226 self.modules.push(Box::new(move |builder| builder.module(module)));
227 self
228 }
229
230 pub fn editors(mut self, editors: EditorRegistry) -> Self {
232 self.editors = editors;
233 self
234 }
235
236 pub fn translation(mut self, language: impl Into<String>, difference: impl Into<String>) -> Self {
240 self.builder = self.builder.translation(language, difference);
241 self
242 }
243
244 pub fn service<T: 'static>(mut self, service: T) -> Self {
246 self.builder = self.builder.service(service);
247 self
248 }
249
250 pub fn dashboard(mut self, dashboard: ravenrs_app::DashboardInfo) -> Self {
252 self.builder = self.builder.dashboard(dashboard);
253 self
254 }
255
256 pub fn list_editor(
258 mut self,
259 name: impl Into<String>,
260 editor: impl Fn(&crate::ListEditorRequest) -> AnyView + 'static,
261 ) -> Self {
262 self.list_editors.push((name.into(), Rc::new(editor)));
263 self
264 }
265
266 pub fn attachment_store(mut self, store: impl AttachmentStore + 'static) -> Self {
268 self.attachment_store = Some(Rc::new(store));
269 self
270 }
271
272 pub fn model_store(mut self, store: impl ModelStore + 'static) -> Self {
274 self.model_store = Some(Rc::new(store));
275 self
276 }
277
278 pub fn seed(mut self, seed: impl FnOnce(&mut dyn ObjectSpace) -> Result<(), String> + 'static) -> Self {
280 self.seed = Some(Box::new(seed));
281 self
282 }
283
284 pub fn keep_in(mut self, storage: impl ravenrs_app::Storage + 'static) -> Self {
290 let storage: Rc<dyn ravenrs_app::Storage> = Rc::new(storage);
291 self.model_store = Some(Rc::new(ravenrs_app::StoredModel::new(storage.clone(), "settings")));
292 self.kept = Some(storage);
293 self
294 }
295
296 pub fn storage<S: ObjectSpace + 'static>(mut self, space: impl Fn() -> S + 'static) -> Self {
298 self.storage = Some(Box::new(move |_| {
299 Rc::new(move || Rc::new(RefCell::new(space())) as Rc<RefCell<dyn ObjectSpace>>)
300 }));
301 self
302 }
303
304 pub fn storage_for<S, F>(mut self, make: impl FnOnce(&[ravenrs_app::EntityInfo]) -> F + 'static) -> Self
307 where
308 S: ObjectSpace + 'static,
309 F: Fn() -> S + 'static,
310 {
311 self.storage = Some(Box::new(move |application| {
312 let space = make(application.entities());
313 Rc::new(move || Rc::new(RefCell::new(space())) as Rc<RefCell<dyn ObjectSpace>>)
314 }));
315 self
316 }
317
318 pub(crate) fn secured(
321 mut self,
322 context: Rc<ravenrs_app::SecurityContext>,
323 unsecured: ravenrs_app::SpaceMaker,
324 ) -> Self {
325 self.seed = None;
326 self.security = Some((context, unsecured));
327 self
328 }
329
330 pub(crate) fn signed_in(mut self, name: String, sign_out: impl Fn() + 'static) -> Self {
332 let sign_out: Rc<dyn Fn()> = Rc::new(sign_out);
333 self.steps.push(Box::new(move |session| {
334 session.use_account(crate::session::Account { name, sign_out });
335 session.register_control(crate::security::MODEL_EDITOR, crate::security::model_editor(session));
336 }));
337 self
338 }
339
340 pub fn with_session(mut self, then: impl FnOnce(&crate::Session) + 'static) -> Self {
342 self.steps.push(Box::new(then));
343 self
344 }
345
346 pub fn build(self) -> Result<AnyView, ModelError> {
348 let texts = texts_of(self.language, &self.texts)?;
349 let words = Words::from_texts(&texts);
350 let mut builder = self.builder.module(SystemModule::with_words(words.clone()));
351 for add in self.modules {
352 builder = add(builder);
353 }
354 if !self.actions.is_empty() {
355 builder = builder.module(OwnActions(self.actions));
356 }
357 let mut application = builder.build()?;
358 if let Some(text) = &self.administrator {
359 let difference = ravenrs_app::Diff::parse(text)?;
360 application.layers_mut().put(&ravenrs_app::ModelLayer::Administrator, difference);
361 }
362 if let Some(store) = &self.model_store {
364 application.layers_mut().load(&ravenrs_app::ModelLayer::User, &**store)?;
365 }
366 application.set_language(self.language.code())?;
367 let unsecured = self.security.as_ref().map(|(_, unsecured)| unsecured.clone());
368 if let Some((context, _)) = &self.security {
369 application.set_security(context.clone());
370 }
371 let mut restored = false;
372 let factory: SpaceFactory = match (self.storage, self.kept) {
373 (Some(storage), _) => storage(&application),
374 (None, Some(kept)) => {
375 let database = MemoryDatabase::new(application.entities().to_vec())?
376 .stored_in(kept, "records")
377 .map_err(|e| ModelError(e.to_string()))?;
378 restored = database.was_restored();
379 Rc::new(move || Rc::new(RefCell::new(database.space())) as Rc<RefCell<dyn ObjectSpace>>)
380 }
381 (None, None) => {
382 let database = MemoryDatabase::new(application.entities().to_vec())?;
383 Rc::new(move || Rc::new(RefCell::new(database.space())) as Rc<RefCell<dyn ObjectSpace>>)
384 }
385 };
386 match unsecured {
387 Some(unsecured) => {
388 let mut space = unsecured();
389 application.run_initial_records(&mut *space).map_err(ModelError)?;
390 }
391 None => {
392 let space = factory();
393 let mut space = space.borrow_mut();
394 application.run_initial_records(&mut *space).map_err(ModelError)?;
395 }
396 }
397 if let Some(seed) = self.seed.filter(|_| !restored) {
398 let space = factory();
399 let mut space = space.borrow_mut();
400 seed(&mut *space).map_err(ModelError)?;
401 space.commit().map_err(|e| ModelError(e.to_string()))?;
402 }
403 let shell = AppShell::from_factory(application, factory, words, self.editors);
404 let session = shell.session();
405 session.use_texts(texts.clone());
406 session.set_access_words(ravenrs_app_a11y::AccessWords::for_language(self.language.code()));
407 for (name, editor) in self.list_editors {
408 session.register_list_editor(name, move |request| editor(request));
409 }
410 if let Some(store) = self.attachment_store {
411 session.use_attachment_store(store);
412 }
413 for step in self.steps {
414 step(&session);
415 }
416 if let Some(store) = self.model_store {
417 session.use_model_store(store).map_err(ModelError)?;
418 }
419 Ok(Box::new(Localized::new(Labels::from_texts(&texts), shell)))
420 }
421
422 pub fn into_view(self) -> AnyView {
424 self.build().unwrap_or_else(|error| Box::new(Alert::new("RavenRS", error.0).tone(Tone::Danger)))
425 }
426}