Skip to content

Commit

Permalink
Merge pull request #1 from giacomocavalieri/erlang-support
Browse files Browse the repository at this point in the history
  • Loading branch information
MystPi authored Mar 5, 2024
2 parents 74d843d + 386a9e2 commit 79a74e9
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 7 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ jobs:
rebar3-version: "3"
# elixir-version: "1.15.4"
- run: gleam deps download
- run: gleam test
- run: gleam test --target=erlang
- run: gleam test --target=javascript
- run: gleam format --check src test
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
version: 1.0.4
title: (erlang) data is colored depending on its type
---
#(
Ok(1234),
"blah",
True,
Nil,
//fn(a) { ... },
3.14,
"A",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
version: 1.0.4
title: (js) data is colored depending on its type
---
#(
Ok(1234),
"blah",
True,
Nil,
//fn(a) { ... },
3.14,
<<1, 2, 3>>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: 1.0.4
title: dictionaries use the dict.from_list function when formatted
---
dict.from_list([
#("bar", 2),
#("baz", 3),
#("foo", 1),
#("bar", 2),
])
71 changes: 71 additions & 0 deletions src/ffi.erl
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
-module(ffi).
-export([decode_nil/1, decode_tuple/1, decode_custom_type/1]).

-define(is_digit_char(X), (X > 47 andalso X < 58)).
-define(is_lowercase_char(X), (X > 96 andalso X < 123)).
-define(is_underscore_char(X), (X == 95)).

decode_nil(X) ->
case X of
nil -> {ok, nil};
_ -> decode_error("Nil", X)
end.

decode_tuple(X) ->
case X of
Tuple when is_tuple(Tuple) -> {ok, tuple_to_list(Tuple)};
_ -> decode_error("Tuple", X)
end.

decode_custom_type(X) ->
case X of
Tuple when is_tuple(Tuple) ->
case tuple_to_list(Tuple) of
[Atom | Elements] when is_atom(Atom) ->
case inspect_maybe_gleam_atom(erlang:atom_to_binary(Atom), none, <<>>) of
{ok, AtomName} -> {ok, {t_custom, AtomName, Elements}};
{error, nil} -> decode_error("CustomType", X)
end;
_ -> decode_error("CustomType", X)
end;
_ -> decode_error("CustomType", X)
end.

decode_error(Expected, Got) ->
ExpectedString = list_to_binary(Expected),
GotString = gleam_stdlib:classify_dynamic(Got),
DecodeError = {decode_error, ExpectedString, GotString, []},
{error, [DecodeError]}.

% This is copy pasted from gleam's stdlib and performs some additional checks to
% make sure the given atom is a gleam's custom type atom. Stdlib doesn't export
% it so I had to copy it.
% It returns `{ok, CamelCaseAtomName}` in case it really is a valid Gleam atom
% name.
inspect_maybe_gleam_atom(<<>>, none, _) ->
{error, nil};
inspect_maybe_gleam_atom(<<First, _Rest/binary>>, none, _) when ?is_digit_char(First) ->
{error, nil};
inspect_maybe_gleam_atom(<<"_", _Rest/binary>>, none, _) ->
{error, nil};
inspect_maybe_gleam_atom(<<"_">>, _PrevChar, _Acc) ->
{error, nil};
inspect_maybe_gleam_atom(<<"_", _Rest/binary>>, $_, _Acc) ->
{error, nil};
inspect_maybe_gleam_atom(<<First, _Rest/binary>>, _PrevChar, _Acc)
when not (?is_lowercase_char(First) orelse ?is_underscore_char(First) orelse ?is_digit_char(First)) ->
{error, nil};
inspect_maybe_gleam_atom(<<First, Rest/binary>>, none, Acc) ->
inspect_maybe_gleam_atom(Rest, First, <<Acc/binary, (uppercase(First))>>);
inspect_maybe_gleam_atom(<<"_", Rest/binary>>, _PrevChar, Acc) ->
inspect_maybe_gleam_atom(Rest, $_, Acc);
inspect_maybe_gleam_atom(<<First, Rest/binary>>, $_, Acc) ->
inspect_maybe_gleam_atom(Rest, First, <<Acc/binary, (uppercase(First))>>);
inspect_maybe_gleam_atom(<<First, Rest/binary>>, _PrevChar, Acc) ->
inspect_maybe_gleam_atom(Rest, First, <<Acc/binary, First>>);
inspect_maybe_gleam_atom(<<>>, _PrevChar, Acc) ->
{ok, Acc};
inspect_maybe_gleam_atom(_, _, _) ->
{error, nil}.

