230 行
8.2 KiB
Ruby
230 行
8.2 KiB
Ruby
require 'set'
|
|
|
|
# Export identifiers are stable; UI translations are supplied separately as column labels.
|
|
class CountsExportProjection
|
|
ROOT_KEYS = { 'resource_id' => 'resource_id', 'version_no' => 'version_no',
|
|
'is_latest' => 'is_latest', 'thumbnail' => 'thumbnail_url',
|
|
'thumbnail_base' => 'thumbnail_base', 'title' => 'title', 'url' => 'url',
|
|
'tags' => 'tags', 'parents' => 'parents', 'video_ms' => 'video_ms',
|
|
'original_created_at' => 'original_created_at',
|
|
'original_created_from' => 'original_created_from',
|
|
'original_created_before' => 'original_created_before',
|
|
'user' => 'created_by_user', 'created_at' => 'created_at',
|
|
'name' => 'name', 'names' => 'names', 'category' => 'category',
|
|
'aliases' => 'aliases', 'deprecated' => 'deprecated' }.freeze
|
|
CHILD_KEYS = { 'id' => 'id', 'versionNo' => 'version_no', 'kind' => 'kind',
|
|
'name' => 'name', 'category' => 'category', 'sections' => 'sections',
|
|
'names' => 'names', 'aliases' => 'aliases',
|
|
'languageCode' => 'language', 'scriptCode' => 'script',
|
|
'autoGenerated' => 'auto_generated' }.freeze
|
|
NAME_FIELDS = ['languageCode', 'scriptCode', 'name', 'aliases', 'autoGenerated'].freeze
|
|
CHILDREN = {
|
|
'tags' => ['id', 'versionNo', 'kind', 'name', 'category', 'sections', 'names'],
|
|
'names' => NAME_FIELDS,
|
|
'aliases' => ['languageCode', 'name', 'scriptCode'],
|
|
'parents' => ['id'] }.freeze
|
|
|
|
attr_reader :columns, :headers
|
|
|
|
def initialize(params, resource:, summary:, tags:)
|
|
@params = params
|
|
@labels = JSON.parse(params.fetch('labels', '{}'))
|
|
raise ArgumentError unless @labels.is_a?(Hash) && @labels.length <= 100
|
|
raise ArgumentError unless @labels.all? { |key, value| valid_label?(key, value) }
|
|
@summary = summary
|
|
allowed = if summary
|
|
tags ? ['kind', 'tag_id', 'name', 'deprecated', 'count'] : ['group', 'count']
|
|
else
|
|
CountsQuery::COLUMNS.fetch(resource)
|
|
end
|
|
@columns = params.fetch('columns', allowed.join(',')).split(',')
|
|
raise ArgumentError if (@columns - allowed).any? || @columns.uniq != @columns
|
|
|
|
@headers = JSON.parse(params.fetch('headers', JSON.generate(@columns)))
|
|
unless (@headers.is_a?(Array) && @headers.length == @columns.length &&
|
|
@headers.all? { _1.is_a?(String) && _1.length <= 200 })
|
|
raise ArgumentError
|
|
end
|
|
@order = params.fetch('order', '').split(',')
|
|
@sorts = params.fetch('child_sort', '').split(',').map do |entry|
|
|
parts = entry.split(':', -1)
|
|
raise ArgumentError unless parts.length == 2
|
|
|
|
path, direction = parts
|
|
raise ArgumentError unless ['asc', 'desc'].include?(direction)
|
|
|
|
[path, direction]
|
|
end
|
|
@selected = params.key?('children') ? params['children'].split(',').to_set : nil
|
|
valid = CHILDREN.flat_map do |key, fields|
|
|
paths(fields, key)
|
|
end
|
|
raise ArgumentError if @selected && (@selected.to_a - valid).any?
|
|
allowed_sorts = valid.map do |path|
|
|
parts = path.split('.')
|
|
field = parts.pop
|
|
"#{ parts.join('.') }/#{ field }"
|
|
end
|
|
if ((@sorts.map(&:first) - allowed_sorts).any? ||
|
|
@sorts.map(&:first).uniq.length != @sorts.length)
|
|
raise ArgumentError
|
|
end
|
|
end
|
|
|
|
def metadata
|
|
@columns.each_with_index.map do |key, index|
|
|
{ key: @summary ? key : ROOT_KEYS.fetch(key), label: @headers[index] }
|
|
end
|
|
end
|
|
|
|
def project(row)
|
|
@columns.to_h do |key|
|
|
if @summary
|
|
value = key == 'group' ? { key: row[:key], name: row[:name] } : row[key.to_sym]
|
|
[key, value]
|
|
else
|
|
value = case key
|
|
when 'thumbnail' then row[:thumbnail_base]
|
|
when 'original_created_at'
|
|
{ from: row[:original_created_from], before: row[:original_created_before] }
|
|
else row[ROOT_KEYS.fetch(key).to_sym]
|
|
end
|
|
value = nested(value || [], CHILDREN.fetch(key), key) if CHILDREN.key?(key)
|
|
[ROOT_KEYS.fetch(key), value]
|
|
end
|
|
end
|
|
end
|
|
|
|
def csv_record(record, row)
|
|
record.map do |key, value|
|
|
key == 'group' ? row[:name] || row[:key] : csv_display(value, key)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def valid_label?(key, value)
|
|
key.is_a?(String) && value.is_a?(String) && key.length <= 100 && value.length <= 300
|
|
end
|
|
|
|
def csv_display(value, key)
|
|
return value ? @labels.fetch('latest', 'true') : '' if key == 'is_latest'
|
|
return @labels.fetch("kind:#{ value }", value) if key == 'kind'
|
|
return @labels.fetch("category:#{ value }", value) if key == 'category'
|
|
if ['language', 'script'].include?(key) && value.is_a?(Hash)
|
|
return @labels.fetch("#{ key }:#{ value[:code] }",
|
|
value[:name] || @labels.fetch('notRegistered', ''))
|
|
end
|
|
return @labels.fetch("boolean:#{ value }", value) if value == true || value == false
|
|
return value.map { csv_display(_1, key) } if value.is_a?(Array)
|
|
if value.is_a?(Hash)
|
|
return value.to_h do |field, item|
|
|
[@labels.fetch(field.to_s, field), csv_display(item, field.to_s)]
|
|
end
|
|
end
|
|
|
|
value
|
|
end
|
|
|
|
def fields_for(key)
|
|
return NAME_FIELDS if key == 'names'
|
|
return ['name', 'scriptCode'] if key == 'aliases'
|
|
|
|
[]
|
|
end
|
|
|
|
def paths(fields, prefix)
|
|
fields.flat_map do |key|
|
|
path = "#{ prefix }.#{ key }"
|
|
[path, *paths(fields_for(key), path)]
|
|
end
|
|
end
|
|
|
|
def fetch(row, key) = row.key?(key) ? row[key] : row[key.to_sym]
|
|
|
|
def named_code(row, kind)
|
|
code = fetch(row, "#{ kind }_code")
|
|
name = fetch(row, "#{ kind }_name")
|
|
{ code:, name: name.present? && name.to_s.downcase != code.to_s.downcase ? name : nil }
|
|
end
|
|
|
|
def stable(value)
|
|
case value
|
|
when Hash
|
|
value.stringify_keys.sort.to_h.transform_values { stable(_1) }
|
|
when Array
|
|
value.map { stable(_1) }
|
|
else
|
|
value
|
|
end
|
|
end
|
|
|
|
def sort_value(row, key)
|
|
if key == 'languageCode'
|
|
language = named_code(row, 'language')
|
|
return @labels.fetch("language:#{ language[:code] }", language[:name])
|
|
end
|
|
return named_code(row, 'script')[:name] if key == 'scriptCode'
|
|
|
|
fetch(row, CHILD_KEYS.fetch(key))
|
|
end
|
|
|
|
def comparison(left, right)
|
|
if left.is_a?(Array) && right.is_a?(Array) && left.length != right.length
|
|
return left.length <=> right.length
|
|
end
|
|
return left <=> right if left.is_a?(Numeric) && right.is_a?(Numeric)
|
|
if [true, false].include?(left) && [true, false].include?(right)
|
|
return (left ? 1 : 0) <=> (right ? 1 : 0)
|
|
end
|
|
|
|
a = left.is_a?(Hash) || left.is_a?(Array) ? JSON.generate(stable(left)) : left.to_s
|
|
b = right.is_a?(Hash) || right.is_a?(Array) ? JSON.generate(stable(right)) : right.to_s
|
|
a <=> b
|
|
end
|
|
|
|
def sorted(rows, prefix, fields)
|
|
sorts = @sorts.filter_map do |path, direction|
|
|
parent, key = path.split('/', 2)
|
|
next unless parent == prefix
|
|
raise ArgumentError unless fields.include?(key)
|
|
|
|
[key, direction]
|
|
end
|
|
rows.each_with_index.sort do |(left, a), (right, b)|
|
|
result = 0
|
|
sorts.each do |key, direction|
|
|
first = sort_value(left, key)
|
|
last = sort_value(right, key)
|
|
result = if first.nil? || last.nil?
|
|
first.nil? == last.nil? ? 0 : (first.nil? ? 1 : -1)
|
|
else
|
|
comparison(first, last) * (direction == 'asc' ? 1 : -1)
|
|
end
|
|
break unless result.zero?
|
|
end
|
|
result.zero? ? a <=> b : result
|
|
end.map(&:first)
|
|
end
|
|
|
|
def nested(rows, fields, prefix)
|
|
keys = fields.select { !@selected || @selected.include?("#{ prefix }.#{ _1 }") }
|
|
ordered = (@order + keys.map { "#{ prefix }.#{ _1 }" }).uniq
|
|
.filter_map { |path| keys.find { "#{ prefix }.#{ _1 }" == path } }
|
|
sorted(rows, prefix, fields).map do |row|
|
|
ordered.to_h do |key|
|
|
stable = CHILD_KEYS.fetch(key)
|
|
value = case key
|
|
when 'languageCode' then named_code(row, 'language')
|
|
when 'scriptCode' then named_code(row, 'script')
|
|
else fetch(row, stable)
|
|
end
|
|
if fields_for(key).any?
|
|
value = nested(value || [], fields_for(key), "#{ prefix }.#{ key }")
|
|
end
|
|
[stable, value]
|
|
end
|
|
end
|
|
end
|
|
end
|