Railsで複雑なCSV出力データを作る

# 0. はじめに
レポート出力用のCSVデータを作成するAPIを構築する。
レポートの数はユーザーによって異なり、１レポートに紐づく詳細の数も異なっている。
そのため、１レポートに関連する詳細を取得して結合する必要がある。
また、ヘッダーについては１レポートの最大詳細数分を表示する必要がある。

（レポートのイメージ）
| 案件名 | 詳細１ | 詳細2 |
| - | - | - |
| 〇〇案件 | △△詳細 |　✳︎✳︎詳細 |
| 〇X案件 | △X詳細 |　 |

# 1. reports_controllerの実装
先に、report_bodyの値を取得して、その中で詳細の最大数を取得する。
取得した詳細の最大数をもとに、report_headerを作成する。

```
class ReportsController < BaseController
  def index
    reports = Report.fetch_reports_by_member(params[:member_id])

    render json: {
      body: report_body(reports),
      header: report_header
    }
  end

  private

  def report_body(reports)
    @report_body = Report.formatted_report_body(reports)
  end

  def report_header
    Report.formatted_report_header(max_details_count)
  end
  
  def max_details_count
    Report.calculate_max_details_count(@report_body)
  end
end
```

# 2. ユーザに紐づくレポートと詳細を取得
includesメソッドで取得した状態のため、クラスは`ActiveRecord::Relation`となる。
評価する際は、firstメソッドを使う。

```
class Report < ApplicationRecord
  has_many :report_details
  belongs_to :member
  
  class << self
    def fetch_reports_by_member(member_id)
      current_member = Member.find_by(member_id: member_id)
      current_member.reports_with_related_attributes
    end
  end
end
```
```
class Member < ApplicationRecord
  has_many :reports

  def reports_with_related_attributes
    reports.includes(:report_details)
  end
end
```

```
class ReportDetail < ApplicationRecord
  belongs_to :report
end
```

# 3. レポートのBodyを取得
reportsをmapで展開して、さらにreportの詳細で展開して値を取得する。
そのため、１レポートの値の数は異なる配列の長さになる。

```
class Report < ApplicationRecord
  ATTRIBUTE_NAMES = ['hoge1', 'hoge2']

  class << self
    def formatted_report_body(reports)
      reports.map do |report|
        formatted_attribute_values(report) + report.related_attribute_values.flatten
      end
    end

    private

    def formatted_attribute_values(record)
      ATTRIBUTE_NAMES.map { |attr_name| record.public_send(attr_name) }
    end
  end

  def related_attribute_values
    report_details.map do |detail|
      [detail.fuga1, detail.fuga2]
    end
  end
end
```

# 4. レポート詳細の最大数を計算
最大数の詳細を保持しているレポートを取得し、そのレポートの配列から、レポート詳細の数を計算する。

```
class Report < ApplicationRecord
  class << self
    def calculate_max_details_count(report_body)
      return 0 if report_body.blank?

      (report_body.max_by(&:count).count - ATTRIBUTE_NAMES.count) / ReportDetail::ATTRIBUTE_NAMES.count
    end
  end
end
```


```
class ReportDetail < ApplicationRecord
  ATTRIBUTE_NAMES = ['fuga1','fuga2']
end
```

# 5. レポートのHeaderを作成
取得した詳細の最大数を基に、ヘッダーを作成する。
詳細については、`flat_map`を使用して配列でreturnするな実装とする。
この時、詳細がない場合も考慮に入れたメソッドを作成する。

```
class Report < ApplicationRecord
  class << self
    def formatted_report_header(max_details_count)
      formatted_attribute_names + ReportDetail.related_attribute_names(max_details_count)
    end

    private

    def formatted_attribute_names
      ATTRIBUTE_NAMES.map { |attr_name| human_attribute_name(attr_name) }
    end
  end
end
```
```
class ReportDetail < ApplicationRecord
  ATTRIBUTE_NAMES = ['fuga1','fuga2']

    def self.related_attribute_names(max_details_count)
    return ATTRIBUTE_NAMES.map { |attr_name| "#{attr_name}1" } if [0, 1].include?(max_details_count)

    (1..max_details_count).flat_map do |i|
      ATTRIBUTE_NAMES.map { |attr_name| "#{attr_name}#{i}" }
    end
  end
end
```

# 6. 最後に
この実装で、CSVデータ作成する場合に、詳細の最大数に応じて実装することができた。
また、取得する属性を定数にしているため、容易にカスタマイズすることができる。