uppercase(X) -> X - 32.
6 changes: 6 additions & 0 deletions src/pprint.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ fn pretty_list(items: List(Dynamic), color: Bool) -> Document {

fn pretty_dict(d: Dict(decoder.Type, decoder.Type), color: Bool) -> Document {
dict.to_list(d)
|> list.sort(fn(one_field, other_field) {
// We need to sort dicts so that those always have a consistent order.
let #(one_key, _one_value) = one_field
let #(other_key, _other_value) = other_field
string.compare(string.inspect(one_key), string.inspect(other_key))
})
|> list.map(fn(field) {
// Format the dict's items into tuple literals
[
Expand Down
9 changes: 6 additions & 3 deletions src/pprint/decoder.gleam
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import gleam/dict.{type Dict}
import gleam/dynamic.{type Dynamic}
import gleam/string
import gleam/result
import gleam/dict.{type Dict}
import gleam/string

// ---- TYPES ------------------------------------------------------------------

Expand Down Expand Up @@ -36,25 +36,28 @@ fn decode_type(value: Dynamic) -> Result(Type, List(dynamic.DecodeError)) {
use <- result.lazy_or(result.map(dynamic.bool(value), TBool))
use <- result.lazy_or(result.map(decode_nil(value), fn(_) { TNil }))
use <- result.lazy_or(result.map(dynamic.bit_array(value), TBitArray))
use <- result.lazy_or(decode_custom_type(value))
use <- result.lazy_or(result.map(decode_tuple(value), TTuple))
use <- result.lazy_or(result.map(dynamic.shallow_list(value), TList))
use <- result.lazy_or(result.map(
dynamic.dict(decode_type, decode_type)(value),
TDict,
))
use <- result.lazy_or(decode_custom_type(value))
// Anything else we just inspect. This could be a function or an external object
// or type from the runtime.
Ok(TForeign(string.inspect(value)))
}

@external(erlang, "ffi", "decode_custom_type")
@external(javascript, "../ffi.mjs", "decode_custom_type")
fn decode_custom_type(value: Dynamic) -> Result(Type, List(dynamic.DecodeError))

@external(erlang, "ffi", "decode_tuple")
@external(javascript, "../ffi.mjs", "decode_tuple")
fn decode_tuple(
value: Dynamic,
) -> Result(List(Dynamic), List(dynamic.DecodeError))

@external(erlang, "ffi", "decode_nil")
@external(javascript, "../ffi.mjs", "decode_nil")
fn decode_nil(value: Dynamic) -> Result(Nil, List(dynamic.DecodeError))
21 changes: 19 additions & 2 deletions test/pprint_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,27 @@ pub fn complex_data_test() {
|> birdie.snap("complex, nested data is formatted nicely")
}

pub fn coloring_test() {
@target(javascript)
pub fn javascript_coloring_test() {
#(Ok(1234), "blah", True, Nil, fn(a) { a }, 3.14, <<1, 2, 3>>)
|> pprint.format_colored
|> birdie.snap("data is colored depending on its type")
|> birdie.snap("(js) data is colored depending on its type")
}

// 🚨 On the erlang target strings are encoded as bit arrays so if we pass it a
// bit array we'll get out a string. This is a behaviour that will inevitably
// differ from target to target so we have to write two different tests to
// make sure it doesn't result in problems.
//
// TODO: we could be smarter and ensure we get the exact same behaviour on all
// targets by turning bitarrays into strings on the JS target though!
// This would ensure that people get consisten outputs most of the times
// without having to rely on the `@target` annotation like we're doing.
@target(erlang)
pub fn erlang_coloring_test() {
#(Ok(1234), "blah", True, Nil, fn(a) { a }, 3.14, <<65>>)
|> pprint.format_colored
|> birdie.snap("(erlang) data is colored depending on its type")
}

pub fn dict_test() {
Expand Down

0 comments on commit 79a74e9

Please sign in to comment.