diff --git a/objc2/CHANGELOG_FOUNDATION.md b/objc2/CHANGELOG_FOUNDATION.md index c07d32e46..b7362eb31 100644 --- a/objc2/CHANGELOG_FOUNDATION.md +++ b/objc2/CHANGELOG_FOUNDATION.md @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added * Added `NSNumber`. +* Added `NSError`. * Implement `UnwindSafe` and `RefUnwindSafe` for all objects. * Implemented `IntoIterator` for references to `NSArray`, `NSMutableArray`, `NSData` and `NSMutableData`. diff --git a/objc2/src/foundation/error.rs b/objc2/src/foundation/error.rs new file mode 100644 index 000000000..456ecde0d --- /dev/null +++ b/objc2/src/foundation/error.rs @@ -0,0 +1,162 @@ +use core::fmt; +use core::panic::{RefUnwindSafe, UnwindSafe}; + +use super::{NSCopying, NSDictionary, NSObject, NSString}; +use crate::extern_class; +use crate::ffi::NSInteger; +use crate::rc::{Id, Shared}; +use crate::{msg_send, msg_send_id}; + +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 905b8fcb6..b831d5b55 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; @@ -72,6 +73,7 @@ mod copying; mod data; mod dictionary; mod enumerator; +mod error; mod exception; mod geometry; mod mutable_array; @@ -134,6 +136,7 @@ mod tests { // TODO: Figure out if Send + Sync is safe? // assert_auto_traits::>(); // assert_auto_traits::>>(); + assert_auto_traits::(); assert_auto_traits::(); assert_auto_traits::(); assert_auto_traits::(); diff --git a/test-ui/ui/msg_send_id_invalid_return.stderr b/test-ui/ui/msg_send_id_invalid_return.stderr index af8e04793..783665d0b 100644 --- a/test-ui/ui/msg_send_id_invalid_return.stderr +++ b/test-ui/ui/msg_send_id_invalid_return.stderr @@ -22,10 +22,10 @@ error[E0277]: the trait bound `objc2::runtime::Class: Message` is not satisfied NSAttributedString NSData NSDictionary + NSError NSException NSMutableArray - NSMutableAttributedString - and 10 others + and 11 others = note: required because of the requirements on the impl of `MsgSendId<&objc2::runtime::Class, Id>` for `RetainSemantics` = note: this error originates in the macro `msg_send_id` (in Nightly builds, run with -Z macro-backtrace for more info) @@ -50,10 +50,10 @@ error[E0277]: the trait bound `objc2::runtime::Class: Message` is not satisfied NSAttributedString NSData NSDictionary + NSError NSException NSMutableArray - NSMutableAttributedString - and 10 others + and 11 others = note: required because of the requirements on the impl of `MsgSendId<&objc2::runtime::Class, Id, Shared>>` for `RetainSemantics` = note: this error originates in the macro `msg_send_id` (in Nightly builds, run with -Z macro-backtrace for more info)