From 3f9c9f992cfad49f3d114fc9f22610bf3640b3d2 Mon Sep 17 00:00:00 2001 From: Ryan McGrath Date: Mon, 30 Mar 2020 20:23:54 -0700 Subject: [PATCH] Almost done with a proper NSUserDefaults wrapper... --- examples/defaults.rs | 24 ++-- src/defaults.rs | 123 ------------------- src/defaults/mod.rs | 223 +++++++++++++++++++++++++++++++++++ src/defaults/value.rs | 148 +++++++++++++++++++++++ src/foundation/dictionary.rs | 27 ++++- 5 files changed, 409 insertions(+), 136 deletions(-) delete mode 100644 src/defaults.rs create mode 100644 src/defaults/mod.rs create mode 100644 src/defaults/value.rs diff --git a/examples/defaults.rs b/examples/defaults.rs index 2a49cf7..d3896ee 100644 --- a/examples/defaults.rs +++ b/examples/defaults.rs @@ -1,7 +1,9 @@ //! This tests the `defaults` module to ensure things behave as they should. +use std::collections::HashMap; + use cacao::macos::app::{App, AppDelegate}; -use cacao::defaults::UserDefaults; +use cacao::defaults::{UserDefaults, DefaultValue}; #[derive(Default)] struct DefaultsTest; @@ -10,16 +12,18 @@ impl AppDelegate for DefaultsTest { fn did_finish_launching(&self) { let mut defaults = UserDefaults::standard(); - match defaults.get_string("LOL") { - Some(s) => { - println!("Retrieved {}", s); - }, + defaults.register({ + let mut map = HashMap::new(); + map.insert("LOL", DefaultValue::string("laugh")); + map.insert("X", DefaultValue::Integer(1)); + map.insert("X2", DefaultValue::Float(1.0)); + map.insert("BOOL", DefaultValue::bool(true)); + map + }); - None => { - defaults.set_string("LOL", "laugh"); - println!("Run this again to get a laugh"); - } - } + println!("Retrieved LOL: {:?}", defaults.get("LOL")); + println!("Retrieved LOL: {:?}", defaults.get("X")); + println!("Retrieved LOL: {:?}", defaults.get("X2")); App::terminate(); } diff --git a/src/defaults.rs b/src/defaults.rs deleted file mode 100644 index f529c4e..0000000 --- a/src/defaults.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! Wraps `NSUserDefaults`, providing an interface to store and query small amounts of data. -//! -//! It may seem a bit verbose at points, but it aims to implement everything on the Objective-C -//! side as closely as possible. - -use std::unreachable; - -use objc::{class, msg_send, sel, sel_impl}; -use objc::runtime::Object; -use objc_id::Id; - -use crate::foundation::{id, nil, YES, NO, BOOL, NSString}; - -/// Wraps and provides methods for interacting with `NSUserDefaults`, which can be used for storing -/// pieces of information (preferences, or _defaults_) to persist across application restores. -/// -/// This should not be used for sensitive data - use the Keychain for that. -#[derive(Debug)] -pub struct UserDefaults(pub Id); - -impl Default for UserDefaults { - /// Equivalent to calling `UserDefaults::standard()`. - fn default() -> Self { - UserDefaults::standard() - } -} - -impl UserDefaults { - /// Returns the `standardUserDefaults`, which is... exactly what it sounds like. - pub fn standard() -> Self { - UserDefaults(unsafe { - Id::from_ptr(msg_send![class!(NSUserDefaults), standardUserDefaults]) - }) - } - - /// Returns a new user defaults to work with. You probably don't want this, and either want - /// `suite()` or `standard()`. - pub fn new() -> Self { - UserDefaults(unsafe { - let alloc: id = msg_send![class!(NSUserDefaults), alloc]; - Id::from_ptr(msg_send![alloc, init]) - }) - } - - /// Returns a user defaults instance for the given suite name. You typically use this to share - /// preferences across apps and extensions. - pub fn suite(named: &str) -> Self { - let name = NSString::new(named); - - UserDefaults(unsafe { - let alloc: id = msg_send![class!(NSUserDefaults), alloc]; - Id::from_ptr(msg_send![alloc, initWithSuiteName:name.into_inner()]) - }) - } - - /// Remove the default associated with the key. If the key doesn't exist, this is a noop. - pub fn remove(&mut self, key: &str) { - let key = NSString::new(key); - - unsafe { - let _: () = msg_send![&*self.0, removeObjectForKey:key.into_inner()]; - } - } - - /// Returns a bool for the given key. If the key doesn't exist, it returns `false`. - /// - /// Note that behind the scenes, this will coerce certain "truthy" and "falsy" values - this is - /// done on the system side, and is not something that can be changed. - /// - /// e.g: - /// `"true"`, `"YES"`, `"1"`, `1`, `1.0` will become `true` - /// `"false"`, `"NO"`, `"0"`, `0`, `0.0` will become `false` - pub fn get_bool(&self, key: &str) -> bool { - let key = NSString::new(key); - - let result: BOOL = unsafe { - msg_send![&*self.0, boolForKey:key.into_inner()] - }; - - match result { - YES => true, - NO => false, - _ => unreachable!() - } - } - - /// Sets the bool for the given key to the specified value. - pub fn set_bool(&mut self, key: &str, value: bool) { - let key = NSString::new(key); - - unsafe { - let _: () = msg_send![&*self.0, setBool:match value { - true => YES, - false => NO - } forKey:key]; - } - } - - /// Returns the given String if it exists, mapping Objective-C's `nil` to `None`. - pub fn get_string(&self, key: &str) -> Option { - let key = NSString::new(key); - - let result: id = unsafe { - msg_send![&*self.0, stringForKey:key.into_inner()] - }; - - if result == nil { - None - } else { - Some(NSString::wrap(result).to_str().to_string()) - } - } - - /// Sets the string for the given key to the specified value. - pub fn set_string(&mut self, key: &str, value: &str) { - let key = NSString::new(key); - let value = NSString::new(value); - - unsafe { - let _: () = msg_send![&*self.0, setObject:value.into_inner() forKey:key.into_inner()]; - } - } -} diff --git a/src/defaults/mod.rs b/src/defaults/mod.rs new file mode 100644 index 0000000..c0b3147 --- /dev/null +++ b/src/defaults/mod.rs @@ -0,0 +1,223 @@ +//! Wraps `NSUserDefaults`, providing an interface to fetch and store small amounts of data. +//! +//! In general, this tries to take an approach popularized by `serde_json`'s `Value` struct. In +//! this case, `DefaultValue` handles wrapping types for insertion/retrieval, shepherding between +//! the Objective-C runtime and your Rust code. +//! +//! It currently supports a number of primitive types, as well as a generic `Data` type for custom +//! usage. Note that the `Data` type is stored internally as an `NSData` instance. +//! +//! Do not use this for storing sensitive data - you want the Keychain for that. +//! +//! In general, you should expect that some allocations are happening under the hood here, due to +//! the way the Objective-C runtime and Cocoa work. Where possible attempts are made to minimize +//! them, but in general... well, profile the rest of your code first, and don't call this stuff in +//! a loop. +//! +//! ## Example +//! ```rust +//! use std::collections::HashMap; +//! use cacao::defaults::{UserDefaults, DefaultValue}; +//! +//! let mut defaults = UserDefaults::standard(); +//! +//! defaults.register({ +//! let map = HashMap::new(); +//! map.insert("test", DefaultValue::string("value")); +//! map +//! }); +//! +//! // Ignore the unwrap() calls, it's a demo ;P +//! let value = defaults.get("test").unwrap().as_str().unwrap(); +//! assert_eq!(value, "value"); +//! ``` + +use std::collections::HashMap; +use std::ffi::CStr; +use std::os::raw::c_char; +use std::unreachable; + +use objc::{class, msg_send, sel, sel_impl}; +use objc::runtime::Object; +use objc_id::Id; + +use crate::foundation::{id, nil, YES, BOOL, NSInteger, NSString, NSDictionary}; + +mod value; +pub use value::DefaultValue; + +/// Wraps and provides methods for interacting with `NSUserDefaults`, which can be used for storing +/// pieces of information (preferences, or _defaults_) to persist across application launches. +/// +/// This should not be used for sensitive data - use the Keychain for that. +#[derive(Debug)] +pub struct UserDefaults(pub Id); + +impl Default for UserDefaults { + /// Equivalent to calling `UserDefaults::standard()`. + fn default() -> Self { + UserDefaults::standard() + } +} + +impl UserDefaults { + /// Returns the `standardUserDefaults`, which is... exactly what it sounds like. + /// + /// _Note that if you're planning to share preferences across things (e.g, an app and an + /// extension) you *probably* want to use `suite()` instead!_ + /// + /// ```rust + /// use cacao::defaults::UserDefaults; + /// + /// let defaults = UserDefaults::standard(); + /// + /// let _ = defaults.get("test"); + /// ``` + pub fn standard() -> Self { + UserDefaults(unsafe { + Id::from_ptr(msg_send![class!(NSUserDefaults), standardUserDefaults]) + }) + } + + /// Returns a user defaults instance for the given suite name. You typically use this to share + /// preferences across apps and extensions. + /// + /// ```rust + /// use cacao::defaults::UserDefaults; + /// + /// let defaults = UserDefaults::suite("com.myapp.shared"); + /// + /// // This value would be shared between apps, extensions, and so on that are in this suite. + /// let _ = defaults.get("test"); + /// ``` + pub fn suite(named: &str) -> Self { + let name = NSString::new(named); + + UserDefaults(unsafe { + let alloc: id = msg_send![class!(NSUserDefaults), alloc]; + Id::from_ptr(msg_send![alloc, initWithSuiteName:name.into_inner()]) + }) + } + + /// You can use this to register defaults at the beginning of your program. Note that these are + /// just that - _defaults_. If a user has done something to cause an actual value to be set + /// here, that value will be returned instead for that key. + /// + /// ```rust + /// use std::collections::HashMap; + /// + /// use cacao::defaults::{UserDefaults, DefaultValue}; + /// + /// let mut defaults = UserDefaults::standard(); + /// + /// defaults.register({ + /// let mut map = HashMap::new(); + /// map.insert("test", DefaultValue::Bool(true)); + /// map + /// }); + /// ``` + pub fn register>(&mut self, values: HashMap) { + let dictionary = NSDictionary::from(values); + + unsafe { + let _: () = msg_send![&*self.0, registerDefaults:dictionary.into_inner()]; + } + } + + /// Inserts a value for the specified key. This synchronously updates the backing + /// `NSUserDefaults` store, and asynchronously persists to the disk. + /// + /// ```rust + /// use cacao::defaults::{UserDefaults, DefaultValue}; + /// + /// let mut defaults = UserDefaults::standard(); + /// defaults.insert("test", DefaultValue::Bool(true)); + /// ``` + pub fn insert>(&mut self, key: K, value: DefaultValue) { + let key = NSString::new(key.as_ref()); + let value: id = (&value).into(); + + unsafe { + let _: () = msg_send![&*self.0, setObject:value forKey:key]; + } + } + + /// Remove the default associated with the key. If the key doesn't exist, this is a noop. + /// + /// ```rust + /// use cacao::defaults::{UserDefaults, DefaultValue}; + /// + /// let mut defaults = UserDefaults::standard(); + /// defaults.remove("test"); + /// ``` + pub fn remove>(&mut self, key: K) { + let key = NSString::new(key.as_ref()); + + unsafe { + let _: () = msg_send![&*self.0, removeObjectForKey:key.into_inner()]; + } + } + + /// Returns a `DefaultValue` for the given key, from which you can further extract the data you + /// need. Note that this does a `nil` check and will return `None` in such cases, with the + /// exception of `bool` values, where it will always return either `true` or `false`. This is + /// due to the underlying storage engine used for `NSUserDefaults`. + /// + /// Note that this also returns owned values, not references. `NSUserDefaults` explicitly + /// returns new immutable copies each time, so we're free to vend them as such. + /// + /// ```rust + /// use cacao::defaults::{UserDefaults, DefaultValue}; + /// + /// let mut defaults = UserDefaults::standard(); + /// defaults.insert("test", DefaultValue::string("value")); + /// + /// let value = defaults.get("test").unwrap().as_str().unwrap(); + /// assert_eq!(value, "value"); + /// ``` + pub fn get>(&self, key: K) -> Option { + let key = NSString::new(key.as_ref()); + + let result: id = unsafe { + msg_send![&*self.0, objectForKey:key.into_inner()] + }; + + if result == nil { + return None; + } + + let is_string: BOOL = unsafe { msg_send![result, isKindOfClass:class!(NSString)] }; + if is_string == YES { + let s = NSString::wrap(result).to_str().to_string(); + return Some(DefaultValue::String(s)); + } + + // This works, but might not be the best approach. We basically need to inspect the + // `NSNumber` returned and see what the wrapped encoding type is. `q` and `d` represent + // `NSInteger` (platform specific) and `double` (f64) respectively, but conceivably we + // might need others. + let is_number: BOOL = unsafe { msg_send![result, isKindOfClass:class!(NSNumber)] }; + if is_number == YES { + unsafe { + let t: *const c_char = msg_send![result, objCType]; + let slice = CStr::from_ptr(t); + + if let Ok(code) = slice.to_str() { + println!("Code: {}", code); + + if code == "q" { + let v: NSInteger = msg_send![result, integerValue]; + return Some(DefaultValue::Integer(v as i64)); + } + + if code == "d" { + let v: f64 = msg_send![result, doubleValue]; + return Some(DefaultValue::Float(v)); + } + } + } + } + + None + } +} diff --git a/src/defaults/value.rs b/src/defaults/value.rs new file mode 100644 index 0000000..d9c619e --- /dev/null +++ b/src/defaults/value.rs @@ -0,0 +1,148 @@ +//! + +use std::collections::HashMap; + +use objc::{class, msg_send, sel, sel_impl}; +use objc_id::Id; + +use crate::foundation::{id, YES, NO, nil, NSInteger, NSDictionary, NSString}; + +#[derive(Clone, Debug, PartialEq)] +pub enum DefaultValue { + Bool(bool), + String(String), + Float(f64), + Integer(i64) +} + +impl DefaultValue { + /// A handy initializer for `DefaultValue::Bool`. + pub fn bool(value: bool) -> Self { + DefaultValue::Bool(value) + } + + /// A handy initializer for `DefaultValue::String`; + pub fn string>(value: S) -> Self { + DefaultValue::String(value.into()) + } + + /// Returns `true` if the value is a boolean value. Returns `false` otherwise. + pub fn is_boolean(&self) -> bool { + match self { + DefaultValue::Bool(_) => true, + _ => false + } + } + + /// If this is a Bool, it returns the associated bool. Returns `None` otherwise. + pub fn as_bool(&self) -> Option { + match self { + DefaultValue::Bool(v) => Some(*v), + _ => None + } + } + + /// Returns `true` if the value is a string. Returns `false` otherwise. + pub fn is_string(&self) -> bool { + match self { + DefaultValue::String(_) => true, + _ => false + } + } + + /// If this is a String, it returns a &str. Returns `None` otherwise. + pub fn as_str(&self) -> Option<&str> { + match self { + DefaultValue::String(s) => Some(s), + _ => None + } + } + + /// Returns `true` if the value is a float. Returns `false` otherwise. + pub fn is_integer(&self) -> bool { + match self { + DefaultValue::Integer(_) => true, + _ => false + } + } + + /// If this is a int, returns it (`i32`). Returns `None` otherwise. + pub fn as_i32(&self) -> Option { + match self { + DefaultValue::Integer(i) => Some(*i as i32), + _ => None + } + } + + /// If this is a int, returns it (`i64`). Returns `None` otherwise. + pub fn as_i64(&self) -> Option { + match self { + DefaultValue::Integer(i) => Some(*i as i64), + _ => None + } + } + + /// Returns `true` if the value is a float. Returns `false` otherwise. + pub fn is_float(&self) -> bool { + match self { + DefaultValue::Float(_) => true, + _ => false + } + } + + /// If this is a float, returns it (`f32`). Returns `None` otherwise. + pub fn as_f32(&self) -> Option { + match self { + DefaultValue::Float(f) => Some(*f as f32), + _ => None + } + } + + /// If this is a float, returns it (`f64`). Returns `None` otherwise. + pub fn as_f64(&self) -> Option { + match self { + DefaultValue::Float(f) => Some(*f as f64), + _ => None + } + } +} + +impl From<&DefaultValue> for id { + /// Shepherds `DefaultValue` types into `NSObject`s that can be stored in `NSUserDefaults`. + // These currently work, but may not be exhaustive and should be looked over past the preview + // period. + fn from(value: &DefaultValue) -> Self { + unsafe { + match value { + DefaultValue::Bool(b) => msg_send![class!(NSNumber), numberWithBool:match b { + true => YES, + false => NO + }], + + DefaultValue::String(s) => NSString::new(&s).into_inner(), + DefaultValue::Float(f) => msg_send![class!(NSNumber), numberWithDouble:*f], + DefaultValue::Integer(i) => msg_send![class!(NSNumber), numberWithInteger:*i as NSInteger] + } + } + } +} + +impl From> for NSDictionary +where + K: AsRef +{ + /// Translates a `HashMap` of `DefaultValue`s into an `NSDictionary`. + fn from(map: HashMap) -> Self { + NSDictionary(unsafe { + let dictionary: id = msg_send![class!(NSMutableDictionary), new]; + + for (key, value) in map.iter() { + let k = NSString::new(key.as_ref()); + let v: id = value.into(); + let _: () = msg_send![dictionary, setObject:v forKey:k]; + } + + Id::from_ptr(dictionary) + }) + } +} diff --git a/src/foundation/dictionary.rs b/src/foundation/dictionary.rs index 919ca33..5de3879 100644 --- a/src/foundation/dictionary.rs +++ b/src/foundation/dictionary.rs @@ -1,10 +1,31 @@ //! A wrapper for `NSDictionary`, which aims to make dealing with the class throughout this //! framework a tad bit simpler. +use objc::{class, msg_send, sel, sel_impl}; use objc::runtime::Object; use objc_id::Id; -#[derive(Debug)] -pub struct NSDictionary(Id); +use crate::foundation::{id, nil, YES, NO, NSString}; -impl NSDictionary {} +/// A wrapper for `NSDictionary`. Behind the scenes we actually wrap `NSMutableDictionary`, and +/// rely on Rust doing the usual borrow-checking guards that it does so well. +#[derive(Debug)] +pub struct NSDictionary(pub Id); + +impl Default for NSDictionary { + fn default() -> Self { + NSDictionary::new() + } +} + +impl NSDictionary { + pub fn new() -> Self { + NSDictionary(unsafe { + Id::from_ptr(msg_send![class!(NSMutableDictionary), new]) + }) + } + + pub fn into_inner(mut self) -> id { + &mut *self.0 + } +}