119 行
3.7 KiB
Ruby
119 行
3.7 KiB
Ruby
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 do |part|
|
||
value =
|
||
case value
|
||
when Array
|
||
index = Integer(part, exception: false)
|
||
raise ArgumentError if index.nil? || index.negative? || index >= value.length
|
||
|
||
value[index]
|
||
when Hash
|
||
raise ArgumentError unless value.key?(part) || value.key?(part.to_sym)
|
||
|
||
value.key?(part) ? value[part] : value[part.to_sym]
|
||
else
|
||
raise ArgumentError
|
||
end
|
||
end
|
||
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|
|
||
json_value = item.key?(key) ? item[key] : item[key.to_sym]
|
||
json_cell(json_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
|