Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

rustler: add a Rust type ErlOption<T> #507

Merged
merged 2 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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