├── .gitignore ├── README.md ├── lib ├── ex_csv.ex └── ex_csv │ ├── parser.ex │ └── table.ex ├── mix.exs ├── mix.lock └── test ├── lib ├── ex_csv │ └── parser_test.exs └── ex_csv_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ExCsv 2 | ===== 3 | 4 | Elixir CSV. 5 | 6 | Note: Currently only supports parsing. 7 | 8 | ## Usage 9 | 10 | ### Parsing 11 | 12 | Parsing a file gives you a `ExCsv.Table` struct: 13 | 14 | ```elixir 15 | File.read!("foo/bar.csv") |> ExCsv.parse 16 | # => {:ok, %ExCsv.Table{...}} 17 | ``` 18 | 19 | (You can alse use `ExCsv.parse!/1` which will raise an error instead 20 | of returning an `{:error, err}` tuple if parsing fails.) 21 | 22 | If your CSV has headings, you can let the parser know up front: 23 | 24 | ```elixir 25 | {:ok, table} = File.read!("foo/bar.csv") |> ExCsv.parse(headings: true) 26 | # => {:ok, %ExCsv.Table{...}} 27 | table.headings 28 | # => ["Person", "Current Age"] 29 | ``` 30 | 31 | Or you can use `ExCsv.with_headings/1` afterwards: 32 | 33 | ```elixir 34 | {:ok, table} = File.read!("foo/bar.csv") 35 | |> ExCsv.parse! 36 | |> ExCsv.with_headings 37 | # => %ExCsv.Table{...} 38 | table.headings 39 | # => ["Person", "Current"] 40 | ``` 41 | 42 | You can also change the set or change headings by using 43 | `ExCsv.with_headings/2`: 44 | 45 | ```elixir 46 | table = File.read!("foo/bar.csv") 47 | |> ExCsv.parse! 48 | |> ExCsv.with_headings(["name", "age"]) 49 | # => %ExCsv.Table{...} 50 | table.headings 51 | # => ["name", "age"] 52 | ``` 53 | 54 | If you need to parse a format that uses another delimiter character, 55 | you can set it as an option (note the single quotes): 56 | 57 | ```elixir 58 | table = File.read!("foo/bar.csv") |> ExCsv.parse!(delimiter: ';') 59 | # => %ExCsv.Table{...} 60 | ``` 61 | 62 | Once you have a `ExCsv.Table`, you can use its `headings` and `body` 63 | directly -- or you enumerate over the table. 64 | 65 | ## Enumerating 66 | 67 | If your `ExCsv.Table` struct does not have headers, iterating over it 68 | will result in a list for each row: 69 | 70 | ```elixir 71 | table = File.read!("foo/bar.csv") 72 | |> ExCsv.parse! 73 | |> Enum.to_list 74 | # [["Jayson", 23], ["Jill", 34], ["Benson", 45]] 75 | ``` 76 | 77 | If your table has headings, you'll get maps: 78 | 79 | ```elixir 80 | table = File.read!("foo/bar.csv") 81 | |> ExCsv.parse!(headings: true) 82 | |> ExCsv.with_headings([:name, :age]) 83 | |> Enum.to_list 84 | # [%{name: "Jayson", age: 23}, 85 | # %{name: "Jill", age: 34}, 86 | # %{name: "Benson", age: 45}] 87 | ``` 88 | 89 | You can build structs from the rows by using `ExCsv.as/1` (if the 90 | headings match the struct attributes): 91 | 92 | ```elixir 93 | table = File.read!("foo/bar.csv") 94 | |> ExCsv.parse!(headings: true) 95 | |> ExCsv.with_headings([:name, :age]) 96 | |> ExCsv.as(Person) 97 | |> Enum.to_list 98 | # [%Person{name: "Jayson", age: 23}, 99 | # %Person{name: "Jill", age: 34}, 100 | # %Person{name: "Benson", age: 45}] 101 | ``` 102 | 103 | If the headings don't match the struct attributes, you can provide a 104 | mapping (of CSV heading name to struct attribute name) with 105 | `ExCsv.as/2`: 106 | 107 | ```elixir 108 | table = File.read!("books.csv") 109 | |> ExCsv.parse!(headings: true) 110 | |> ExCsv.as(Author, %{"name" => :title, "author" => :name}) 111 | |> Enum.to_list 112 | # [%Author{name: "John Scalzi", title: "A War for Old Men"}, 113 | # %Author{name: "Margaret Atwood", title: "A Handmaid's Tale"}] 114 | ``` 115 | 116 | ## Contributing 117 | 118 | Please fork and send pull requests (preferably from non-master 119 | branches), including tests (`ExUnit.Case`). 120 | 121 | Report bugs and request features via Issues; PRs are even better! 122 | 123 | ## License 124 | 125 | The MIT License (MIT) 126 | 127 | Copyright (c) 2014 CargoSense, Inc. 128 | 129 | Permission is hereby granted, free of charge, to any person obtaining a copy 130 | of this software and associated documentation files (the "Software"), to deal 131 | in the Software without restriction, including without limitation the rights 132 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 133 | copies of the Software, and to permit persons to whom the Software is 134 | furnished to do so, subject to the following conditions: 135 | 136 | The above copyright notice and this permission notice shall be included in 137 | all copies or substantial portions of the Software. 138 | 139 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 140 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 141 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 142 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 143 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 144 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 145 | THE SOFTWARE. 146 | -------------------------------------------------------------------------------- /lib/ex_csv.ex: -------------------------------------------------------------------------------- 1 | defmodule ExCsv do 2 | 3 | defdelegate parse(text), to: ExCsv.Parser 4 | defdelegate parse(text, settings), to: ExCsv.Parser 5 | defdelegate parse!(text), to: ExCsv.Parser 6 | defdelegate parse!(text, settings), to: ExCsv.Parser 7 | 8 | def headings(%{headings: headings}), do: headings 9 | def body(%{body: body}), do: body 10 | 11 | def as(%{headings: []}, _row_struct) do 12 | raise ArgumentError, "Must use ExCsv.row/3 and provide a list of keys to use for a table without headings" 13 | end 14 | def as(table, row_struct) do 15 | %{ table | row_struct: row_struct } 16 | end 17 | 18 | def as(table, row_struct, row_mapping) do 19 | %{ table | row_struct: row_struct, row_mapping: row_mapping } 20 | end 21 | 22 | def with_headings(table, headings), do: %{ table | headings: headings } 23 | def with_headings(%{body: [first | rest]} = table) do 24 | %{ table | body: rest, headings: first } 25 | end 26 | 27 | def without_headings(table), do: %{ table | headings: [] } 28 | end 29 | -------------------------------------------------------------------------------- /lib/ex_csv/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule ExCsv.Parser do 2 | defstruct delimiter: 44, return: 13, newline: 10, quote: 34, headings: false, quoting: false, quote_at: nil, eat_next_quote: true 3 | 4 | def parse!(text, opts \\ []) do 5 | case parse(text, opts) do 6 | {:ok, table} -> table 7 | {:error, err} -> raise ArgumentError, err 8 | end 9 | end 10 | 11 | def parse(text, opts \\ []) do 12 | do_parse(text, opts |> configure) 13 | end 14 | 15 | defp do_parse(iodata, config) when is_list(iodata) do 16 | iodata |> IO.iodata_to_binary |> do_parse(config) 17 | end 18 | defp do_parse(string, config) when is_binary(string) do 19 | {result, state} = string |> skip_dotall |> build([[""]], config) 20 | if state.quoting do 21 | info = result |> hd |> hd |> String.slice(0, 10) 22 | {:error, "quote meets end of file; started near: #{info}"} 23 | else 24 | [head | tail] = result |> rstrip |> Enum.reverse |> Enum.map(&(Enum.reverse(&1))) 25 | case config.headings do 26 | true -> {:ok, %ExCsv.Table{headings: head, body: tail}} 27 | false -> {:ok, %ExCsv.Table{body: [head | tail]}} 28 | end 29 | end 30 | end 31 | 32 | defp configure(settings) do 33 | settings |> configure(%ExCsv.Parser{}) 34 | end 35 | 36 | defp configure([], config), do: config 37 | defp configure([head | tail], config) do 38 | tail |> configure(config |> Map.merge(head |> setting)) 39 | end 40 | 41 | # The delimiter, newline, and quote settings need to be integers 42 | # @spec setting({atom, char_list}) :: %{atom => integer} 43 | defp setting({key, value}) when key in [:delimiter, :newline, :quote] do 44 | [{key, value |> hd}] |> Enum.into(%{}) 45 | end 46 | defp setting({key, value}), do: [{key, value}] |> Enum.into(%{}) 47 | 48 | # DELIMITER 49 | # At the beginning of a row 50 | defp build(<> <> rest, [[] | previous_rows], %{delimiter: char, quoting: false} = config) do 51 | current_row = [new_field, new_field] 52 | rows = [current_row | previous_rows] 53 | rest |> skip_whitespace |> build(rows, config) 54 | end 55 | # After the beginning of a row 56 | defp build(<> <> rest, [[current_field | previous_fields] | previous_rows], %{delimiter: char, quoting: false} = config) do 57 | current_row = [new_field | [current_field |> String.rstrip | previous_fields]] 58 | rows = [current_row | previous_rows] 59 | rest |> skip_whitespace |> build(rows, config) 60 | end 61 | 62 | # QUOTE 63 | # Start quote at the beginning of a field (don't retain this quote pair) 64 | defp build(<> <> rest, [["" | _previous_fields] | _previous_rows] = rows, %{quote: char, quoting: false} = config) do 65 | rest |> build(rows, %{ config | quoting: true, eat_next_quote: true }) 66 | end 67 | # Start quote in the middle of a field (retain this quote pair) 68 | defp build(<> <> rest, [[current_field | previous_fields] | previous_rows], %{quote: char, quoting: false} = config) do 69 | current_row = [current_field <> <> | previous_fields] 70 | rows = [current_row | previous_rows] 71 | rest |> build(rows, %{ config | quoting: true, eat_next_quote: false }) 72 | end 73 | # End quote and don't retain the quote character (full-field quoting) 74 | defp build(<> <> rest, rows, %{quote: char, quoting: true, eat_next_quote: true} = config) do 75 | rest |> skip_whitespace |> build(rows, %{ config | quoting: false }) 76 | end 77 | # End quote and retain the quote character (partial field quoting) 78 | defp build(<> <> rest, [[current_field | previous_fields] | previous_rows], %{quote: char, quoting: true, eat_next_quote: false} = config) do 79 | current_row = [current_field <> <> | previous_fields] 80 | rows = [current_row | previous_rows] 81 | rest |> build(rows, %{ config | quoting: false }) 82 | end 83 | 84 | # NEWLINE 85 | defp build(<> <> rest, [[current_field | previous_fields] | previous_rows], %{return: rt, newline: nl, quoting: false} = config) do 86 | build_newline(rest, current_field, previous_fields, previous_rows, config) 87 | end 88 | defp build(<> <> rest, [[current_field | previous_fields] | previous_rows], %{return: rt, quoting: false} = config) do 89 | build_newline(rest, current_field, previous_fields, previous_rows, config) 90 | end 91 | defp build(<> <> rest, [[current_field | previous_fields] | previous_rows], %{newline: nl, quoting: false} = config) do 92 | build_newline(rest, current_field, previous_fields, previous_rows, config) 93 | end 94 | 95 | # NORMAL CHARACTER 96 | # Starting the first field in the current row 97 | defp build(<> <> rest, [[] | previous_rows], config) do 98 | current_row = [<>] 99 | rows = [current_row | previous_rows] 100 | rest |> build(rows, config) 101 | end 102 | # Adding to the last field in the current row 103 | defp build(<> <> rest, [[current_field | previous_fields] | previous_rows], config) do 104 | current_row = [current_field <> <> | previous_fields] 105 | rows = [current_row | previous_rows] 106 | rest |> build(rows, config) 107 | end 108 | 109 | # EOF 110 | defp build("", rows, config), do: {rows, config} 111 | 112 | defp build_newline(rest, current_field, previous_fields, previous_rows, config) do 113 | current_row = [current_field |> String.rstrip | previous_fields] 114 | rows = [new_row | [current_row | previous_rows]] 115 | rest |> skip_whitespace |> build(rows, config) 116 | end 117 | 118 | defp rstrip([[""] | rows]), do: rows 119 | defp rstrip(rows), do: rows 120 | 121 | defp skip_whitespace(<> <> rest) when char in '\s\r' do 122 | skip_whitespace(rest) 123 | end 124 | defp skip_whitespace(string), do: string 125 | 126 | defp skip_dotall(<> <> rest) when char in '\s\r\n\t' do 127 | skip_dotall(rest) 128 | end 129 | defp skip_dotall(string), do: string 130 | 131 | defp new_field, do: "" 132 | defp new_row, do: [new_field] 133 | 134 | end 135 | -------------------------------------------------------------------------------- /lib/ex_csv/table.ex: -------------------------------------------------------------------------------- 1 | defmodule ExCsv.Table do 2 | 3 | defstruct headings: [], body: [], row_struct: nil, row_mapping: nil 4 | 5 | defimpl Enumerable, for: __MODULE__ do 6 | def count(%ExCsv.Table{body: body}), do: body |> length 7 | def member?(%ExCsv.Table{}, _), do: {:error, __MODULE__} 8 | 9 | def reduce(_, {:halt, acc}, _fun), do: {:halted, acc} 10 | def reduce(%ExCsv.Table{} = table, {:suspend, acc}, fun) do 11 | {:suspended, acc, &reduce(table, &1, fun)} 12 | end 13 | def reduce(%ExCsv.Table{body: []}, {:cont, acc}, _fun), do: {:done, acc} 14 | def reduce(%ExCsv.Table{body: [h|t], headings: []} = table, {:cont, acc}, fun) do 15 | value = h |> construct(table) 16 | reduce(%{ table | body: t }, fun.(value, acc), fun) 17 | end 18 | def reduce(%ExCsv.Table{body: [h|t], headings: headings} = table, {:cont, acc}, fun) do 19 | value = Enum.zip(headings, h) |> Enum.into(%{}) |> construct(table) 20 | reduce(%{ table | body: t }, fun.(value, acc), fun) 21 | end 22 | 23 | defp construct(row, %{row_struct: nil}), do: row 24 | defp construct(row, %{row_struct: row_struct, row_mapping: nil }) when is_map(row) do 25 | base = struct(row_struct) 26 | add = Enum.map row, fn {k, v} -> 27 | {k |> String.to_existing_atom, v} 28 | end 29 | struct(base, add) 30 | end 31 | defp construct(row, %{row_struct: row_struct, row_mapping: row_mapping }) when is_map(row) and is_map(row_mapping) do 32 | base = struct(row_struct) 33 | add = Enum.reduce row_mapping, [], fn ({from, to}, acc) -> 34 | from = from |> to_string 35 | if row |> Map.has_key?(from) do 36 | acc ++ [{to, row[from]}] 37 | else 38 | acc 39 | end 40 | end 41 | struct(base, add) 42 | end 43 | defp construct(row, %{row_struct: row_struct, row_mapping: row_mapping}) when is_list(row) and is_list(row_mapping) do 44 | base = struct(row_struct) 45 | add = Enum.zip(row_mapping, row |> Enum.take(row_mapping |> length)) 46 | struct(base, add) 47 | end 48 | 49 | end 50 | 51 | end 52 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ExCsv.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :ex_csv, 6 | version: "0.1.5", 7 | elixir: "~> 1.0", 8 | deps: deps, 9 | package: package] 10 | end 11 | 12 | # Configuration for the OTP application 13 | # 14 | # Type `mix help compile.app` for more information 15 | def application do 16 | [applications: [:logger]] 17 | end 18 | 19 | defp package do 20 | [maintainers: ["Bruce Williams", "Ben Wilson"], 21 | licenses: ["MIT License"], 22 | description: "CSV for Elixir", 23 | links: %{github: "https://github.com/CargoSense/ex_csv"}] 24 | end 25 | 26 | # Dependencies can be Hex packages: 27 | # 28 | # {:mydep, "~> 0.3.0"} 29 | # 30 | # Or git/path repositories: 31 | # 32 | # {:mydep, git: "https://github.com/elixir-lang/mydep.git", tag: "0.1.0"} 33 | # 34 | # Type `mix help deps` for more examples and options 35 | defp deps do 36 | [ 37 | {:ex_doc, "~> 0.12.0", only: :dev}, 38 | {:earmark, "~> 0.2.0", only: :dev}, 39 | ] 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"earmark": {:hex, :earmark, "0.2.1", "ba6d26ceb16106d069b289df66751734802777a3cbb6787026dd800ffeb850f3", [:mix], []}, 2 | "ex_doc": {:hex, :ex_doc, "0.12.0", "b774aabfede4af31c0301aece12371cbd25995a21bb3d71d66f5c2fe074c603f", [:mix], [{:earmark, "~> 0.2", [hex: :earmark, optional: false]}]}} 3 | -------------------------------------------------------------------------------- /test/lib/ex_csv/parser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExCsv.ParserTest do 2 | use ExUnit.Case 3 | 4 | test "one simple line with parse" do 5 | assert ExCsv.Parser.parse(~s) |> body == [["a", "b", "c"]] 6 | end 7 | test "one simple line with parse!" do 8 | assert ExCsv.Parser.parse!(~s).body == [["a", "b", "c"]] 9 | end 10 | test "one simple line with a space in a field" do 11 | assert ExCsv.Parser.parse(~s) |> body == [["a", "ba t", "c"]] 12 | end 13 | test "one simple line with a quoted field containing the delimiter" do 14 | assert ExCsv.Parser.parse(~s) |> body == [["a", "ba,t", "c"]] 15 | end 16 | test "one simple line with a quoted field containing a newline" do 17 | assert ExCsv.Parser.parse(~s) |> body == [["a", "ba\nt", "c"]] 18 | end 19 | test "one simple line with a quoted field containing the delimiter followed by whitespace" do 20 | assert ExCsv.Parser.parse(~s) |> body == [["a", "ba,t", "c"]] 21 | end 22 | test "one simple line with a quoted portion of a field containing the delimiter" do 23 | assert ExCsv.Parser.parse(~s) |> body == [["a", ~s, "c"]] 24 | end 25 | test "one simple line with a quoted field that is not finished" do 26 | assert {:error, _} = ExCsv.Parser.parse(~s) 27 | end 28 | test "one simple line with a quoted field that is not finished with parse!" do 29 | assert_raise ArgumentError, fn -> ExCsv.Parser.parse!(~s) end 30 | end 31 | test "one simple line that starts with a delimiter" do 32 | assert ExCsv.Parser.parse(~s<,a,b,c>) |> body == [["", "a", "b", "c"]] 33 | end 34 | test "one simple line that ends with a delimiter" do 35 | assert ExCsv.Parser.parse(~s) |> body == [["a", "b", "c", ""]] 36 | end 37 | test "one simple line with post-delimiter whitespace" do 38 | assert ExCsv.Parser.parse(~s) |> body == [["a", "b", "c"]] 39 | end 40 | test "one simple line with pre-delimiter whitespace" do 41 | assert ExCsv.Parser.parse(~s) |> body == [["a", "b", "c"]] 42 | end 43 | 44 | test "two simple lines" do 45 | assert ExCsv.Parser.parse(~s) |> body == [["a", "b", "c"], ["d", "e", "f"]] 46 | end 47 | test "two simple lines with pre-newline whitespace" do 48 | assert ExCsv.Parser.parse(~s) |> body == [["a", "b", "c"], ["d", "e", "f"]] 49 | end 50 | test "two simple lines with post-newline whitespace" do 51 | assert ExCsv.Parser.parse(~s) |> body == [["a", "b", "c"], ["d", "e", "f"]] 52 | end 53 | test "two simple lines with second starting with a delimiter" do 54 | assert ExCsv.Parser.parse(~s) |> body == [["a", "b", "c"], ["", "e", "f"]] 55 | end 56 | 57 | test "two simple lines with return character" do 58 | assert ExCsv.Parser.parse(~s) |> body == [["a", "b", "c"], ["d", "e", "f"]] 59 | end 60 | 61 | test "two simple lines with return and newline" do 62 | assert ExCsv.Parser.parse(~s) |> body == [["a", "b", "c"], ["d", "e", "f"]] 63 | end 64 | 65 | defp body({:ok, %{body: body}}), do: body 66 | 67 | end 68 | -------------------------------------------------------------------------------- /test/lib/ex_csv_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EasyRow do 2 | defstruct cat: nil, dog: nil, bird: nil 3 | end 4 | 5 | defmodule ExCsvTest do 6 | use ExUnit.Case 7 | 8 | test "parse delegate without custom setting" do 9 | {:ok, %{body: [~w(a b c)]}} = ExCsv.parse("a,b,c") 10 | end 11 | 12 | test "parse delegate with a custom setting" do 13 | {:ok, %{body: [~w(a b c)]}} = ExCsv.parse("a;b;c", delimiter: ';') 14 | end 15 | 16 | test ".headings when not requested" do 17 | {:ok, table} = ExCsv.parse("a,b,c") 18 | assert table |> ExCsv.headings == [] 19 | end 20 | 21 | test "without headings" do 22 | {:ok, table} = ExCsv.parse("a,b,c") 23 | assert table |> Enum.to_list == [["a", "b", "c"]] 24 | end 25 | 26 | test "with headings" do 27 | {:ok, table} = ExCsv.parse("from,to,cc\na,b,c", headings: true) 28 | assert table |> Enum.to_list == [%{"from" => "a", "to" => "b", "cc" => "c"}] 29 | end 30 | 31 | test "with headings, post parse" do 32 | {:ok, table} = ExCsv.parse("from,to,cc\na,b,c") 33 | assert table |> ExCsv.with_headings |> Enum.to_list == [%{"from" => "a", "to" => "b", "cc" => "c"}] 34 | end 35 | 36 | test "with headings, post parse and provided" do 37 | {:ok, table} = ExCsv.parse("a,b,c") 38 | assert table |> ExCsv.with_headings(~w(From To CC)) |> Enum.to_list == [%{"From" => "a", "To" => "b", "CC" => "c"}] 39 | end 40 | 41 | test "with headings, then removing them" do 42 | {:ok, table} = ExCsv.parse("from,to,cc\na,b,c", headings: true) 43 | assert table |> ExCsv.without_headings |> Enum.to_list == [~w"a b c"] 44 | end 45 | 46 | test ".headings when requested" do 47 | {:ok, table} = ExCsv.parse("a,b,c", headings: true) 48 | assert table |> ExCsv.headings == ["a", "b", "c"] 49 | end 50 | 51 | test "table with headings piping to ExCsv.as" do 52 | {:ok, table} = ExCsv.parse("cat,dog,bird\na,b,c\nd,e,f", headings: true) 53 | assert table |> ExCsv.as(EasyRow) |> Enum.to_list == [%EasyRow{cat: "a", dog: "b", bird: "c"}, 54 | %EasyRow{cat: "d", dog: "e", bird: "f"}] 55 | end 56 | 57 | test "table with headings piping to ExCsv.as with a string mapping" do 58 | {:ok, table} = ExCsv.parse("field1,field2,field3\na,b,c\nd,e,f", headings: true) 59 | mapping = %{"field1" => :dog, "field2" => :cat, "field3" => :bird} 60 | assert table |> ExCsv.as(EasyRow, mapping) |> Enum.to_list == [%EasyRow{dog: "a", cat: "b", bird: "c"}, 61 | %EasyRow{dog: "d", cat: "e", bird: "f"}] 62 | end 63 | 64 | test "table with headings piping to ExCsv.as with a symbol mapping" do 65 | {:ok, table} = ExCsv.parse("field1,field2,field3\na,b,c\nd,e,f", headings: true) 66 | mapping = %{field1: :dog, field2: :cat, field3: :bird} 67 | assert table |> ExCsv.as(EasyRow, mapping) |> Enum.to_list == [%EasyRow{dog: "a", cat: "b", bird: "c"}, 68 | %EasyRow{dog: "d", cat: "e", bird: "f"}] 69 | end 70 | 71 | test "table without headings and without a mapping list, piping to ExCsv.as" do 72 | {:ok, table} = ExCsv.parse("a,b,c\nd,e,f") 73 | assert_raise ArgumentError, fn -> 74 | assert table |> ExCsv.as(EasyRow) 75 | end 76 | end 77 | 78 | test "table without headings and with a mapping list of the same size, piping to ExCsv.as" do 79 | {:ok, table} = ExCsv.parse("a,b,c\nd,e,f") 80 | assert table |> ExCsv.as(EasyRow, [:bird, :cat, :dog]) |> Enum.to_list == [%EasyRow{bird: "a", cat: "b", dog: "c"}, 81 | %EasyRow{bird: "d", cat: "e", dog: "f"}] 82 | end 83 | 84 | test "table without headings and with a mapping list of a smaller size, piping to ExCsv.as" do 85 | {:ok, table} = ExCsv.parse("a,b,c\nd,e,f") 86 | assert table |> ExCsv.as(EasyRow, [:bird, :cat]) |> Enum.to_list == [%EasyRow{bird: "a", cat: "b", dog: nil}, 87 | %EasyRow{bird: "d", cat: "e", dog: nil}] 88 | end 89 | 90 | end 91 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------