├── .github └── workflows │ ├── CI.yml │ ├── Release.yml │ └── Test Coverage.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src ├── delegate.rs ├── lib.rs └── observable.rs └── tests ├── test_delegate.rs └── test_observable.rs /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions-rs/toolchain@v1 12 | with: 13 | profile: minimal 14 | toolchain: stable 15 | override: true 16 | - uses: actions-rs/cargo@v1 17 | with: 18 | command: check 19 | 20 | test: 21 | name: Test Suite 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v2 25 | - uses: actions-rs/toolchain@v1 26 | with: 27 | profile: minimal 28 | toolchain: stable 29 | override: true 30 | - uses: actions-rs/cargo@v1 31 | with: 32 | command: test 33 | 34 | fmt: 35 | name: Rustfmt 36 | runs-on: ubuntu-latest 37 | steps: 38 | - uses: actions/checkout@v2 39 | - uses: actions-rs/toolchain@v1 40 | with: 41 | profile: minimal 42 | toolchain: stable 43 | override: true 44 | - run: rustup component add rustfmt 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: fmt 48 | args: --all -- --check 49 | 50 | clippy: 51 | name: Clippy 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v2 55 | - uses: actions-rs/toolchain@v1 56 | with: 57 | profile: minimal 58 | toolchain: stable 59 | override: true 60 | - run: rustup component add clippy 61 | - uses: actions-rs/cargo@v1 62 | with: 63 | command: clippy 64 | args: -- -D warnings 65 | -------------------------------------------------------------------------------- /.github/workflows/Release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | name: Release 5 | 6 | jobs: 7 | check: 8 | name: Publish to crates.io 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions-rs/toolchain@v1 13 | with: 14 | profile: minimal 15 | toolchain: stable 16 | override: true 17 | - uses: actions-rs/cargo@v1 18 | with: 19 | command: publish 20 | env: 21 | CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_API_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/Test Coverage.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Test coverage 4 | 5 | jobs: 6 | grcov: 7 | name: Test coverage 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Install toolchain 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: nightly 17 | override: true 18 | profile: minimal 19 | 20 | - name: Execute tests 21 | uses: actions-rs/cargo@v1 22 | with: 23 | command: test 24 | args: --all 25 | env: 26 | CARGO_INCREMENTAL: 0 27 | RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off" 28 | 29 | - name: Gather coverage data 30 | id: coverage 31 | uses: actions-rs/grcov@v0.1 32 | 33 | - name: Codecov upload 34 | uses: codecov/codecov-action@v3 35 | with: 36 | files: ${{ steps.coverage.outputs.report }} 37 | fail_ci_if_error: true 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 0.2.0 4 | 5 | - Squeak is now a `no_std` crate. 6 | 7 | ## Version 0.1.0 8 | 9 | - Initial release. 10 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "squeak" 3 | version = "0.2.0" 4 | description = "Library providing types allowing execution of callbacks in response to values being broadcast or mutated." 5 | authors = ["Antoine Gersant "] 6 | keywords = ["observable", "delegate", "event", "reactive", "no_std"] 7 | categories = ["rust-patterns", "data-structures", "no-std"] 8 | repository = "https://github.com/agersant/squeak" 9 | license = "MIT OR Apache-2.0" 10 | edition = "2021" 11 | rust-version = "1.60.0" 12 | 13 | [dev-dependencies] 14 | parking_lot = "0.12.1" 15 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Squeak 2 | 3 | [![build_badge]][build_link] [![crates.io_badge]][crates.io_link] [![docs_badge]][docs_link] 4 | 5 | [build_badge]: https://img.shields.io/github/actions/workflow/status/agersant/squeak/CI.yml?branch=master 6 | [build_link]: https://github.com/agersant/squeak/actions/workflows/CI.yml?query=branch%3A+branch%3Amaster++ 7 | [crates.io_badge]: https://img.shields.io/badge/crates.io-squeak-green 8 | [crates.io_link]: https://crates.io/crates/squeak 9 | [docs_badge]: https://img.shields.io/badge/docs.rs-squeak-blue 10 | [docs_link]: https://docs.rs/squeak/latest/squeak/ 11 | 12 | Squeak is a zero-dependency Rust library to facilitate event-driven programming. 13 | 14 | # Examples 15 | 16 | ```rust 17 | use squeak::{Delegate, Response}; 18 | 19 | let on_damage_received = Delegate::new(); 20 | on_damage_received.subscribe(|amount| { 21 | println!("Received {amount} damage"); 22 | Response::StaySubscribed 23 | }); 24 | 25 | on_damage_received.broadcast(16); // Prints "Received 16 damage" 26 | on_damage_received.broadcast(14); // Prints "Received 14 damage" 27 | on_damage_received.broadcast(28); // Prints "Received 28 damage" 28 | ``` 29 | 30 | ```rust 31 | use squeak::{Observable, Response}; 32 | 33 | let mut health = Observable::new(100); 34 | health.subscribe(|updated_health| { 35 | println!("Health is now {updated_health}"); 36 | Response::StaySubscribed 37 | }); 38 | 39 | health.mutate(|h| *h -= 10); // Prints "Health is now 90" 40 | health.mutate(|h| *h -= 5); // Prints "Health is now 85" 41 | health.mutate(|h| *h += 25); // Prints "Health is now 110" 42 | ``` 43 | -------------------------------------------------------------------------------- /src/delegate.rs: -------------------------------------------------------------------------------- 1 | use alloc::vec::Vec; 2 | use alloc::{borrow::Borrow, boxed::Box, collections::BTreeMap, fmt::Debug}; 3 | 4 | use core::cell::RefCell; 5 | use core::sync::atomic::{AtomicU64, Ordering}; 6 | 7 | type BoxedCallback<'a, T> = Box Response + 'a + Send>; 8 | type SubscriptionId = u64; 9 | 10 | static NEXT_SUBSCRIPTION_ID: AtomicU64 = AtomicU64::new(0); 11 | 12 | /// Maintains a list of callbacks that can be explicitely triggered 13 | /// by calling [`Delegate::broadcast`]. 14 | #[derive(Default)] 15 | pub struct Delegate<'d, T> { 16 | pub(crate) subscriptions: RefCell>>, 17 | } 18 | 19 | /// Represents a subscription created via [`Delegate::subscribe`] or [`Observable::subscribe`](crate::Observable::subscribe). 20 | /// 21 | /// It can be passed to [`Delegate::unsubscribe`] or [`Observable::unsubscribe`](crate::Observable::unsubscribe) to cancel the subscription. 22 | #[derive(Eq, Hash, PartialEq)] 23 | pub struct Subscription { 24 | id: SubscriptionId, 25 | } 26 | 27 | /// Returned by [`Delegate`] and [`Observable`](crate::Observable) subscription callbacks. 28 | /// Depending on the value returned, the subscription will stay active or be cancelled. 29 | pub enum Response { 30 | StaySubscribed, 31 | CancelSubscription, 32 | } 33 | 34 | impl<'d, T> Delegate<'d, T> { 35 | pub fn new() -> Self { 36 | Self { 37 | subscriptions: RefCell::new(BTreeMap::new()), 38 | } 39 | } 40 | 41 | /// Registers a new callback that will be called when this delegate broadcasts 42 | /// a new value. 43 | /// 44 | /// ```rust 45 | /// use squeak::{Delegate, Response}; 46 | /// 47 | /// let on_damage_received = Delegate::new(); 48 | /// on_damage_received.subscribe(|amount| { 49 | /// println!("Received {amount} damage"); 50 | /// Response::StaySubscribed 51 | /// }); 52 | /// on_damage_received.broadcast(5); // Prints "Received 5 damage" 53 | /// ``` 54 | /// 55 | /// The output of the callback function determines whether it will be called 56 | /// again when [`broadcast`] is called in the future. 57 | /// 58 | pub fn subscribe Response + 'd + Send>(&self, callback: C) -> Subscription { 59 | let id = NEXT_SUBSCRIPTION_ID.fetch_add(1, Ordering::SeqCst); 60 | let subscription = Subscription { id }; 61 | self.subscriptions 62 | .borrow_mut() 63 | .insert(subscription.id, Box::new(callback)); 64 | subscription 65 | } 66 | 67 | /// Removes a callback that was previously registered. 68 | /// 69 | /// ```rust 70 | /// use squeak::{Delegate, Response}; 71 | /// 72 | /// let on_damage_received = Delegate::new(); 73 | /// let subscription = on_damage_received.subscribe(|amount| { 74 | /// println!("Received {amount} damage"); 75 | /// Response::StaySubscribed 76 | /// }); 77 | /// on_damage_received.broadcast(5); // Prints "Received 5 damage" 78 | /// on_damage_received.unsubscribe(subscription); 79 | /// on_damage_received.broadcast(10); // Does not print anything 80 | /// ``` 81 | /// - Attempting to unsubscribe using a [`Subscription`] that was created by a different [`Delegate`] has no effect. 82 | /// - Attempting to unsubscribe a [`Subscription`] multiple times has no effect. 83 | /// - Attempting to unsubscribe from within callback function has no effect. 84 | pub fn unsubscribe(&self, subscription: Subscription) { 85 | self.subscriptions.borrow_mut().remove(&subscription.id); 86 | } 87 | 88 | /// Executes all registered callbacks, providing `value` as their argument. 89 | /// 90 | /// ```rust 91 | /// use squeak::{Delegate, Response}; 92 | /// 93 | /// let on_renamed = Delegate::new(); 94 | /// on_renamed.subscribe(|new_name: &String| { 95 | /// println!("New name is {new_name}"); 96 | /// Response::StaySubscribed 97 | /// }); 98 | /// on_renamed.broadcast(String::from("Lisa")); 99 | /// on_renamed.broadcast(&String::from("Trevor")); 100 | /// on_renamed.broadcast(&mut String::from("Jill")); 101 | /// ``` 102 | pub fn broadcast>(&self, value: U) { 103 | let subscriptions_to_notify = self 104 | .subscriptions 105 | .borrow() 106 | .keys() 107 | .copied() 108 | .collect::>(); 109 | for subscription in subscriptions_to_notify { 110 | let (_, mut callback) = self 111 | .subscriptions 112 | .borrow_mut() 113 | .remove_entry(&subscription) 114 | .unwrap(); 115 | match callback(value.borrow()) { 116 | Response::CancelSubscription => (), 117 | Response::StaySubscribed => { 118 | self.subscriptions 119 | .borrow_mut() 120 | .insert(subscription, callback); 121 | } 122 | }; 123 | } 124 | } 125 | } 126 | 127 | impl Delegate<'_, ()> { 128 | /// This convenience function broadcasts the unit type on delegates with no payload. 129 | /// 130 | /// ```rust 131 | /// use squeak::{Delegate, Response}; 132 | /// 133 | /// let on_respawn = Delegate::new(); 134 | /// on_respawn.subscribe(|_| { 135 | /// println!("Respawned"); 136 | /// Response::StaySubscribed 137 | /// }); 138 | /// on_respawn.notify(); 139 | /// ``` 140 | pub fn notify(&self) { 141 | self.broadcast(()); 142 | } 143 | } 144 | 145 | impl Debug for Delegate<'_, T> 146 | where 147 | T: Debug, 148 | { 149 | fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result { 150 | f.debug_struct("Delegate") 151 | .field( 152 | "subscriptions", 153 | &format_args!("{} active subscriptions", self.subscriptions.borrow().len()), 154 | ) 155 | .finish() 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! This library provides types allowing execution of callbacks in response to 2 | //! values being broadcast or mutated. 3 | //! 4 | //!
5 | //! 6 | //! # Manually broadcast values 7 | //! 8 | //! Delegates can be used to manually broadcast values to subscribers: 9 | //! 10 | //! ```rust 11 | //! use squeak::{Delegate, Response}; 12 | //! 13 | //! let on_damage_received = Delegate::new(); 14 | //! on_damage_received.subscribe(|amount| { 15 | //! println!("Received {amount} damage"); 16 | //! Response::StaySubscribed 17 | //! }); 18 | //! 19 | //! on_damage_received.broadcast(16); // Prints "Received 16 damage" 20 | //! on_damage_received.broadcast(14); // Prints "Received 14 damage" 21 | //! on_damage_received.broadcast(28); // Prints "Received 28 damage" 22 | //! ``` 23 | //! 24 | //! # Automatically broadcast when a variable is mutated 25 | //! 26 | //! Observables own a value and execute callbacks whenever the value is mutated: 27 | //! 28 | //! ```rust 29 | //! 30 | //! use squeak::{Observable, Response}; 31 | //! 32 | //! let mut health = Observable::new(100); 33 | //! health.subscribe(|updated_health| { 34 | //! println!("Health is now {updated_health}"); 35 | //! Response::StaySubscribed 36 | //! }); 37 | //! 38 | //! health.mutate(|h| *h -= 10); // Prints "Health is now 90" 39 | //! health.mutate(|h| *h -= 5); // Prints "Health is now 85" 40 | //! health.mutate(|h| *h += 25); // Prints "Health is now 110" 41 | //! ``` 42 | //! 43 | #![no_std] 44 | extern crate alloc; 45 | 46 | mod delegate; 47 | mod observable; 48 | 49 | pub use delegate::{Delegate, Response, Subscription}; 50 | pub use observable::Observable; 51 | -------------------------------------------------------------------------------- /src/observable.rs: -------------------------------------------------------------------------------- 1 | use alloc::fmt::Debug; 2 | use core::ops::Deref; 3 | 4 | use crate::{Delegate, Response, Subscription}; 5 | 6 | /// Wrapper type which owns a value and executes callbacks every time a call is made to mutate the value. 7 | /// 8 | /// ``` rust 9 | /// use squeak::{Observable, Response}; 10 | /// 11 | /// let mut health = Observable::new(100); 12 | /// health.subscribe(|updated_health| { 13 | /// println!("Health is now {updated_health}"); 14 | /// Response::StaySubscribed 15 | /// }); 16 | /// 17 | /// health.mutate(|h| *h -= 10); // Prints "Health is now 90" 18 | /// health.mutate(|h| *h -= 5); // Prints "Health is now 85" 19 | /// health.mutate(|h| *h += 25); // Prints "Health is now 110" 20 | /// ``` 21 | /// 22 | /// Observables implement [`std::ops::Deref`], which means the inner value can be accessed 23 | /// via `*my_observable`. 24 | #[derive(Debug)] 25 | pub struct Observable<'o, T> { 26 | value: T, 27 | delegate: Delegate<'o, T>, 28 | } 29 | 30 | impl<'o, T> Observable<'o, T> { 31 | /// Creates a new observable with an initial value 32 | /// 33 | /// ```rust 34 | /// use squeak::Observable; 35 | /// let name = Observable::new(String::from("DefaultName")); 36 | /// ``` 37 | pub fn new(value: T) -> Self { 38 | Self { 39 | value, 40 | delegate: Delegate { 41 | subscriptions: Default::default(), 42 | }, 43 | } 44 | } 45 | 46 | /// Registers a new callback that will be called when the value contained in this observable is mutated. 47 | /// 48 | /// ```rust 49 | /// use squeak::{Observable, Response}; 50 | /// 51 | /// let mut health = Observable::new(100); 52 | /// health.subscribe(|updated_health| { 53 | /// println!("Health is now {updated_health}"); 54 | /// Response::StaySubscribed 55 | /// }); 56 | /// ``` 57 | /// 58 | /// The output of the callback function determines whether it will be called 59 | /// again when [`broadcast`] is called in the future. 60 | pub fn subscribe Response + 'o + Send>(&self, callback: C) -> Subscription { 61 | self.delegate.subscribe(callback) 62 | } 63 | 64 | /// Removes a callback that was previously registered. 65 | /// 66 | /// ```rust 67 | /// use squeak::{Observable, Response}; 68 | /// 69 | /// let mut health = Observable::new(100); 70 | /// let subscription = health.subscribe(|updated_health| { 71 | /// println!("Health is now {updated_health}"); 72 | /// Response::StaySubscribed 73 | /// }); 74 | /// 75 | /// health.unsubscribe(subscription); 76 | /// ``` 77 | pub fn unsubscribe(&self, subscription: Subscription) { 78 | self.delegate.unsubscribe(subscription); 79 | } 80 | 81 | /// Returns a reference to a delegate that will execute subscription functions 82 | /// when the observable is mutated. This is useful when writing a struct that has 83 | /// an observable member, but users of the struct should only have access to its 84 | /// value by subscribing. 85 | /// 86 | /// ```rust 87 | /// use squeak::{Delegate, Observable}; 88 | /// 89 | /// struct MyStruct<'a> { 90 | /// observe_only: Observable<'a, u32>, 91 | /// } 92 | /// 93 | /// impl<'a> MyStruct<'a> { 94 | /// pub fn delegate(&self) -> &Delegate<'a, u32> { 95 | /// self.observe_only.delegate() 96 | /// } 97 | /// } 98 | /// 99 | /// ``` 100 | /// 101 | pub fn delegate(&self) -> &Delegate<'o, T> { 102 | &self.delegate 103 | } 104 | 105 | /// Execute a function which may mutate the value contained in this observable. 106 | /// Subscription callbacks will be executed regardless of what happens inside 107 | /// the `mutation` function. 108 | /// 109 | /// ```rust 110 | /// use squeak::Observable; 111 | /// 112 | /// let mut name = Observable::new(String::from("DefaultName")); 113 | /// name.mutate(|n| n.push_str("X")); 114 | /// name.mutate(|n| n.push_str("Y")); 115 | /// name.mutate(|n| n.push_str("Z")); 116 | /// 117 | /// assert_eq!(name.as_str(), "DefaultNameXYZ"); 118 | /// ``` 119 | pub fn mutate(&mut self, mutation: M) 120 | where 121 | M: FnOnce(&mut T), 122 | { 123 | mutation(&mut self.value); 124 | self.delegate.broadcast(&self.value); 125 | } 126 | } 127 | 128 | impl Default for Observable<'_, T> 129 | where 130 | T: Default, 131 | { 132 | fn default() -> Self { 133 | Self::new(Default::default()) 134 | } 135 | } 136 | 137 | impl Deref for Observable<'_, T> { 138 | type Target = T; 139 | fn deref(&self) -> &Self::Target { 140 | &self.value 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /tests/test_delegate.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::{Mutex, ReentrantMutex}; 2 | use std::{cell::RefCell, ops::Deref, sync::Arc}; 3 | 4 | use squeak::{Delegate, Response}; 5 | 6 | #[test] 7 | fn delegate_executes_callbacks() { 8 | let mut call_count = 0; 9 | { 10 | let d = Delegate::new(); 11 | d.subscribe(|_| { 12 | call_count += 1; 13 | Response::StaySubscribed 14 | }); 15 | d.notify(); 16 | d.notify(); 17 | d.notify(); 18 | } 19 | assert_eq!(call_count, 3); 20 | } 21 | 22 | #[test] 23 | fn can_subscribe_during_callback() { 24 | let d = Arc::new(ReentrantMutex::new(Delegate::new())); 25 | let outer_count = Arc::new(Mutex::new(RefCell::new(0))); 26 | let inner_count = Arc::new(Mutex::new(RefCell::new(0))); 27 | { 28 | let d_clone = d.clone(); 29 | let outer_count_clone = outer_count.clone(); 30 | let inner_count_clone = inner_count.clone(); 31 | d.lock().subscribe(move |_| { 32 | *outer_count_clone.lock().borrow_mut() += 1; 33 | let inner_count_clone_clone = inner_count_clone.clone(); 34 | d_clone.lock().subscribe(move |_| { 35 | *inner_count_clone_clone.lock().borrow_mut() += 1; 36 | Response::CancelSubscription 37 | }); 38 | Response::CancelSubscription 39 | }); 40 | d.lock().notify(); 41 | d.lock().notify(); 42 | } 43 | assert_eq!(*outer_count.lock().borrow(), 1); 44 | assert_eq!(*inner_count.lock().borrow(), 1); 45 | } 46 | 47 | #[test] 48 | fn delegate_does_not_execute_unsubscribed_callbacks() { 49 | let mut call_count = 0; 50 | { 51 | let d = Delegate::new(); 52 | let subscription = d.subscribe(|_| { 53 | call_count += 1; 54 | Response::StaySubscribed 55 | }); 56 | d.unsubscribe(subscription); 57 | d.notify(); 58 | } 59 | assert_eq!(call_count, 0); 60 | } 61 | 62 | #[test] 63 | fn cannot_unsubscribe_using_subscription_from_a_different_delegate() { 64 | let mut call_count = 0; 65 | { 66 | let d1 = Delegate::<()>::new(); 67 | let d2 = Delegate::<()>::new(); 68 | let _s1 = d1.subscribe(|_| { 69 | call_count += 1; 70 | Response::StaySubscribed 71 | }); 72 | let s2 = d2.subscribe(|_| Response::StaySubscribed); 73 | d1.unsubscribe(s2); 74 | d1.notify(); 75 | } 76 | assert_eq!(call_count, 1); 77 | } 78 | 79 | #[test] 80 | fn unsubscribing_within_callback_is_noop() { 81 | let d = Arc::new(ReentrantMutex::new(Delegate::new())); 82 | let call_count = Arc::new(Mutex::new(RefCell::new(0))); 83 | let subscription = Arc::new(Mutex::new(RefCell::new(None))); 84 | 85 | let d_clone = d.clone(); 86 | let call_count_clone = call_count.clone(); 87 | let subscription_clone = subscription.clone(); 88 | 89 | subscription 90 | .lock() 91 | .replace(Some(d.lock().subscribe(move |_| { 92 | let old_count = *call_count_clone.lock().borrow(); 93 | *call_count_clone.lock().borrow_mut() = old_count + 1; 94 | if let Some(subscription) = subscription_clone.lock().deref().borrow_mut().take() { 95 | d_clone.lock().unsubscribe(subscription); 96 | } 97 | Response::StaySubscribed 98 | }))); 99 | 100 | d.lock().notify(); 101 | d.lock().notify(); 102 | assert_eq!(*call_count.lock().borrow(), 2); 103 | } 104 | 105 | #[test] 106 | fn can_unsubscribe_using_response_value() { 107 | let mut call_count = 0; 108 | { 109 | let d = Delegate::new(); 110 | d.subscribe(|_| { 111 | call_count += 1; 112 | Response::CancelSubscription 113 | }); 114 | d.notify(); 115 | d.notify(); 116 | } 117 | assert_eq!(call_count, 1); 118 | } 119 | -------------------------------------------------------------------------------- /tests/test_observable.rs: -------------------------------------------------------------------------------- 1 | use squeak::{Observable, Response}; 2 | 3 | #[test] 4 | fn observable_broadcasts_new_values() { 5 | let mut seen_value = 0; 6 | { 7 | let mut o = Observable::new(0); 8 | o.subscribe(|new_value| { 9 | seen_value = *new_value; 10 | Response::StaySubscribed 11 | }); 12 | o.mutate(|value| *value = 42); 13 | } 14 | assert_eq!(seen_value, 42); 15 | } 16 | 17 | #[test] 18 | fn observable_no_longer_notifies_after_unsubscribe() { 19 | let mut call_count = 0; 20 | { 21 | let mut o = Observable::new(0); 22 | let s = o.subscribe(|_| { 23 | call_count += 1; 24 | Response::StaySubscribed 25 | }); 26 | o.mutate(|value| *value = 42); 27 | o.unsubscribe(s); 28 | o.mutate(|value| *value = 43); 29 | } 30 | assert_eq!(call_count, 1); 31 | } 32 | --------------------------------------------------------------------------------