require 'csv' class PostImportSourceParser MAX_ROWS = 100 MAX_BYTES = 1.megabyte MAX_CELL_BYTES = 20.kilobytes MAX_COLUMNS = 50 def initialize source:, format:, has_header: false, json_path: nil @source = source.to_s @format = format.to_s @has_header = ActiveModel::Type::Boolean.new.cast(has_header) @json_path = json_path.to_s end def parse raise ArgumentError, '入力が大きすぎます.' if @source.bytesize > MAX_BYTES @format == 'json' ? parse_json : parse_delimited end private def parse_delimited separator = @format == 'csv' ? ',' : "\t" table = CSV.parse(@source, col_sep: separator).map { |row| row.map { _1.to_s.strip } } raise ArgumentError, '入力が空です.' if table.empty? headers = @has_header ? table.shift : nil ensure_limits!(table) width = table.map(&:length).max.to_i raise ArgumentError, "列数は #{ MAX_COLUMNS } 列までです." if width > MAX_COLUMNS { columns: (0...width).map { |index| { key: index.to_s, label: "#{ column_name(index) }:#{ headers&.[](index).presence || '列' }" } }, rows: table.each_with_index.map { |values, index| { source_row: index + (@has_header ? 2 : 1), values: } } } rescue CSV::MalformedCSVError => e raise ArgumentError, "CSV・TSV の形式が不正です: #{ e.message }" end def parse_json value = JSON.parse(@source, max_nesting: 20) @json_path.split('.').reject(&:blank?).each { |part| value = value.is_a?(Array) ? value[Integer(part)] : value.fetch(part) } raise ArgumentError, 'JSON の投稿候補は配列にしてください.' unless value.is_a?(Array) ensure_limits!(value) unless value.all?(Hash) invalid = value.each_with_index.filter_map { |item, index| index + 1 unless item.is_a?(Hash) } raise ArgumentError, "JSON の投稿候補は object の配列にしてください: 行 #{ invalid.join(', ') }" end keys = value.flat_map(&:keys).map(&:to_s).uniq raise ArgumentError, "列数は #{ MAX_COLUMNS } 列までです." if keys.length > MAX_COLUMNS { columns: keys.each_with_index.map { |key, index| { key:, label: "#{ column_name(index) }:#{ key }" } }, rows: value.each_with_index.map { |item, index| { source_row: index + 1, values: keys.map { |key| value = item.key?(key) ? item[key] : item[key.to_sym] json_cell(value) } } } } rescue JSON::ParserError, KeyError, ArgumentError => e raise ArgumentError, "JSON の形式が不正です: #{ e.message }" end def ensure_limits! rows raise ArgumentError, "取込件数は #{ MAX_ROWS } 件までです." if rows.length > MAX_ROWS raise ArgumentError, 'セルが大きすぎます.' if rows.flatten.any? { _1.to_s.bytesize > MAX_CELL_BYTES } end def json_cell value case value when nil then '' when String, Numeric, TrueClass, FalseClass then value.to_s when Array, Hash then JSON.generate(value) else raise ArgumentError, 'JSON のセル値が不正です.' end end def column_name index name = +'' loop do name.prepend((65 + (index % 26)).chr) index = (index / 26) - 1 break if index < 0 end name end end