Almost done with a proper NSUserDefaults wrapper...

This commit is contained in:
Ryan McGrath 2020-03-30 20:23:54 -07:00
parent e4ddfb975a
commit 3f9c9f992c
No known key found for this signature in database
GPG key ID: 811674B62B666830
5 changed files with 409 additions and 136 deletions

View file

@ -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();
}

View file

@ -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<Object>);
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<String> {
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()];
}
}
}

223
src/defaults/mod.rs Normal file
View file

@ -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<Object>);
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<K: AsRef<str>>(&mut self, values: HashMap<K, DefaultValue>) {
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<K: AsRef<str>>(&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<K: AsRef<str>>(&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<K: AsRef<str>>(&self, key: K) -> Option<DefaultValue> {
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
}
}

148
src/defaults/value.rs Normal file
View file

@ -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<S: Into<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<bool> {
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<i32> {
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<i64> {
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<f32> {
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<f64> {
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<K> From<HashMap<K, DefaultValue>> for NSDictionary
where
K: AsRef<str>
{
/// Translates a `HashMap` of `DefaultValue`s into an `NSDictionary`.
fn from(map: HashMap<K, DefaultValue>) -> 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)
})
}
}

View file

@ -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<Object>);
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<Object>);
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
}
}