aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorinoas <mail@inoas.com>2022-05-17 18:48:03 +0000
committerGitHub <noreply@github.com>2022-05-17 19:48:03 +0100
commit5c26af70c60c51531972a760482a3580402b7d68 (patch)
tree50f1df6f5f1c3f09e550a977b45d510eb0a3a6c1
parentf9d57db847ff2a8471c9136bd309c870b610d04d (diff)
downloadgleam_stdlib-5c26af70c60c51531972a760482a3580402b7d68.tar.gz
gleam_stdlib-5c26af70c60c51531972a760482a3580402b7d68.zip
add string.inspect() (#296)
-rw-r--r--CHANGELOG.md1
-rw-r--r--src/gleam/string.gleam15
-rw-r--r--src/gleam_stdlib.erl68
-rw-r--r--test/gleam/string_test.gleam281
4 files changed, 359 insertions, 6 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2457140..4f58fbd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
- The `float` module gains the `divide` function.
- The `int` module gains the `divide`, `power`, and `square_root` functions.
+- The `string` module gains the `first`, `last`, `capitalise` and `inspect` functions.
- The `string` module gains the `first`, `last`, and `capitalise` functions.
- Fixed a bug where `string.reverse` would break utf8 strings on target JavaScript.
- Fixed a bug where `string.slice` would break utf8 strings on target JavaScript.
diff --git a/src/gleam/string.gleam b/src/gleam/string.gleam
index 45d4d31..d06e2dd 100644
--- a/src/gleam/string.gleam
+++ b/src/gleam/string.gleam
@@ -823,3 +823,18 @@ pub fn capitalise(s: String) -> String {
_ -> ""
}
}
+
+pub fn inspect(value: a) -> String {
+ do_inspect(value)
+ |> string_builder.to_string
+}
+
+if javascript {
+ external fn do_inspect(value: a) -> string_builder.StringBuilder =
+ "../gleam.mjs" "inspect"
+}
+
+if erlang {
+ external fn do_inspect(value: a) -> string_builder.StringBuilder =
+ "gleam_stdlib" "inspect"
+}
diff --git a/src/gleam_stdlib.erl b/src/gleam_stdlib.erl
index cc1475f..9dc769b 100644
--- a/src/gleam_stdlib.erl
+++ b/src/gleam_stdlib.erl
@@ -9,7 +9,8 @@
bit_string_slice/3, decode_bit_string/1, compile_regex/2, regex_scan/2,
percent_encode/1, percent_decode/1, regex_check/2, regex_split/2,
base_decode64/1, parse_query/1, bit_string_concat/1, size_of_tuple/1,
- decode_tuple/1, tuple_get/2, classify_dynamic/1, print/1, println/1]).
+ decode_tuple/1, tuple_get/2, classify_dynamic/1, print/1, println/1,
+ inspect/1]).
%% Taken from OTP's uri_string module
-define(DEC2HEX(X),
@@ -47,9 +48,9 @@ classify_dynamic(X) when is_float(X) -> <<"Float">>;
classify_dynamic(X) when is_list(X) -> <<"List">>;
classify_dynamic(X) when is_boolean(X) -> <<"Bool">>;
classify_dynamic(X) when is_map(X) -> <<"Map">>;
-classify_dynamic(X) when is_tuple(X) ->
+classify_dynamic(X) when is_tuple(X) ->
iolist_to_binary(["Tuple of ", integer_to_list(tuple_size(X)), " elements"]);
-classify_dynamic(X) when
+classify_dynamic(X) when
is_function(X, 0) orelse is_function(X, 1) orelse is_function(X, 2) orelse
is_function(X, 3) orelse is_function(X, 4) orelse is_function(X, 5) orelse
is_function(X, 6) orelse is_function(X, 7) orelse is_function(X, 8) orelse
@@ -81,7 +82,7 @@ decode_list(Data) -> decode_error_msg(<<"List">>, Data).
decode_field(Data, Key) ->
case Data of
#{Key := Value} -> {ok, Value};
- _ ->
+ _ ->
decode_error(<<"field"/utf8>>, <<"nothing"/utf8>>)
end.
@@ -227,7 +228,7 @@ wrap_list(X) -> [X].
parse_query(Query) ->
case uri_string:dissect_query(Query) of
{error, _, _} -> {error, nil};
- Pairs ->
+ Pairs ->
Pairs1 = lists:map(fun
({K, true}) -> {K, <<"">>};
(Pair) -> Pair
@@ -296,7 +297,7 @@ uri_parse(String) ->
maps_get_optional(Uri, userinfo),
maps_get_optional(Uri, host),
maps_get_optional(Uri, port),
- maps_get_or(Uri, path, <<>>),
+ maps_get_or(Uri, path, <<>>),
maps_get_optional(Uri, query),
maps_get_optional(Uri, fragment)
}}
@@ -319,3 +320,58 @@ print(String) ->
println(String) ->
io:put_chars([String, $\n]),
nil.
+
+inspect(true) ->
+ "True";
+inspect(false) ->
+ "False";
+inspect(Any) when is_atom(Any) ->
+ lists:map(
+ fun(Part) ->
+ [Head | Tail] = string:next_grapheme(unicode:characters_to_binary(Part)),
+ [string:uppercase([Head]), Tail]
+ end,
+ re:split(erlang:atom_to_list(Any), "_+", [{return, iodata}])
+ );
+inspect(Any) when is_integer(Any) ->
+ erlang:integer_to_list(Any);
+inspect(Any) when is_float(Any) ->
+ io_lib_format:fwrite_g(Any);
+inspect(Any) when is_binary(Any) ->
+ Pattern = [$"],
+ Replacement = [$\\, $\\, $"],
+ Escaped = re:replace(Any, Pattern, Replacement, [{return, binary}, global]),
+ ["\"", Escaped, "\""];
+inspect(Any) when is_list(Any) ->
+ ["[",
+ lists:join(<<", ">>,
+ lists:map(fun inspect/1, Any)
+ ),
+ "]"];
+inspect(Any) when is_tuple(Any) % Record constructors
+ andalso is_atom(element(1, Any))
+ andalso element(1, Any) =/= false
+ andalso element(1, Any) =/= true
+ andalso element(1, Any) =/= nil
+->
+ [Atom | ArgsList] = erlang:tuple_to_list(Any),
+ Args =
+ lists:join(<<", ">>,
+ lists:map(fun inspect/1, ArgsList)
+ ),
+ [inspect(Atom), "(", Args, ")"];
+inspect(Any) when is_tuple(Any) ->
+ ["#(",
+ lists:join(<<", ">>,
+ lists:map(fun inspect/1, erlang:tuple_to_list(Any))
+ ),
+ ")"];
+inspect(Any) when is_function(Any) ->
+ {arity, Arity} = erlang:fun_info(Any, arity),
+ ArgsAsciiCodes = lists:seq($a, $a + Arity - 1),
+ Args = lists:join(<<", ">>,
+ lists:map(fun(Arg) -> <<Arg>> end, ArgsAsciiCodes)
+ ),
+ ["//fn(", Args, ") { ... }"];
+inspect(Any) ->
+ ["//erl(", io_lib:format("~p", [Any]), ")"].
diff --git a/test/gleam/string_test.gleam b/test/gleam/string_test.gleam
index 4c73b41..fafea89 100644
--- a/test/gleam/string_test.gleam
+++ b/test/gleam/string_test.gleam
@@ -486,3 +486,284 @@ pub fn capitalise_test() {
|> string.capitalise
|> should.equal("る")
}
+
+type InspectType(a, b) {
+ InspectTypeZero
+ InspectTypeOne(a)
+ InspectTypeTwo(a, b)
+}
+
+pub fn inspect_test() {
+ string.inspect(True)
+ |> should.equal("True")
+
+ string.inspect(False)
+ |> should.equal("False")
+
+ string.inspect([True, False])
+ |> should.equal("[True, False]")
+
+ string.inspect([False, False])
+ |> should.equal("[False, False]")
+
+ string.inspect([True, True])
+ |> should.equal("[True, True]")
+
+ string.inspect([Nil, Nil])
+ |> should.equal("[Nil, Nil]")
+
+ string.inspect(#(True, False))
+ |> should.equal("#(True, False)")
+
+ string.inspect(#(False, False))
+ |> should.equal("#(False, False)")
+
+ string.inspect(#(True, True))
+ |> should.equal("#(True, True)")
+
+ string.inspect(#(Nil, True))
+ |> should.equal("#(Nil, True)")
+
+ string.inspect(#(Nil, False))
+ |> should.equal("#(Nil, False)")
+
+ string.inspect(#(True, Nil))
+ |> should.equal("#(True, Nil)")
+
+ string.inspect(#(False, Nil))
+ |> should.equal("#(False, Nil)")
+
+ string.inspect(-1)
+ |> should.equal("-1")
+
+ string.inspect(0)
+ |> should.equal("0")
+
+ string.inspect(1)
+ |> should.equal("1")
+
+ string.inspect([])
+ |> should.equal("[]")
+
+ string.inspect([1])
+ |> should.equal("[1]")
+
+ string.inspect([1, 2])
+ |> should.equal("[1, 2]")
+
+ string.inspect([[1], [1]])
+ |> should.equal("[[1], [1]]")
+
+ string.inspect(-1.5)
+ |> should.equal("-1.5")
+
+ string.inspect(1.5)
+ |> should.equal("1.5")
+
+ string.inspect([1.5])
+ |> should.equal("[1.5]")
+
+ string.inspect("")
+ |> should.equal("\"\"")
+
+ string.inspect("1")
+ |> should.equal("\"1\"")
+
+ string.inspect("Hello Joe!")
+ |> should.equal("\"Hello Joe!\"")
+
+ string.inspect("Hello \"Manuel\"!")
+ |> should.equal("\"Hello \\\"Manuel\\\"!\"")
+
+ string.inspect("💜 Gleam")
+ |> should.equal("\"💜 Gleam\"")
+
+ string.inspect(["1"])
+ |> should.equal("[\"1\"]")
+
+ string.inspect(#())
+ |> should.equal("#()")
+
+ string.inspect(#(1))
+ |> should.equal("#(1)")
+
+ string.inspect(#("1"))
+ |> should.equal("#(\"1\")")
+
+ string.inspect(#(1.5))
+ |> should.equal("#(1.5)")
+
+ string.inspect([#(1, 2, 3), #(1, 2, 3)])
+ |> should.equal("[#(1, 2, 3), #(1, 2, 3)]")
+
+ string.inspect(#([1, 2, 3], "🌈", #(1, "1", True)))
+ |> should.equal("#([1, 2, 3], \"🌈\", #(1, \"1\", True))")
+
+ string.inspect(Nil)
+ |> should.equal("Nil")
+
+ string.inspect(Ok(1))
+ |> should.equal("Ok(1)")
+
+ string.inspect(Ok(True))
+ |> should.equal("Ok(True)")
+
+ string.inspect(Ok(False))
+ |> should.equal("Ok(False)")
+
+ string.inspect(Ok(Nil))
+ |> should.equal("Ok(Nil)")
+
+ string.inspect(Error(2))
+ |> should.equal("Error(2)")
+
+ string.inspect(Error(True))
+ |> should.equal("Error(True)")
+
+ string.inspect(Error(False))
+ |> should.equal("Error(False)")
+
+ string.inspect(Error(Nil))
+ |> should.equal("Error(Nil)")
+
+ string.inspect(InspectTypeZero)
+ |> should.equal("InspectTypeZero")
+
+ string.inspect(InspectTypeOne(1))
+ |> should.equal("InspectTypeOne(1)")
+
+ string.inspect(InspectTypeTwo(1, 2))
+ |> should.equal("InspectTypeTwo(1, 2)")
+
+ string.inspect(InspectTypeOne([1]))
+ |> should.equal("InspectTypeOne([1])")
+
+ string.inspect(InspectTypeOne("1"))
+ |> should.equal("InspectTypeOne(\"1\")")
+
+ string.inspect(InspectTypeOne(["1"]))
+ |> should.equal("InspectTypeOne([\"1\"])")
+
+ string.inspect(InspectTypeOne(#([1], "a")))
+ |> should.equal("InspectTypeOne(#([1], \"a\"))")
+
+ string.inspect(Ok)
+ |> should.equal("//fn(a) { ... }")
+
+ string.inspect(Error)
+ |> should.equal("//fn(a) { ... }")
+
+ string.inspect(fn() { Nil })
+ |> should.equal("//fn() { ... }")
+
+ string.inspect(fn(a) {
+ a
+ Nil
+ })
+ |> should.equal("//fn(a) { ... }")
+
+ string.inspect(fn(a, b) {
+ a
+ b
+ Nil
+ })
+ |> should.equal("//fn(a, b) { ... }")
+
+ string.inspect(fn(x, y) {
+ x
+ y
+ Nil
+ })
+ |> should.equal("//fn(a, b) { ... }")
+
+ string.inspect(fn(foo: Int, bar: String) -> Bool {
+ foo
+ bar
+ False
+ })
+ |> should.equal("//fn(a, b) { ... }")
+
+ string.inspect(#(InspectTypeOne, InspectTypeTwo))
+ |> should.equal("#(//fn(a) { ... }, //fn(a, b) { ... })")
+
+ string.inspect(InspectTypeOne(InspectTypeZero))
+ |> should.equal("InspectTypeOne(InspectTypeZero)")
+}
+
+if javascript {
+ pub fn target_inspect_test() {
+ // Due to Erlang's internal representation, on Erlang this will pass, instead:
+ // |> should.equal("InspectTypeZero(InspectTypeZero)")
+ //
+ string.inspect(#(InspectTypeZero, InspectTypeZero))
+ |> should.equal("#(InspectTypeZero, InspectTypeZero)")
+
+ // Due to JavaScript's `Number` type `Float`s without digits return as `Int`s.
+ //
+ string.inspect(-1.0)
+ |> should.equal("-1")
+
+ string.inspect(0.0)
+ |> should.equal("0")
+
+ string.inspect(1.0)
+ |> should.equal("1")
+
+ string.inspect([1.0])
+ |> should.equal("[1]")
+
+ string.inspect(#(1.0))
+ |> should.equal("#(1)")
+ }
+}
+
+if erlang {
+ import gleam/regex
+
+ external fn create_erlang_pid() -> String =
+ "erlang" "self"
+
+ external fn create_erlang_reference() -> String =
+ "erlang" "make_ref"
+
+ pub fn target_inspect_test() {
+ // Erlang's internal representation does not allow a correct differentiation
+ // |> should.equal("#(InspectTypeZero, InspectTypeZero)")
+ //
+ string.inspect(#(InspectTypeZero, InspectTypeZero))
+ |> should.equal("InspectTypeZero(InspectTypeZero)")
+
+ // Unlike JavaScript, Erlang correctly differentiates between 1 and 1.0
+ //
+ string.inspect(-1.0)
+ |> should.equal("-1.0")
+
+ string.inspect(0.0)
+ |> should.equal("0.0")
+
+ string.inspect(1.0)
+ |> should.equal("1.0")
+
+ string.inspect([1.0])
+ |> should.equal("[1.0]")
+
+ string.inspect(#(1.0))
+ |> should.equal("#(1.0)")
+
+ // Looks like `//erl(<0.83.0>)`
+ assert Ok(regular_expression) =
+ regex.from_string("^\\/\\/erl\\(<[0-9]+\\.[0-9]+\\.[0-9]+>\\)$")
+ string.inspect(create_erlang_pid())
+ |> regex.check(regular_expression, _)
+ |> should.equal(True)
+
+ // Looks like: `//erl(#Ref<0.1809744150.4035444737.100468>)`
+ assert Ok(regular_expression) =
+ regex.from_string(
+ "^\\/\\/erl\\(#Ref<[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+>\\)$",
+ )
+ string.inspect(create_erlang_reference())
+ |> regex.check(regular_expression, _)
+ |> should.equal(True)
+ }
+}