├── .gitignore ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── src ├── prelude.rs ├── path.rs ├── sled_store.rs ├── rocksdb_store.rs ├── local_storage_store.rs ├── redb_store.rs ├── persistent_resource.rs └── lib.rs ├── LICENSE ├── LICENSE-MIT ├── examples ├── basic.rs ├── enumkeys.rs ├── migration.rs └── persistent_resource.rs ├── Cargo.toml ├── benches └── inserts.rs ├── README.md └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .vscode/ 4 | bevy_pkv.sled/ 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Enable version updates for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" -------------------------------------------------------------------------------- /src/prelude.rs: -------------------------------------------------------------------------------- 1 | //! Convenient re-exports for common bevy_pkv functionality 2 | 3 | pub use crate::PkvStore; 4 | 5 | #[cfg(feature = "bevy")] 6 | pub use crate::{PersistentResourceAppExtensions, PersistentResourcePlugin}; 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | bevy_pkv is dual-licensed under either 2 | 3 | * MIT License (./LICENSE-MIT or http://opensource.org/licenses/MIT) 4 | * Apache License, Version 2.0 (./LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) 5 | 6 | at your option. 7 | -------------------------------------------------------------------------------- /src/path.rs: -------------------------------------------------------------------------------- 1 | use crate::Location; 2 | use std::path::Path; 3 | 4 | impl Location<'_> { 5 | pub fn get_path(&self) -> std::path::PathBuf { 6 | match self { 7 | Self::CustomPath(path) => path.to_path_buf(), 8 | Self::PlatformDefault(config) => { 9 | let dirs = directories::ProjectDirs::from( 10 | config.qualifier.as_deref().unwrap_or(""), 11 | &config.organization, 12 | &config.application, 13 | ); 14 | match dirs.as_ref() { 15 | Some(dirs) => dirs.data_dir(), 16 | None => Path::new("."), // todo: maybe warn? 17 | } 18 | .to_path_buf() 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use bevy::{log::LogPlugin, prelude::*}; 2 | use bevy_pkv::PkvStore; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | struct User { 7 | name: String, 8 | } 9 | 10 | fn setup(mut pkv: ResMut) { 11 | // strings 12 | if let Ok(username) = pkv.get::("username") { 13 | info!("Welcome back {username}"); 14 | } else { 15 | info!("First time user, setting username to 'alice'"); 16 | pkv.set_string("username", "alice") 17 | .expect("failed to store username"); 18 | 19 | // alternatively, using the slightly less efficient generic api: 20 | pkv.set("username", &"alice".to_string()) 21 | .expect("failed to store username"); 22 | } 23 | 24 | // serde types 25 | if let Ok(user) = pkv.get::("user") { 26 | info!("Welcome back {}", user.name); 27 | } else { 28 | info!("First time user, setting user to 'bob'"); 29 | let user = User { 30 | name: "bob".to_string(), 31 | }; 32 | pkv.set("user", &user).expect("failed to store User struct"); 33 | } 34 | } 35 | 36 | fn main() { 37 | // When building for WASM, print panics to the browser console 38 | #[cfg(target_arch = "wasm32")] 39 | console_error_panic_hook::set_once(); 40 | 41 | App::new() 42 | .insert_resource(PkvStore::new("BevyPkv", "BasicExample")) 43 | .add_plugins(MinimalPlugins) 44 | .add_plugins(LogPlugin::default()) 45 | .add_systems(Startup, setup) 46 | .run(); 47 | } 48 | -------------------------------------------------------------------------------- /examples/enumkeys.rs: -------------------------------------------------------------------------------- 1 | use bevy::{log::LogPlugin, prelude::*}; 2 | use bevy_pkv::PkvStore; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize)] 6 | struct User { 7 | name: String, 8 | } 9 | 10 | fn setup(mut pkv: ResMut) { 11 | // strings 12 | if let Ok(username) = pkv.get::(PkvKeys::UserName) { 13 | info!("Welcome back {username}"); 14 | } else { 15 | info!("First time user, setting username to 'alice'"); 16 | pkv.set_string(PkvKeys::UserName, "alice") 17 | .expect("failed to store username"); 18 | 19 | // alternatively, using the slightly less efficient generic api: 20 | pkv.set(PkvKeys::UserName, &"alice".to_string()) 21 | .expect("failed to store username"); 22 | } 23 | 24 | // serde types 25 | if let Ok(user) = pkv.get::(PkvKeys::User) { 26 | info!("Welcome back {}", user.name); 27 | } else { 28 | info!("First time user, setting user to 'bob'"); 29 | let user = User { 30 | name: "bob".to_string(), 31 | }; 32 | pkv.set(PkvKeys::User, &user) 33 | .expect("failed to store User struct"); 34 | } 35 | } 36 | 37 | fn main() { 38 | // When building for WASM, print panics to the browser console 39 | #[cfg(target_arch = "wasm32")] 40 | console_error_panic_hook::set_once(); 41 | 42 | App::new() 43 | .insert_resource(PkvStore::new("BevyPkv", "EnumExample")) 44 | .add_plugins(MinimalPlugins) 45 | .add_plugins(LogPlugin::default()) 46 | .add_systems(Startup, setup) 47 | .run(); 48 | } 49 | 50 | #[derive(strum_macros::AsRefStr)] 51 | enum PkvKeys { 52 | User, 53 | UserName, 54 | } 55 | -------------------------------------------------------------------------------- /examples/migration.rs: -------------------------------------------------------------------------------- 1 | //! this example just shows how you can use serde aliases if you rename fields 2 | //! 3 | //! Guess it's more like a serde crash course than an intro to this crate. 4 | //! 5 | //! And it's also a test to show that aliases do work 6 | 7 | use bevy::{log::LogPlugin, prelude::*}; 8 | use bevy_pkv::PkvStore; 9 | use serde::{Deserialize, Serialize}; 10 | 11 | #[derive(Serialize, Deserialize)] 12 | struct UserV1 { 13 | nick: String, 14 | favorite_color: String, 15 | } 16 | 17 | #[derive(Serialize, Deserialize)] 18 | struct UserV2 { 19 | #[serde(alias = "nick")] 20 | name: String, 21 | #[serde(default = "existing_user_default_quest")] 22 | quest: Quest, 23 | // notice we no longer care about favorite colors 24 | } 25 | 26 | fn existing_user_default_quest() -> Quest { 27 | // Assume existing users have already played the tutorial 28 | // and go straight for the holy grail 29 | Quest::SeekHolyGrail 30 | } 31 | 32 | #[derive(Serialize, Deserialize, Debug, Default)] 33 | enum Quest { 34 | #[default] 35 | Tutorial, 36 | SeekHolyGrail, 37 | } 38 | 39 | fn setup(mut pkv: ResMut) { 40 | // Let's pretend someone has registered with the UserV1 definition 41 | let user_v1 = UserV1 { 42 | nick: "old bob".to_string(), 43 | favorite_color: "beige".to_string(), 44 | }; 45 | pkv.set("user", &user_v1) 46 | .expect("failed to store User struct"); 47 | 48 | // When we serialize with the updated struct with the serde annotations, 49 | // the renamed fields work, and the new fields are assigned values accordingly 50 | let user_v2 = pkv.get::("user").unwrap(); 51 | info!("Welcome back {}", user_v2.name); 52 | info!("Current quest: {:?}", user_v2.quest); 53 | } 54 | 55 | fn main() { 56 | // When building for WASM, print panics to the browser console 57 | #[cfg(target_arch = "wasm32")] 58 | console_error_panic_hook::set_once(); 59 | 60 | App::new() 61 | .insert_resource(PkvStore::new("BevyPkv", "MigrationExample")) 62 | .add_plugins(MinimalPlugins) 63 | .add_plugins(LogPlugin::default()) 64 | .add_systems(Startup, setup) 65 | .run(); 66 | } 67 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_pkv" 3 | version = "0.14.0" 4 | edition = "2021" 5 | license = "MIT OR Apache-2.0" 6 | authors = ["Johan Helsing "] 7 | description = "Persistent key value store for apps/games" 8 | keywords = ["gamedev", "key-value-store", "bevy"] 9 | categories = ["game-development"] 10 | repository = "https://github.com/johanhelsing/bevy_pkv" 11 | 12 | [dependencies] 13 | thiserror = "2" 14 | serde = { version = "1", features = ["derive"] } 15 | bevy_ecs = { version = "0.17", optional = true } # we need for deriving Resource in PkvStore 16 | bevy_app = { version = "0.17", optional = true } # we need for Plugin trait and App type 17 | 18 | [features] 19 | default = ["bevy", "redb"] 20 | bevy = ["dep:bevy_ecs", "dep:bevy_app"] 21 | rocksdb = ["dep:rocksdb"] 22 | sled = ["dep:sled"] 23 | redb = ["dep:redb"] 24 | 25 | [target.'cfg(target_arch = "wasm32")'.dependencies] 26 | web-sys = { version = "0.3", default-features = false, features = [ 27 | "Storage", 28 | "Window", 29 | ] } 30 | wasm-bindgen = { version = "0.2", default-features = false } 31 | serde_json = "1" 32 | 33 | [target.'cfg(not(target_arch = "wasm32"))'.dependencies] 34 | rocksdb = { version = "0.24", optional = true } 35 | sled = { version = "0.34", optional = true } 36 | rmp-serde = "1.1" 37 | directories = "6.0" 38 | redb = { version = "3.1", optional = true } 39 | 40 | [dev-dependencies] 41 | bevy = { version = "0.17", default-features = false, features = ["bevy_log"]} 42 | strum_macros = "0.27" 43 | tempfile = "3" 44 | 45 | [build-dependencies] 46 | cfg_aliases = "0.2" 47 | 48 | [target.'cfg(target_arch = "wasm32")'.dev-dependencies] 49 | console_error_panic_hook = "0.1" 50 | easybench-wasm = "0.2" 51 | web-sys = { version = "0.3", default-features = false, features = ["console"] } 52 | 53 | [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] 54 | criterion = { version = "0.7", features = ["html_reports"] } 55 | 56 | [[bench]] 57 | name = "inserts" 58 | harness = false 59 | 60 | [[example]] 61 | name = "basic" 62 | required-features = ["bevy"] 63 | 64 | [[example]] 65 | name = "migration" 66 | required-features = ["bevy"] 67 | 68 | [[example]] 69 | name = "enumkeys" 70 | required-features = ["bevy"] 71 | 72 | [[example]] 73 | name = "persistent_resource" 74 | required-features = ["bevy"] 75 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | pull_request: 6 | 7 | name: ci 8 | 9 | jobs: 10 | check-default: 11 | name: Check default (redb) 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v6 15 | - uses: dtolnay/rust-toolchain@stable 16 | - uses: Swatinem/rust-cache@v2 17 | - run: cargo check --all-targets 18 | 19 | check-rocksdb: 20 | name: Check rocksdb 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v6 24 | - uses: dtolnay/rust-toolchain@stable 25 | - uses: Swatinem/rust-cache@v2 26 | - run: cargo check --all-targets --no-default-features --features rocksdb 27 | 28 | check-redb: 29 | name: Check sled 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v6 33 | - uses: dtolnay/rust-toolchain@stable 34 | - uses: Swatinem/rust-cache@v2 35 | - run: cargo check --all-targets --no-default-features --features sled 36 | 37 | check-wasm: 38 | name: Check wasm 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v6 42 | - name: Install wasm toolchain 43 | uses: dtolnay/rust-toolchain@stable 44 | with: 45 | toolchain: stable 46 | target: wasm32-unknown-unknown 47 | - uses: Swatinem/rust-cache@v2 48 | - run: RUSTFLAGS='--cfg=getrandom_backend="wasm_js"' cargo check --target wasm32-unknown-unknown --all-targets 49 | 50 | test-default: 51 | name: Test default (redb) 52 | runs-on: ubuntu-latest 53 | steps: 54 | - uses: actions/checkout@v6 55 | - uses: dtolnay/rust-toolchain@stable 56 | - uses: Swatinem/rust-cache@v2 57 | - run: cargo test 58 | 59 | test-rocksdb: 60 | name: Test rocksdb 61 | runs-on: ubuntu-latest 62 | steps: 63 | - uses: actions/checkout@v6 64 | - uses: dtolnay/rust-toolchain@stable 65 | - uses: Swatinem/rust-cache@v2 66 | - run: cargo test --no-default-features --features rocksdb 67 | 68 | test-sled: 69 | name: Test sled 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v6 73 | - uses: dtolnay/rust-toolchain@stable 74 | - uses: Swatinem/rust-cache@v2 75 | - run: cargo test --no-default-features --features sled 76 | 77 | lints: 78 | name: Lints 79 | runs-on: ubuntu-latest 80 | steps: 81 | - uses: actions/checkout@v6 82 | - name: Install stable toolchain 83 | uses: dtolnay/rust-toolchain@stable 84 | with: 85 | components: rustfmt, clippy 86 | - uses: Swatinem/rust-cache@v2 87 | - run: cargo fmt --all -- --check 88 | - run: cargo clippy -- -D warnings 89 | -------------------------------------------------------------------------------- /benches/inserts.rs: -------------------------------------------------------------------------------- 1 | use bevy_pkv::PkvStore; 2 | 3 | fn insert_values(store: &mut PkvStore, pairs: &Vec<(String, String)>) { 4 | for (key, value) in pairs { 5 | store.set::(key, value).unwrap(); 6 | } 7 | } 8 | 9 | #[cfg(not(target_arch = "wasm32"))] 10 | mod native { 11 | use super::insert_values; 12 | use bevy_pkv::PkvStore; 13 | use criterion::{criterion_group, BatchSize, Criterion}; 14 | 15 | fn insert_benchmark(c: &mut Criterion) { 16 | c.bench_function("insert 100", |b| { 17 | b.iter_batched( 18 | || { 19 | let store = PkvStore::new("BevyPkv", "InsertBench"); 20 | let values = (0..100).map(|i| (i.to_string(), i.to_string())).collect(); 21 | // todo: clear the store here 22 | (store, values) 23 | }, 24 | |(mut store, pairs)| insert_values(&mut store, &pairs), 25 | BatchSize::PerIteration, 26 | ) 27 | }); 28 | } 29 | 30 | criterion_group!(benches, insert_benchmark); 31 | } 32 | 33 | #[cfg(target_arch = "wasm32")] 34 | mod wasm { 35 | use super::insert_values; 36 | use bevy_pkv::PkvStore; 37 | use easybench_wasm::bench_env; 38 | use web_sys::console; 39 | 40 | // Easy bench clones the environment, but store doesn't implement clone 41 | struct HackStore(PkvStore); 42 | 43 | impl Clone for HackStore { 44 | fn clone(&self) -> Self { 45 | let store = PkvStore::new("BevyPkv", "InsertBench"); 46 | // todo: clear store as well 47 | Self(store) 48 | } 49 | } 50 | 51 | pub fn main() { 52 | console::log_1(&"Hi".to_string().into()); 53 | { 54 | let values: Vec<(String, String)> = 55 | (0..100).map(|i| (i.to_string(), i.to_string())).collect(); 56 | let env = (HackStore(PkvStore::new("BevyPkv", "InsertBench")), values); 57 | console::log_1( 58 | &format!( 59 | "insert 100: {}", 60 | bench_env(env, |(store, values)| insert_values(&mut store.0, values)), 61 | ) 62 | .into(), 63 | ); 64 | } 65 | { 66 | let values: Vec<(String, String)> = 67 | (0..1000).map(|i| (i.to_string(), i.to_string())).collect(); 68 | let env = (HackStore(PkvStore::new("BevyPkv", "InsertBench")), values); 69 | console::log_1( 70 | &format!( 71 | "insert 1000: {}", 72 | bench_env(env, |(store, values)| insert_values(&mut store.0, values)), 73 | ) 74 | .into(), 75 | ); 76 | } 77 | } 78 | } 79 | 80 | #[cfg(target_arch = "wasm32")] 81 | fn main() { 82 | wasm::main() 83 | } 84 | 85 | #[cfg(not(target_arch = "wasm32"))] 86 | criterion::criterion_main!(native::benches); 87 | -------------------------------------------------------------------------------- /examples/persistent_resource.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use bevy::{log::LogPlugin, prelude::*, time::common_conditions::on_timer}; 4 | use bevy_pkv::{PersistentResourceAppExtensions, PkvStore}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | // Example 1: Resource with Default implementation 8 | // This will be automatically persisted and restored 9 | #[derive(Resource, Serialize, Deserialize, Default, Debug)] 10 | struct GameSettings { 11 | volume: f32, 12 | difficulty: u8, 13 | } 14 | 15 | // Example 2: Resource using a factory function with arguments 16 | // Useful when you need custom initialization logic or external data 17 | #[derive(Resource, Serialize, Deserialize, Debug)] 18 | struct PlayerProfile { 19 | name: String, 20 | play_count: u32, 21 | created_at: u64, 22 | } 23 | 24 | impl PlayerProfile { 25 | fn new(name: String) -> Self { 26 | Self { 27 | name, 28 | play_count: 0, 29 | created_at: std::time::SystemTime::now() 30 | .duration_since(std::time::SystemTime::UNIX_EPOCH) 31 | .unwrap() 32 | .as_secs(), 33 | } 34 | } 35 | } 36 | 37 | fn setup(settings: Res, profile: Res) { 38 | info!("=== Persistent Resource Example ==="); 39 | info!("Game Settings: {:?}", *settings); 40 | info!("Player Profile: {:?}", *profile); 41 | info!("Resources update every 3 seconds. Stop and restart to see persistence!"); 42 | } 43 | 44 | fn update_settings(mut settings: ResMut) { 45 | // Simulate user changing settings 46 | settings.volume = (settings.volume + 0.1).min(1.0); 47 | settings.difficulty = (settings.difficulty + 1).min(3); 48 | info!( 49 | "Updated settings: volume={:.1}, difficulty={}", 50 | settings.volume, settings.difficulty 51 | ); 52 | } 53 | 54 | fn update_profile(mut profile: ResMut) { 55 | // Simulate a play session - increment play count 56 | profile.play_count += 1; 57 | info!("Updated profile: play count is now {}", profile.play_count); 58 | } 59 | 60 | fn main() { 61 | // When building for WASM, print panics to the browser console 62 | #[cfg(target_arch = "wasm32")] 63 | console_error_panic_hook::set_once(); 64 | 65 | App::new() 66 | .insert_resource(PkvStore::new("BevyPkv", "PersistentResourceExample")) 67 | .add_plugins(MinimalPlugins) 68 | .add_plugins(LogPlugin::default()) 69 | // Method 1: Use Default implementation - automatically persisted 70 | .init_persistent_resource::() 71 | // Method 2: Use factory function with arguments - useful for custom initialization 72 | .init_persistent_resource_with(|| { 73 | let user_name = std::env::var("USERNAME") // Windows 74 | .or_else(|_| std::env::var("USER")) // Unix/Linux/macOS 75 | .unwrap_or_else(|_| "Player".to_string()); // Fallback 76 | PlayerProfile::new(user_name) 77 | }) 78 | .add_systems(PostStartup, setup) 79 | .add_systems( 80 | Update, 81 | // Update resources every 3 seconds to show persistence 82 | (update_settings, update_profile).run_if(on_timer(Duration::from_secs(3))), 83 | ) 84 | .run(); 85 | } 86 | -------------------------------------------------------------------------------- /src/sled_store.rs: -------------------------------------------------------------------------------- 1 | use crate::{Location, StoreImpl}; 2 | use serde::{de::DeserializeOwned, Serialize}; 3 | 4 | #[derive(Debug)] 5 | pub struct SledStore { 6 | db: sled::Db, 7 | } 8 | 9 | pub use SledStore as InnerStore; 10 | 11 | /// Errors that can occur during `PkvStore::get` 12 | #[derive(thiserror::Error, Debug)] 13 | pub enum GetError { 14 | /// An internal error from the sled crate 15 | #[error("Sled error")] 16 | Sled(#[from] sled::Error), 17 | /// Error when deserializing the value 18 | #[error("MessagePack deserialization error")] 19 | MessagePack(#[from] rmp_serde::decode::Error), 20 | /// The value for the given key was not found 21 | #[error("No value found for the given key")] 22 | NotFound, 23 | } 24 | 25 | /// Errors that can occur during `PkvStore::set` 26 | #[derive(thiserror::Error, Debug)] 27 | pub enum SetError { 28 | /// An internal error from the sled crate 29 | #[error("Sled error")] 30 | Sled(#[from] sled::Error), 31 | /// Error when serializing the value 32 | #[error("MessagePack serialization error")] 33 | MessagePack(#[from] rmp_serde::encode::Error), 34 | } 35 | 36 | /// Errors that can occur during `PkvStore::remove` 37 | #[derive(thiserror::Error, Debug)] 38 | pub enum RemoveError { 39 | /// An internal error from the sled crate 40 | #[error("Sled error")] 41 | Sled(#[from] sled::Error), 42 | /// Error when deserializing the value 43 | #[error("MessagePack deserialization error")] 44 | MessagePack(#[from] rmp_serde::decode::Error), 45 | /// The value for the given key was not found 46 | #[error("No value found for the given key")] 47 | NotFound, 48 | } 49 | 50 | impl SledStore { 51 | pub(crate) fn new(location: Location) -> Self { 52 | let db_path = location.get_path().join("bevy_pkv.sled"); 53 | let db = sled::open(db_path).expect("Failed to init key value store"); 54 | Self { db } 55 | } 56 | } 57 | 58 | impl StoreImpl for SledStore { 59 | type GetError = GetError; 60 | type SetError = SetError; 61 | type RemoveError = RemoveError; 62 | 63 | /// Serialize and store the value 64 | fn set(&mut self, key: &str, value: &T) -> Result<(), Self::SetError> { 65 | let mut serializer = rmp_serde::Serializer::new(Vec::new()).with_struct_map(); 66 | value.serialize(&mut serializer)?; 67 | self.db.insert(key, serializer.into_inner())?; 68 | self.db.flush()?; 69 | Ok(()) 70 | } 71 | 72 | /// More or less the same as set::, but can take a &str 73 | fn set_string(&mut self, key: &str, value: &str) -> Result<(), Self::SetError> { 74 | let bytes = rmp_serde::to_vec(value)?; 75 | self.db.insert(key, bytes)?; 76 | self.db.flush()?; 77 | Ok(()) 78 | } 79 | 80 | /// Get the value for the given key 81 | /// returns Err(GetError::NotFound) if the key does not exist in the key value store. 82 | fn get(&self, key: &str) -> Result { 83 | let bytes = self.db.get(key)?.ok_or(Self::GetError::NotFound)?; 84 | let value = rmp_serde::from_slice(&bytes)?; 85 | Ok(value) 86 | } 87 | 88 | /// Clear all keys and their values 89 | /// clear is also a kind of store so it will return SetError on failure 90 | fn clear(&mut self) -> Result<(), Self::SetError> { 91 | self.db.clear()?; 92 | self.db.flush()?; 93 | Ok(()) 94 | } 95 | 96 | fn remove(&mut self, key: &str) -> Result<(), Self::RemoveError> { 97 | self.db.remove(key)?; 98 | Ok(()) 99 | } 100 | 101 | fn remove_and_get( 102 | &mut self, 103 | key: &str, 104 | ) -> Result, Self::RemoveError> { 105 | let bytes = self.db.remove(key)?.ok_or(Self::RemoveError::NotFound)?; 106 | let value = rmp_serde::from_slice(&bytes)?; 107 | Ok(value) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_pkv 2 | 3 | [![crates.io](https://img.shields.io/crates/v/bevy_pkv.svg)](https://crates.io/crates/bevy_pkv) 4 | ![MIT/Apache 2.0](https://img.shields.io/badge/license-MIT%2FApache-blue.svg) 5 | [![docs.rs](https://img.shields.io/docsrs/bevy_pkv)](https://docs.rs/bevy_pkv) 6 | [![ci](https://github.com/johanhelsing/bevy_pkv/actions/workflows/ci.yml/badge.svg)](https://github.com/johanhelsing/bevy_pkv/actions/workflows/ci.yml) 7 | 8 | `bevy_pkv` is a cross-platform persistent key value store for rust apps. 9 | 10 | Use it for storing things like settings, save games etc. 11 | 12 | Currently, the Bevy dependency is optional, so it may be used in other games/apps as well. 13 | 14 | ## Usage with Bevy 15 | 16 | Add a store resource to your app 17 | 18 | ```rust no_run 19 | # #[cfg(feature = "bevy")] { // ignore this line 20 | use bevy::prelude::*; 21 | use bevy_pkv::PkvStore; 22 | 23 | fn main() { 24 | App::new() 25 | .add_plugins(DefaultPlugins) 26 | .insert_resource(PkvStore::new("FooCompany", "BarGame")) 27 | // ...insert systems etc. 28 | .run(); 29 | } 30 | # } 31 | ``` 32 | 33 | This will create or load a store in the appropriate location for your system, and make it available to bevy systems: 34 | 35 | ```rust ignore 36 | fn setup(mut pkv: ResMut) { 37 | if let Ok(username) = pkv.get::("username") { 38 | info!("Welcome back {username}"); 39 | } else { 40 | pkv.set_string("username", "alice") 41 | .expect("failed to store username"); 42 | 43 | // alternatively, using the slightly less efficient generic api: 44 | pkv.set("username", &"alice".to_string()) 45 | .expect("failed to store username"); 46 | } 47 | } 48 | ``` 49 | 50 | ### Using Custom Types 51 | 52 | You can also store and retrieve your own types that implement `serde::Serialize` and `Deserialize`: 53 | 54 | ```rust ignore 55 | #[derive(Serialize, Deserialize)] 56 | struct User { 57 | name: String, 58 | } 59 | 60 | fn setup(mut pkv: ResMut) { 61 | if let Ok(user) = pkv.get::("user") { 62 | info!("Welcome back {}", user.name); 63 | } else { 64 | let user = User { 65 | name: "bob".to_string(), 66 | }; 67 | pkv.set("user", &user).expect("failed to store user"); 68 | } 69 | } 70 | ``` 71 | 72 | ### Persistent Resources 73 | 74 | For resources that should automatically persist when they change, use the convenient `init_persistent_resource` API: 75 | 76 | ```rust ignore 77 | use bevy::prelude::*; 78 | use bevy_pkv::prelude::*; 79 | use serde::{Serialize, Deserialize}; 80 | 81 | #[derive(Resource, Serialize, Deserialize, Default)] 82 | struct GameSettings { 83 | volume: f32, 84 | difficulty: u8, 85 | } 86 | 87 | App::new() 88 | .add_plugins(DefaultPlugins) 89 | .insert_resource(PkvStore::new("FooCompany", "BarGame")) 90 | .init_persistent_resource::() 91 | .run(); 92 | ``` 93 | 94 | This automatically loads the resource from storage on startup and saves it whenever it changes! 95 | 96 | See the [examples](./examples) for further usage 97 | 98 | ## Usage without Bevy 99 | 100 | Disable the default features when adding the dependency: 101 | 102 | ```toml 103 | bevy_pkv = {version = "0.9", default-features = false} 104 | ``` 105 | 106 | ## Implementation details 107 | 108 | ### Native 109 | 110 | `redb` and `rmp_serde` (MessagePack) is used for storage. It's creating a `bevy_pkv.redb` db in the appropriate application data directory for your system. 111 | 112 | Alternatively, disable default-features and enable the `rocksdb` feature to use a RocksDB-based implementation or `sled` feature to use sled db. 113 | 114 | ### Wasm 115 | 116 | `Window.localStorage` and `serde_json` is used for storage. Perhaps IndexedDb and something else would have been a better choice, but its API is complicated, and I wanted a simple implementation and a simple synchronous API. 117 | 118 | ## Bevy version support 119 | 120 | The `main` branch targets the latest bevy release. 121 | 122 | |bevy|bevy\_pkv| 123 | |----|---| 124 | |0.17|0.14, main| 125 | |0.16|0.13| 126 | |0.15|0.12| 127 | |0.14|0.11| 128 | |0.13|0.10| 129 | |0.12|0.9| 130 | |0.11|0.8| 131 | |0.10|0.7| 132 | |0.9 |0.6| 133 | |0.8 |0.5| 134 | |0.7 |0.2, 0.3, 0.4| 135 | |0.6 |0.1| 136 | 137 | ## License 138 | 139 | MIT or Apache-2.0 140 | -------------------------------------------------------------------------------- /src/rocksdb_store.rs: -------------------------------------------------------------------------------- 1 | use crate::{Location, StoreImpl}; 2 | use serde::{de::DeserializeOwned, Serialize}; 3 | 4 | #[derive(Debug)] 5 | pub struct RocksDBStore { 6 | db: rocksdb::DB, 7 | } 8 | 9 | pub use RocksDBStore as InnerStore; 10 | 11 | /// Errors that can occur during `PkvStore::get` 12 | #[derive(thiserror::Error, Debug)] 13 | pub enum GetError { 14 | /// An internal error from the rocksdb crate 15 | #[error("Rocksdb error")] 16 | Rocksdb(#[from] rocksdb::Error), 17 | /// Error when deserializing the value 18 | #[error("MessagePack deserialization error")] 19 | MessagePack(#[from] rmp_serde::decode::Error), 20 | /// The value for the given key was not found 21 | #[error("No value found for the given key")] 22 | NotFound, 23 | } 24 | /// Errors that can occur during `PkvStore::set` 25 | #[derive(thiserror::Error, Debug)] 26 | pub enum SetError { 27 | /// An internal error from the rocksdb crate 28 | #[error("Rocksdb error")] 29 | Rocksdb(#[from] rocksdb::Error), 30 | /// Error when serializing the value 31 | #[error("MessagePack serialization error")] 32 | MessagePack(#[from] rmp_serde::encode::Error), 33 | } 34 | 35 | /// Errors that can occur during `PkvStore::remove` 36 | #[derive(thiserror::Error, Debug)] 37 | pub enum RemoveError { 38 | /// An internal error from the rocksdb crate 39 | #[error("Rocksdb error")] 40 | Rocksdb(#[from] rocksdb::Error), 41 | /// Error when deserializing the value 42 | #[error("MessagePack deserialization error")] 43 | MessagePack(#[from] rmp_serde::decode::Error), 44 | /// The value for the given key was not found 45 | #[error("No value found for the given key")] 46 | NotFound, 47 | } 48 | 49 | impl RocksDBStore { 50 | pub(crate) fn new(location: Location) -> Self { 51 | let mut options = rocksdb::Options::default(); 52 | options.set_error_if_exists(false); 53 | options.create_if_missing(true); 54 | options.create_missing_column_families(true); 55 | 56 | let db_path = location.get_path().join("bevy_rocksdb_pkv"); 57 | let db = rocksdb::DB::open(&options, db_path).expect("Failed to init key value store"); 58 | Self { db } 59 | } 60 | } 61 | 62 | impl StoreImpl for RocksDBStore { 63 | type GetError = GetError; 64 | type SetError = SetError; 65 | type RemoveError = RemoveError; 66 | 67 | /// Serialize and store the value 68 | fn set(&mut self, key: &str, value: &T) -> Result<(), Self::SetError> { 69 | let mut serializer = rmp_serde::Serializer::new(Vec::new()).with_struct_map(); 70 | value.serialize(&mut serializer)?; 71 | self.db.put(key, serializer.into_inner())?; 72 | 73 | Ok(()) 74 | } 75 | 76 | /// More or less the same as set::, but can take a &str 77 | fn set_string(&mut self, key: &str, value: &str) -> Result<(), Self::SetError> { 78 | let bytes = rmp_serde::to_vec(value)?; 79 | self.db.put(key, bytes)?; 80 | 81 | Ok(()) 82 | } 83 | 84 | /// Get the value for the given key 85 | /// returns Err(GetError::NotFound) if the key does not exist in the key value store. 86 | fn get(&self, key: &str) -> Result { 87 | let bytes = self.db.get(key)?.ok_or(Self::GetError::NotFound)?; 88 | let value = rmp_serde::from_slice(&bytes)?; 89 | Ok(value) 90 | } 91 | 92 | /// Clear all keys and their values 93 | /// The RocksDB adapter uses an iterator to achieve this, unlike sled 94 | fn clear(&mut self) -> Result<(), Self::SetError> { 95 | let kv_iter = self.db.iterator(rocksdb::IteratorMode::Start); 96 | 97 | for kv in kv_iter { 98 | let (key, _) = kv?; 99 | self.db.delete(key)?; 100 | } 101 | 102 | Ok(()) 103 | } 104 | 105 | fn remove(&mut self, key: &str) -> Result<(), Self::RemoveError> { 106 | self.db.delete(key)?; 107 | Ok(()) 108 | } 109 | 110 | fn remove_and_get( 111 | &mut self, 112 | key: &str, 113 | ) -> Result, Self::RemoveError> { 114 | let bytes = self.db.get(key)?.ok_or(Self::RemoveError::NotFound)?; 115 | let value = rmp_serde::from_slice(&bytes)?; 116 | self.db.delete(key)?; 117 | Ok(value) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/local_storage_store.rs: -------------------------------------------------------------------------------- 1 | use crate::{Location, PlatformDefault, StoreImpl}; 2 | 3 | #[derive(Debug, Default)] 4 | pub struct LocalStorageStore { 5 | prefix: String, 6 | } 7 | 8 | pub use LocalStorageStore as InnerStore; 9 | 10 | #[derive(thiserror::Error, Debug)] 11 | pub enum GetError { 12 | #[error("No value found for the given key")] 13 | NotFound, 14 | #[error("error deserializing json")] 15 | Json(#[from] serde_json::Error), 16 | #[error("JavaScript error from getItem")] 17 | GetItem(wasm_bindgen::JsValue), 18 | } 19 | 20 | #[derive(thiserror::Error, Debug)] 21 | pub enum SetError { 22 | #[error("JavaScript error from setItem")] 23 | SetItem(wasm_bindgen::JsValue), 24 | #[error("Error serializing as json")] 25 | Json(#[from] serde_json::Error), 26 | #[error("JavaScript error from clear")] 27 | Clear(wasm_bindgen::JsValue), 28 | } 29 | 30 | #[derive(thiserror::Error, Debug)] 31 | pub enum RemoveError { 32 | #[error("No value found for the given key")] 33 | NotFound, 34 | #[error("error deserializing json")] 35 | Json(#[from] serde_json::Error), 36 | #[error("JavaScript error from getItem")] 37 | GetItem(wasm_bindgen::JsValue), 38 | #[error("JavaScript error from clear")] 39 | Clear(wasm_bindgen::JsValue), 40 | } 41 | 42 | impl LocalStorageStore { 43 | fn storage(&self) -> web_sys::Storage { 44 | web_sys::window() 45 | .expect("No window") 46 | .local_storage() 47 | .expect("Failed to get local storage") 48 | .expect("No local storage") 49 | } 50 | 51 | pub(crate) fn new(constructor_bundle: Location) -> Self { 52 | let Location::PlatformDefault(config) = constructor_bundle; 53 | let PlatformDefault { 54 | qualifier, 55 | organization, 56 | application, 57 | } = config; 58 | Self { 59 | prefix: match qualifier.as_deref() { 60 | Some(qualifier) => format!("{qualifier}.{organization}.{application}"), 61 | None => format!("{organization}.{application}"), 62 | }, 63 | } 64 | } 65 | 66 | fn format_key(&self, key: &str) -> String { 67 | format!("{}{}", self.prefix, key) 68 | } 69 | } 70 | 71 | impl StoreImpl for LocalStorageStore { 72 | type GetError = GetError; 73 | type SetError = SetError; 74 | type RemoveError = RemoveError; 75 | 76 | fn set_string(&mut self, key: &str, value: &str) -> Result<(), SetError> { 77 | let json = serde_json::to_string(value)?; 78 | let storage = self.storage(); 79 | let key = self.format_key(key); 80 | storage.set_item(&key, &json).map_err(SetError::SetItem)?; 81 | Ok(()) 82 | } 83 | 84 | fn get(&self, key: &str) -> Result { 85 | let storage = self.storage(); 86 | let key = self.format_key(key); 87 | let entry = storage.get_item(&key).map_err(GetError::GetItem)?; 88 | let json = entry.as_ref().ok_or(GetError::NotFound)?; 89 | let value: T = serde_json::from_str(json)?; 90 | Ok(value) 91 | } 92 | 93 | fn set(&mut self, key: &str, value: &T) -> Result<(), SetError> { 94 | let json = serde_json::to_string(value)?; 95 | let storage = self.storage(); 96 | let key = self.format_key(key); 97 | storage.set_item(&key, &json).map_err(SetError::SetItem)?; 98 | Ok(()) 99 | } 100 | 101 | /// Because the data is cleared by looping through it, it may take time or run slowly 102 | fn clear(&mut self) -> Result<(), SetError> { 103 | let storage = self.storage(); 104 | let length = storage.length().map_err(SetError::Clear)?; 105 | let prefix = &self.prefix; 106 | for index in (0..length).rev() { 107 | if let Some(key) = storage.key(index).map_err(SetError::Clear)? { 108 | if key.starts_with(prefix) { 109 | storage.remove_item(&key).map_err(SetError::Clear)?; 110 | } 111 | } 112 | } 113 | Ok(()) 114 | } 115 | 116 | fn remove(&mut self, key: &str) -> Result<(), Self::RemoveError> { 117 | let storage = self.storage(); 118 | let key = self.format_key(key); 119 | storage.remove_item(&key).map_err(RemoveError::Clear)?; 120 | Ok(()) 121 | } 122 | 123 | fn remove_and_get( 124 | &mut self, 125 | key: &str, 126 | ) -> Result, Self::RemoveError> { 127 | let previous_value = self.get(key).map_err(|_| RemoveError::NotFound)?; 128 | self.remove(key)?; 129 | Ok(Some(previous_value)) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/redb_store.rs: -------------------------------------------------------------------------------- 1 | use crate::{Location, StoreImpl}; 2 | use redb::{Database, ReadableDatabase, TableDefinition}; 3 | use serde::{de::DeserializeOwned, Serialize}; 4 | use std::fmt::{Debug, Formatter}; 5 | pub struct ReDbStore { 6 | db: Database, 7 | } 8 | impl Debug for ReDbStore { 9 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { 10 | writeln!(f, "ReDb")?; 11 | Ok(()) 12 | } 13 | } 14 | pub use ReDbStore as InnerStore; 15 | 16 | /// Errors that can occur during `PkvStore::get` 17 | #[derive(thiserror::Error, Debug)] 18 | pub enum GetError { 19 | /// An internal storage error from the `redb` crate 20 | #[error("ReDbStorageError error")] 21 | ReDbStorageError(#[from] redb::StorageError), 22 | /// An internal transaction error from the `redb` crate 23 | #[error("ReDbTransactionError error")] 24 | ReDbTransactionError(#[from] redb::TransactionError), 25 | /// An internal table error from the `redb` crate 26 | #[error("ReDbTableError error")] 27 | ReDbTableError(#[from] redb::TableError), 28 | /// The value for the given key was not found 29 | #[error("No value found for the given key")] 30 | NotFound, 31 | /// Error when deserializing the value 32 | #[error("MessagePack deserialization error")] 33 | MessagePack(#[from] rmp_serde::decode::Error), 34 | } 35 | 36 | /// Errors that can occur during `PkvStore::` 37 | #[derive(thiserror::Error, Debug)] 38 | pub enum RemoveError { 39 | /// An internal commit error from the `redb` crate 40 | #[error("ReDbCommitError error")] 41 | ReDbCommitError(#[from] redb::CommitError), 42 | /// An internal storage error from the `redb` crate 43 | #[error("ReDbStorageError error")] 44 | ReDbStorageError(#[from] redb::StorageError), 45 | /// An internal transaction error from the `redb` crate 46 | #[error("ReDbTransactionError error")] 47 | ReDbTransactionError(#[from] redb::TransactionError), 48 | /// An internal table error from the `redb` crate 49 | #[error("ReDbTableError error")] 50 | ReDbTableError(#[from] redb::TableError), 51 | /// Error when serializing the value 52 | #[error("MessagePack serialization error")] 53 | MessagePack(#[from] rmp_serde::encode::Error), 54 | } 55 | 56 | /// Errors that can occur during `PkvStore::set` 57 | #[derive(thiserror::Error, Debug)] 58 | pub enum SetError { 59 | /// An internal commit error from the `redb` crate 60 | #[error("ReDbCommitError error")] 61 | ReDbCommitError(#[from] redb::CommitError), 62 | /// An internal storage error from the `redb` crate 63 | #[error("ReDbStorageError error")] 64 | ReDbStorageError(#[from] redb::StorageError), 65 | /// An internal transaction error from the `redb` crate 66 | #[error("ReDbTransactionError error")] 67 | ReDbTransactionError(#[from] redb::TransactionError), 68 | /// An internal table error from the `redb` crate 69 | #[error("ReDbTableError error")] 70 | ReDbTableError(#[from] redb::TableError), 71 | /// Error when serializing the value 72 | #[error("MessagePack serialization error")] 73 | MessagePack(#[from] rmp_serde::encode::Error), 74 | } 75 | 76 | impl ReDbStore { 77 | pub(crate) fn new(location: Location) -> Self { 78 | let dir_path = location.get_path(); 79 | std::fs::create_dir_all(&dir_path) 80 | .expect("Failed to create directory to init key value store"); 81 | let db_path = dir_path.join("bevy_pkv.redb"); 82 | let db = Database::create(db_path).expect("Failed to init key value store"); 83 | 84 | let write_txn = db.begin_write().unwrap(); 85 | write_txn.open_table(TABLE).unwrap(); 86 | write_txn.commit().unwrap(); 87 | 88 | Self { db } 89 | } 90 | } 91 | 92 | const TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("redb"); 93 | 94 | impl StoreImpl for ReDbStore { 95 | type GetError = GetError; 96 | type SetError = SetError; 97 | type RemoveError = RemoveError; 98 | 99 | /// More or less the same as set::, but can take a &str 100 | fn set_string(&mut self, key: &str, value: &str) -> Result<(), Self::SetError> { 101 | let bytes = rmp_serde::to_vec(value)?; 102 | let write_txn = self.db.begin_write()?; 103 | { 104 | let mut table = write_txn.open_table(TABLE).unwrap(); 105 | table.insert(key, bytes.as_slice())?; 106 | } 107 | write_txn.commit()?; 108 | 109 | Ok(()) 110 | } 111 | 112 | /// Get the value for the given key 113 | /// returns Err(GetError::NotFound) if the key does not exist in the key value store. 114 | fn get(&self, key: &str) -> Result { 115 | let read_txn = self.db.begin_read()?; 116 | let table = read_txn.open_table(TABLE)?; 117 | let key = table.get(key)?.ok_or(Self::GetError::NotFound)?; 118 | let bytes = key.value(); 119 | let value = rmp_serde::from_slice(bytes)?; 120 | Ok(value) 121 | } 122 | 123 | /// Serialize and store the value 124 | fn set(&mut self, key: &str, value: &T) -> Result<(), Self::SetError> { 125 | let mut serializer = rmp_serde::Serializer::new(Vec::new()).with_struct_map(); 126 | value.serialize(&mut serializer)?; 127 | let write_txn = self.db.begin_write()?; 128 | { 129 | let mut table = write_txn.open_table(TABLE).unwrap(); 130 | table.insert(key, serializer.into_inner().as_slice())?; 131 | } 132 | write_txn.commit()?; 133 | 134 | Ok(()) 135 | } 136 | 137 | fn remove_and_get( 138 | &mut self, 139 | key: &str, 140 | ) -> Result, Self::RemoveError> { 141 | let value: Option; 142 | let write_txn = self.db.begin_write()?; 143 | { 144 | let mut table = write_txn.open_table(TABLE).unwrap(); 145 | value = match table.remove(key)? { 146 | Some(kv) => rmp_serde::from_slice(kv.value()).ok(), 147 | None => None, 148 | }; 149 | } 150 | write_txn.commit()?; 151 | 152 | Ok(value) 153 | } 154 | 155 | fn remove(&mut self, key: &str) -> Result<(), Self::RemoveError> { 156 | let write_txn = self.db.begin_write()?; 157 | { 158 | let mut table = write_txn.open_table(TABLE).unwrap(); 159 | table.remove(key)?; 160 | } 161 | write_txn.commit()?; 162 | 163 | Ok(()) 164 | } 165 | 166 | /// Clear all keys and their values 167 | fn clear(&mut self) -> Result<(), Self::SetError> { 168 | let write_txn = self.db.begin_write()?; 169 | write_txn.delete_table(TABLE)?; 170 | write_txn.commit()?; 171 | Ok(()) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/persistent_resource.rs: -------------------------------------------------------------------------------- 1 | //! Plugin for automatically persisting resources when they change 2 | 3 | use serde::{de::DeserializeOwned, Serialize}; 4 | 5 | use bevy_app::{App, Plugin, PostUpdate}; 6 | use bevy_ecs::prelude::*; 7 | 8 | use crate::PkvStore; 9 | 10 | /// A plugin that automatically persists a resource when it changes using a [`PkvStore`] 11 | /// 12 | /// This plugin will: 13 | /// - Load the resource from persistent storage on startup (if it exists) 14 | /// - Save the resource to persistent storage whenever it changes (runs in [`PostUpdate`]) 15 | /// - Use the type name as the storage key automatically 16 | /// 17 | /// The save system runs in [`PostUpdate`] to ensure it captures all changes made during 18 | /// the frame by `PreUpdate`, `Update`, and other systems. 19 | /// 20 | /// # Convenience Methods 21 | /// 22 | /// For easier usage, consider using the [`PersistentResourceAppExtensions`] trait methods instead: 23 | /// - [`init_persistent_resource()`](PersistentResourceAppExtensions::init_persistent_resource) for types with `Default` 24 | /// - [`init_persistent_resource_with()`](PersistentResourceAppExtensions::init_persistent_resource_with) for custom factory functions 25 | /// 26 | /// # Example 27 | /// 28 | /// ```rust 29 | /// use bevy::prelude::*; 30 | /// use bevy_pkv::{PkvStore, PersistentResourcePlugin}; 31 | /// use serde::{Deserialize, Serialize}; 32 | /// 33 | /// #[derive(Resource, Serialize, Deserialize, Default)] 34 | /// struct GameSettings { 35 | /// volume: f32, 36 | /// difficulty: u8, 37 | /// } 38 | /// 39 | /// // For types that implement Default, you can use new(): 40 | /// App::new() 41 | /// .insert_resource(PkvStore::new("ExampleOrg", "ExampleApp")) 42 | /// .add_plugins(PersistentResourcePlugin::::new()) 43 | /// .run(); 44 | /// 45 | /// // Or with a custom factory function (works for any type, with or without Default): 46 | /// App::new() 47 | /// .insert_resource(PkvStore::new("ExampleOrg", "ExampleApp")) 48 | /// .add_plugins(PersistentResourcePlugin::::with_default(|| GameSettings { 49 | /// volume: 0.8, 50 | /// difficulty: 2, 51 | /// })) 52 | /// .run(); 53 | /// 54 | /// // For types without Default, you must use with_default(): 55 | /// #[derive(Resource, Serialize, Deserialize)] 56 | /// struct CustomSettings { 57 | /// name: String, 58 | /// } 59 | /// 60 | /// App::new() 61 | /// .insert_resource(PkvStore::new("ExampleOrg", "ExampleApp")) 62 | /// .add_plugins(PersistentResourcePlugin::::with_default(|| CustomSettings { 63 | /// name: "Player".to_string(), 64 | /// })) 65 | /// .run(); 66 | /// ``` 67 | pub struct PersistentResourcePlugin { 68 | _phantom: std::marker::PhantomData, 69 | factory: Option T + Send + Sync + 'static>>, 70 | } 71 | 72 | impl PersistentResourcePlugin 73 | where 74 | T: Resource + Serialize + DeserializeOwned + Default + Send + Sync + 'static, 75 | { 76 | /// Create a new [`PersistentResourcePlugin`] that uses the type name as the storage key 77 | /// and `T::default()` for creating default values 78 | /// 79 | /// # Convenience Alternative 80 | /// 81 | /// Consider using [`App::init_persistent_resource()`](PersistentResourceAppExtensions::init_persistent_resource) 82 | /// for a more concise API. 83 | pub fn new() -> Self { 84 | Self { 85 | _phantom: std::marker::PhantomData, 86 | factory: Some(Box::new(|| T::default())), 87 | } 88 | } 89 | } 90 | 91 | impl PersistentResourcePlugin 92 | where 93 | T: Resource + Serialize + DeserializeOwned + Send + Sync + 'static, 94 | { 95 | /// Create a new [`PersistentResourcePlugin`] with a custom factory function for default values 96 | /// 97 | /// # Convenience Alternative 98 | /// 99 | /// Consider using [`App::init_persistent_resource_with()`](PersistentResourceAppExtensions::init_persistent_resource_with) 100 | /// for a more concise API. 101 | pub fn with_default(factory: F) -> Self 102 | where 103 | F: Fn() -> T + Send + Sync + 'static, 104 | { 105 | Self { 106 | _phantom: std::marker::PhantomData, 107 | factory: Some(Box::new(factory)), 108 | } 109 | } 110 | } 111 | 112 | impl Default for PersistentResourcePlugin 113 | where 114 | T: Resource + Serialize + DeserializeOwned + Default + Send + Sync + 'static, 115 | { 116 | fn default() -> Self { 117 | Self::new() 118 | } 119 | } 120 | 121 | impl Plugin for PersistentResourcePlugin 122 | where 123 | T: Resource + Serialize + DeserializeOwned + Send + Sync + 'static, 124 | { 125 | fn build(&self, app: &mut App) { 126 | let key = std::any::type_name::(); 127 | let pkv = app.world_mut().resource_mut::(); 128 | let resource = pkv.get::(key).unwrap_or_else(|_| { 129 | // We always have a factory function now (either from with_default or new) 130 | self.factory 131 | .as_ref() 132 | .expect("PersistentResourcePlugin should always have a factory function")( 133 | ) 134 | }); 135 | app.insert_resource(resource); 136 | app.add_systems(PostUpdate, save_resource::.run_if(resource_changed::)); 137 | } 138 | } 139 | 140 | fn save_resource(resource: Res, mut pkv: ResMut) 141 | where 142 | T: Resource + Serialize + DeserializeOwned + Send + Sync + 'static, 143 | { 144 | let key = std::any::type_name::(); 145 | if let Err(e) = pkv.set(key, &*resource) { 146 | eprintln!("Failed to persist resource: {:?}", e); 147 | } 148 | } 149 | 150 | /// Extension trait for [`App`] to provide convenient methods for initializing persistent resources 151 | /// 152 | /// These methods provide a more ergonomic API compared to manually adding [`PersistentResourcePlugin`]. 153 | /// 154 | /// # See Also 155 | /// 156 | /// - [`PersistentResourcePlugin`] - The underlying plugin these methods use 157 | pub trait PersistentResourceAppExtensions { 158 | /// Initialize a persistent resource that implements [`Default`] 159 | /// 160 | /// This is a convenience method equivalent to: 161 | /// ```rust,ignore 162 | /// app.add_plugins(PersistentResourcePlugin::::new()) 163 | /// ``` 164 | /// 165 | /// # Example 166 | /// ```rust,ignore 167 | /// use bevy::prelude::*; 168 | /// use bevy_pkv::{PkvStore, PersistentResourceAppExtensions}; 169 | /// use serde::{Deserialize, Serialize}; 170 | /// 171 | /// #[derive(Resource, Serialize, Deserialize, Default)] 172 | /// struct GameSettings { 173 | /// volume: f32, 174 | /// difficulty: u8, 175 | /// } 176 | /// 177 | /// App::new() 178 | /// .insert_resource(PkvStore::new("MyGame", "Settings")) 179 | /// .init_persistent_resource::() 180 | /// .run(); 181 | /// ``` 182 | /// 183 | /// # See Also 184 | /// 185 | /// - [`PersistentResourcePlugin::new()`] - The equivalent plugin method 186 | fn init_persistent_resource(&mut self) -> &mut Self 187 | where 188 | T: Resource + Serialize + DeserializeOwned + Default + Send + Sync + 'static; 189 | 190 | /// Initialize a persistent resource with a custom default factory function 191 | /// 192 | /// This is a convenience method equivalent to: 193 | /// ```rust,ignore 194 | /// app.add_plugins(PersistentResourcePlugin::::with_default(factory)) 195 | /// ``` 196 | /// 197 | /// # Example 198 | /// ```rust,ignore 199 | /// use bevy::prelude::*; 200 | /// use bevy_pkv::{PkvStore, PersistentResourceAppExtensions}; 201 | /// use serde::{Deserialize, Serialize}; 202 | /// 203 | /// #[derive(Resource, Serialize, Deserialize)] 204 | /// struct GameSettings { 205 | /// volume: f32, 206 | /// difficulty: u8, 207 | /// } 208 | /// 209 | /// App::new() 210 | /// .insert_resource(PkvStore::new("MyGame", "Settings")) 211 | /// .init_persistent_resource_with(|| GameSettings { volume: 0.8, difficulty: 2 }) 212 | /// .run(); 213 | /// ``` 214 | /// 215 | /// # See Also 216 | /// 217 | /// - [`PersistentResourcePlugin::with_default()`] - The equivalent plugin method 218 | fn init_persistent_resource_with(&mut self, factory: F) -> &mut Self 219 | where 220 | T: Resource + Serialize + DeserializeOwned + Send + Sync + 'static, 221 | F: Fn() -> T + Send + Sync + 'static; 222 | } 223 | 224 | impl PersistentResourceAppExtensions for App { 225 | fn init_persistent_resource(&mut self) -> &mut Self 226 | where 227 | T: Resource + Serialize + DeserializeOwned + Default + Send + Sync + 'static, 228 | { 229 | self.add_plugins(PersistentResourcePlugin::::new()) 230 | } 231 | 232 | fn init_persistent_resource_with(&mut self, factory: F) -> &mut Self 233 | where 234 | T: Resource + Serialize + DeserializeOwned + Send + Sync + 'static, 235 | F: Fn() -> T + Send + Sync + 'static, 236 | { 237 | self.add_plugins(PersistentResourcePlugin::::with_default(factory)) 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | #![doc = include_str!("../README.md")] 3 | 4 | #[cfg(all(rocksdb_backend, sled_backend, redb_backend))] 5 | compile_error!( 6 | "the \"rocksdb\", \"redb\" and \"sled\" features may not be enabled at the same time" 7 | ); 8 | 9 | #[cfg(not(any(rocksdb_backend, sled_backend, redb_backend, wasm)))] 10 | compile_error!("either the \"rocksdb\", \"redb\" or \"sled\" feature must be enabled on native"); 11 | 12 | use serde::{de::DeserializeOwned, Serialize}; 13 | 14 | #[cfg(feature = "bevy")] 15 | mod persistent_resource; 16 | 17 | #[cfg(feature = "bevy")] 18 | pub use persistent_resource::{PersistentResourceAppExtensions, PersistentResourcePlugin}; 19 | 20 | pub mod prelude; 21 | 22 | trait StoreImpl { 23 | type GetError; 24 | type SetError; 25 | type RemoveError; 26 | 27 | fn set_string(&mut self, key: &str, value: &str) -> Result<(), Self::SetError> { 28 | self.set(key, &value.to_string()) 29 | } 30 | fn get(&self, key: &str) -> Result; 31 | fn set(&mut self, key: &str, value: &T) -> Result<(), Self::SetError>; 32 | fn remove(&mut self, key: &str) -> Result<(), Self::RemoveError>; 33 | fn remove_and_get( 34 | &mut self, 35 | key: &str, 36 | ) -> Result, Self::RemoveError>; 37 | fn clear(&mut self) -> Result<(), Self::SetError>; 38 | } 39 | 40 | #[cfg(wasm)] 41 | mod local_storage_store; 42 | 43 | #[cfg(wasm)] 44 | use local_storage_store::{self as backend}; 45 | 46 | #[cfg(sled_backend)] 47 | mod sled_store; 48 | 49 | #[cfg(sled_backend)] 50 | use sled_store::{self as backend}; 51 | 52 | #[cfg(rocksdb_backend)] 53 | mod rocksdb_store; 54 | 55 | #[cfg(rocksdb_backend)] 56 | use rocksdb_store::{self as backend}; 57 | 58 | // todo: Look into unifying these types? 59 | pub use backend::{GetError, RemoveError, SetError}; 60 | 61 | enum Location<'a> { 62 | PlatformDefault(&'a PlatformDefault), 63 | #[cfg(any(sled_backend, rocksdb_backend, redb_backend))] 64 | CustomPath(&'a std::path::Path), 65 | } 66 | 67 | #[cfg(redb_backend)] 68 | mod redb_store; 69 | 70 | #[cfg(redb_backend)] 71 | use redb_store::{self as backend}; 72 | 73 | #[cfg(any(sled_backend, rocksdb_backend, redb_backend))] 74 | mod path; 75 | 76 | /// Main resource for setting/getting values 77 | #[derive(Debug)] 78 | #[cfg_attr(feature = "bevy", derive(bevy_ecs::prelude::Resource))] 79 | pub struct PkvStore { 80 | inner: backend::InnerStore, 81 | } 82 | 83 | #[allow(clippy::result_large_err)] 84 | impl PkvStore { 85 | /// Creates or opens a persistent key value store 86 | /// 87 | /// The given `organization` and `application` are used to create a backing file 88 | /// in a corresponding location on the users device. Usually within the home or user folder 89 | pub fn new(organization: &str, application: &str) -> Self { 90 | let config = PlatformDefault { 91 | qualifier: None, 92 | organization: organization.to_string(), 93 | application: application.to_string(), 94 | }; 95 | Self::new_in_location(&config) 96 | } 97 | 98 | /// Creates or opens a persistent key value store 99 | /// 100 | /// Like [`PkvStore::new`], but also provide a qualifier. 101 | /// Some operating systems use the qualifier as part of the path to the store. 102 | /// The qualifier is usually "com", "org" etc. 103 | pub fn new_with_qualifier(qualifier: &str, organization: &str, application: &str) -> Self { 104 | let config = PlatformDefault { 105 | qualifier: Some(qualifier.to_string()), 106 | organization: organization.to_string(), 107 | application: application.to_string(), 108 | }; 109 | Self::new_in_location(&config) 110 | } 111 | 112 | /// Creates or opens a persistent key value store 113 | /// 114 | /// Like [`PkvStore::new`], but requires a direct path. 115 | /// The `path` is used to create a backing file 116 | /// in a corresponding location on the users device. 117 | #[cfg(any(sled_backend, rocksdb_backend, redb_backend))] 118 | pub fn new_in_dir>(path: P) -> Self { 119 | let inner = backend::InnerStore::new(Location::CustomPath(path.as_ref())); 120 | Self { inner } 121 | } 122 | 123 | fn new_in_location(config: &PlatformDefault) -> Self { 124 | let inner = backend::InnerStore::new(Location::PlatformDefault(config)); 125 | Self { inner } 126 | } 127 | 128 | /// Serialize and store the value 129 | pub fn set(&mut self, key: impl AsRef, value: &T) -> Result<(), SetError> { 130 | self.inner.set(key.as_ref(), value) 131 | } 132 | 133 | /// More or less the same as set::, but can take a &str 134 | pub fn set_string(&mut self, key: impl AsRef, value: &str) -> Result<(), SetError> { 135 | self.inner.set_string(key.as_ref(), value) 136 | } 137 | 138 | /// Get the value for the given key 139 | /// returns Err(GetError::NotFound) if the key does not exist in the key value store. 140 | pub fn get(&self, key: impl AsRef) -> Result { 141 | self.inner.get(key.as_ref()) 142 | } 143 | /// Remove the value from the store for the given key 144 | /// returns the removed value if one existed 145 | pub fn remove_and_get( 146 | &mut self, 147 | key: impl AsRef, 148 | ) -> Result, RemoveError> { 149 | self.inner.remove_and_get(key.as_ref()) 150 | } 151 | 152 | /// Remove the value from the store for the given key 153 | pub fn remove(&mut self, key: impl AsRef) -> Result<(), RemoveError> { 154 | self.inner.remove(key.as_ref()) 155 | } 156 | 157 | /// Clear all key values data 158 | /// returns Err(SetError) if clear error 159 | pub fn clear(&mut self) -> Result<(), SetError> { 160 | self.inner.clear() 161 | } 162 | } 163 | 164 | struct PlatformDefault { 165 | qualifier: Option, 166 | organization: String, 167 | application: String, 168 | } 169 | 170 | #[cfg(test)] 171 | mod tests { 172 | use crate::PkvStore; 173 | use serde::{Deserialize, Serialize}; 174 | 175 | // note: These tests use the real deal. Might be a good idea to clean the BevyPkv folder in .local/share 176 | // to get fresh tests. 177 | 178 | fn setup() { 179 | // When building for WASM, print panics to the browser console 180 | #[cfg(target_arch = "wasm32")] 181 | console_error_panic_hook::set_once(); 182 | } 183 | 184 | #[test] 185 | fn set_string() { 186 | setup(); 187 | let mut store = PkvStore::new("BevyPkv", "test_set_string"); 188 | store.set_string("hello", "goodbye").unwrap(); 189 | let ret = store.get::("hello"); 190 | assert_eq!(ret.unwrap(), "goodbye"); 191 | } 192 | 193 | #[cfg(any(sled_backend, rocksdb_backend, redb_backend))] 194 | #[test] 195 | fn new_in_dir() { 196 | setup(); 197 | 198 | let dirs = directories::ProjectDirs::from("", "BevyPkv", "test_new_in_dir"); 199 | let parent_dir = match dirs.as_ref() { 200 | Some(dirs) => dirs.data_dir(), 201 | None => std::path::Path::new("."), // todo: maybe warn? 202 | }; 203 | 204 | let mut store = PkvStore::new_in_dir(parent_dir); 205 | 206 | store 207 | .set_string("hello_custom_path", "goodbye_custom_path") 208 | .unwrap(); 209 | let ret = store.get::("hello_custom_path"); 210 | assert_eq!(ret.unwrap(), "goodbye_custom_path"); 211 | } 212 | 213 | #[cfg(any(sled_backend, rocksdb_backend, redb_backend))] 214 | #[test] 215 | fn empty_db_not_found() { 216 | use crate::GetError; 217 | 218 | setup(); 219 | 220 | let dir = tempfile::tempdir().expect("failed to create temp dir"); 221 | let store = PkvStore::new_in_dir(dir.path()); 222 | 223 | let err = store.get::("not_there").unwrap_err(); 224 | 225 | // todo: Use assert_matches! when stable 226 | assert!(matches!(err, GetError::NotFound)); 227 | } 228 | 229 | #[test] 230 | fn clear() { 231 | setup(); 232 | let mut store = PkvStore::new("BevyPkv", "test_clear"); 233 | 234 | // More than 1 key-value pair was added to the test because the 235 | // RocksDB adapter uses an iterator in order to implement .clear() 236 | store.set_string("key1", "goodbye").unwrap(); 237 | store.set_string("key2", "see yeah!").unwrap(); 238 | 239 | let ret = store.get::("key1").unwrap(); 240 | let ret2 = store.get::("key2").unwrap(); 241 | 242 | assert_eq!(ret, "goodbye"); 243 | assert_eq!(ret2, "see yeah!"); 244 | 245 | store.clear().unwrap(); 246 | let ret = store.get::("key1").ok(); 247 | let ret2 = store.get::("key2").ok(); 248 | assert_eq!((ret, ret2), (None, None)) 249 | } 250 | 251 | #[derive(Serialize, Deserialize, PartialEq, Eq, Debug)] 252 | struct User { 253 | name: String, 254 | age: u8, 255 | } 256 | 257 | #[test] 258 | fn set() { 259 | setup(); 260 | let mut store = PkvStore::new("BevyPkv", "test_set"); 261 | let user = User { 262 | name: "alice".to_string(), 263 | age: 32, 264 | }; 265 | store.set("user", &user).unwrap(); 266 | assert_eq!(store.get::("user").unwrap(), user); 267 | } 268 | 269 | #[test] 270 | fn remove() { 271 | setup(); 272 | let mut store = PkvStore::new("BevyPkv", "test_remove"); 273 | let user = User { 274 | name: "alice".to_string(), 275 | age: 32, 276 | }; 277 | store.set("user", &user).unwrap(); 278 | store.remove("user").unwrap(); 279 | assert_eq!(store.get::("user").ok(), None); 280 | } 281 | 282 | #[test] 283 | fn remove_and_get() { 284 | setup(); 285 | let mut store = PkvStore::new("BevyPkv", "test_remove_and_get"); 286 | let user = User { 287 | name: "alice".to_string(), 288 | age: 32, 289 | }; 290 | store.set("user", &user).unwrap(); 291 | let removed_user = store.remove_and_get::("user").unwrap().unwrap(); 292 | assert_eq!(user, removed_user); 293 | assert_eq!(store.get::("user").ok(), None); 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------