Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added an Elixir implementation #15

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Elixir/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Used by "mix format"
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]
31 changes: 31 additions & 0 deletions Elixir/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump

# Also ignore archive artifacts (built via "mix archive.build").
*.ez

# Ignore package tarball (built via "mix hex.build").
rsv-*.tar

# Temporary files, for example, from tests.
/tmp/

# Ignore my local setup
/shell.nix
/.envrc
/.direnv/
10 changes: 9 additions & 1 deletion Elixir/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# RSV Implementation for Elixir

See https://github.com/Stenway/RSV-Challenge/issues/3
See https://github.com/Stenway/RSV-Challenge/issues/3

Be sure to have Elixir and Mix installed. The tests depend on the `Jason` dependency for parsing the test fixtures, so be sure to run `mix deps.get`.

The tests can then be run with `mix test`.

The tests relies on test fixtures found in `../TestFiles`. Currently all the valid and invalid tests pass, but this might not be the case if an edge case has been added that this Elixir implementation doesn't account for. Should that be the case, please go ahead and fix it :)

The `RSV` module exposes two functions, `encode!/1` and `decode!/1`. Both will raise if they get invalid data. Many improvements could be made; non-bang versions of the two functions could be added (versions of the functions that returns ok/error-tuples instead of raising). Also, it would be interesting to support streaming. Feel free to add all this.
57 changes: 57 additions & 0 deletions Elixir/lib/rsv.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
defmodule RSV do
@moduledoc """
Documentation for `RSV`.
"""

defmodule ParseError do
defexception [:message]
end

@terminate_value 0xFF
@terminate_row 0xFD
@null 0xFE

@doc """
Encode data containing lists of lists of String and Nil
"""
def encode!(data_rows) when is_list(data_rows) do
for row <- data_rows, into: <<>> do
<<for(value <- row, into: <<>>, do: encode_value(value))::binary, @terminate_row>>
end
end

defp encode_value(nil), do: <<@null, @terminate_value>>
defp encode_value(value), do: <<value::binary, @terminate_value>>

@doc """
Decode RSV encoding data
"""
def decode!(<<data::binary>>), do: do_decode(data, <<>>, [], [])

defp do_decode(<<>>, <<>>, [], acc), do: Enum.reverse(acc)

defp do_decode(<<char, rest::binary>>, value_acc, row_acc, acc) when char < 0xF8,
do: do_decode(rest, value_acc <> <<char>>, row_acc, acc)

defp do_decode(<<@terminate_row, rest::binary>>, <<>>, current_row, acc),
do: do_decode(rest, <<>>, [], [current_row | acc])

defp do_decode(<<@null, @terminate_value, rest::binary>>, <<>>, current_row, acc),
do: do_decode(rest, <<>>, current_row ++ [nil], acc)

defp do_decode(<<@terminate_value, rest::binary>>, current_value, current_row, acc) do
if String.valid?(current_value) do
do_decode(rest, <<>>, current_row ++ [current_value], acc)
else
# We are strict about UTF-8 encoding and decoding
raise ParseError, "Invalid string data: #{inspect(current_value)}"
end
end

# Parse Errors
defp do_decode(<<>>, "", _row, _acc),
do: raise(ParseError, "Reached end of data, expected an end of row terminator")

defp do_decode(<<char, _rest::binary>>, _, _, _) when char >= 0xF8,
do: raise(ParseError, "Encountered an invalid UTF-8 byte (<<#{char}>>).")
end
29 changes: 29 additions & 0 deletions Elixir/mix.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
defmodule Rsv.MixProject do
use Mix.Project

def project do
[
app: :rsv,
version: "0.1.0",
elixir: "~> 1.16",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end

# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end

# Run "mix help deps" to learn about dependencies.
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
{:jason, "~> 1.4", only: :test}
]
end
end
3 changes: 3 additions & 0 deletions Elixir/mix.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
%{
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
}
53 changes: 53 additions & 0 deletions Elixir/test/rsv_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
defmodule RSVTest do
use ExUnit.Case, async: true

doctest RSV

@fixture_dir Path.relative_to_cwd("../TestFiles/")

describe "valid input" do
setup context do
# Get the current file name based on the test name by stripping
# the prefix in the test name generated by ExUnit ("test valids
# ...")
file_name = String.replace_prefix("#{context.test}", "test #{context.describe} ", "")
root = Path.rootname(file_name)

with {:ok, data} <- File.read("#{root}.rsv"),
{:ok, expectation_data} <- File.read("#{root}.json"),
{:ok, expectation} <- Jason.decode(expectation_data) do
{:ok, raw: data, expectation: expectation}
end
end

for test_name <- Path.wildcard(Path.expand("Valid*.rsv", @fixture_dir)) do
test test_name, context do
# First we check if we match the expected result (from the
# json file) if we decode the raw data
assert context.expectation == RSV.decode!(context.raw)
# Then we attempt to decode and then re-encode; our encoder
# should produce the same data as the given input
assert context.raw == context.raw |> RSV.decode!() |> RSV.encode!()
end
end
end

describe "invalid input" do
setup context do
# Get the current file name based on the test name by stripping
# the prefix in the test name generated by ExUnit ("test valids
# ...")
file_name = String.replace_prefix("#{context.test}", "test #{context.describe} ", "")

{:ok, raw: File.read!(file_name)}
end

for test_name <- Path.wildcard(Path.expand("Invalid*.rsv", @fixture_dir)) do
test test_name, context do
assert_raise RSV.ParseError, fn ->
_should_raise = RSV.decode!(context.raw)
end
end
end
end
end
1 change: 1 addition & 0 deletions Elixir/test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ExUnit.start()