Last active
April 17, 2017 11:11
-
-
Save ndac-todoroki/22cb4bf30d60a8ce481643fd83ae490a to your computer and use it in GitHub Desktop.
JSON parser on Elixir (for studying Elixir)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule JSON do | |
@object_start "{" | |
@object_end "}" | |
@list_start "[" | |
@list_end "]" | |
@splitter "," | |
@keyval_seperator ":" | |
@string_sybil "\"" | |
@newline_sybil "\n" | |
@space " " | |
@numbers ~w(0 1 2 3 4 5 6 7 8 9) | |
defp to_number(string) when is_binary(string) do | |
try do | |
String.to_float(string) | |
rescue | |
ArgumentError -> String.to_integer(string) | |
end | |
end | |
# Parse JSON String to Elixir object. | |
# | |
# @param json [String] input JSON string | |
# @return { :ok, result } | |
def parse(json) when is_binary(json) do | |
{:ok, { result, _length}} = parse(json, 0) | |
{:ok, result} | |
end | |
defp parse(string, position) when is_binary(string) and is_integer(position) do | |
head = String.at(string, position) | |
case head do | |
@object_start -> parse_object(string, position) | |
@list_start -> parse_list(string, position) | |
@string_sybil -> parse_string(string, position) | |
"t" -> parse_true(string, position) | |
"f" -> parse_false(string, position) | |
"n" -> parse_null(string, position) | |
n when n in [@space, @newline_sybil] -> | |
parse(string, position + 1) | |
n when n in @numbers -> | |
parse_number(string, position) | |
_ -> raise "Unparsable error at #{head}" | |
end | |
end | |
# @return {:ok, {result_map, end_poz}} | |
defp parse_object(string, position, prev_map\\%{}) when is_integer(position) do | |
head = String.at(string, position) | |
case head doElixir | |
@object_start -> | |
parse_object(string, position + 1, prev_map) | |
@string_sybil -> | |
{:ok, { map = %{}, end_poz } } = parse_keyval(string, position) | |
parse_object(string, end_poz + 1, Map.merge(prev_map, map)) | |
n when n in [@splitter, @space, @newline_sybil] -> | |
parse_object(string, position + 1, prev_map) | |
@object_end -> | |
{:ok, { prev_map, position }} # positionが } なので end_poz になる | |
_ -> | |
raise "JSON Object's key must be a String, but got #{head}" | |
end | |
end | |
# @return {:ok, {result_string, end_poz}} | |
defp parse_string(string, position) do | |
cond do | |
String.at(string, position) == "\"" -> | |
{:ok, {start..final}} = parse_string(string, position + 1, 0) | |
{:ok, {String.slice(string, start..final), position + (final - start) + 2}} | |
true -> | |
raise "not a valid JSON string" | |
end | |
end | |
# Stringの\"から\"までの間だけのrangeを返す | |
defp parse_string(string, position, counter) when is_integer(counter) do | |
head = String.at(string, position + counter) | |
case head do | |
"\\" -> | |
case String.at(string, position + counter + 2) do | |
l when l in ~w(\" \/ \\ \b \f \n \r \t) -> | |
parse_string(string, position, counter + 2) # escapes next letter | |
"\\u" -> | |
parse_string(string, position, counter + 5) | |
_ -> | |
raise "backslash not followed by an escapable letter!" | |
end | |
nil -> raise "out of bounds. maybe string ends with a unescaped backslash?" | |
@string_sybil -> | |
{:ok, {position..position + counter - 1}} # ends | |
_ -> | |
parse_string(string, position, counter + 1) | |
end | |
end | |
# @return {:ok, { %{key => val}, end_poz }} | |
defp parse_keyval(string, position) when is_binary(string) do | |
{:ok, {key, key_end}} = parse_string(string, position) | |
{:ok, value_poz} = get_value_poz(string, key_end + 1) | |
{:ok, {val, end_poz}} = parse(string, value_poz) | |
{:ok, {%{key => val}, end_poz}} | |
end | |
defp get_value_poz(string, poz) when is_integer(poz) do | |
head = String.at(string, poz) | |
case head do | |
@space -> get_value_poz(string, poz + 1) | |
@keyval_seperator -> get_value_poz(string, poz + 1) | |
_ -> {:ok, poz} | |
end | |
end | |
# {:ok, {float, end_poz}} | |
defp parse_number(string, position, temp\\"") when is_binary(string) do | |
head = String.at(string, position) | |
case head do | |
n when n in ["." | @numbers] -> | |
parse_number(string, position + 1, temp <> head) | |
_ -> | |
{:ok, { to_number(temp), position - 1 }} | |
end | |
end | |
defp parse_list(string, position, prev_list\\[]) when is_binary(string) and is_integer(position) do | |
head = String.at(string, position) | |
case head do | |
@list_start -> | |
parse_list(string, position + 1) | |
n when n in [@splitter, @space] -> | |
parse_list(string, position + 1, prev_list) | |
@list_end -> | |
{:ok, { Enum.reverse(prev_list), position }} | |
_ -> | |
{:ok, { result, end_poz }} = parse(string, position) | |
parse_list(string, end_poz + 1, [result | prev_list]) | |
end | |
end | |
defp parse_true(string, position) do | |
"true" = String.slice(string, position..position+3) | |
{:ok, {true, position + 4}} | |
end | |
defp parse_false(string, position) do | |
"false" = String.slice(string, position..position+4) | |
{:ok, {true, position + 5}} | |
end | |
defp parse_null(string, position) do | |
"null" = String.slice(string, position..position+3) | |
{:ok, {nil, position + 4}} | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment