Skip to content

Commit

Permalink
Merge pull request #507 from tatsuya6502/erl-option
Browse files Browse the repository at this point in the history
rustler: add a Rust type `ErlOption<T>`
  • Loading branch information
filmor committed May 25, 2023
2 parents 90e4ef8 + 67d614a commit d8aa66d
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 1 deletion.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ See [`UPGRADE.md`](./UPGRADE.md) for additional help when upgrading to newer ver

## [unreleased]

## Added

* `ErlOption<T>` to provide an ergonomic option type for Erlang (#507, thanks @tatsuya6502)

### Changed

* Use Cargo features to define the NIF version level (#537), deprecating
Expand Down
3 changes: 2 additions & 1 deletion rustler/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions rustler/src/types/atom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand Down
216 changes: 216 additions & 0 deletions rustler/src/types/erlang_option.rs
Original file line number Diff line number Diff line change
@@ -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<T>`][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<T>` provides methods to convert to/from `Option<T>`.
///
/// ```rust
/// use rustler::ErlOption;
///
/// // Create new `ErlOption<i32>` values via convenient functions.
/// let _ = ErlOption::some(1); // Wraps `Some(1)`.
/// let _ = ErlOption::<i32>::none();
///
/// // Convert Option<i32> values to ErlOption<i32> values.
/// let _ = ErlOption::from(Some(2));
/// let _: ErlOption<_> = Some(3).into();
/// let _: ErlOption<i32> = None.into();
///
/// // Get a reference of enclosing Option<T> from an ErlOption<T>.
/// let _: &Option<i32> = ErlOption::some(4).as_ref();
///
/// // Get a mutable reference of enclosing Option<T> from an ErlOption<T>.
/// let _: &mut Option<i32> = ErlOption::some(5).as_mut();
///
/// // Convert an ErlOption<i32> value to an Option<i32> value.
/// let _: Option<i32> = ErlOption::some(6).into();
///
/// // Compare ErlOption<T> with Option<T>.
/// assert_eq!(ErlOption::some(7), Some(7));
/// assert!(ErlOption::some(8) > Some(7));
///
/// // Call Option<T>'s methods on an ErlOption<T> 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<T>(Option<T>);

impl<T> ErlOption<T> {
/// A convenience function to create an `ErlOption<T>` from a `Some(T)` value.
pub fn some(v: T) -> Self {
Some(v).into()
}

/// A convenience function to create an `ErlOption<T>` 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<T> Default for ErlOption<T> {
fn default() -> Self {
Self(None)
}
}

impl<T> From<Option<T>> for ErlOption<T> {
fn from(v: Option<T>) -> Self {
ErlOption(v)
}
}

impl<T> From<ErlOption<T>> for Option<T> {
fn from(v: ErlOption<T>) -> Self {
v.0
}
}

impl<T> AsMut<Option<T>> for ErlOption<T> {
fn as_mut(&mut self) -> &mut Option<T> {
&mut self.0
}
}

impl<T> AsRef<Option<T>> for ErlOption<T> {
fn as_ref(&self) -> &Option<T> {
&self.0
}
}

impl<T> Deref for ErlOption<T> {
type Target = Option<T>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

impl<T> DerefMut for ErlOption<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}

impl<T> PartialEq<Option<T>> for ErlOption<T>
where
T: PartialEq,
{
fn eq(&self, other: &Option<T>) -> bool {
&self.0 == other
}
}

impl<T> PartialEq<ErlOption<T>> for Option<T>
where
T: PartialEq,
{
fn eq(&self, other: &ErlOption<T>) -> bool {
self == &other.0
}
}

impl<T> PartialOrd<Option<T>> for ErlOption<T>
where
T: PartialOrd,
{
fn partial_cmp(&self, other: &Option<T>) -> Option<std::cmp::Ordering> {
self.0.partial_cmp(other)
}
}

impl<T> PartialOrd<ErlOption<T>> for Option<T>
where
T: PartialOrd,
{
fn partial_cmp(&self, other: &ErlOption<T>) -> Option<std::cmp::Ordering> {
self.partial_cmp(&other.0)
}
}

impl<T> Encoder for ErlOption<T>
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<T>
where
T: Decoder<'a>,
{
fn decode(term: Term<'a>) -> NifResult<Self> {
if let Ok(term) = term.decode::<T>() {
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::<i32>::none().as_ref(), &None as &Option<i32>);
}

#[test]
fn test_conversions() {
// Convert Option<i32> values to ErlOption<i32> values.
assert_eq!(ErlOption::from(Some(2)), ErlOption::some(2));
assert_eq!(Into::<ErlOption<i32>>::into(Some(3)), ErlOption::some(3));
assert_eq!(Into::<ErlOption<i32>>::into(None), ErlOption::none());

// Convert an ErlOption<i32> value to an Option<i32> value.
assert_eq!(Into::<Option<i32>>::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));
}
}
3 changes: 3 additions & 0 deletions rustler/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}
Expand Down
1 change: 1 addition & 0 deletions rustler_tests/lib/rustler_test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions rustler_tests/native/rustler_test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions rustler_tests/native/rustler_test/src/test_primitives.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use rustler::ErlOption;

#[rustler::nif]
pub fn add_u32(a: u32, b: u32) -> u32 {
a + b
Expand All @@ -18,6 +20,11 @@ pub fn option_inc(opt: Option<f64>) -> Option<f64> {
opt.map(|num| num + 1.0)
}

#[rustler::nif]
pub fn erlang_option_inc(opt: ErlOption<f64>) -> ErlOption<f64> {
opt.as_ref().map(|num| num + 1.0).into()
}

#[rustler::nif]
pub fn result_to_int(res: Result<bool, &str>) -> Result<usize, String> {
match res {
Expand Down
6 changes: 6 additions & 0 deletions rustler_tests/test/primitives_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down

0 comments on commit d8aa66d

Please sign in to comment.