diff --git a/.gitignore b/.gitignore index 20177b4..1bfb900 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ deps *.beam *.plt *.swp +*.swo erl_crash.dump ebin/*.beam doc/*.html diff --git a/priv/default.css b/priv/default.css new file mode 100644 index 0000000..d7f1ad5 --- /dev/null +++ b/priv/default.css @@ -0,0 +1,95 @@ +/* color pallette */ +* { + --white: #f7f8fb; + --lgray0: #f5fbfd; + --lgray1: #daddd6; + --lgray2: #d3d8d5; + --mgray1: #687864; + --dgreen1: #21341e; + --lgreen1: #e5eef2; + --black: #003d27; + + --sans: Helvetica, Liberation Sans, FreeSans, Roboto, sans-serif; + --mono: Liberation Mono, FreeMono, Roboto Mono, monospace; + --fsdef: 12pt; +} + +/* body */ +body { + background: var(--white); + color: var(--dgray1); + font-size: var(--fsdef); + font-family: var(--sans); + margin: 0; + padding: 0; + line-height: 1.4; +} + + +.content { + max-width: 800px; + margin: 0 auto; + /* + background: #ff0; + */ +} + + +/* add some top padding to content */ + +.content-title { + text-align: center; + text-decoration: underline; +} + +.content-body { + width: 100%; + margin: 0 auto; + /* + background: #f00; + */ + + + padding-left: 10px; + padding-right: 10px; + + box-sizing: border-box; +} + +/* element-specific styling */ + +a { + color: var(--mgray1); +} + + +#wfc-input { + font-family: var(--mono); + font-size: var(--fsdef); + background: var(--lgreen1); + + width: 100%; + + box-sizing: border-box; + + border-radius: 6px; + border: 1px solid var(--lgray1); + + padding: 5px; +} + +#wfc-output { + background: var(--lgray0); + font-family: var(--mono); + font-size: var(--fsdef); + + width: 100%; + resize: vertical; + + box-sizing: border-box; + + border-radius: 6px; + border: 1px solid var(--lgray1); + + padding: 5px; +} diff --git a/priv/index.html b/priv/index.html index 245c764..ce3511f 100644 --- a/priv/index.html +++ b/priv/index.html @@ -1,12 +1,114 @@ - + - FEWD Hello! - + WF Compiler Demo + -

+
+

WFC Demo

+ +
+ + + +

Settings

+ Auto-resize output
+ Auto-scroll output to bottom +
+
+ + diff --git a/priv/scratch.txt b/priv/scratch.txt new file mode 100644 index 0000000..db3b7c7 --- /dev/null +++ b/priv/scratch.txt @@ -0,0 +1,56 @@ + /* header */ + div#header { + background: var(--lgray2); + height: 50px; + width: 100%; + } + + img#header-logo { + height: 40px; + margin-top: 5px; + margin-bottom: 5px; + margin-left: 5px; + } + + +.main { + padding-top: 20px; +} + +.content-diagram { + max-width: 100%; + margin: 0 auto; + padding: 0 auto; +} + + +/* Pandoc makes some wacky choices with how it lays out code blocks + * + * this means all blocks that do not have a
 block as an ancestor
+ *
+ * ie the `inline code`
+ */
+code:not(pre *) {
+    border-radius:  6px;
+    border:         1px solid var(--lgray1);
+    padding-left:   3px;
+    padding-right:  3px;
+    padding-top:    1px;
+    padding-bottom: 1px;
+    background:     var(--lgreen1);
+}
+
+/* this is specifically for ```fenced blocks``` */
+pre {
+    border-radius:  6px;
+    border:         1px solid var(--lgray1);
+    padding:        5px;
+    background:     var(--lgreen1);
+    overflow:       scroll;
+}
+
+/* All `unfenced` or ```fenced``` blocks */
+code {
+    font-family:    Liberation Mono, Roboto Mono, monospace;
+    color:          var(--dgreen1);
+}
diff --git a/src/fd_client.erl b/src/fd_client.erl
index d54b592..9d16135 100644
--- a/src/fd_client.erl
+++ b/src/fd_client.erl
@@ -28,6 +28,7 @@
 %%% Type and Record Definitions
 
 -include("http.hrl").
+-include("$zx_include/zx_logger.hrl").
 
 
 -record(s, {socket = none :: none | gen_tcp:socket(),
@@ -131,19 +132,24 @@ loop(Parent, Debug, State = #s{socket = Socket, next = Next}) ->
     ok = inet:setopts(Socket, [{active, once}]),
     receive
         {tcp, Socket, Message} ->
-            %ok = io:format("~p received: ~tp~n", [self(), Message]),
+            tell("~p Next = ~p", [?LINE, Next]),
             Received =
                 case Next of
                     none -> Message;
                     _    -> <>
                 end,
+            tell("qhl_parse(Socket, ~tp)", [Received]),
             case qhl:parse(Socket, Received) of
                 {ok, Req, NewNext} ->
+                    tell("qhl return: {ok, ~p, ~p}", [Req, NewNext]),
                     handle_request(Socket, Req),
                     NewState = State#s{next = NewNext},
                     loop(Parent, Debug, NewState);
                 Error ->
-                    io:format("~tp:~tp error: ~tp~n", [self(), ?LINE, Error]),
+                    %% should trigger bad request
+                    io:format("~p QHL parse error: ~tp", [?LINE, Error]),
+                    io:format("~p bad request:~n~ts", [?LINE, Received]),
+                    http_err(Socket, 400),
                     gen_tcp:shutdown(Socket, read_write),
                     exit(normal)
             end;
@@ -155,18 +161,6 @@ loop(Parent, Debug, State = #s{socket = Socket, next = Next}) ->
         Unexpected ->
             ok = io:format("~p Unexpected message: ~tp", [self(), Unexpected]),
             loop(Parent, Debug, State)
-        %{tcp, Socket, <<"bye\r\n">>} ->
-        %    ok = io:format("~p Client saying goodbye. Bye!~n", [self()]),
-        %    ok = gen_tcp:send(Socket, "Bye!\r\n"),
-        %    ok = gen_tcp:shutdown(Socket, read_write),
-        %    exit(normal);
-        %{relay, Sender, Message} when Sender == self() ->
-        %    ok = gen_tcp:send(Socket, ["Message from YOU: ", Message]),
-        %    loop(Parent, Debug, State);
-        %{relay, Sender, Message} ->
-        %    From = io_lib:format("Message from ~tp: ", [Sender]),
-        %    ok = gen_tcp:send(Socket, [From, Message]),
-        %    loop(Parent, Debug, State);
     end.
 
 
@@ -224,12 +218,23 @@ system_replace_state(StateFun, State) ->
 %%% http request handling
 
 handle_request(Sock, R = #request{method = M, path = P}) when M =/= undefined, P =/= undefined ->
-    zx:tell("~p ~p ~ts", [self(), M, P]),
+    tell("~p ~ts", [M, P]),
     route(Sock, M, P, R).
 
 
-route(Sock, get, <<"/">>, _) -> home(Sock);
-route(Sock, _, _, _)         -> http_err(Sock, 404).
+route(Sock, get, Route, _Request) ->
+    case Route of
+         <<"/">>            -> home(Sock);
+         <<"/default.css">> -> default_css(Sock);
+         _                  -> http_err(Sock, 404)
+    end;
+route(Sock, post, Route, Request) ->
+    case Route of
+         <<"/wfcin">>       -> wfcin(Sock, Request);
+         _                  -> http_err(Sock, 404)
+    end;
+route(Sock, _, _, _) ->
+    http_err(Sock, 404).
 
 
 home(Sock) ->
@@ -240,13 +245,47 @@ home(Sock) ->
             Resp = #response{headers = [{"content-type", "text/html"}],
                              body    = Body},
             respond(Sock, Resp);
+        Error ->
+            tell("error: ~p~n", [self(), Error]),
+            http_err(Sock, 500)
+    end.
+
+default_css(Sock) ->
+    %% fixme: cache
+    Path_IH = filename:join([zx:get_home(), "priv", "default.css"]),
+    case file:read_file(Path_IH) of
+        {ok, Body} ->
+            Resp = #response{headers = [{"content-type", "text/css"}],
+                             body    = Body},
+            respond(Sock, Resp);
         Error ->
             io:format("~p error: ~p~n", [self(), Error]),
             http_err(Sock, 500)
     end.
 
-http_err(_, _) ->
-    error(todo).
+wfcin(Sock, Request) ->
+    tell("wfcin request: ~tp", [Request]),
+    http_err(Sock, 501).
+
+%% FIXME
+http_err(Sock, N) ->
+    Slogan = qhl:slogan(N),
+    Body = [""
+            ""
+                ""
+                    ""
+                    "QHL: ", integer_to_list(N), " ", Slogan, ""
+                ""
+                ""
+                    "

" + "QHL: ", integer_to_list(N), " ", Slogan, + "

" + "" + ""], + Resp = #response{code = N, + headers = [{"content/type", "text/html"}], + body = Body}, + respond(Sock, Resp). respond(Sock, Response) -> diff --git a/src/fewd.erl b/src/fewd.erl index 54ef416..51a2f94 100644 --- a/src/fewd.erl +++ b/src/fewd.erl @@ -10,9 +10,9 @@ -license("BSD-2-Clause-FreeBSD"). -export([listen/1, ignore/0]). --export([start/0, start/1]). -export([start/2, stop/1]). +-include("$zx_include/zx_logger.hrl"). -spec listen(PortNum) -> Result @@ -35,28 +35,6 @@ ignore() -> fd_client_man:ignore(). --spec start() -> ok. -%% @doc -%% Start the server in an "ignore" state. - -start() -> - ok = application:ensure_started(sasl), - ok = application:start(fewd), - io:format("Starting..."). - - --spec start(PortNum) -> ok - when PortNum :: inet:port_number(). -%% @doc -%% Start the server and begin listening immediately. Slightly more convenient when -%% playing around in the shell. - -start(PortNum) -> - ok = start(), - ok = fd_client_man:listen(PortNum), - io:format("Startup complete, listening on ~w~n", [PortNum]). - - -spec start(normal, term()) -> {ok, pid()}. %% @private %% Called by OTP to kick things off. This is for the use of the "application" part of @@ -64,7 +42,9 @@ start(PortNum) -> %% See: http://erlang.org/doc/apps/kernel/application.html start(normal, _Args) -> - fd_sup:start_link(). + Result = fd_sup:start_link(), + ok = listen(8000), + Result. -spec stop(term()) -> ok. diff --git a/src/qhl.erl b/src/qhl.erl index a6c6134..a57869f 100644 --- a/src/qhl.erl +++ b/src/qhl.erl @@ -49,52 +49,62 @@ parse(Socket, Received) -> %% socket. parse(Socket, Received, M = #request{method = undefined}) -> + io:format("~p parse(~p, ~p, ~p)~n", [?LINE, Socket, Received, M]), case read_method(Socket, Received) of {ok, Method, Rest} -> parse(Socket, Rest, M#request{method = Method}); Error -> Error end; parse(Socket, Received, M = #request{path = undefined}) -> + io:format("~p parse(~p, ~p, ~p)~n", [?LINE, Socket, Received, M]), case read_path(Socket, Received) of {ok, Path, Rest} -> parse(Socket, Rest, M#request{path = Path}); Error -> Error end; parse(Socket, Received, M = #request{qargs = undefined}) -> + io:format("~p parse(~p, ~p, ~p)~n", [?LINE, Socket, Received, M]), case read_qargs(Socket, Received) of {ok, Qargs, Rest} -> parse(Socket, Rest, M#request{qargs = Qargs}); Error -> Error end; parse(Socket, Received, M = #request{fragment = undefined}) -> + io:format("~p parse(~p, ~p, ~p)~n", [?LINE, Socket, Received, M]), case read_fragment(Socket, Received) of {ok, Fragment, Rest} -> parse(Socket, Rest, M#request{fragment = Fragment}); Error -> Error end; parse(Socket, Received, M = #request{version = undefined}) -> + io:format("~p parse(~p, ~p, ~p)~n", [?LINE, Socket, Received, M]), case read_version(Socket, Received) of {ok, Version, Rest} -> parse(Socket, Rest, M#request{version = Version}); Error -> Error end; parse(Socket, Received, M = #request{headers = undefined}) -> + io:format("~p parse(~p, ~p, ~p)~n", [?LINE, Socket, Received, M]), case read_headers(Socket, Received) of {ok, Headers, Rest} -> parse(Socket, Rest, M#request{headers = Headers}); Error -> Error end; parse(Socket, Received, M = #request{enctype = undefined}) -> + io:format("~p parse(~p, ~p, ~p)~n", [?LINE, Socket, Received, M]), case read_enctype(M) of {ok, Enctype} -> parse(Socket, Received, M#request{enctype = Enctype}); Error -> Error end; parse(Socket, Received, M = #request{cookies = undefined}) -> + io:format("~p parse(~p, ~p, ~p)~n", [?LINE, Socket, Received, M]), case read_cookies(M) of {ok, Cookies} -> parse(Socket, Received, M#request{cookies = Cookies}); Error -> Error end; parse(Socket, Received, M = #request{size = undefined}) -> + io:format("~p parse(~p, ~p, ~p)~n", [?LINE, Socket, Received, M]), case read_size(M) of {ok, 0} -> {ok, M#request{size = 0}, none}; {ok, Size} -> parse(Socket, Received, M#request{size = Size}); Error -> Error end; parse(Socket, Received, M = #request{method = get, body = undefined, size = Size}) -> + io:format("~p parse(~p, ~p, ~p)~n", [?LINE, Socket, Received, M]), case read_body(Received, Size) of {ok, Body} -> {ok, M#request{body = Body}, none}; {ok, Body, Next} -> {ok, M#request{body = Body}, Next}; @@ -107,6 +117,7 @@ parse(Socket, method = post, enctype = urlencoded, size = Size}) -> + io:format("~p parse(~p, ~p, ~p)~n", [?LINE, Socket, Received, M]), case read_body(Received, Size) of {ok, Body} -> {ok, M#request{body = parts_to_map(posted(Body))}, none}; @@ -130,6 +141,7 @@ parse(Socket, method = post, enctype = {multipart, Boundary}, size = Size}) -> + io:format("~p parse(~p, ~p, ~p)~n", [?LINE, Socket, Received, M]), case read_multipart(Socket, Received, Boundary, Size) of {ok, Parts} -> {ok, M#request{body = parts_to_map(Parts)}, none}; {ok, Parts, Next} -> {ok, M#request{body = parts_to_map(Parts)}, Next}; @@ -141,10 +153,12 @@ parse(Socket, method = post, enctype = json, size = Size}) -> + io:format("~p parse(~p, ~p, ~p)~n", [?LINE, Socket, Received, M]), case read_body(Received, Size) of {ok, Body} -> read_json(M#request{body = Body}, none); {ok, Body, Next} -> read_json(M#request{body = Body}, Next); {incomplete, Body} -> + io:format("~p {incomplete, ~p}~n", [?LINE, Body]), case accumulate(Socket, M#request{body = Body}) of {ok, NewM = #request{body = NewBody}} -> read_json(NewM#request{body = NewBody}, none); @@ -514,6 +528,7 @@ read_size(#request{method = options}) -> read_body(Received, Size) -> + io:format("~p read_body(~p, ~p)~n", [?LINE, Received, Size]), case Received of <> -> {ok, Bin}; @@ -811,9 +826,11 @@ accumulate(Socket, M = #request{size = Size, body = Body}) -> end. accumulate(Socket, Remaining, Received) when Remaining > 0 -> + io:format("~p accumulate(~p, ~p, ~p)~n", [?LINE, Socket, Remaining, Received]), ok = inet:setopts(Socket, [{active, once}]), receive {tcp, Socket, Bin} -> + io:format("~p~n", [?LINE]), Size = byte_size(Bin), if Size == Remaining -> @@ -828,10 +845,15 @@ accumulate(Socket, Remaining, Received) when Remaining > 0 -> {ok, NewReceived, Next} end; {tcp_closed, Socket} -> + io:format("~p~n", [?LINE]), {error, tcp_closed}; {tcp_error, Socket, Reason} -> - {error, {tcp_error, Reason}} - after 3000 -> + io:format("~p~n", [?LINE]), + {error, {tcp_error, Reason}}; + X -> + io:format("~p raseevd: ~p~n", [?LINE, X]) + after 10_000 -> + io:format("~p~n", [?LINE]), {error, timeout} end; accumulate(_, 0, Received) -> diff --git a/src/wfc_parse.erl b/src/wfc_parse.erl deleted file mode 100644 index 5821c26..0000000 --- a/src/wfc_parse.erl +++ /dev/null @@ -1,4 +0,0 @@ --module(wfc_parse). - --export([ -]). diff --git a/src/zj.erl b/src/zj.erl new file mode 100644 index 0000000..0040ca8 --- /dev/null +++ b/src/zj.erl @@ -0,0 +1,694 @@ +%%% @doc +%%% ZJ: The tiny JSON parser +%%% +%%% This module exports four functions and accepts no options. +%%% @end + +-module(zj). +-vsn("1.1.2"). +-author("Craig Everett "). +-copyright("Craig Everett "). +-license("MIT"). + +-export([encode/1, decode/1, + binary_encode/1, binary_decode/1]). + +-export_type([value/0, bin_value/0]). + +-type value() :: string() + | number() + | true + | false + | undefined + | [value()] + | #{string() := value()}. + + +-type bin_value() :: binary() + | number() + | true + | false + | undefined + | [bin_value()] + | #{binary() := bin_value()}. + + + +%%% Character constants + +-define(BKSPC, 16#08). +-define(H_TAB, 16#09). +-define(NEW_L, 16#0A). +-define(FORMF, 16#0C). +-define(CAR_R, 16#0D). +-define(SPACE, 16#20). + + +%%% Interface Functions + +-spec encode(term()) -> string(). + +%% @doc +%% Take any convertable Erlang term and convert it to a JSON string. +%% +%% As JSON can only satirically be referred to as "a serialization format", it is +%% almost impossible to map any interesting data between Erlang (or any other language) +%% and JSON. For example, tuples do not exist in JSON, so converting an Erlang tuple +%% turns it into a list (a JSON array). Atoms also do not exist, so atoms other than +%% the ternay logic values `true', `false' and `null' become strings (those three +%% remain as atoms, with the added detail that JSON `null' maps to Erlang +%% `undefined'). +%% +%% Unless care is taken to pick types that JSON can accurately express (integers, +%% floats, strings, maps, lists, ternary logic atoms) it is not possible to guarantee +%% (or even reasonable to expect) that `Term == decode(encode(Term))' will be true. +%% +%% This function crashes when it fails. Things that will cause a crash are trying to +%% convert non-UTF-8 binaries to strings, use non-string values as object keys, +%% encode an unaligned bitstring, etc. +%% +%% Note that Erlang terms are converted as type primitives, meaning that compound +%% functional structures like GB-trees, dicts, sets, etc. will wind up having their +%% underlying structures converted as-is which is almost never what you want. It is +%% usually best to reduce compound values down to primitives (lists or maps) before +%% running encode. +%% +%% The only unsupported Erlang pritmitive is bitstrings. Care has NOT been taken to +%% ensure separation between actual binary data and binaries that are supposed to be +%% interpreted as strings. The same is true of deep list data: it just comes out raw +%% unless you flatten or convert it to a utf8 string with the unicode module. +%% +%% NOTE: If you need a serialization format that is less ambiguous and expresses more +%% types consider using BERT (language-independent implementations of Erlang external +%% binary format) instead: http://bert-rpc.org + +encode(true) -> "true"; +encode(false) -> "false"; +encode(undefined) -> "null"; +encode([]) -> "[]"; +encode(T) when is_atom(T) -> quote(atom_to_list(T)); +encode(T) when is_float(T) -> float_to_list(T); +encode(T) when is_integer(T) -> integer_to_list(T); +encode(T) when is_pid(T) -> quote(pid_to_list(T)); +encode(T) when is_port(T) -> quote(port_to_list(T)); +encode(T) when is_function(T) -> quote(erlang:fun_to_list(T)); +encode(T) when is_reference(T) -> quote(ref_to_list(T)); +encode(T) -> unicode:characters_to_list(encode_value(T)). + + +-spec decode(Stream) -> Result + when Stream :: unicode:chardata(), + Result :: {ok, value()} + | {error, Parsed, Remainder} + | {incomplete, Parsed, Remainder}, + Parsed :: value(), + Remainder :: unicode:chardata() + | unicode:external_chardata() + | binary(). +%% @doc +%% Take any IO data acceptable to the unicode module and return a parsed data structure. +%% In the event of a parsing error whatever part of the structure could be successfully +%% parsed will be returned along with the remainder of the string. Note that the string +%% remainder may have been changed to a different form by unicode:characters_to_list/1. +%% If the unicode library itself runs into a problem performing the initial conversion +%% its error return (`error' or `incomplete') will be returned directly. + +decode(Stream) -> + case unicode:characters_to_list(Stream) of + E when is_tuple(E) -> E; + [16#FEFF | String] -> parse(seek(String)); + String -> parse(seek(String)) + end. + + +-spec binary_encode(term()) -> binary(). +%% @doc +%% A strict encoding routine that works very similarly to `encode/1' but with a few +%% differences: +%% ``` +%% - Lists and Strings are firmly separated: +%% ALL lists are lists of discrete values, never strings. +%% ALL binaries are always UTF-8 strings. +%% An Erlang string or io_list will be encoded as JSON array. +%% - This function generates a UTF-8 binary, not a list. +%% - The burden is on the user to ensure that io_lists are collapsed to unicode +%% binaries via `unicode:characters_to_binary/1' before passing in string values. +%% - Erlang strings (lists) are still accepted as map/object keys. +%% ''' +%% +%% NOTE: +%% Most cases are better served by `encode/1', as most code deals in strings and not +%% arrays of integer values. +%% +%% Using this function requires a little bit more work up front (because ununified +%% io_list() data will always be interpreted as a JSON array), but provides a way to +%% reliably generate lists or strings in an unambiguous way in the special case where +%% your code is generating both strings and lists of integer values that may overlap +%% with valid UTF-8 codepoint values. + +binary_encode(true) -> <<"true">>; +binary_encode(false) -> <<"false">>; +binary_encode(undefined) -> <<"null">>; +binary_encode(T) when is_atom(T) -> <<"\"", (atom_to_binary(T, utf8))/binary, "\"">>; +binary_encode(T) when is_float(T) -> float_to_binary(T); +binary_encode(T) when is_integer(T) -> integer_to_binary(T); +binary_encode(T) when is_pid(T) -> <<"\"", (list_to_binary(pid_to_list(T)))/binary, "\"">>; +binary_encode(T) when is_port(T) -> <<"\"", (list_to_binary(port_to_list(T)))/binary, "\"">>; +binary_encode(T) when is_function(T) -> <<"\"", (list_to_binary(erlang:fun_to_list(T)))/binary, "\"">>; +binary_encode(T) when is_reference(T) -> <<"\"", (list_to_binary(ref_to_list(T)))/binary, "\"">>; +binary_encode(T) -> unicode:characters_to_binary(b_encode_value(T)). + + +-spec binary_decode(Stream) -> Result + when Stream :: unicode:chardata(), + Result :: {ok, bin_value()} + | {error, Parsed, Remainder} + | {incomplete, Parsed, Remainder}, + Parsed :: bin_value(), + Remainder :: binary(). +%% @doc +%% Almost identical in behavior to `decode/1' except this returns strings as binaries +%% and arrays of integers as Erlang lists (which may also be valid strings if the +%% values are valid UTF-8 codepoints). +%% +%% NOTE: +%% This function returns map keys as binaries + +binary_decode(Stream) -> + case b_decode(Stream) of + {error, Part, Rest} -> {error, Part, unicode:characters_to_binary(Rest)}; + Result -> Result + end. + + + +%%% Encoding Functions + +encode_value(true) -> "true"; +encode_value(false) -> "false"; +encode_value(undefined) -> "null"; +encode_value(T) when is_atom(T) -> quote(atom_to_list(T)); +encode_value(T) when is_float(T) -> float_to_list(T); +encode_value(T) when is_integer(T) -> integer_to_list(T); +encode_value(T) when is_binary(T) -> maybe_string(T); +encode_value(T) when is_list(T) -> maybe_array(T); +encode_value(T) when is_map(T) -> pack_object(T); +encode_value(T) when is_tuple(T) -> pack_array(tuple_to_list(T)); +encode_value(T) when is_pid(T) -> quote(pid_to_list(T)); +encode_value(T) when is_port(T) -> quote(port_to_list(T)); +encode_value(T) when is_function(T) -> quote(erlang:fun_to_list(T)); +encode_value(T) when is_reference(T) -> quote(ref_to_list(T)). + + +maybe_string(T) -> + L = binary_to_list(T), + true = io_lib:printable_unicode_list(L), + quote(L). + + +maybe_array(T) -> + case io_lib:printable_unicode_list(T) of + true -> quote(T); + false -> pack_array(T) + end. + + +quote(T) -> [$" | escape(T)]. + +escape([]) -> [$"]; +escape([$\b | T]) -> [$\\, $b | escape(T)]; +escape([$\f | T]) -> [$\\, $f | escape(T)]; +escape([$\n | T]) -> [$\\, $n | escape(T)]; +escape([$\r | T]) -> [$\\, $r | escape(T)]; +escape([$\t | T]) -> [$\\, $t | escape(T)]; +escape([$\" | T]) -> [$\\, $" | escape(T)]; +escape([$\\ | T]) -> [$\\, $\\ | escape(T)]; +escape([H | T]) -> [H | escape(T)]. + + +pack_array([]) -> "[]"; +pack_array([H | []]) -> [$[, encode_value(H), $]]; +pack_array([H | T]) -> [$[, encode_value(H), $,, encode_array(T), $]]. + +encode_array([H | []]) -> encode_value(H); +encode_array([H | T]) -> [encode_value(H), $,, encode_array(T)]. + + +pack_object(M) -> + case maps:to_list(M) of + [] -> + "{}"; + [{K, V} | T] when is_list(K) -> + true = io_lib:printable_unicode_list(K), + Init = [$", K, $", $:, encode_value(V)], + [${, lists:foldl(fun pack_object/2, Init, T), $}]; + [{K, V} | T] when is_binary(K) -> + Key = unicode:characters_to_list(K), + true = io_lib:printable_unicode_list(Key), + Init = [$", Key, $", $:, encode_value(V)], + [${, lists:foldl(fun pack_object/2, Init, T), $}]; + [{K, V} | T] when is_float(K) -> + Key = float_to_list(K), + Init = [$", Key, $", $:, encode_value(V)], + [${, lists:foldl(fun pack_object/2, Init, T), $}]; + [{K, V} | T] when is_integer(K) -> + Key = integer_to_list(K), + Init = [$", Key, $", $:, encode_value(V)], + [${, lists:foldl(fun pack_object/2, Init, T), $}]; + [{K, V} | T] when is_atom(K) -> + Init = [$", atom_to_list(K), $", $:, encode_value(V)], + [${, lists:foldl(fun pack_object/2, Init, T), $}] + end. + +pack_object({K, V}, L) when is_list(K) -> + true = io_lib:printable_unicode_list(K), + [$", K, $", $:, encode_value(V), $, | L]; +pack_object({K, V}, L) when is_binary(K) -> + Key = unicode:characters_to_list(K), + true = io_lib:printable_unicode_list(Key), + [$", Key, $", $:, encode_value(V), $, | L]; +pack_object({K, V}, L) when is_float(K) -> + Key = float_to_list(K), + [$", Key, $", $:, encode_value(V), $, | L]; +pack_object({K, V}, L) when is_integer(K) -> + Key = integer_to_list(K), + [$", Key, $", $:, encode_value(V), $, | L]; +pack_object({K, V}, L) when is_atom(K) -> + [$", atom_to_list(K), $", $:, encode_value(V), $, | L]. + + +b_encode_value(true) -> <<"true">>; +b_encode_value(false) -> <<"false">>; +b_encode_value(undefined) -> <<"null">>; +b_encode_value(T) when is_atom(T) -> [$", atom_to_binary(T, utf8), $"]; +b_encode_value(T) when is_float(T) -> float_to_binary(T); +b_encode_value(T) when is_integer(T) -> integer_to_binary(T); +b_encode_value(T) when is_binary(T) -> [$", b_maybe_string(T), $"]; +b_encode_value(T) when is_list(T) -> b_pack_array(T); +b_encode_value(T) when is_map(T) -> b_pack_object(T); +b_encode_value(T) when is_tuple(T) -> b_pack_array(tuple_to_list(T)); +b_encode_value(T) when is_pid(T) -> [$", list_to_binary(pid_to_list(T)), $"]; +b_encode_value(T) when is_port(T) -> [$", list_to_binary(port_to_list(T)), $"]; +b_encode_value(T) when is_function(T) -> [$", list_to_binary(erlang:fun_to_list(T)), $"]; +b_encode_value(T) when is_reference(T) -> [$", list_to_binary(ref_to_list(T)), $"]. + + +b_maybe_string(T) -> + S = unicode:characters_to_binary(T), + true = is_binary(S), + S. + + +b_pack_array([]) -> "[]"; +b_pack_array([H | []]) -> [$[, b_encode_value(H), $]]; +b_pack_array([H | T]) -> [$[, b_encode_value(H), $,, b_encode_array(T), $]]. + +b_encode_array([H | []]) -> b_encode_value(H); +b_encode_array([H | T]) -> [b_encode_value(H), $,, b_encode_array(T)]. + + +b_pack_object(M) -> + case maps:to_list(M) of + [] -> + "{}"; + [{K, V} | T] when is_list(K) -> + true = io_lib:printable_unicode_list(K), + Init = [$", K, $", $:, b_encode_value(V)], + [${, lists:foldl(fun b_pack_object/2, Init, T), $}]; + [{K, V} | T] when is_binary(K) -> + true = io_lib:printable_unicode_list(unicode:characters_to_list(K)), + Init = [$", K, $", $:, b_encode_value(V)], + [${, lists:foldl(fun b_pack_object/2, Init, T), $}]; + [{K, V} | T] when is_float(K) -> + Key = float_to_list(K), + Init = [$", Key, $", $:, b_encode_value(V)], + [${, lists:foldl(fun b_pack_object/2, Init, T), $}]; + [{K, V} | T] when is_integer(K) -> + Key = integer_to_list(K), + Init = [$", Key, $", $:, b_encode_value(V)], + [${, lists:foldl(fun b_pack_object/2, Init, T), $}]; + [{K, V} | T] when is_atom(K) -> + Init = [$", atom_to_binary(K, utf8), $", $:, b_encode_value(V)], + [${, lists:foldl(fun b_pack_object/2, Init, T), $}] + end. + +b_pack_object({K, V}, L) when is_list(K) -> + true = io_lib:printable_unicode_list(K), + [$", K, $", $:, b_encode_value(V), $, | L]; +b_pack_object({K, V}, L) when is_binary(K) -> + true = io_lib:printable_unicode_list(unicode:characters_to_list(K)), + [$", K, $", $:, b_encode_value(V), $, | L]; +b_pack_object({K, V}, L) when is_float(K) -> + Key = float_to_list(K), + [$", Key, $", $:, b_encode_value(V), $, | L]; +b_pack_object({K, V}, L) when is_integer(K) -> + Key = integer_to_list(K), + [$", Key, $", $:, b_encode_value(V), $, | L]; +b_pack_object({K, V}, L) when is_atom(K) -> + [$", atom_to_list(K), $", $:, b_encode_value(V), $, | L]. + + +%%% Decode Functions + +-spec parse(Stream) -> Result + when Stream :: string(), + Result :: {ok, value()} + | {error, Extracted :: value(), Remaining :: string()}. +%% @private +%% The top-level dispatcher. This packages the top level value (or top-level error) +%% for return to the caller. A very similar function (value/1) is used for inner +%% values. + +parse([${ | Rest]) -> + case object(Rest) of + {ok, Object, ""} -> {ok, Object}; + {ok, Object, More} -> polish(Object, seek(More)); + Error -> Error + end; +parse([$[ | Rest]) -> + case array(Rest) of + {ok, Array, ""} -> {ok, Array}; + {ok, Array, More} -> polish(Array, seek(More)); + Error -> Error + end; +parse([$" | Rest]) -> + case string(Rest) of + {ok, String, ""} -> {ok, String}; + {ok, String, More} -> polish(String, seek(More)); + Error -> Error + end; +parse([I | Rest]) when I == $-; $0 =< I, I =< $9 -> + case number_int(Rest, [I]) of + {ok, Number, ""} -> {ok, Number}; + {ok, Number, More} -> polish(Number, seek(More)); + Error -> Error + end; +parse("true" ++ More) -> + polish(true, seek(More)); +parse("false" ++ More) -> + polish(false, seek(More)); +parse("null" ++ More) -> + polish(undefined, seek(More)); +parse(Other) -> + {error, [], Other}. + + +polish(Value, "") -> {ok, Value}; +polish(Value, More) -> {error, Value, More}. + + +value([${ | Rest]) -> object(Rest); +value([$[ | Rest]) -> array(Rest); +value([$" | Rest]) -> string(Rest); +value([I | Rest]) when I == $-; $0 =< I, I =< $9 -> number_int(Rest, [I]); +value("true" ++ Rest) -> {ok, true, Rest}; +value("false" ++ Rest) -> {ok, false, Rest}; +value("null" ++ Rest) -> {ok, undefined, Rest}; +value(_) -> error. + + +object([$} | Rest]) -> {ok, #{}, Rest}; +object(String) -> object(seek(String), #{}). + +object([$} | Rest], Map) -> + {ok, Map, Rest}; +object([$" | Rest], Map) -> + case string(Rest) of + {ok, Key, Remainder} -> object_value(seek(Remainder), Key, Map); + {error, _, _} -> {error, Map, Rest} + end; +object(Rest, Map) -> + {error, Map, Rest}. + +object_value([$: | Rest], Key, Map) -> + object_value_parse(seek(Rest), Key, Map); +object_value(Rest, Key, Map) -> + {error, maps:put(Key, undefined, Map), Rest}. + +object_value_parse(String, Key, Map) -> + case value(String) of + {ok, Value, Rest} -> object_next(seek(Rest), maps:put(Key, Value, Map)); + {error, Value, Rest} -> {error, maps:put(Key, Value, Map), Rest}; + error -> {error, Map, String} + end. + + +object_next([$, | Rest], Map) -> object(seek(Rest), Map); +object_next([$} | Rest], Map) -> {ok, Map, seek(Rest)}; +object_next(Rest, Map) -> {error, Map, Rest}. + + +array([$] | Rest]) -> {ok, [], Rest}; +array(String) -> array(seek(String), []). + +array([$] | Rest], List) -> + {ok, lists:reverse(List), seek(Rest)}; +array(String, List) -> + case value(String) of + {ok, Value, Rest} -> array_next(seek(Rest), [Value | List]); + {error, Value, Rest} -> {error, lists:reverse([Value | List]), Rest}; + error -> {error, lists:reverse(List), String} + end. + +array_next([$, | Rest], List) -> array(seek(Rest), List); +array_next([$] | Rest], List) -> {ok, lists:reverse(List), seek(Rest)}; +array_next(Rest, List) -> {error, lists:reverse(List), Rest}. + + +string(Stream) -> string(Stream, ""). + +string([$" | Rest], String) -> + {ok, lists:reverse(String), Rest}; +string([$\\, $" | Rest], String) -> + string(Rest, [$" | String]); +string([$\\, $\\ | Rest], String) -> + string(Rest, [$\\ | String]); +string([$\\, $b | Rest], String) -> + string(Rest, [?BKSPC | String]); +string([$\\, $t | Rest], String) -> + string(Rest, [?H_TAB | String]); +string([$\\, $n | Rest], String) -> + string(Rest, [?NEW_L | String]); +string([$\\, $f | Rest], String) -> + string(Rest, [?FORMF | String]); +string([$\\, $r | Rest], String) -> + string(Rest, [?CAR_R | String]); +string([$\\, $u, A, B, C, D | Rest], String) + when (($0 =< A andalso A =< $9) or ($A =< A andalso A =< $F) or ($a =< A andalso A =< $f)) + and (($0 =< B andalso B =< $9) or ($A =< B andalso B =< $F) or ($a =< B andalso B =< $f)) + and (($0 =< C andalso C =< $9) or ($A =< C andalso C =< $F) or ($a =< C andalso C =< $f)) + and (($0 =< D andalso D =< $9) or ($A =< D andalso D =< $F) or ($a =< D andalso D =< $f)) -> + Char = list_to_integer([A, B, C, D], 16), + string(Rest, [Char | String]); +string(Stream = [$\\, $u | _], String) -> + {error, String, Stream}; +string([$\\, Char | Rest], String) + when Char == 16#20; + Char == 16#21; + 16#23 =< Char, Char =< 16#5B; + 16#5D =< Char, Char =< 16#10FFFF -> + string(Rest, [$\\, Char | String]); +string([Char | Rest], String) + when Char == 16#20; + Char == 16#21; + 16#23 =< Char, Char =< 16#5B; + 16#5D =< Char, Char =< 16#10FFFF -> + string(Rest, [Char | String]); +string(Rest, String) -> + {error, lists:reverse(String), Rest}. + + +number_int([$. | Rest], String) -> + number_float(Rest, [$. | String]); +number_int([$e, Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_float_exp(Rest, [Char, $+, $e, $0, $. | String]); +number_int([$E, Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_float_exp(Rest, [Char, $+, $e, $0, $. | String]); +number_int([$e, $+, Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_float_exp(Rest, [Char, $+, $e, $0, $. | String]); +number_int([$E, $+, Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_float_exp(Rest, [Char, $+, $e, $0, $. | String]); +number_int([$e, $-, Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_float_exp(Rest, [Char, $-, $e, $0, $. | String]); +number_int([$E, $-, Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_float_exp(Rest, [Char, $-, $e, $0, $. | String]); +number_int([Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_int(Rest, [Char | String]); +number_int(Rest, "-") -> + {error, "", [$- | Rest]}; +number_int(Rest, String) -> + {ok, list_to_integer(lists:reverse(String)), seek(Rest)}. + +number_float([Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_float(Rest, [Char | String]); +number_float([$E, Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_float_exp(Rest, [Char, $+, $e | String]); +number_float([$e, Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_float_exp(Rest, [Char, $+, $e | String]); +number_float([$E, $+, Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_float_exp(Rest, [Char, $+, $e | String]); +number_float([$e, $+, Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_float_exp(Rest, [Char, $+, $e | String]); +number_float([$E, $-, Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_float_exp(Rest, [Char, $-, $e | String]); +number_float([$e, $-, Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_float_exp(Rest, [Char, $-, $e | String]); +number_float(Rest, String) -> + Target = lists:reverse(String), + try + Number = list_to_float(Target), + {ok, Number, seek(Rest)} + catch + error:badarg -> {error, "", Target ++ Rest} + end. + + +number_float_exp([Char | Rest], String) when $0 =< Char, Char =< $9 -> + number_float_exp(Rest, [Char | String]); +number_float_exp(Rest, String) -> + Target = lists:reverse(String), + try + Number = list_to_float(Target), + {ok, Number, seek(Rest)} + catch + error:badarg -> {error, "", Target ++ Rest} + end. + + +seek([?H_TAB | Rest]) -> seek(Rest); +seek([?NEW_L | Rest]) -> seek(Rest); +seek([?CAR_R | Rest]) -> seek(Rest); +seek([?SPACE | Rest]) -> seek(Rest); +seek(String) -> String. + + +b_decode(Stream) -> + case unicode:characters_to_list(Stream) of + E when is_tuple(E) -> E; + [16#FEFF | String] -> binary_parse(seek(String)); + String -> binary_parse(seek(String)) + end. + +-spec binary_parse(Stream) -> Result + when Stream :: string(), + Result :: {ok, bin_value()} + | {error, Extracted :: bin_value(), Remaining :: binary()}. +%% @private +%% The top-level dispatcher. This packages the top level value (or top-level error) +%% for return to the caller. A very similar function (b_value/1) is used for inner +%% values. + +binary_parse([${ | Rest]) -> + case b_object(Rest) of + {ok, Object, ""} -> {ok, Object}; + {ok, Object, More} -> b_polish(Object, seek(More)); + Error -> Error + end; +binary_parse([$[ | Rest]) -> + case b_array(Rest) of + {ok, Array, ""} -> {ok, Array}; + {ok, Array, More} -> b_polish(Array, seek(More)); + Error -> Error + end; +binary_parse([$" | Rest]) -> + case string(Rest) of + {ok, String, ""} -> + case unicode:characters_to_binary(String) of + E when is_tuple(E) -> E; + Result -> {ok, Result} + end; + {ok, String, More} -> + case unicode:characters_to_binary(String) of + E when is_tuple(E) -> E; + Result -> b_polish(Result, seek(More)) + end; + Error -> + Error + end; +binary_parse([I | Rest]) when I == $-; $0 =< I, I =< $9 -> + case number_int(Rest, [I]) of + {ok, Number, ""} -> {ok, Number}; + {ok, Number, More} -> b_polish(Number, seek(More)); + Error -> Error + end; +binary_parse("true" ++ More) -> + b_polish(true, seek(More)); +binary_parse("false" ++ More) -> + b_polish(false, seek(More)); +binary_parse("null" ++ More) -> + b_polish(undefined, seek(More)); +binary_parse(Other) -> + {error, [], Other}. + + +b_polish(Value, "") -> {ok, Value}; +b_polish(Value, More) -> {error, Value, More}. + + +b_value([${ | Rest]) -> b_object(Rest); +b_value([$[ | Rest]) -> b_array(Rest); +b_value([$" | Rest]) -> b_string(Rest); +b_value([I | Rest]) when I == $-; $0 =< I, I =< $9 -> number_int(Rest, [I]); +b_value("true" ++ Rest) -> {ok, true, Rest}; +b_value("false" ++ Rest) -> {ok, false, Rest}; +b_value("null" ++ Rest) -> {ok, undefined, Rest}; +b_value(_) -> error. + + +b_string(Stream) -> + case string(Stream) of + {ok, String, More} -> + case unicode:characters_to_binary(String) of + E when is_tuple(E) -> E; + Result -> {ok, Result, More} + end; + Error -> Error + end. + + +b_object([$} | Rest]) -> {ok, #{}, Rest}; +b_object(String) -> b_object(seek(String), #{}). + +b_object([$} | Rest], Map) -> + {ok, Map, Rest}; +b_object([$" | Rest], Map) -> + case string(Rest) of + {ok, Key, Remainder} -> + b_object_value(seek(Remainder), unicode:characters_to_binary(Key), Map); + {error, _, _} -> + {error, Map, Rest} + end; +b_object(Rest, Map) -> + {error, Map, Rest}. + +b_object_value([$: | Rest], Key, Map) -> b_object_value_parse(seek(Rest), Key, Map); +b_object_value(Rest, Key, Map) -> {error, maps:put(Key, undefined, Map), Rest}. + +b_object_value_parse(String, Key, Map) -> + case b_value(String) of + {ok, Value, Rest} -> b_object_next(seek(Rest), maps:put(Key, Value, Map)); + {error, Value, Rest} -> {error, maps:put(Key, Value, Map), Rest}; + error -> {error, Map, String} + end. + + +b_object_next([$, | Rest], Map) -> b_object(seek(Rest), Map); +b_object_next([$} | Rest], Map) -> {ok, Map, seek(Rest)}; +b_object_next(Rest, Map) -> {error, Map, Rest}. + + +b_array([$] | Rest]) -> {ok, [], Rest}; +b_array(String) -> b_array(seek(String), []). + +b_array([$] | Rest], List) -> + {ok, lists:reverse(List), seek(Rest)}; +b_array(String, List) -> + case b_value(String) of + {ok, Value, Rest} -> b_array_next(seek(Rest), [Value | List]); + {error, Value, Rest} -> {error, lists:reverse([Value | List]), Rest}; + error -> {error, lists:reverse(List), String} + end. + +b_array_next([$, | Rest], List) -> b_array(seek(Rest), List); +b_array_next([$] | Rest], List) -> {ok, lists:reverse(List), seek(Rest)}; +b_array_next(Rest, List) -> {error, lists:reverse(List), Rest}.