//! Contains definition of the Store //! //! ## What it is? //! //! 1. [DataStore] is dynamic list of values. //! 2. Storage of values is not defined for [DataStore]. It might be in memory, or SQL, or csv file, or whatever else //! 3. [DataStore] supports views. View tracks subset of the data kept in the store. //! //! ## Why? //! //! Relm4 is notorious in mixing business logic and view. Stores allows to give a strict separation between //! business model and what and how it's shown to the user. //! //! As side effect it simplifies relm4 factories usage. #![warn( missing_debug_implementations, missing_docs, rust_2018_idioms, unreachable_pub )] mod factory_prototype; pub mod math; mod pagination; mod position; mod record_with_location; pub mod redraw_messages; mod store_id; mod store_msg; mod store_size; mod store_view_component; mod store_view_msg; mod store; pub mod window; use reexport::relm4; use std::fmt::Debug; use std::marker::Sized; use relm4::Sender; use record::Id; use record::Identifiable; use record::Record; use record::TemporaryIdAllocator; use crate::math::Range; pub use factory_prototype::StoreViewPrototype; pub use factory_prototype::FactoryContainerWidgets; pub use factory_prototype::StoreViewInnerComponent; pub use pagination::Pagination; pub use position::Position; pub use record_with_location::RecordWithLocation; pub use store::Store; pub use store_id::StoreId; pub use store_msg::StoreMsg; pub use store_size::StoreSize; pub use store_view_component::StoreViewComponent; pub use store_view_component::StoreViewInterfaceError; pub use store_view_msg::StoreViewMsg; /// DataStore is a trait describing collections specialized in housekeeping business model data /// /// DataStore is designed with upsert in mind. /// /// ## DataStore implementations are collections /// /// That means they own (in terms of borrow checker) some instances of your business model data. /// Also it must give you ability to query your data so you can see what's inside. /// /// ## DataStore implementations are housekeeping your data /// /// DataStore is a place which knows how to mange CRUD operations on your business data. /// Different implementations of this trait might use different strategies to keep your data. /// Some may keep it in the files on the local file system. Some others might store it in database. /// Wherever your data are being kept data store needs to track when they have been changed and /// sometimes how so they can store them. /// /// ## When can I use it? /// /// You can use data store if it can own the data safely. This boils down to three requirements /// /// - no internal mutation
/// Data store needs to be certain about the data which it holds /// - they are serializable
/// Data store needs to be able to create a copies of the data /// - identifiable /// In presence of copies data store needs to be able to tell which data is just a different /// representation of another instance of data /// /// ### No internal mutation /// /// If you create a model which has internal mutability then it would need know to which data store /// it belongs to so it can notify it about the changes. But data store is collection so it needs to /// know the data which are kept inside. So we have a perfect circle of dependencies which would make /// it insane in terms of implementation. /// /// Another reason is that store implementation in really more like simple database or database proxy. /// As such it must own the "truth" of what the current state of the records is. If your records are /// internally mutable, you might introduce side effects which are not recorded by the database. /// /// ### They are serializable /// /// DataStore implementations needs to be able to save a data for example in a file and read from it. /// Also since there is no internal mutation allowed that means data store needs to be able to give you /// copies of data inside of it so you can mutate them. If you disallow internal mutation rust's [Clone] /// is good enough for generic case. Specific implementations might have extra serialization requirements. /// /// ### They are identifiable /// /// DataStore gives you the copy of a data so you can mutate it. Now you would like to update the data /// in the store. But which data you are talking about? Equality operation is not going to work since /// you just mutated them. That's way your model needs to provide an identifier which is good enough /// so the data store can depend on it to identify the instances of the data. /// /// ## To the writers of the store /// /// If you implement a store to be used by the external applications do yourself and users a favor and /// please /// /// 1. Expose StoreId allocator /// 2. Make sure your store will work with any sufficiently good enough `Id` without depending /// on the exact underlying id type. If you require any property to be present for an `Id` just add it /// to the list of requirements of your store and ask user to give you that. /// /// If there are limitation on that please somewhere at the beginning of the docs note what limitations /// are around this values. It can easily become a deal breaker for users of your library. pub trait DataStore: Identifiable::Type, Id=StoreId> { /// Type of records kept in the data store type Record: Record + Debug + Clone + 'static; /// Kind of messages accepted by the data store type Messages; /// Id allocator for this data store /// /// ## TL;DR; /// /// You should keep it as /// /// ```text /// type Allocator = DefaultIdAllocator; /// ``` /// /// ## Longer version /// /// As long as possible (and little bit longer) you should use `[DefaultIdAllocator]` Overriding it /// might be necessary in some super rare cases where you have more then one data store for the same /// kind of data or you have dynamic number of data stores of given kind. Both of the cases are in the /// "please don't do that" area from design perspective. /// /// If you are reading this section in 99% of cases creating a custom DataStore which will be backed by /// more then one backend is the proper solution for your issues. type Allocator: TemporaryIdAllocator; /// Registers message in the data store /// /// Data store might handle it immediately or might do queue it for later. It's up to the store /// implementation what to do with a message // fn inbox(&self, m: StoreMsg); /// Total amount of available records in the store fn len(&self) -> usize; /// Returns true if store doesn't contain any records yet fn is_empty(&self) -> bool; /// Returns the record from the store /// /// If returns [None] then it means there is no such record in the store fn get(&self, id: &Id) -> Option; /// Returns records which are in the store at the given range /// /// Returned vector doesn't need to be ordered by position. /// If range is out of bounds returned vector will be empty. fn get_range(&self, range: &Range) -> Vec; /// Attaches sender to the store /// /// Sender is used to send a message whenever there are changes in the store fn listen(&self, id: StoreId, sender: Sender>); /// Removes handler from the store /// /// Changes to the store will not be delivered after this handler is removed fn unlisten(&self, handler_ref: StoreId); /// Returns sender for this store fn sender(&self) -> Sender; /// Sends a message to this store fn send(&self, msg: Self::Messages); } /// StoreView allows you to access part of the data in the data store /// /// StoreView is a special kind of data store which tracks subset of the data in the other data store. /// This is useful in various scenarios /// /// - showing data to the user
/// Showing all ten billion of records at the same time is time consuming and will overwhelm user /// - editing the data
/// Your business model has two data sets `A` and `B` and there is `1-*` relationship between the data. /// There are valid scenarios when you would like to edit item in `A` and give the ability to modify /// related items in `B` at the same time. pub trait StoreView: DataStore { /// Type describing configuration parts of the store view behavior type Configuration: ?Sized + StoreViewPrototype; /// How many records should be visible at any point of time /// /// If there is less elements in the store/page it's possible that less records will be shown. /// StoreView should never show more records then this value fn window_size(&self) -> usize; /// Returns range in the parent store for data in the current window fn get_window(&self) -> Range; /// Moves the window to the new range fn set_window(&self, range: Range); /// Returns vector with list of records in the current view /// /// Returned records are **clones** of the actual records fn get_view_data(&self) -> Vec>; /// Returns current number of elements visible via the store view /// /// Always a number between `[0, [StoreView::window_size])` fn current_len(&self) -> usize; /// Returns the position of the record in the view /// /// If returns `None` that means record is not in the view fn get_position(&self, id: &Id) -> Option; /// Advance the store view to the next page (if it exists) of underlying store fn next_page(&self); /// Goes back the the previous page (if it exists) of underlying store fn prev_page(&self); /// Goes to the first page of data in the underlying store fn first_page(&self); /// Goes to the last page of the data in the underlying store fn last_page(&self); /// Returns current size of unhandled messages in the view fn inbox_queue_size(&self) -> usize; } /// Structure used by backends to send back information about what should be sent to the views /// after updates are done on the backend side #[derive(Debug)] pub struct Replies where Record: record::Record + Debug + Clone + 'static { /// List of messages to be sent to the store views pub replies: Vec>, } /// This trait should be implemented for backends of the data stores /// /// It's basically `DataStore - Sender` pub trait Backend { /// Type of records kept in the data store type Record: Record + Debug + Clone + 'static; /// Registers message in the data store /// /// Data store might handle it immediately or might do queue it for later. It's up to the store /// implementation what to do with a message // fn inbox(&self, m: StoreMsg); /// Total amount of available records in the store fn len(&self) -> usize; /// Returns true if store doesn't contain any records yet fn is_empty(&self) -> bool; /// Returns the record from the store /// /// If returns [None] then it means there is no such record in the store fn get(&self, id: &Id) -> Option; /// Returns records which are in the store at the given range /// /// Returned vector doesn't need to be ordered by position. /// If range is out of bounds returned vector will be empty. fn get_range(&self, range: &Range) -> Vec; /// Handles messages fn inbox(&mut self, msg: StoreMsg) -> Replies; } /// Default trait describing how the records should be sorted by backend pub trait Sorter: Copy + Debug { /// Compares `lhs` with `rhs` /// /// If sorter is being used implementation assumes that `cmp` constitutes a total order fn cmp(&self, lhs: &Record, rhs: &Record) -> std::cmp::Ordering; } /// Trait implemented by the data store which supports switching of the natural order /// /// If you use [`Store`] then it will use `OrderBy` defined by the backend pub trait OrderedStore: DataStore { /// sets natural order of the records fn set_order(&self, order: OrderBy); } /// Trait implemented by the data store backends supporting switching of the natural order /// /// When you define your own implementation of the [`Backend`] in most cases /// it will look like: /// /// ```text /// trait MyBackend>{ /// ... /// } /// ``` /// /// This implementation doesn't limit what `OrderBy` is so you have as much of freedom as /// possible when you need to do something special pub trait OrderedBackend: Backend { /// sets natural order of the records fn set_order(&mut self, order: OrderBy) -> Replies; }