Skip to content

Commit

Permalink
Revamped code to use results lib.
Browse files Browse the repository at this point in the history
  • Loading branch information
oubiwann committed Jun 9, 2021
1 parent d152b4b commit a1fd36b
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 195 deletions.
66 changes: 36 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@

## About

Coers is a very small library to provide small coercion
on primitive types in Erlang. This library was built
essentially for internal tools at derniercri.io
Coers is a very small library to provide small coercion on primitive types in Erlang.
This library was built essentially for internal tools at derniercri.io

## Build & Test

Expand All @@ -26,47 +25,54 @@ assessment.

## Usage

Each coercion is wrapped into a special record:
Each coercion is wrapped into a [special record](https://github.com/erlsci/results/blob/main/include/results.hrl):

```erlang
-record(result, {
succeeded :: boolean(),
value :: term()
value :: term(),
error :: term(),
}).
```

If a coercion fail, the `value` member is assigned with a default value and the `succeed`
member is `false`. If the coersion succeed, the `value` member becomes the coerced data and the
`succeed` member becomes `true`.
If a coercion fail, the `value` field is undefined and the `error` field is populated with
an appropriate error. If the coersion succeed, the `value` field becomes the coerced data and the
`error` field is undefined.

You can use these 3 combinators to have information about coercion status :
You can use these functions from the coers API to examine the coercion status:

- `-spec succeed(result()) -> boolean().`
- `-spec fail(result()) -> boolean().`
- `-spec value(result()) -> term().`
- `coers:value(Result)`
- `coers:error(Result)`
- `coers:has_error(Result)`

For example :

```erlang
1> X = coers:to_int("10").
{result,true,10}
2> Y = coers:to_int("foo").
{result,false,0}
3> [coers:succeed(X), coers:succeed(Y), coers:fail(X), coers:fail(Y)].
[true,false,false,true]
4> [coers:value(X), coers:value(Y)].
[10,0]
1> R1 = coers:to_int("10").
{result,10,undefined}
2> R2 = coers:to_int("foo").
{result,undefined,{badarg,"Could not convert \"foo\" (type any) to int"}}
```

Note that, via the `rational` Erlang library, fractions are supported:
Additional convenience functions are available via the [results library](https://github.com/erlsci/results):

``` erlang
5> coers:to_rational("1/42").
{result,true,{fraction,1,42}}
6> coers:to_rational(<<"1/42">>).
{result,true,{fraction,1,42}}
7> coers:to_rational({1, 42}).
{result,true,{fraction,1,42}}
3> results:has_values([R1, R2]).
[true,false]
4> results:has_errors([R1, R2]).
[false,true]
5> results:values([R1, R2]).
[10,undefined]
```

Note that fractions are supported (via the [rationals](https://github.com/erlsci/rationals) Erlang library):

``` erlang
6> coers:to_rational("1/42").
{result,{fraction,1,42},undefined}
7> coers:to_rational(<<"1/42">>).
{result,{fraction,1,42},undefined}
8> coers:to_rational({1, 42}).
{result,{fraction,1,42},undefined}
```

Example usgage in LFE:
Expand All @@ -92,9 +98,9 @@ Example usgage in LFE:

MIT

Copyright © 2016, Xavier van De Woestyne
Copyright © 2020-2021, Erlang-Aided Enrichment Center

Copyright © 2020, Duncan McGreggor <[email protected]>.
Copyright © 2016, Xavier van De Woestyne


[//]: ---Named-Links---
Expand Down
2 changes: 1 addition & 1 deletion rebar.config
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

{deps, [
{rationals, "0.1.0"},
{results, "0.1.0"}
{results, "0.2.0"}
]}.

{xref_checks, [
Expand Down
102 changes: 71 additions & 31 deletions src/coers.erl
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
%% @author X. Van de Woestyne <[email protected]>
%% @copyright 2016 X. Van de Woestyne
%% @doc coers provide small function for value coercion.

-module(coers).

-compile({no_auto_import, [error/1]}).

%% API of Coers
-export([
value/1,
error/1,
has_error/1,
is_ascii_char/1,
maybe_string/1,
to_string/1,
Expand All @@ -30,6 +31,18 @@
-define(RATIO_REGEX, "^(?<SIGN>[+-])?(?<NUM>\\d+)/(?<DEN>\\d+)$").
-define(NUM_REGEX, "^[+-]?(\\d+([.]\\d*)?([eE][+-]?\\d+)?|[.]\\d+([eE][+-]?\\d+)?)$").

-spec value(result()) -> term().
value(Result) ->
results:value(Result).

-spec error(result()) -> term().
error(Result) ->
results:error(Result).

-spec has_error(result()) -> boolean().
has_error(Result) ->
results:has_error(Result).

%% @doc determine if an integer is a potential Ascii Char
-spec is_ascii_char(integer()) -> boolean().
is_ascii_char(X) when is_integer(X) ->
Expand Down Expand Up @@ -61,8 +74,10 @@ to_string(Term) ->
%% @doc Replace value if coercion failed
%% the suceeded flag is preserved
-spec to_string(term(), term()) -> result().
to_string(Term, Default) when is_record(Default, result) ->
results:attempt(to_string(Term), Default);
to_string(Term, Default) ->
results:attempt(to_string(Term), Default).
results:attempt(to_string(Term), to_string(Default)).

%% @doc an ugly and magic coercion from string to term()
-spec of_string(string()) -> result().
Expand All @@ -81,19 +96,24 @@ of_string(String) ->
{ok, Exprs} ->
{value, Val, []} = erl_eval:exprs(Exprs, []),
results:new(Val);
{error, {_, _, _}} ->
{error, {_, erl_parse, _}} ->
%% TODO extract the error message and add to new_error
results:new_error({error, "tbd"})
format_error_msg(erl_parse, "Could not convert string ~p", [String]);
{error, {Err, A, B}} ->
%% TODO extract the error message and add to new_error
format_error_msg(Err, "Could not convert string ~p, ~p, ~p", [String, A, B])
end;
{error, {_, _, _}, _} ->
{error, {Err, A, B}, _} ->
%% TODO extract the error message and add to new_error
results:new_error({error, "tbd"})
format_error_msg(Err, "Could not convert string ~p, ~p, ~p", [String, A, B])
end.

%% @doc try coercion or define a default value the suceeded flag is preserved
-spec of_string(string(), term()) -> result().
of_string(Str, Default) ->
results:attempt(of_string(Str), Default).
of_string(Term, Default) when is_record(Default, result) ->
results:attempt(of_string(Term), Default);
of_string(Term, Default) ->
results:attempt(of_string(Term), of_string(Default)).

%% @doc numeric alignement of a string (float of int)
-spec numeric_align(string()) -> atom().
Expand Down Expand Up @@ -121,25 +141,28 @@ to_int(Obj) when is_bitstring(Obj) -> to_int(binary_to_list(Obj));
to_int(Obj) when is_list(Obj) ->
try list_to_integer(Obj) of
Val -> results:new(Val)
catch _:_ ->
catch error:Err ->
case numeric_align(Obj) of
float -> to_int(list_to_float(Obj));
_ -> results:new(0)
Type -> format_error_msg(Err, "Could not convert ~p (type ~p) to int", [Obj, Type])
end
end;
to_int(Obj) when is_atom(Obj) ->
try Soft = atom_to_list(Obj), to_int(Soft) of
Result -> Result
catch _:_ ->
results:new(0)
catch error:Err ->
format_error_msg(Err, "Could not convert ~p to int", [Obj])
end;
to_int(_) -> results:new(0).
to_int(Obj) -> format_error_msg(error, "Could not convert ~p to int", [Obj]).

%% @doc try coercion or define a default value
%% the suceeded flag is preserved
-spec to_int(term(), term()) -> result().
to_int(Term, Default) when is_record(Default, result) ->
results:attempt(to_int(Term), Default);
to_int(Term, Default) ->
results:attempt(to_int(Term), Default).
results:attempt(to_int(Term), to_int(Default)).


%% @doc try to coerce a term to a float
-spec to_float(term()) -> result().
Expand All @@ -148,41 +171,46 @@ to_float(Obj) when is_bitstring(Obj) -> to_float(binary_to_list(Obj));
to_float(Obj) when is_list(Obj) ->
try list_to_float(Obj) of
Val -> results:new(Val)
catch _:_ ->
catch error:Err ->
case numeric_align(Obj) of
integer -> to_float(list_to_integer(Obj));
_ -> results:new(0.0)
Type -> format_error_msg(Err, "Could not convert ~p (type ~p) to float", [Obj, Type])
end
end;
to_float(Obj) when is_atom(Obj) ->
try Pred = atom_to_list(Obj), to_float(Pred) of
to_float(Obj) when is_atom(Obj) ->
try Obj2 = atom_to_list(Obj), to_float(Obj2) of
Result -> Result
catch _:_ ->
results:new(0.0)
catch error:Err ->
format_error_msg(Err, "Could not convert ~p to flost", [Obj])
end;
to_float(_) -> results:new(0.0).
to_float(Obj) ->
format_error_msg(error, "Could not convert ~p to flost", [Obj]).

%% @doc try coercion or define a default value the suceeded flag is preserved
-spec to_float(term(), term()) -> result().
to_float(Term, Default) when is_record(Default, result) ->
results:attempt(to_float(Term), Default);
to_float(Term, Default) ->
results:attempt(to_float(Term), Default).
results:attempt(to_float(Term), to_float(Default)).

%% @doc try to coerce a term to an atom
-spec to_atom(term()) -> result().
to_atom(Obj) when is_atom(Obj) -> results:new(Obj);
to_atom(Obj) when is_list(Obj) ->
try list_to_atom(Obj) of
Val -> results:new(Val)
catch _:_ -> results:new(false)
catch Err:_ -> results:new_error(Err)
end;
to_atom(Obj) ->
Pred = to_string(Obj),
to_atom(results:value(Pred)).

%% @doc try coercion or define a default value the suceeded flag is preserved
-spec to_atom(term(), term()) -> result().
to_atom(Term, Default) when is_record(Default, result) ->
results:attempt(to_atom(Term), Default);
to_atom(Term, Default) ->
results:attempt(to_atom(Term), Default).
results:attempt(to_atom(Term), to_atom(Default)).

%% @doc try to coerce a term to a boolean
-spec to_bool(term()) -> result().
Expand Down Expand Up @@ -213,14 +241,26 @@ to_rational(Obj) when is_list(Obj) ->
case re:run(Obj, ?RATIO_REGEX, [{capture, ['SIGN', 'NUM', 'DEN'], list}]) of
{match, [Sign, Num, Denom]} ->
results:new(rationals:new(results:value(to_int(Sign ++ Num)), results:value(to_int(Denom))));
_ ->
results:new(rationals:new(0, 1))
Err ->
format_error_msg(Err, "Could not convert ~p to ratio", [Obj])
end;
to_rational(Obj) when is_bitstring(Obj) ->
to_rational(binary_to_list(Obj));
to_rational({Num, Denom}=Obj) when is_tuple(Obj) ->
results:new(rationals:new(results:value(to_int(Num)), results:value(to_int(Denom)))).

-spec to_rational(term(), term()) -> result().
to_rational(Obj, Default) ->
results:attempt(to_rational(Obj), results:value(to_rational(Default))).
to_rational(Term, Default) when is_record(Default, result) ->
results:attempt(to_rational(Term), Default);
to_rational(Term, Default) ->
results:attempt(to_rational(Term), to_rational(Default)).

%% Private functions

format_error_msg(Err, Msg, FmtArgs) ->
results:new_error({Err, lists:flatten(io_lib:format(Msg, FmtArgs))}).

maybe_wrap_default(Result) when is_record(Result, result) ->
Result;
maybe_wrap_default(Val) ->
results:new(Val).
Loading

0 comments on commit a1fd36b

Please sign in to comment.