From 2111550b1f860a176384d517fb8e8e2fb6a527e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Tue, 3 Nov 2026 13:56:18 +0100 Subject: [PATCH] Add try_decode and make decoders return errors instead of throwing Assisted-By: Claude Opus 5.5 --- CHANGELOG.md | 10 + README.md | 24 ++- c_include/fine.hpp | 410 +++++++++++++++++++++++++------------- test/c_src/finest.cpp | 33 +++ test/lib/finest/nif.ex | 4 + test/test/finest_test.exs | 25 +++ 6 files changed, 360 insertions(+), 146 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ca72e5..c8f38a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## Unreleased + +### Added + +- Added `fine::try_decode`, which does not throw when the type does not match. + +### Changed + +- **(Breaking)** Changed the `Decoder` interface to require `try_decode` instead of `decode`. Custom decoders must return `fine::DecodeResult`. + ## [v0.1.6](https://github.com/elixir-nx/fine/tree/v0.1.6) (2026-04-09) ### Fixed diff --git a/README.md b/README.md index 59e952b..1d3b12d 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,22 @@ auto term = fine::encode(env, message); auto message = fine::decode(env, term); ``` +`fine::decode` throws `std::invalid_argument` if the term cannot be +decoded. Use `fine::try_decode` to get a `fine::DecodeResult` instead: + +```c++ +auto result = fine::try_decode(env, term); +if (result) { + auto value = std::move(result).value(); +} else { + auto message = result.error().message(env); +} +``` + +The result contains either a value or a `fine::DecodeError`. +`DecodeError::message(env)` formats the error, including the failing term. +Other exceptions, such as allocation errors, still propagate. + Fine provides implementations for the following types: | C++ Type | Encoder | Decoder | Elixir Type | @@ -216,8 +232,9 @@ the following specializations: // Note that the specialization must be defined in the `fine` namespace. namespace fine { template <> struct Decoder { - static MyType decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - // ... + static DecodeResult + try_decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + // Return MyType or DecodeError{description, term}. } }; @@ -229,6 +246,9 @@ namespace fine { } ``` +Return `DecodeError{description, term}` for invalid input. Omit `term` to +use the description as the complete message. + ## Resource objects Resource objects is a mechanism for passing pointers to C++ data diff --git a/c_include/fine.hpp b/c_include/fine.hpp index cabe575..3e38956 100644 --- a/c_include/fine.hpp +++ b/c_include/fine.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -38,8 +39,66 @@ namespace fine { +// An error caused by a term that cannot be decoded as the requested type. +// +// The description is owned. The optional failing term belongs to its Erlang +// environment and must stay within that environment's lifetime and thread. +class DecodeError { +public: + // Creates an error with a description and an optional failing term. + explicit DecodeError(std::string message, + std::optional term = std::nullopt) + : message_(std::move(message)), term_(term) {} + + // Returns the description, appending the formatted failing term if present. + // + // The stored term must still be valid in env. The returned string owns + // its data and can outlive the environment. + std::string message(ErlNifEnv *env) const; + +private: + std::string message_; + std::optional term_; +}; + +// The result of decoding a term, containing either a value or a DecodeError. +template class DecodeResult { + using Value = std::remove_cv_t; + +public: + // Creates a successful result with the given value. + DecodeResult(Value value) : value_(std::move(value)) {} + // Creates a failed result with the given error. + DecodeResult(DecodeError error) : error_(std::move(error)) {} + + // Returns true if the result contains a decoded value. + explicit operator bool() const noexcept { return value_.has_value(); } + + // Returns the decoded value. + // + // Throws std::bad_optional_access if the result contains an error. + Value &value() & { return value_.value(); } + const Value &value() const & { return value_.value(); } + Value &&value() && { return std::move(value_).value(); } + + // Returns the decoding error. + // + // Throws std::bad_optional_access if the result contains a value. + DecodeError &error() & { return error_.value(); } + const DecodeError &error() const & { return error_.value(); } + DecodeError &&error() && { return std::move(error_).value(); } + +private: + std::optional value_; + std::optional error_; +}; + // Forward declarations +template +DecodeResult> try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term); + template T decode(ErlNifEnv *env, const ERL_NIF_TERM &term); template ERL_NIF_TERM encode(ErlNifEnv *env, const T &value); @@ -400,11 +459,36 @@ inline std::string format_term(ErlNifEnv *, ERL_NIF_TERM term, return buffer; } +inline std::string DecodeError::message(ErlNifEnv *env) const { + if (term_) { + return message_ + ", got: " + format_term(env, *term_); + } + return message_; +} + +// Decodes the given Erlang term as a value of the specified type. +// +// Returns DecodeError if the term cannot be decoded. Other exceptions, +// such as allocation errors, still propagate. +// +// The given type must have a specialized Decoder implementation with +// try_decode(). +template +DecodeResult> try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { + return Decoder>::try_decode(env, term); +} + // Decodes the given Erlang term as a value of the specified type. // -// The given type must have a specialized Decoder implementation. +// The given type must have a specialized Decoder implementation with +// try_decode(). Throws std::invalid_argument if the term cannot be decoded. template T decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - return Decoder>::decode(env, term); + auto result = try_decode(env, term); + if (!result) { + throw std::invalid_argument(result.error().message(env)); + } + return std::move(result).value(); } // Encodes the given value as a Erlang term. @@ -433,49 +517,49 @@ template struct Decoder {}; template struct Encoder {}; template <> struct Decoder { - static Term decode(ErlNifEnv *, const ERL_NIF_TERM &term) { + static DecodeResult try_decode(ErlNifEnv *, const ERL_NIF_TERM &term) { return Term(term); } }; template <> struct Decoder { - static int64_t decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + static DecodeResult try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { int64_t integer; if (!enif_get_int64(env, term, reinterpret_cast(&integer))) { - throw std::invalid_argument("decode failed, expected an integer, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected an integer", term}; } return integer; } }; template <> struct Decoder { - static uint64_t decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + static DecodeResult try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { uint64_t integer; if (!enif_get_uint64(env, term, reinterpret_cast(&integer))) { - throw std::invalid_argument( - "decode failed, expected an unsigned integer, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected an unsigned integer", term}; } return integer; } }; template <> struct Decoder { - static double decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + static DecodeResult try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { double number; if (!enif_get_double(env, term, &number)) { - throw std::invalid_argument("decode failed, expected a float, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected a float", term}; } return number; } }; template <> struct Decoder { - static bool decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + static DecodeResult try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { char atom_string[6]; auto length = enif_get_atom(env, term, atom_string, 6, ERL_NIF_LATIN1); @@ -487,43 +571,47 @@ template <> struct Decoder { return false; } - throw std::invalid_argument("decode failed, expected a boolean, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected a boolean", term}; } }; template <> struct Decoder { - static ErlNifPid decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + static DecodeResult try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { ErlNifPid pid; if (!enif_is_pid(env, term)) { - throw std::invalid_argument("decode failed, expected a local pid, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected a local pid", term}; } if (!enif_get_local_pid(env, term, &pid)) { // If the term is a PID and it is not local, it means it's a remote PID. - throw std::invalid_argument("decode failed, expected a local pid, but " - "got a remote one. NIFs can " - "only send messages to local PIDs and " - "remote PIDs cannot be decoded"); + return DecodeError{"decode failed, expected a local pid, but " + "got a remote one. NIFs can " + "only send messages to local PIDs and " + "remote PIDs cannot be decoded"}; } return pid; } }; template <> struct Decoder { - static ErlNifBinary decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + static DecodeResult try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { ErlNifBinary binary; if (!enif_inspect_binary(env, term, &binary)) { - throw std::invalid_argument("decode failed, expected a binary, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected a binary", term}; } return binary; } }; template <> struct Decoder { - static std::string_view decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - auto binary = fine::decode(env, term); + static DecodeResult try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { + auto result = fine::try_decode(env, term); + if (!result) { + return std::move(result).error(); + } + auto &binary = result.value(); return std::string_view(reinterpret_cast(binary.data), binary.size); } @@ -533,17 +621,22 @@ template struct Decoder, Alloc>> { using string = std::basic_string, Alloc>; - static string decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - return string(fine::decode(env, term)); + static DecodeResult try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { + auto result = fine::try_decode(env, term); + if (!result) { + return std::move(result).error(); + } + return string(result.value()); } }; template <> struct Decoder { - static Atom decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + static DecodeResult try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { unsigned int length; if (!enif_get_atom_length(env, term, &length, FINE_ERL_NIF_CHAR_ENCODING)) { - throw std::invalid_argument("decode failed, expected an atom, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected an atom", term}; } auto buffer = std::make_unique(length + 1); @@ -551,8 +644,7 @@ template <> struct Decoder { // Note that enif_get_atom writes the NULL byte at the end if (!enif_get_atom(env, term, buffer.get(), length + 1, FINE_ERL_NIF_CHAR_ENCODING)) { - throw std::invalid_argument("decode failed, expected an atom, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected an atom", term}; } return Atom(std::string(buffer.get(), length)); @@ -560,100 +652,106 @@ template <> struct Decoder { }; template struct Decoder> { - static std::optional decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + static DecodeResult> try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { char atom_string[4]; - if (enif_get_atom(env, term, atom_string, 4, ERL_NIF_LATIN1) == 4) { - if (strcmp(atom_string, "nil") == 0) { - return std::nullopt; - } + if (enif_get_atom(env, term, atom_string, 4, ERL_NIF_LATIN1) == 4 && + strcmp(atom_string, "nil") == 0) { + return std::optional(); } - return fine::decode(env, term); + auto result = fine::try_decode(env, term); + if (!result) { + return std::move(result).error(); + } + return std::optional(std::move(result).value()); } }; template struct Decoder> { - static std::variant decode(ErlNifEnv *env, - const ERL_NIF_TERM &term) { - return do_decode(env, term); + using Variant = std::variant; + + static DecodeResult try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { + return do_decode<0>(env, term); } private: - template - static std::variant do_decode(ErlNifEnv *env, + template + static DecodeResult do_decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - try { - return fine::decode(env, term); - } catch (const std::invalid_argument &) { - if constexpr (sizeof...(Rest) > 0) { - return do_decode(env, term); - } else { - throw std::invalid_argument( - "decode failed, none of the variant types could be decoded, got: " + - format_term(env, term)); + if constexpr (Index == sizeof...(Args)) { + return DecodeError{ + "decode failed, none of the variant types could be decoded", term}; + } else { + using T = std::variant_alternative_t; + auto result = fine::try_decode(env, term); + if (result) { + return Variant(std::in_place_index, std::move(result).value()); } + return do_decode(env, term); } } }; template struct Decoder> { - static std::tuple decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - constexpr auto expected_size = sizeof...(Args); + using Tuple = std::tuple; + static DecodeResult try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { + constexpr auto expected_size = sizeof...(Args); int size; const ERL_NIF_TERM *terms; if (!enif_get_tuple(env, term, &size, &terms)) { - throw std::invalid_argument("decode failed, expected a tuple, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected a tuple", term}; } - if (size != expected_size) { - throw std::invalid_argument("decode failed, expected tuple to have " + - std::to_string(expected_size) + - " elements, but had " + std::to_string(size) + - ", got: " + format_term(env, term)); + return DecodeError{"decode failed, expected tuple to have " + + std::to_string(expected_size) + + " elements, but had " + std::to_string(size), + term}; } - - return do_decode(env, terms, std::make_index_sequence()); + return do_decode<0>(env, terms); } private: - template - static std::tuple do_decode(ErlNifEnv *env, - const ERL_NIF_TERM *terms, - std::index_sequence) { - return std::make_tuple(fine::decode(env, terms[Indices])...); + template + static DecodeResult + do_decode(ErlNifEnv *env, const ERL_NIF_TERM *terms, Values &&...values) { + if constexpr (Index == sizeof...(Args)) { + return Tuple(std::forward(values)...); + } else { + using T = std::tuple_element_t; + auto result = fine::try_decode(env, terms[Index]); + if (!result) { + return std::move(result).error(); + } + return do_decode(env, terms, std::forward(values)..., + std::move(result).value()); + } } }; template struct Decoder> { - static std::pair decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { - int size; - const ERL_NIF_TERM *terms; - if (!enif_get_tuple(env, term, &size, &terms)) { - throw std::invalid_argument("decode failed, expected a tuple, got: " + - format_term(env, term)); - } - - if (size != 2) { - throw std::invalid_argument( - "decode failed, expected tuple to have 2 elements, but had " + - std::to_string(size) + ", got: " + format_term(env, term)); + static DecodeResult> try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { + auto result = fine::try_decode>(env, term); + if (!result) { + return std::move(result).error(); } - - return std::make_pair(fine::decode(env, terms[0]), - fine::decode(env, terms[1])); + auto &tuple = result.value(); + return std::pair(std::move(std::get<0>(tuple)), + std::move(std::get<1>(tuple))); } }; template struct Decoder> { - static std::vector decode(ErlNifEnv *env, - const ERL_NIF_TERM &term) { + static DecodeResult> + try_decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { unsigned int length; if (!enif_get_list_length(env, term, &length)) { - throw std::invalid_argument("decode failed, expected a list, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected a list", term}; } std::vector vector; @@ -663,8 +761,11 @@ template struct Decoder> { ERL_NIF_TERM head, tail; while (enif_get_list_cell(env, list, &head, &tail)) { - auto elem = fine::decode(env, head); - vector.emplace_back(std::move(elem)); + auto elem_result = fine::try_decode(env, head); + if (!elem_result) { + return std::move(elem_result).error(); + } + vector.emplace_back(std::move(elem_result).value()); list = tail; } @@ -674,26 +775,32 @@ template struct Decoder> { template struct Decoder> { - static std::map decode(ErlNifEnv *env, - const ERL_NIF_TERM &term) { + static DecodeResult> + try_decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { std::map map; ERL_NIF_TERM key_term, value_term; ErlNifMapIterator iter; if (!enif_map_iterator_create(env, term, &iter, ERL_NIF_MAP_ITERATOR_FIRST)) { - throw std::invalid_argument("decode failed, expected a map, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected a map", term}; } // Define RAII cleanup for the iterator auto cleanup = IterCleanup{env, iter}; while (enif_map_iterator_get_pair(env, &iter, &key_term, &value_term)) { - auto key = fine::decode(env, key_term); - auto value = fine::decode(env, value_term); + auto key_result = fine::try_decode(env, key_term); + if (!key_result) { + return std::move(key_result).error(); + } + auto value_result = fine::try_decode(env, value_term); + if (!value_result) { + return std::move(value_result).error(); + } - map.insert_or_assign(std::move(key), std::move(value)); + map.insert_or_assign(std::move(key_result).value(), + std::move(value_result).value()); enif_map_iterator_next(env, &iter); } @@ -712,26 +819,32 @@ struct Decoder> { template struct Decoder> { - static std::unordered_map - decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + static DecodeResult> + try_decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { std::unordered_map map; ERL_NIF_TERM key_term, value_term; ErlNifMapIterator iter; if (!enif_map_iterator_create(env, term, &iter, ERL_NIF_MAP_ITERATOR_FIRST)) { - throw std::invalid_argument("decode failed, expected a map, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected a map", term}; } // Define RAII cleanup for the iterator auto cleanup = IterCleanup{env, iter}; while (enif_map_iterator_get_pair(env, &iter, &key_term, &value_term)) { - auto key = fine::decode(env, key_term); - auto value = fine::decode(env, value_term); + auto key_result = fine::try_decode(env, key_term); + if (!key_result) { + return std::move(key_result).error(); + } + auto value_result = fine::try_decode(env, value_term); + if (!value_result) { + return std::move(value_result).error(); + } - map.insert_or_assign(std::move(key), std::move(value)); + map.insert_or_assign(std::move(key_result).value(), + std::move(value_result).value()); enif_map_iterator_next(env, &iter); } @@ -750,13 +863,12 @@ struct Decoder> { template struct Decoder> { - static std::multimap decode(ErlNifEnv *env, - const ERL_NIF_TERM &term) { + static DecodeResult> + try_decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { unsigned int length; if (!enif_get_list_length(env, term, &length)) { - throw std::invalid_argument("decode failed, expected a list, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected a list", term}; } std::multimap map; @@ -765,9 +877,11 @@ struct Decoder> { ERL_NIF_TERM head, tail; while (enif_get_list_cell(env, list, &head, &tail)) { - auto entry = fine::decode>(env, head); - - map.emplace(std::move(entry)); + auto entry_result = fine::try_decode>(env, head); + if (!entry_result) { + return std::move(entry_result).error(); + } + map.emplace(std::move(entry_result).value()); list = tail; } @@ -778,13 +892,12 @@ struct Decoder> { template struct Decoder> { - static std::unordered_multimap - decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + static DecodeResult> + try_decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { unsigned int length; if (!enif_get_list_length(env, term, &length)) { - throw std::invalid_argument("decode failed, expected a list, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected a list", term}; } std::unordered_multimap map; @@ -793,9 +906,11 @@ struct Decoder> { ERL_NIF_TERM head, tail; while (enif_get_list_cell(env, list, &head, &tail)) { - auto entry = fine::decode>(env, head); - - map.emplace(std::move(entry)); + auto entry_result = fine::try_decode>(env, head); + if (!entry_result) { + return std::move(entry_result).error(); + } + map.emplace(std::move(entry_result).value()); list = tail; } @@ -805,14 +920,13 @@ struct Decoder> { }; template struct Decoder> { - static ResourcePtr decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + static DecodeResult> try_decode(ErlNifEnv *env, + const ERL_NIF_TERM &term) { void *ptr; auto type = ResourcePtr::resource_type; if (!enif_get_resource(env, term, type, &ptr)) { - throw std::invalid_argument( - "decode failed, expected a resource reference, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected a resource reference", term}; } enif_keep_resource(ptr); @@ -824,50 +938,58 @@ template struct Decoder> { template struct Decoder> { - static T decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { + static DecodeResult try_decode(ErlNifEnv *env, const ERL_NIF_TERM &term) { ERL_NIF_TERM struct_value; if (!enif_get_map_value(env, term, encode(env, __private__::atoms::__struct__), &struct_value)) { - throw std::invalid_argument("decode failed, expected a struct, got: " + - format_term(env, term)); + return DecodeError{"decode failed, expected a struct", term}; } // Make sure __struct__ matches const auto &struct_atom = *T::module; if (enif_compare(struct_value, encode(env, struct_atom)) != 0) { - throw std::invalid_argument("decode failed, expected a " + - struct_atom.to_string() + - " struct, got: " + format_term(env, term)); + return DecodeError{"decode failed, expected a " + + struct_atom.to_string() + " struct", + term}; } - T ex_struct; - + T ex_struct{}; + std::optional error; constexpr auto fields = T::fields(); - std::apply( [&](auto... field) { - (set_field(env, term, ex_struct, std::get<0>(field), - std::get<1>(field)), - ...); + return (set_field(env, term, ex_struct, std::get<0>(field), + std::get<1>(field), error) && + ...); }, fields); - - return ex_struct; + if (error) { + return std::move(*error); + } + return std::move(ex_struct); } private: template - static void set_field(ErlNifEnv *env, ERL_NIF_TERM term, T &ex_struct, - U T::*field_ptr, const Atom *atom) { + static bool set_field(ErlNifEnv *env, ERL_NIF_TERM term, T &ex_struct, + U T::*field_ptr, const Atom *atom, + std::optional &error) { ERL_NIF_TERM value; if (!enif_get_map_value(env, term, encode(env, *atom), &value)) { - throw std::invalid_argument( - "decode failed, expected the struct to have " + atom->to_string() + - " field, got: " + format_term(env, term)); + error = DecodeError{"decode failed, expected the struct to have " + + atom->to_string() + " field", + term}; + return false; } - ex_struct.*(field_ptr) = fine::decode(env, value); + auto result = fine::try_decode(env, value); + if (!result) { + error = std::move(result).error(); + return false; + } + ex_struct.*field_ptr = std::move(result).value(); + return true; } }; diff --git a/test/c_src/finest.cpp b/test/c_src/finest.cpp index d818493..e6e2b29 100644 --- a/test/c_src/finest.cpp +++ b/test/c_src/finest.cpp @@ -83,6 +83,32 @@ struct ExError { template using Result = std::variant, fine::Error>; +template struct FailingAllocator { + using value_type = T; + T *allocate(std::size_t) { throw std::bad_alloc(); } + void deallocate(T *, std::size_t) {} +}; + +Result try_decode_int64(ErlNifEnv *env, fine::Term term) { + auto result = fine::try_decode(env, term); + if (!result) { + return fine::Error(result.error().message(env)); + } + return fine::Ok(result.value()); +} +FINE_NIF(try_decode_int64, 0); + +bool try_decode_allocation_error(ErlNifEnv *env, fine::Term term) { + using Vector = std::vector>; + try { + fine::try_decode>(env, term); + return false; + } catch (const std::bad_alloc &) { + return true; + } +} +FINE_NIF(try_decode_allocation_error, 0); + int64_t add(ErlNifEnv *, int64_t x, int64_t y) { return x + y; } FINE_NIF(add, 0); @@ -149,6 +175,13 @@ codec_variant_int64_or_string(ErlNifEnv *, } FINE_NIF(codec_variant_int64_or_string, 0); +using TupleVariant = std::variant, + std::tuple>; +TupleVariant codec_variant_tuples(ErlNifEnv *, TupleVariant value) { + return value; +} +FINE_NIF(codec_variant_tuples, 0); + std::tuple codec_tuple_int64_and_string(ErlNifEnv *, std::tuple term) { diff --git a/test/lib/finest/nif.ex b/test/lib/finest/nif.ex index bef82dd..82d4856 100644 --- a/test/lib/finest/nif.ex +++ b/test/lib/finest/nif.ex @@ -12,6 +12,9 @@ defmodule Finest.NIF do end end + def try_decode_int64(_term), do: err!() + def try_decode_allocation_error(_term), do: err!() + def add(_x, _y), do: err!() def codec_term(_term), do: err!() @@ -28,6 +31,7 @@ defmodule Finest.NIF do def codec_nullopt(), do: err!() def codec_optional_int64(_term), do: err!() def codec_variant_int64_or_string(_term), do: err!() + def codec_variant_tuples(_term), do: err!() def codec_tuple_int64_and_string(_term), do: err!() def codec_result_string_int64_ok(_term), do: err!() def codec_result_string_int64_error(_term), do: err!() diff --git a/test/test/finest_test.exs b/test/test/finest_test.exs index 9714564..e434632 100644 --- a/test/test/finest_test.exs +++ b/test/test/finest_test.exs @@ -7,6 +7,21 @@ defmodule FinestTest do assert NIF.add(1, 2) == 3 end + describe "try_decode" do + test "returns values and formats errors on request" do + assert NIF.try_decode_int64(42) == {:ok, 42} + + assert NIF.try_decode_int64(:hello) == + {:error, "decode failed, expected an integer, got: hello"} + end + + test "propagates allocation errors instead of trying another alternative" do + assert NIF.try_decode_allocation_error([1]) + assert NIF.try_decode_allocation_error(["value"]) + refute NIF.try_decode_allocation_error(:not_a_list) + end + end + describe "codec" do test "term" do assert NIF.codec_term(10) == 10 @@ -126,6 +141,10 @@ defmodule FinestTest do fn -> NIF.codec_variant_int64_or_string(10.0) end + + assert NIF.codec_variant_tuples({42, "value"}) == {42, "value"} + assert NIF.codec_variant_tuples({"value", 42}) == {"value", 42} + assert_raise ArgumentError, fn -> NIF.codec_variant_tuples({:invalid, 42}) end end test "tuple" do @@ -333,6 +352,12 @@ defmodule FinestTest do fn -> NIF.codec_struct(~D"2000-01-01") end + + assert_raise ArgumentError, + ~s(decode failed, expected an integer, got: <<"invalid">>), + fn -> + NIF.codec_struct(%Finest.Point{x: "invalid", y: 2}) + end end test "exception struct" do