Current section

Files

Jump to
ex_yarn lib ex_yarn.ex
Raw

lib/ex_yarn.ex

defmodule ExYarn do
@moduledoc """
The library's main module
This module should be used as the public entrypoint of the library. It exposes
a single function, `parse/2`, which is used to parse a lockfile's contents.
## Example
Given the following `yarn.lock` file:
```yaml
foo:
"bar" true
foo 10
foobar: barfoo
```
The module should be used in the following manner:
iex> {:ok, input} = File.read("yarn.lock")
iex> ExYarn.parse(input, "yarn.lock")
{:ok, :success, %{"foo" => %{"bar" => true, "foo" => 10, "foobar" => "barfoo"}}}
"""
alias ExYarn.{ParseError, Parser}
@merge_conflict_ancestor "|||||||"
@merge_conflict_end ">>>>>>>"
@merge_conflict_sep "======="
@merge_conflict_start "<<<<<<<"
@typedoc """
The possible return values of `parse/2`
Types details:
- `{:ok, :merge, map()}`: The lockfile contained a merge conflict but was still successfully parsed
- `{:ok, :success, map()}`: The locfile was successfully parsed
- `{:error, _}`: There was an error while parsing the lockfile.
The second element indicates the type of error that occured
- `{:error, :conflict, FunctionClauseError}`: The lockfile contained a merge conflict which could not be parsed successfully.
"""
@type parseResult ::
{:ok, :merge | :success, map()}
| {:error, ParseError.t()}
| {:error, YamlElixir.FileNotFoundError}
| {:error, YamlElixir.ParsingError}
| {:error, :conflict, FunctionClauseError}
@doc """
Receives the lockfile's contents, and optionnally the lockfile's name as inputs
and returns the parse result
The lockfile's name is used to determine whether to parse the lockfile as an
official yarn lockfile (i.e. with yarn's custom format) or as a regular YAML
file. The function defaults to parsing the file as a yarn lockfile.
"""
@spec parse(String.t(), String.t()) :: parseResult()
def parse(str, file_loc \\ "lockfile") do
str = String.replace_prefix(str, "\uFEFF", "")
if has_merge_conflict?(str) do
parse_with_conflict(str, file_loc)
else
parse_file(str, file_loc)
end
end
defp extract_conflict_variants(str) do
{variant1, variant2} =
String.split(str, ~r/\r?\n/)
|> extract_conflict_variants([], [])
{Enum.join(variant1, "\n"), Enum.join(variant2, "\n")}
end
defp extract_conflict_variants([line | lines], variants1, variants2) do
if String.starts_with?(line, @merge_conflict_start) do
{variant1, lines} = get_first_variant(lines, variants1, false)
{variant2, common_end} = get_second_variant(lines, variants2)
{variant1 ++ common_end, variant2 ++ common_end}
else
variants1 = [line | variants1]
variants2 = [line | variants2]
extract_conflict_variants(lines, variants1, variants2)
end
end
defp get_first_variant([line | lines], variants1, skip) do
cond do
line == @merge_conflict_sep -> {Enum.reverse(variants1), lines}
skip or String.starts_with?(line, @merge_conflict_ancestor) -> get_first_variant(lines, variants1, true)
true -> get_first_variant(lines, [line | variants1], skip)
end
end
defp get_second_variant([line | lines], variants2) do
if String.starts_with?(line, @merge_conflict_end) do
{Enum.reverse(variants2), lines}
else
get_second_variant(lines, [line | variants2])
end
end
defp has_merge_conflict?(str) do
String.contains?(str, @merge_conflict_start) and
String.contains?(str, @merge_conflict_sep) and
String.contains?(str, @merge_conflict_end)
end
@spec parse_file(String.t(), String.t()) :: parseResult()
defp parse_file(str, file_loc) do
if String.ends_with?(file_loc, ".yml") do
parse_file_yaml(str)
else
parse_file_yarn(str)
end
end
defp parse_file_yaml(str) do
case YamlElixir.read_from_string(str) do
{:error, error} -> {:error, error}
{:ok, result} -> {:ok, {:success, result}}
end
end
defp parse_file_yarn(str) do
case Parser.parse(str) do
{:error, error} ->
case YamlElixir.read_from_string(str) do
{:error, _} -> {:error, error}
{:ok, result} -> {:ok, :success, result}
end
{:ok, result} ->
{:ok, :success, result}
end
end
@spec parse_with_conflict(String.t(), String.t()) :: parseResult()
defp parse_with_conflict(str, file_loc) do
{variant1, variant2} = extract_conflict_variants(str)
try do
parse_result_1 = parse(variant1, file_loc)
parse_result_2 = parse(variant2, file_loc)
interpret_conflict_results(parse_result_1, parse_result_2)
rescue
e in FunctionClauseError -> {:error, :conflict, e}
end
end
defp interpret_conflict_results({:error, error}, _parse_result_2) do
{:error, error}
end
defp interpret_conflict_results(_parse_result_1, {:error, error}) do
{:error, error}
end
defp interpret_conflict_results(parse_result_1, parse_result_2) do
{:ok, _, parsed_obj_1} = parse_result_1
{:ok, _, parsed_obj_2} = parse_result_2
{:ok, :merge, Map.merge(parsed_obj_1, parsed_obj_2)}
end
end