Skip to content

Commit d8aa66d

Browse files
authored
Merge pull request #507 from tatsuya6502/erl-option
rustler: add a Rust type `ErlOption<T>`
2 parents 90e4ef8 + 67d614a commit d8aa66d

File tree

9 files changed

+244
-1
lines changed

9 files changed

+244
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ See [`UPGRADE.md`](./UPGRADE.md) for additional help when upgrading to newer ver
99

1010
## [unreleased]
1111

12+
## Added
13+
14+
* `ErlOption<T>` to provide an ergonomic option type for Erlang (#507, thanks @tatsuya6502)
15+
1216
### Changed
1317

1418
* Use Cargo features to define the NIF version level (#537), deprecating

rustler/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ mod term;
3838

3939
pub use crate::term::Term;
4040
pub use crate::types::{
41-
Atom, Binary, Decoder, Encoder, ListIterator, LocalPid, MapIterator, NewBinary, OwnedBinary,
41+
Atom, Binary, Decoder, Encoder, ErlOption, ListIterator, LocalPid, MapIterator, NewBinary,
42+
OwnedBinary,
4243
};
4344
pub mod resource;
4445
pub use crate::resource::ResourceArc;

rustler/src/types/atom.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ atoms! {
271271
/// The `nil` atom.
272272
nil,
273273

274+
/// The `undefined` atom, commonly used in Erlang libraries to express the
275+
/// absence of value.
276+
undefined,
277+
274278
/// The `ok` atom, commonly used in success tuples.
275279
ok,
276280

rustler/src/types/erlang_option.rs

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
use super::atom;
2+
use crate::{Decoder, Encoder, Env, Error, NifResult, Term};
3+
4+
use std::ops::{Deref, DerefMut};
5+
6+
/// A wrapper type for [`Option<T>`][option] to provide Erlang style encoding. It
7+
/// uses `undefined` atom instead of `nil` when the enclosing value is `None`.
8+
///
9+
/// Useful for interacting with Erlang libraries as `undefined` is commonly used in
10+
/// Erlang to represent the absence of a value.
11+
///
12+
/// [option]: https://doc.rust-lang.org/stable/core/option/enum.Option.html
13+
///
14+
/// # Examples
15+
///
16+
/// `ErlOption<T>` provides methods to convert to/from `Option<T>`.
17+
///
18+
/// ```rust
19+
/// use rustler::ErlOption;
20+
///
21+
/// // Create new `ErlOption<i32>` values via convenient functions.
22+
/// let _ = ErlOption::some(1); // Wraps `Some(1)`.
23+
/// let _ = ErlOption::<i32>::none();
24+
///
25+
/// // Convert Option<i32> values to ErlOption<i32> values.
26+
/// let _ = ErlOption::from(Some(2));
27+
/// let _: ErlOption<_> = Some(3).into();
28+
/// let _: ErlOption<i32> = None.into();
29+
///
30+
/// // Get a reference of enclosing Option<T> from an ErlOption<T>.
31+
/// let _: &Option<i32> = ErlOption::some(4).as_ref();
32+
///
33+
/// // Get a mutable reference of enclosing Option<T> from an ErlOption<T>.
34+
/// let _: &mut Option<i32> = ErlOption::some(5).as_mut();
35+
///
36+
/// // Convert an ErlOption<i32> value to an Option<i32> value.
37+
/// let _: Option<i32> = ErlOption::some(6).into();
38+
///
39+
/// // Compare ErlOption<T> with Option<T>.
40+
/// assert_eq!(ErlOption::some(7), Some(7));
41+
/// assert!(ErlOption::some(8) > Some(7));
42+
///
43+
/// // Call Option<T>'s methods on an ErlOption<T> via Deref and DerefMut.
44+
/// assert!(ErlOption::some(9).is_some());
45+
/// assert_eq!(ErlOption::some(10).unwrap(), 10);
46+
/// assert_eq!(ErlOption::some(12).map(|v| v + 1), ErlOption::some(13));
47+
/// ```
48+
///
49+
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
50+
pub struct ErlOption<T>(Option<T>);
51+
52+
impl<T> ErlOption<T> {
53+
/// A convenience function to create an `ErlOption<T>` from a `Some(T)` value.
54+
pub fn some(v: T) -> Self {
55+
Some(v).into()
56+
}
57+
58+
/// A convenience function to create an `ErlOption<T>` enclosing the `None`
59+
/// value.
60+
pub fn none() -> Self {
61+
Default::default()
62+
}
63+
}
64+
65+
// NOTE: Manually implement the Default instead of deriving it. This is because
66+
// deriving requires `T` to be `Default` as well, but we do not need that.
67+
impl<T> Default for ErlOption<T> {
68+
fn default() -> Self {
69+
Self(None)
70+
}
71+
}
72+
73+
impl<T> From<Option<T>> for ErlOption<T> {
74+
fn from(v: Option<T>) -> Self {
75+
ErlOption(v)
76+
}
77+
}
78+
79+
impl<T> From<ErlOption<T>> for Option<T> {
80+
fn from(v: ErlOption<T>) -> Self {
81+
v.0
82+
}
83+
}
84+
85+
impl<T> AsMut<Option<T>> for ErlOption<T> {
86+
fn as_mut(&mut self) -> &mut Option<T> {
87+
&mut self.0
88+
}
89+
}
90+
91+
impl<T> AsRef<Option<T>> for ErlOption<T> {
92+
fn as_ref(&self) -> &Option<T> {
93+
&self.0
94+
}
95+
}
96+
97+
impl<T> Deref for ErlOption<T> {
98+
type Target = Option<T>;
99+
100+
fn deref(&self) -> &Self::Target {
101+
&self.0
102+
}
103+
}
104+
105+
impl<T> DerefMut for ErlOption<T> {
106+
fn deref_mut(&mut self) -> &mut Self::Target {
107+
&mut self.0
108+
}
109+
}
110+
111+
impl<T> PartialEq<Option<T>> for ErlOption<T>
112+
where
113+
T: PartialEq,
114+
{
115+
fn eq(&self, other: &Option<T>) -> bool {
116+
&self.0 == other
117+
}
118+
}
119+
120+
impl<T> PartialEq<ErlOption<T>> for Option<T>
121+
where
122+
T: PartialEq,
123+
{
124+
fn eq(&self, other: &ErlOption<T>) -> bool {
125+
self == &other.0
126+
}
127+
}
128+
129+
impl<T> PartialOrd<Option<T>> for ErlOption<T>
130+
where
131+
T: PartialOrd,
132+
{
133+
fn partial_cmp(&self, other: &Option<T>) -> Option<std::cmp::Ordering> {
134+
self.0.partial_cmp(other)
135+
}
136+
}
137+
138+
impl<T> PartialOrd<ErlOption<T>> for Option<T>
139+
where
140+
T: PartialOrd,
141+
{
142+
fn partial_cmp(&self, other: &ErlOption<T>) -> Option<std::cmp::Ordering> {
143+
self.partial_cmp(&other.0)
144+
}
145+
}
146+
147+
impl<T> Encoder for ErlOption<T>
148+
where
149+
T: Encoder,
150+
{
151+
fn encode<'c>(&self, env: Env<'c>) -> Term<'c> {
152+
match self.0 {
153+
Some(ref value) => value.encode(env),
154+
None => atom::undefined().encode(env),
155+
}
156+
}
157+
}
158+
159+
impl<'a, T> Decoder<'a> for ErlOption<T>
160+
where
161+
T: Decoder<'a>,
162+
{
163+
fn decode(term: Term<'a>) -> NifResult<Self> {
164+
if let Ok(term) = term.decode::<T>() {
165+
Ok(Self(Some(term)))
166+
} else {
167+
let decoded_atom: atom::Atom = term.decode()?;
168+
if decoded_atom == atom::undefined() {
169+
Ok(Self(None))
170+
} else {
171+
Err(Error::BadArg)
172+
}
173+
}
174+
}
175+
}
176+
177+
#[cfg(test)]
178+
mod test {
179+
use super::ErlOption;
180+
181+
#[test]
182+
fn test_creations() {
183+
assert_eq!(ErlOption::some(1).as_ref(), &Some(1));
184+
assert_eq!(ErlOption::<i32>::none().as_ref(), &None as &Option<i32>);
185+
}
186+
187+
#[test]
188+
fn test_conversions() {
189+
// Convert Option<i32> values to ErlOption<i32> values.
190+
assert_eq!(ErlOption::from(Some(2)), ErlOption::some(2));
191+
assert_eq!(Into::<ErlOption<i32>>::into(Some(3)), ErlOption::some(3));
192+
assert_eq!(Into::<ErlOption<i32>>::into(None), ErlOption::none());
193+
194+
// Convert an ErlOption<i32> value to an Option<i32> value.
195+
assert_eq!(Into::<Option<i32>>::into(ErlOption::some(6)), Some(6));
196+
}
197+
198+
#[test]
199+
fn test_as_ref() {
200+
assert_eq!(ErlOption::some(4).as_ref(), &Some(4));
201+
assert_eq!(ErlOption::some(5).as_mut(), &mut Some(5));
202+
}
203+
204+
#[test]
205+
fn test_compare() {
206+
assert_eq!(ErlOption::some(7), Some(7));
207+
assert!(ErlOption::some(8) > Some(7));
208+
}
209+
210+
#[test]
211+
fn test_deref() {
212+
assert!(ErlOption::some(9).is_some());
213+
assert_eq!(ErlOption::some(10).unwrap(), 10);
214+
assert_eq!(ErlOption::some(12).map(|v| v + 1), ErlOption::some(13));
215+
}
216+
}

rustler/src/types/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ pub mod truthy;
3737

3838
pub mod elixir_struct;
3939

40+
pub mod erlang_option;
41+
pub use self::erlang_option::ErlOption;
42+
4043
pub trait Encoder {
4144
fn encode<'a>(&self, env: Env<'a>) -> Term<'a>;
4245
}

