@koyo-miyamura です!
最近は本業の傍ら、Elixirコミュニティの方で副業しており、実務でのElixir実装にハマっております
前回下記の記事でCSVエクスポート機能を作成しました
ここから一歩踏み込んで、Ecto から取得したデータをCSV文字列に変換するモジュールを作成しつつ、使用するライブラリをよりメジャーな NimbleCSV に変えて実装してみようと思います
モジュールの作成
作りたいモジュールのインターフェース
作成するモジュール名を PaginateSample.Utils.CsvConverter
として下記のようなインターフェースを想定します
Ecto から取得したレコードの配列とそこから抽出したいフィールドのキーワードリストを入力として受け取り、CSV文字列を出力します
defmodule PaginateSampleWeb.ExportUserController do
use PaginateSampleWeb, :controller
alias PaginateSample.Users
+ alias PaginateSample.Utils.CsvConverter
def create(conn, _params) do
send_download(conn, {:binary, csv_data()}, filename: "export.csv")
end
defp csv_data do
+ CsvConverter.to_csv(users(), fields())
- users()
- |> CSV.encode(headers: fields())
- |> Enum.join()
end
defp users do
Users.list_all_users()
end
defp fields do
[:id, :name, :inserted_at, :updated_at]
end
end
モジュールの実装
実装は下記のようになります。また doctest も書いてみます
defmodule PaginateSample.Utils.CsvConverter do
@moduledoc """
CSV Converter
"""
@doc ~S"""
Convert struct lists to CSV
## Examples
iex> import DummyStruct
iex> records = [%DummyStruct{id: 1, name: :hoge, age: 10}, %DummyStruct{id: 2, name: :fuga, age: 20}, %DummyStruct{id: 3, name: :hogefuga, age: 30}]
iex> PaginateSample.Utils.CsvConverter.to_csv(records, [:name, :age])
"name,age\r\nhoge,10\r\nfuga,20\r\nhogefuga,30\r\n"
iex> PaginateSample.Utils.CsvConverter.to_csv(records, [:age])
"age\r\n10\r\n20\r\n30\r\n"
iex> PaginateSample.Utils.CsvConverter.to_csv(records, [:not_found])
"not_found\r\n\r\n\r\n\r\n"
"""
def to_csv(records, fields) do
records
|> CSV.encode(headers: fields)
|> Enum.join()
end
end
モジュールのテスト
doctest をするためにテストファイルを配置します
defmodule PaginateSample.Utils.CsvConverterTest do
use ExUnit.Case
doctest PaginateSample.Utils.CsvConverter
end
doctest 中で使用するテスト用ダミー struct を定義しておきます(doctest 中で struct 定義はうまく機能しなかった )
defmodule DummyStruct do
defstruct id: nil, name: nil, age: nil
end
テスト実行すると無事通りますね
$ mix test test/paginate_sample/utils/csv_converter_test.exs
.
Finished in 0.02 seconds (0.00s async, 0.02s sync)
1 doctest, 0 failures
Randomized with seed 513464
使用するライブラリを NimbleCSV にする
今まで使用していた CSV ライブラリはやや更新が滞っています
なのでよりアクティブに開発されている NimbleCSV を使用する実装に変えてみます
NimbleCSV を用いた実装
defmodule PaginateSample.Utils.CsvConverter do
@moduledoc """
CSV Converter
"""
@doc ~S"""
Convert struct lists to CSV
## Examples
iex> import DummyStruct
iex> records = [%DummyStruct{id: 1, name: :hoge, age: 10}, %DummyStruct{id: 2, name: :fuga, age: 20}, %DummyStruct{id: 3, name: :hogefuga, age: 30}]
iex> PaginateSample.Utils.CsvConverter.to_csv(records, [:name, :age])
"name,age\r\nhoge,10\r\nfuga,20\r\nhogefuga,30\r\n"
iex> PaginateSample.Utils.CsvConverter.to_csv(records, [:age])
"age\r\n10\r\n20\r\n30\r\n"
iex> PaginateSample.Utils.CsvConverter.to_csv(records, [:not_found])
"not_found\r\n\r\n\r\n\r\n"
"""
def to_csv(records, fields) do
([fields] ++ csv_content(records, fields))
|> NimbleCSV.RFC4180.dump_to_stream()
|> Enum.join()
end
defp csv_content(records, fields) do
records
|> Enum.map(fn record ->
fields
|> Enum.map(&Map.get(record, &1))
end)
end
end
単に Map.take(fields)
とすると、fields 順にならないので Enum.map
と Map.get
を組み合わせるのがポイントです
(もっとスマートな書き方あったら教えてください)
テストも通りますね
$ mix test test/paginate_sample/utils/csv_converter_test.exs
.
Finished in 0.02 seconds (0.00s async, 0.02s sync)
1 doctest, 0 failures
Randomized with seed 654005
まとめ
リポジトリはこちらです