diff --git a/src/param_spec.rs b/src/param_spec.rs index 0f0c936f..08d7a13f 100644 --- a/src/param_spec.rs +++ b/src/param_spec.rs @@ -10,6 +10,8 @@ use StaticType; use Type; use Value; +use std::char::CharTryFromError; +use std::convert::TryFrom; use std::ffi::CStr; // Can't use get_type here as this is not a boxed type but another fundamental type @@ -826,7 +828,7 @@ define_param_spec!( 9 ); -define_param_spec_default!(ParamSpecUnichar, char, |x| from_glib(x)); +define_param_spec_default!(ParamSpecUnichar, Result, TryFrom::try_from); define_param_spec!( ParamSpecEnum, diff --git a/src/translate.rs b/src/translate.rs index 9578c093..7835adca 100644 --- a/src/translate.rs +++ b/src/translate.rs @@ -20,6 +20,71 @@ //! } //! ``` //! +//! Implementing `OptionToGlib` on a Rust type `T` allows specifying a sentinel to indicate +//! a `None` value and auto-implementing `FromGlib` for `Option`, which would not be +//! possible in dependent crates due to the [orphan rule](https://doc.rust-lang.org/book/ch10-02-traits.html#implementing-a-trait-on-a-type). +//! In the example below, `ToGlib` is auto-implemented for `Option`. +//! +//! ``` +//! # use glib::translate::*; +//! struct SpecialU32(u32); +//! impl ToGlib for SpecialU32 { +//! type GlibType = libc::c_uint; +//! fn to_glib(&self) -> libc::c_uint { +//! self.0 as libc::c_uint +//! } +//! } +//! impl OptionToGlib for SpecialU32 { +//! const GLIB_NONE: Self::GlibType = 0xFFFFFF; +//! } +//! ``` +//! +//! In order to auto-implement `FromGlib` for `Option`, proceed as follows: +//! +//! ``` +//! # use glib::translate::*; +//! # struct SpecialU32(u32); +//! # impl ToGlib for SpecialU32 { +//! # type GlibType = libc::c_uint; +//! # fn to_glib(&self) -> libc::c_uint { +//! # self.0 as libc::c_uint +//! # } +//! # } +//! # impl OptionToGlib for SpecialU32 { +//! # const GLIB_NONE: Self::GlibType = 0xFFFFFF; +//! # } +//! impl TryFromGlib for SpecialU32 { +//! type Error = NoneError; +//! fn try_from_glib(val: libc::c_uint) -> Result { +//! if val == SpecialU32::GLIB_NONE { +//! return Err(NoneError); +//! } +//! Ok(SpecialU32(val as u32)) +//! } +//! } +//! ``` +//! +//! The `TryFromGlib` trait can also be implemented when the Glib type range is larger than the +//! target Rust type's range. In the example below, the Rust type `U32` can be built from a signed +//! `libc::c_long`, which means that the negative range is not valid. +//! +//! ``` +//! # use std::convert::TryFrom; +//! # use std::num::TryFromIntError; +//! # use glib::translate::*; +//! struct U32(u32); +//! impl TryFromGlib for U32 { +//! type Error = TryFromIntError; +//! fn try_from_glib(val: libc::c_long) -> Result { +//! Ok(U32(u32::try_from(val)?)) +//! } +//! } +//! ``` +//! +//! Finally, you can define `TryFromGlib` with both `None` and `Invalid` alternatives by setting +//! the associated `type Error = NoneOrInvalidError` (where `I` is the `Error` type when the +//! value is invalid), which results in auto-implementing `FromGlib` for `Result, I>`. +//! //! `ToGlibPtr`, `FromGlibPtrNone`, `FromGlibPtrFull` and `FromGlibPtrBorrow` work on `gpointer`s //! and ensure correct ownership of values //! according to [Glib ownership transfer rules](https://gi.readthedocs.io/en/latest/annotations/giannotations.html). @@ -68,10 +133,12 @@ use glib_sys; use libc::{c_char, size_t}; use std::char; -use std::cmp::Ordering; +use std::cmp::{Eq, Ordering, PartialEq}; use std::collections::HashMap; +use std::error::Error; use std::ffi::{CStr, CString}; use std::ffi::{OsStr, OsString}; +use std::fmt; use std::mem; #[cfg(not(windows))] use std::os::unix::prelude::*; @@ -277,6 +344,31 @@ impl ToGlib for Ordering { } } +/// A Rust type `T` for which `Option` translates to the same glib type as T. +// The copy bound on `Self::GlibType` is requested by clippy by virtue of: +// https://rust-lang.github.io/rust-clippy/master/index.html#declare_interior_mutable_const +pub trait OptionToGlib: ToGlib +where + Self::GlibType: Copy, +{ + const GLIB_NONE: Self::GlibType; +} + +impl ToGlib for Option +where + T::GlibType: Copy, +{ + type GlibType = T::GlibType; + + #[inline] + fn to_glib(&self) -> Self::GlibType { + match self { + Some(t) => t.to_glib(), + None => T::GLIB_NONE, + } + } +} + /// Provides the default pointer type to be used in some container conversions. /// /// It's `*mut c_char` for `String`, `*mut GtkButton` for `gtk::Button`, etc. @@ -1081,13 +1173,6 @@ impl FromGlib for bool { } } -impl FromGlib for char { - #[inline] - fn from_glib(val: u32) -> char { - char::from_u32(val).expect("Valid Unicode character expected") - } -} - impl FromGlib for Ordering { #[inline] fn from_glib(val: i32) -> Ordering { @@ -1095,42 +1180,106 @@ impl FromGlib for Ordering { } } -impl FromGlib for Option { +/// Translate from a Glib type which can result in an undefined and/or invalid value. +pub trait TryFromGlib: Sized { + type Error; + fn try_from_glib(val: G) -> Result; +} + +/// Error type for `TryFromGlib` when the Glib value is None. +#[derive(Debug, PartialEq, Eq)] +pub struct NoneError; + +impl fmt::Display for NoneError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(fmt, "value is None") + } +} + +impl std::error::Error for NoneError {} + +impl> FromGlib for Option { #[inline] - fn from_glib(val: u32) -> Option { - match val { - 0 => None, - _ => char::from_u32(val), + fn from_glib(val: G) -> Option { + T::try_from_glib(val).ok() + } +} + +/// Error type for `TryFromGlib` when the Glib value can be None or invalid. +#[derive(Debug)] +pub enum NoneOrInvalidError { + Invalid(I), + None, +} + +impl NoneOrInvalidError { + /// Builds the `None` variante. + pub fn none() -> Self { + NoneOrInvalidError::None + } + + /// Returns `true` if `self` is the `None` variante. + // FIXME `matches!` was introduced in rustc 1.42.0, current MSRV is 1.40.0 + // FIXME uncomment when CI can upgrade to 1.47.1 + //#[allow(clippy::match_like_matches_macro)] + pub fn is_none(&self) -> bool { + match self { + NoneOrInvalidError::None => true, + _ => false, + } + } + + /// Returns `true` if `self` is the `Invalid` variante. + // FIXME `matches!` was introduced in rustc 1.42.0, current MSRV is 1.40.0 + // FIXME uncomment when CI can upgrade to 1.47.1 + //#[allow(clippy::match_like_matches_macro)] + pub fn is_invalid(&self) -> bool { + match self { + NoneOrInvalidError::None => false, + _ => true, } } } -impl FromGlib for Option { - #[inline] - fn from_glib(val: i32) -> Option { - if val >= 0 { - Some(val as u32) - } else { - None +impl From for NoneOrInvalidError { + fn from(invalid: I) -> Self { + NoneOrInvalidError::Invalid(invalid) + } +} + +impl fmt::Display for NoneOrInvalidError { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + NoneOrInvalidError::Invalid(err) => fmt::Display::fmt(err, fmt), + NoneOrInvalidError::None => write!(fmt, "value is None"), } } } -impl FromGlib for Option { - #[inline] - fn from_glib(val: i64) -> Option { - if val >= 0 { - Some(val as u64) - } else { - None +impl Error for NoneOrInvalidError {} + +impl PartialEq for NoneOrInvalidError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (NoneOrInvalidError::Invalid(self_err), NoneOrInvalidError::Invalid(other_err)) => { + self_err.eq(other_err) + } + (NoneOrInvalidError::None, NoneOrInvalidError::None) => true, + _ => false, } } } -impl FromGlib for Option { +impl Eq for NoneOrInvalidError {} + +impl>> FromGlib for Result, I> { #[inline] - fn from_glib(val: i32) -> Option { - FromGlib::from_glib(i64::from(val)) + fn from_glib(val: G) -> Result, I> { + match T::try_from_glib(val) { + Ok(value) => Ok(Some(value)), + Err(NoneOrInvalidError::None) => Ok(None), + Err(NoneOrInvalidError::Invalid(err)) => Err(err), + } } } @@ -2261,4 +2410,127 @@ mod tests { ::FileTest::EXISTS | ::FileTest::IS_DIR )); } + + #[test] + fn none_value() { + const CLONG_NONE: libc::c_long = -1; + + #[derive(Debug, PartialEq, Eq)] + struct SpecialU32(u32); + impl ToGlib for SpecialU32 { + type GlibType = libc::c_uint; + fn to_glib(&self) -> libc::c_uint { + self.0 as libc::c_uint + } + } + impl OptionToGlib for SpecialU32 { + const GLIB_NONE: Self::GlibType = CLONG_NONE as libc::c_uint; + } + + assert_eq!(SpecialU32(0).to_glib(), 0); + assert_eq!(SpecialU32(42).to_glib(), 42); + assert_eq!(Some(SpecialU32(0)).to_glib(), 0); + assert_eq!(Some(SpecialU32(42)).to_glib(), 42); + assert_eq!(Option::None::.to_glib(), SpecialU32::GLIB_NONE); + + impl TryFromGlib for SpecialU32 { + type Error = NoneError; + fn try_from_glib(val: libc::c_uint) -> Result { + if val == SpecialU32::GLIB_NONE { + return Err(NoneError); + } + + Ok(SpecialU32(val as u32)) + } + } + + assert_eq!(SpecialU32::try_from_glib(0), Ok(SpecialU32(0))); + assert_eq!(SpecialU32::try_from_glib(42), Ok(SpecialU32(42))); + assert_eq!( + SpecialU32::try_from_glib(SpecialU32::GLIB_NONE), + Err(NoneError) + ); + + assert_eq!(Option::::from_glib(0), Some(SpecialU32(0))); + assert_eq!(Option::::from_glib(42), Some(SpecialU32(42))); + assert!(Option::::from_glib(SpecialU32::GLIB_NONE).is_none()); + } + + #[test] + fn invalid_value() { + use std::convert::TryFrom; + use std::num::TryFromIntError; + + #[derive(Debug, PartialEq, Eq)] + struct U32(u32); + + impl TryFromGlib for U32 { + type Error = TryFromIntError; + fn try_from_glib(val: libc::c_long) -> Result { + Ok(U32(u32::try_from(val)?)) + } + } + + assert_eq!(U32::try_from_glib(0), Ok(U32(0))); + assert_eq!(U32::try_from_glib(42), Ok(U32(42))); + assert!(U32::try_from_glib(-1).is_err()); + assert!(U32::try_from_glib(-42).is_err()); + } + + #[test] + fn none_or_invalid_value() { + use std::convert::TryFrom; + use std::num::TryFromIntError; + + #[derive(Debug, PartialEq, Eq)] + struct SpecialU32(u32); + impl ToGlib for SpecialU32 { + type GlibType = libc::c_long; + fn to_glib(&self) -> libc::c_long { + self.0 as libc::c_long + } + } + impl OptionToGlib for SpecialU32 { + const GLIB_NONE: Self::GlibType = -1; + } + + assert_eq!(SpecialU32(0).to_glib(), 0); + assert_eq!(SpecialU32(42).to_glib(), 42); + assert_eq!(Some(SpecialU32(42)).to_glib(), 42); + assert_eq!(Option::None::.to_glib(), SpecialU32::GLIB_NONE); + + impl TryFromGlib for SpecialU32 { + type Error = NoneOrInvalidError; + fn try_from_glib( + val: libc::c_long, + ) -> Result> { + if val == SpecialU32::GLIB_NONE { + return Err(NoneOrInvalidError::None); + } + + Ok(SpecialU32(u32::try_from(val)?)) + } + } + + assert_eq!(SpecialU32::try_from_glib(0), Ok(SpecialU32(0))); + assert_eq!(SpecialU32::try_from_glib(42), Ok(SpecialU32(42))); + assert!(SpecialU32::try_from_glib(SpecialU32::GLIB_NONE) + .unwrap_err() + .is_none()); + assert!(SpecialU32::try_from_glib(-42).unwrap_err().is_invalid()); + + assert_eq!( + Result::, _>::from_glib(0), + Ok(Some(SpecialU32(0))) + ); + assert_eq!( + Result::, _>::from_glib(42), + Ok(Some(SpecialU32(42))) + ); + assert_eq!( + Result::, _>::from_glib(SpecialU32::GLIB_NONE), + Ok(None) + ); + assert!(Result::, _>::from_glib(-42).is_err()); + } }