diff --git a/objc2/CHANGELOG_FOUNDATION.md b/objc2/CHANGELOG_FOUNDATION.md index 4018d0f96..cb2fc3a68 100644 --- a/objc2/CHANGELOG_FOUNDATION.md +++ b/objc2/CHANGELOG_FOUNDATION.md @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). `NSData` and `NSMutableData`. * Implemented `Extend` for `NSMutableArray`. * Add extra `Extend<&u8>` impl for `NSMutableData`. +* Added `NSError`. ### Changed * **BREAKING**: Moved from external crate `objc2_foundation` into diff --git a/objc2/src/foundation/error.rs b/objc2/src/foundation/error.rs new file mode 100644 index 000000000..4eb59e977 --- /dev/null +++ b/objc2/src/foundation/error.rs @@ -0,0 +1,162 @@ +use core::fmt; +use core::panic::{RefUnwindSafe, UnwindSafe}; + +use objc2::ffi::NSInteger; +use objc2::rc::{Id, Shared}; +use objc2::{msg_send, msg_send_id}; + +use crate::{extern_class, NSCopying, NSDictionary, NSObject, NSString}; + +extern_class! { + /// Information about an error condition including a domain, a + /// domain-specific error code, and application-specific information. + /// + /// See also Apple's [documentation on error handling][err], and their + /// NSError [API reference][api]. + /// + /// [err]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorHandling/ErrorHandling.html#//apple_ref/doc/uid/TP40001806-CH201-SW1 + /// [api]: https://developer.apple.com/documentation/foundation/nserror?language=objc + #[derive(PartialEq, Eq, Hash)] + unsafe pub struct NSError: NSObject; +} + +// SAFETY: Error objects are immutable data containers. +unsafe impl Sync for NSError {} +unsafe impl Send for NSError {} + +impl UnwindSafe for NSError {} +impl RefUnwindSafe for NSError {} + +pub type NSErrorUserInfoKey = NSString; +pub type NSErrorDomain = NSString; + +/// Creation methods. +impl NSError { + /// Construct a new [`NSError`] with the given code in the given domain. + pub fn new(code: NSInteger, domain: &NSString) -> Id { + unsafe { Self::with_user_info(code, domain, None) } + } + + // TODO: Figure out safety of `user_info` dict! + unsafe fn with_user_info( + code: NSInteger, + domain: &NSString, + user_info: Option<&NSDictionary>, + ) -> Id { + // SAFETY: `domain` and `user_info` are copied to the error object, so + // even if the `&NSString` came from a `&mut NSMutableString`, we're + // still good! + unsafe { + msg_send_id![ + msg_send_id![Self::class(), alloc], + initWithDomain: domain, + code: code, + userInfo: user_info, + ] + .expect("unexpected NULL NSError") + } + } +} + +/// Accessor methods. +impl NSError { + pub fn domain(&self) -> Id { + unsafe { msg_send_id![self, domain].expect("unexpected NULL NSError domain") } + } + + pub fn code(&self) -> NSInteger { + unsafe { msg_send![self, code] } + } + + pub fn user_info(&self) -> Option, Shared>> { + unsafe { msg_send_id![self, userInfo] } + } + + pub fn localized_description(&self) -> Id { + unsafe { + msg_send_id![self, localizedDescription].expect( + "unexpected NULL localized description; a default should have been generated!", + ) + } + } + + // TODO: localizedRecoveryOptions + // TODO: localizedRecoverySuggestion + // TODO: localizedFailureReason + // TODO: helpAnchor + // TODO: +setUserInfoValueProviderForDomain:provider: + // TODO: +userInfoValueProviderForDomain: + + // TODO: recoveryAttempter + // TODO: attemptRecoveryFromError:... + + // TODO: Figure out if this is a good design, or if we should do something + // differently (like a Rusty name for the function, or putting a bunch of + // statics in a module instead)? + #[allow(non_snake_case)] + pub fn NSLocalizedDescriptionKey() -> &'static NSErrorUserInfoKey { + extern "C" { + #[link_name = "NSLocalizedDescriptionKey"] + static VALUE: &'static NSErrorUserInfoKey; + } + unsafe { VALUE } + } + + // TODO: Other NSErrorUserInfoKey values + // TODO: NSErrorDomain values +} + +impl fmt::Debug for NSError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("NSError") + .field("domain", &self.domain()) + .field("code", &self.code()) + .field("user_info", &self.user_info()) + .finish() + } +} + +impl fmt::Display for NSError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.localized_description()) + } +} + +impl std::error::Error for NSError {} + +unsafe impl NSCopying for NSError { + type Ownership = Shared; + type Output = Self; +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::format; + + use crate::ns_string; + + #[test] + fn custom_domain() { + let error = NSError::new(42, ns_string!("MyDomain")); + assert_eq!(error.code(), 42); + assert_eq!(&*error.domain(), ns_string!("MyDomain")); + let expected = if cfg!(feature = "apple") { + "The operation couldn’t be completed. (MyDomain error 42.)" + } else { + "MyDomain 42" + }; + assert_eq!(format!("{}", error), expected); + } + + #[test] + fn basic() { + let error = NSError::new(-999, ns_string!("NSURLErrorDomain")); + let expected = if cfg!(feature = "apple") { + "The operation couldn’t be completed. (NSURLErrorDomain error -999.)" + } else { + "NSURLErrorDomain -999" + }; + assert_eq!(format!("{}", error), expected); + } +} diff --git a/objc2/src/foundation/mod.rs b/objc2/src/foundation/mod.rs index ef038a9b2..09f354158 100644 --- a/objc2/src/foundation/mod.rs +++ b/objc2/src/foundation/mod.rs @@ -33,6 +33,7 @@ pub use self::copying::{NSCopying, NSMutableCopying}; pub use self::data::NSData; pub use self::dictionary::NSDictionary; pub use self::enumerator::{NSEnumerator, NSFastEnumeration, NSFastEnumerator}; +pub use self::error::{NSError, NSErrorDomain, NSErrorUserInfoKey}; pub use self::exception::NSException; pub use self::geometry::{CGFloat, NSPoint, NSRect, NSSize}; pub use self::mutable_array::NSMutableArray; @@ -71,6 +72,7 @@ mod copying; mod data; mod dictionary; mod enumerator; +mod error; mod exception; mod geometry; mod mutable_array;