rustler_tests/lib/rustler_test.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ defmodule RustlerTest do
2525
def add_i32(_, _), do: err()
2626
def echo_u8(_), do: err()
2727
def option_inc(_), do: err()
28+
def erlang_option_inc(_), do: err()
2829
def result_to_int(_), do: err()
2930

3031
def sum_list(_), do: err()

rustler_tests/native/rustler_test/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ rustler::init!(
2121
test_primitives::add_i32,
2222
test_primitives::echo_u8,
2323
test_primitives::option_inc,
24+
test_primitives::erlang_option_inc,
2425
test_primitives::result_to_int,
2526
test_list::sum_list,
2627
test_list::make_list,

rustler_tests/native/rustler_test/src/test_primitives.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use rustler::ErlOption;
2+
13
#[rustler::nif]
24
pub fn add_u32(a: u32, b: u32) -> u32 {
35
a + b
@@ -18,6 +20,11 @@ pub fn option_inc(opt: Option<f64>) -> Option<f64> {
1820
opt.map(|num| num + 1.0)
1921
}
2022

23+
#[rustler::nif]
24+
pub fn erlang_option_inc(opt: ErlOption<f64>) -> ErlOption<f64> {
25+
opt.as_ref().map(|num| num + 1.0).into()
26+
}
27+
2128
#[rustler::nif]
2229
pub fn result_to_int(res: Result<bool, &str>) -> Result<usize, String> {
2330
match res {

rustler_tests/test/primitives_test.exs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ defmodule RustlerTest.PrimitivesTest do
2020
assert_raise ArgumentError, fn -> RustlerTest.option_inc("hello") end
2121
end
2222

23+
test "erlang option decoding and encoding" do
24+
assert 33.0 == RustlerTest.erlang_option_inc(32.0)
25+
assert :undefined == RustlerTest.erlang_option_inc(:undefined)
26+
assert_raise ArgumentError, fn -> RustlerTest.erlang_option_inc("hello") end
27+
end
28+
2329
test "result decoding and encoding" do
2430
assert {:ok, 1} == RustlerTest.result_to_int({:ok, true})
2531
assert {:ok, 0} == RustlerTest.result_to_int({:ok, false})

0 commit comments

Comments
 (0)