diff --git a/rustler/src/lib.rs b/rustler/src/lib.rs index 637aff7c..ce700ef6 100644 --- a/rustler/src/lib.rs +++ b/rustler/src/lib.rs @@ -38,7 +38,8 @@ mod term; pub use crate::term::Term; pub use crate::types::{ - Atom, Binary, Decoder, Encoder, ListIterator, LocalPid, MapIterator, NewBinary, OwnedBinary, + Atom, Binary, Decoder, Encoder, ErlOption, ListIterator, LocalPid, MapIterator, NewBinary, + OwnedBinary, }; pub mod resource; pub use crate::resource::ResourceArc; diff --git a/rustler/src/types/atom.rs b/rustler/src/types/atom.rs index 13fcaf23..b51f2bf7 100644 --- a/rustler/src/types/atom.rs +++ b/rustler/src/types/atom.rs @@ -271,6 +271,10 @@ atoms! { /// The `nil` atom. nil, + /// The `undefined` atom, commonly used in Erlang libraries to express the + /// absence of value. + undefined, + /// The `ok` atom, commonly used in success tuples. ok, diff --git a/rustler/src/types/erlang_option.rs b/rustler/src/types/erlang_option.rs new file mode 100644 index 00000000..97fd9eec --- /dev/null +++ b/rustler/src/types/erlang_option.rs @@ -0,0 +1,216 @@ +use super::atom; +use crate::{Decoder, Encoder, Env, Error, NifResult, Term}; + +use std::ops::{Deref, DerefMut}; + +/// A wrapper type for [`Option`][option] to provide Erlang style encoding. It +/// uses `undefined` atom instead of `nil` when the enclosing value is `None`. +/// +/// Useful for interacting with Erlang libraries as `undefined` is commonly used in +/// Erlang to represent the absence of a value. +/// +/// [option]: https://doc.rust-lang.org/stable/core/option/enum.Option.html +/// +/// # Examples +/// +/// `ErlOption` provides methods to convert to/from `Option`. +/// +/// ```rust +/// use rustler::ErlOption; +/// +/// // Create new `ErlOption` values via convenient functions. +/// let _ = ErlOption::some(1); // Wraps `Some(1)`. +/// let _ = ErlOption::::none(); +/// +/// // Convert Option values to ErlOption values. +/// let _ = ErlOption::from(Some(2)); +/// let _: ErlOption<_> = Some(3).into(); +/// let _: ErlOption = None.into(); +/// +/// // Get a reference of enclosing Option from an ErlOption. +/// let _: &Option = ErlOption::some(4).as_ref(); +/// +/// // Get a mutable reference of enclosing Option from an ErlOption. +/// let _: &mut Option = ErlOption::some(5).as_mut(); +/// +/// // Convert an ErlOption value to an Option value. +/// let _: Option = ErlOption::some(6).into(); +/// +/// // Compare ErlOption with Option. +/// assert_eq!(ErlOption::some(7), Some(7)); +/// assert!(ErlOption::some(8) > Some(7)); +/// +/// // Call Option's methods on an ErlOption via Deref and DerefMut. +/// assert!(ErlOption::some(9).is_some()); +/// assert_eq!(ErlOption::some(10).unwrap(), 10); +/// assert_eq!(ErlOption::some(12).map(|v| v + 1), ErlOption::some(13)); +/// ``` +/// +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ErlOption(Option); + +impl ErlOption { + /// A convenience function to create an `ErlOption` from a `Some(T)` value. + pub fn some(v: T) -> Self { + Some(v).into() + } + + /// A convenience function to create an `ErlOption` enclosing the `None` + /// value. + pub fn none() -> Self { + Default::default() + } +} + +// NOTE: Manually implement the Default instead of deriving it. This is because +// deriving requires `T` to be `Default` as well, but we do not need that. +impl Default for ErlOption { + fn default() -> Self { + Self(None) + } +} + +impl From> for ErlOption { + fn from(v: Option) -> Self { + ErlOption(v) + } +} + +impl From> for Option { + fn from(v: ErlOption) -> Self { + v.0 + } +} + +impl AsMut> for ErlOption { + fn as_mut(&mut self) -> &mut Option { + &mut self.0 + } +} + +impl AsRef> for ErlOption { + fn as_ref(&self) -> &Option { + &self.0 + } +} + +impl Deref for ErlOption { + type Target = Option; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ErlOption { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl PartialEq> for ErlOption +where + T: PartialEq, +{ + fn eq(&self, other: &Option) -> bool { + &self.0 == other + } +} + +impl PartialEq> for Option +where + T: PartialEq, +{ + fn eq(&self, other: &ErlOption) -> bool { + self == &other.0 + } +} + +impl PartialOrd> for ErlOption +where + T: PartialOrd, +{ + fn partial_cmp(&self, other: &Option) -> Option { + self.0.partial_cmp(other) + } +} + +impl PartialOrd> for Option +where + T: PartialOrd, +{ + fn partial_cmp(&self, other: &ErlOption) -> Option { + self.partial_cmp(&other.0) + } +} + +impl Encoder for ErlOption +where + T: Encoder, +{ + fn encode<'c>(&self, env: Env<'c>) -> Term<'c> { + match self.0 { + Some(ref value) => value.encode(env), + None => atom::undefined().encode(env), + } + } +} + +impl<'a, T> Decoder<'a> for ErlOption +where + T: Decoder<'a>, +{ + fn decode(term: Term<'a>) -> NifResult { + if let Ok(term) = term.decode::() { + Ok(Self(Some(term))) + } else { + let decoded_atom: atom::Atom = term.decode()?; + if decoded_atom == atom::undefined() { + Ok(Self(None)) + } else { + Err(Error::BadArg) + } + } + } +} + +#[cfg(test)] +mod test { + use super::ErlOption; + + #[test] + fn test_creations() { + assert_eq!(ErlOption::some(1).as_ref(), &Some(1)); + assert_eq!(ErlOption::::none().as_ref(), &None as &Option); + } + + #[test] + fn test_conversions() { + // Convert Option values to ErlOption values. + assert_eq!(ErlOption::from(Some(2)), ErlOption::some(2)); + assert_eq!(Into::>::into(Some(3)), ErlOption::some(3)); + assert_eq!(Into::>::into(None), ErlOption::none()); + + // Convert an ErlOption value to an Option value. + assert_eq!(Into::>::into(ErlOption::some(6)), Some(6)); + } + + #[test] + fn test_as_ref() { + assert_eq!(ErlOption::some(4).as_ref(), &Some(4)); + assert_eq!(ErlOption::some(5).as_mut(), &mut Some(5)); + } + + #[test] + fn test_compare() { + assert_eq!(ErlOption::some(7), Some(7)); + assert!(ErlOption::some(8) > Some(7)); + } + + #[test] + fn test_deref() { + assert!(ErlOption::some(9).is_some()); + assert_eq!(ErlOption::some(10).unwrap(), 10); + assert_eq!(ErlOption::some(12).map(|v| v + 1), ErlOption::some(13)); + } +} diff --git a/rustler/src/types/mod.rs b/rustler/src/types/mod.rs index bd1d5364..5aa0393e 100644 --- a/rustler/src/types/mod.rs +++ b/rustler/src/types/mod.rs @@ -37,6 +37,9 @@ pub mod truthy; pub mod elixir_struct; +pub mod erlang_option; +pub use self::erlang_option::ErlOption; + pub trait Encoder { fn encode<'a>(&self, env: Env<'a>) -> Term<'a>; } diff --git a/rustler_tests/lib/rustler_test.ex b/rustler_tests/lib/rustler_test.ex index f8d016c4..0d8bb277 100644 --- a/rustler_tests/lib/rustler_test.ex +++ b/rustler_tests/lib/rustler_test.ex @@ -25,6 +25,7 @@ defmodule RustlerTest do def add_i32(_, _), do: err() def echo_u8(_), do: err() def option_inc(_), do: err() + def erlang_option_inc(_), do: err() def result_to_int(_), do: err() def sum_list(_), do: err() diff --git a/rustler_tests/native/rustler_test/src/lib.rs b/rustler_tests/native/rustler_test/src/lib.rs index 0a52d18b..03a744d3 100644 --- a/rustler_tests/native/rustler_test/src/lib.rs +++ b/rustler_tests/native/rustler_test/src/lib.rs @@ -21,6 +21,7 @@ rustler::init!( test_primitives::add_i32, test_primitives::echo_u8, test_primitives::option_inc, + test_primitives::erlang_option_inc, test_primitives::result_to_int, test_list::sum_list, test_list::make_list, diff --git a/rustler_tests/native/rustler_test/src/test_primitives.rs b/rustler_tests/native/rustler_test/src/test_primitives.rs index 922c452b..bcee1673 100644 --- a/rustler_tests/native/rustler_test/src/test_primitives.rs +++ b/rustler_tests/native/rustler_test/src/test_primitives.rs @@ -1,3 +1,5 @@ +use rustler::ErlOption; + #[rustler::nif] pub fn add_u32(a: u32, b: u32) -> u32 { a + b @@ -18,6 +20,11 @@ pub fn option_inc(opt: Option) -> Option { opt.map(|num| num + 1.0) } +#[rustler::nif] +pub fn erlang_option_inc(opt: ErlOption) -> ErlOption { + opt.as_ref().map(|num| num + 1.0).into() +} + #[rustler::nif] pub fn result_to_int(res: Result) -> Result { match res { diff --git a/rustler_tests/test/primitives_test.exs b/rustler_tests/test/primitives_test.exs index b37d355f..3462ace8 100644 --- a/rustler_tests/test/primitives_test.exs +++ b/rustler_tests/test/primitives_test.exs @@ -20,6 +20,12 @@ defmodule RustlerTest.PrimitivesTest do assert_raise ArgumentError, fn -> RustlerTest.option_inc("hello") end end + test "erlang option decoding and encoding" do + assert 33.0 == RustlerTest.erlang_option_inc(32.0) + assert :undefined == RustlerTest.erlang_option_inc(:undefined) + assert_raise ArgumentError, fn -> RustlerTest.erlang_option_inc("hello") end + end + test "result decoding and encoding" do assert {:ok, 1} == RustlerTest.result_to_int({:ok, true}) assert {:ok, 0} == RustlerTest.result_to_int({:ok, false})