232 lines
5.8 KiB
Elixir
232 lines
5.8 KiB
Elixir
defmodule BDS.Sidecar do
|
|
@moduledoc false
|
|
|
|
alias BDS.Persistence
|
|
|
|
@list_item_prefix " - "
|
|
@document_marker "---"
|
|
@always_quoted_keys MapSet.new(["originalName", "title", "alt", "caption", "author"])
|
|
|
|
def serialize_document(fields) when is_list(fields) do
|
|
serialized_fields =
|
|
fields
|
|
|> Enum.flat_map(&serialize_field/1)
|
|
|> Enum.join("\n")
|
|
|
|
[@document_marker, serialized_fields, @document_marker]
|
|
|> Enum.reject(&(&1 == ""))
|
|
|> Enum.join("\n")
|
|
end
|
|
|
|
def parse_document(contents) when is_binary(contents) do
|
|
{:ok,
|
|
contents
|
|
|> String.split("\n", trim: true)
|
|
|> parse_lines(%{})}
|
|
end
|
|
|
|
defp serialize_field({_key, nil}), do: []
|
|
defp serialize_field({_key, ""}), do: []
|
|
|
|
defp serialize_field({key, values}) when is_list(values) do
|
|
serialized_values = values |> Enum.map(&serialize_inline_list_scalar/1) |> Enum.join(", ")
|
|
["#{key}: [#{serialized_values}]"]
|
|
end
|
|
|
|
defp serialize_field({key, value}) when is_boolean(value) do
|
|
["#{key}: #{if(value, do: "true", else: "false")}"]
|
|
end
|
|
|
|
defp serialize_field({key, value}) when is_integer(value) do
|
|
rendered =
|
|
if timestamp_key?(key) do
|
|
Persistence.timestamp_to_iso8601(value)
|
|
else
|
|
Integer.to_string(value)
|
|
end
|
|
|
|
["#{key}: #{rendered}"]
|
|
end
|
|
|
|
defp serialize_field({key, value}) do
|
|
["#{key}: #{serialize_scalar(key, value)}"]
|
|
end
|
|
|
|
defp parse_lines([], acc), do: acc
|
|
|
|
defp parse_lines([line | rest], acc) do
|
|
cond do
|
|
String.starts_with?(line, @list_item_prefix) ->
|
|
parse_lines(rest, acc)
|
|
|
|
String.ends_with?(line, ":") ->
|
|
key = String.trim_trailing(line, ":")
|
|
{items, remaining} = take_list_items(rest, [])
|
|
parse_lines(remaining, Map.put(acc, key, Enum.reverse(items)))
|
|
|
|
String.contains?(line, ": ") ->
|
|
[key, raw_value] = String.split(line, ": ", parts: 2)
|
|
parse_lines(rest, Map.put(acc, key, parse_scalar(key, raw_value)))
|
|
|
|
true ->
|
|
parse_lines(rest, acc)
|
|
end
|
|
end
|
|
|
|
defp take_list_items([line | rest], items) do
|
|
if String.starts_with?(line, @list_item_prefix) do
|
|
value = line |> String.replace_prefix(@list_item_prefix, "") |> then(&parse_scalar(nil, &1))
|
|
take_list_items(rest, [value | items])
|
|
else
|
|
{items, [line | rest]}
|
|
end
|
|
end
|
|
|
|
defp take_list_items([], items), do: {items, []}
|
|
|
|
defp parse_scalar(key, value) when is_binary(key) and is_binary(value) do
|
|
trimmed = String.trim(value)
|
|
parsed = parse_generic_scalar(trimmed)
|
|
|
|
cond do
|
|
timestamp_key?(key) ->
|
|
if is_binary(parsed) do
|
|
Persistence.parse_timestamp(parsed) || parsed
|
|
else
|
|
parsed
|
|
end
|
|
|
|
true ->
|
|
parsed
|
|
end
|
|
end
|
|
|
|
defp parse_scalar(nil, value) when is_binary(value) do
|
|
value
|
|
|> String.trim()
|
|
|> parse_generic_scalar()
|
|
end
|
|
|
|
defp parse_generic_scalar("true"), do: true
|
|
defp parse_generic_scalar("false"), do: false
|
|
defp parse_generic_scalar("[]"), do: []
|
|
|
|
defp parse_generic_scalar("[" <> _rest = value) do
|
|
parse_inline_list(value)
|
|
end
|
|
|
|
defp parse_generic_scalar(value) do
|
|
if Regex.match?(~r/^-?\d+$/, value) do
|
|
String.to_integer(value)
|
|
else
|
|
parse_string(value)
|
|
end
|
|
end
|
|
|
|
defp parse_string("\"" <> rest) do
|
|
rest
|
|
|> String.trim_trailing("\"")
|
|
|> String.replace("\\n", "\n")
|
|
|> String.replace("\\\"", "\"")
|
|
|> String.replace("\\\\", "\\")
|
|
end
|
|
|
|
defp parse_string("'" <> rest) do
|
|
rest
|
|
|> String.trim_trailing("'")
|
|
|> String.replace("\\n", "\n")
|
|
|> String.replace("\\'", "'")
|
|
|> String.replace("\\\\", "\\")
|
|
end
|
|
|
|
defp parse_string(value), do: value
|
|
|
|
defp serialize_scalar(_key, value) when is_boolean(value) do
|
|
if(value, do: "true", else: "false")
|
|
end
|
|
|
|
defp serialize_scalar(key, value) when is_integer(value) do
|
|
if is_binary(key) and timestamp_key?(key) do
|
|
Persistence.timestamp_to_iso8601(value)
|
|
else
|
|
Integer.to_string(value)
|
|
end
|
|
end
|
|
|
|
defp serialize_scalar(key, value) do
|
|
string_value = to_string(value)
|
|
|
|
if is_binary(key) and MapSet.member?(@always_quoted_keys, key) do
|
|
quote_string(string_value)
|
|
else
|
|
maybe_quote_string(string_value)
|
|
end
|
|
end
|
|
|
|
defp serialize_inline_list_scalar(value) when is_binary(value), do: quote_string(value)
|
|
defp serialize_inline_list_scalar(value), do: serialize_scalar(nil, value)
|
|
|
|
defp parse_inline_list(value) do
|
|
inner =
|
|
value
|
|
|> String.trim()
|
|
|> String.trim_leading("[")
|
|
|> String.trim_trailing("]")
|
|
|> String.trim()
|
|
|
|
if inner == "" do
|
|
[]
|
|
else
|
|
parse_inline_list_items(inner)
|
|
end
|
|
end
|
|
|
|
defp parse_inline_list_items(inner) do
|
|
if String.contains?(inner, "\"") or String.contains?(inner, "'") do
|
|
Regex.scan(~r/"((?:\\.|[^"])*)"|'((?:\\.|[^'])*)'/, inner, capture: :all_but_first)
|
|
|> Enum.map(fn captures ->
|
|
captures
|
|
|> Enum.find(&(&1 != ""))
|
|
|> parse_inline_string_item()
|
|
end)
|
|
else
|
|
inner
|
|
|> String.split(",", trim: true)
|
|
|> Enum.map(&parse_scalar(nil, String.trim(&1)))
|
|
end
|
|
end
|
|
|
|
defp parse_inline_string_item(nil), do: ""
|
|
|
|
defp parse_inline_string_item(value) do
|
|
value
|
|
|> String.replace("\\n", "\n")
|
|
|> String.replace("\\\"", "\"")
|
|
|> String.replace("\\'", "'")
|
|
|> String.replace("\\\\", "\\")
|
|
end
|
|
|
|
defp maybe_quote_string(value) do
|
|
if Regex.match?(~r/^[\p{L}\p{N} ._\/-]+$/u, value) do
|
|
value
|
|
else
|
|
quote_string(value)
|
|
end
|
|
end
|
|
|
|
defp quote_string(value) do
|
|
escaped =
|
|
value
|
|
|> String.replace("\\", "\\\\")
|
|
|> String.replace("\"", "\\\"")
|
|
|> String.replace("\n", "\\n")
|
|
|
|
"\"#{escaped}\""
|
|
end
|
|
|
|
defp timestamp_key?(key) do
|
|
rendered = to_string(key)
|
|
String.ends_with?(rendered, "_at") or String.ends_with?(rendered, "At")
|
|
end
|
|
end
|