Adds documentation to some lib functions (#168)

* Add sigs and docs to transformers

* Add sigs and docs to downloaders

* Adds sigs and docs to importers
This commit is contained in:
Justin Edmund 2025-01-18 03:09:29 -08:00 committed by GitHub
parent 0d5d4d5f59
commit 7d164b540c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1080 additions and 20 deletions

View file

@ -2,9 +2,39 @@
module Granblue
module Downloaders
# Abstract base class for downloading game asset images in various sizes.
# Handles local and S3 storage, with support for test mode and verbose logging.
#
# @abstract Subclass must implement {#object_type}, {#base_url}, and {#directory_for_size}
#
# @example Downloading assets for a specific ID
# class MyDownloader < BaseDownloader
# def object_type; "weapon"; end
# def base_url; "http://example.com/assets"; end
# def directory_for_size(size)
# case size
# when "main" then "large"
# when "grid" then "medium"
# when "square" then "small"
# end
# end
# end
#
# downloader = MyDownloader.new("1234", storage: :both)
# downloader.download
#
# @note Supports three image sizes: main, grid, and square
# @note Can store images locally, in S3, or both
class BaseDownloader
# @return [Array<String>] Available image size variants
SIZES = %w[main grid square].freeze
# Initialize a new downloader instance
# @param id [String] ID of the object to download images for
# @param test_mode [Boolean] When true, only logs actions without downloading
# @param verbose [Boolean] When true, enables detailed logging
# @param storage [Symbol] Storage mode (:local, :s3, or :both)
# @return [void]
def initialize(id, test_mode: false, verbose: false, storage: :both)
@id = id
@base_url = base_url
@ -15,6 +45,8 @@ module Granblue
ensure_directories_exist unless @test_mode
end
# Download images for all sizes
# @return [void]
def download
log_info "-> #{@id}"
return if @test_mode
@ -28,6 +60,12 @@ module Granblue
private
# Process download for a specific size variant
# @param url [String] URL to download from
# @param size [String] Size variant being processed
# @param path [String] Local path for download
# @param last [Boolean] Whether this is the last size being processed
# @return [void]
def process_download(url, size, path, last: false)
filename = File.basename(url)
s3_key = build_s3_key(size, filename)
@ -54,11 +92,19 @@ module Granblue
log_info "\t404 returned\t#{url}"
end
# Download file to local storage
# @param url [String] Source URL
# @param download_uri [String] Local destination path
# @return [void]
def download_to_local(url, download_uri)
download = URI.parse(url).open
IO.copy_stream(download, download_uri)
end
# Stream file directly to S3
# @param url [String] Source URL
# @param s3_key [String] S3 object key
# @return [void]
def stream_to_s3(url, s3_key)
return if @aws_service.file_exists?(s3_key)
@ -67,6 +113,11 @@ module Granblue
end
end
# Download file to both local storage and S3
# @param url [String] Source URL
# @param download_uri [String] Local destination path
# @param s3_key [String] S3 object key
# @return [void]
def download_to_both(url, download_uri, s3_key)
download = URI.parse(url).open
@ -82,17 +133,23 @@ module Granblue
end
end
# Check if file should be downloaded based on storage mode
# @param local_path [String] Local file path
# @param s3_key [String] S3 object key
# @return [Boolean] true if file should be downloaded
def should_download?(local_path, s3_key)
case @storage
when :local
if @storage == :local
!File.exist?(local_path)
when :s3
elsif @storage == :s3
!@aws_service.file_exists?(s3_key)
when :both
else
# :both
!File.exist?(local_path) || !@aws_service.file_exists?(s3_key)
end
end
# Ensure local directories exist for each size
# @return [void]
def ensure_directories_exist
return unless store_locally?
@ -101,45 +158,85 @@ module Granblue
end
end
# Check if local storage is being used
# @return [Boolean] true if storing locally
def store_locally?
%i[local both].include?(@storage)
end
# Get local download path for a size
# @param size [String] Image size variant
# @return [String] Local directory path
def download_path(size)
"#{Rails.root}/download/#{object_type}-#{size}"
end
# Build S3 key for an image
# @param size [String] Image size variant
# @param filename [String] Image filename
# @return [String] Complete S3 key
def build_s3_key(size, filename)
"#{object_type}-#{size}/#{filename}"
end
# Log informational message if verbose
# @param message [String] Message
def log_info(message)
puts message if @verbose
end
# Download elemental variant image
# @param url [String] Source URL
# @param size [String] Image size variant
# @param path [String] Destination path
# @param filename [String] Image filename
# @return [void]
def download_elemental_image(url, size, path, filename)
return if @test_mode
filepath = "#{path}/#{filename}"
download = URI.parse(url).open
URI.open(url) do |file|
content = file.read
if content
File.open(filepath, 'wb') do |output|
output.write(content)
end
else
raise "Failed to read content from #{url}"
end
end
log_info "-> #{size}:\t#{url}..."
IO.copy_stream(download, filepath)
rescue OpenURI::HTTPError
log_info "\t404 returned\t#{url}"
rescue StandardError => e
log_info "\tError downloading #{url}: #{e.message}"
end
# Get asset type (e.g., "weapon", "character")
# @abstract
# @return [String] Asset type name
def object_type
raise NotImplementedError, 'Subclasses must define object_type'
end
# Get base URL for assets
# @abstract
# @return [String] Base URL
def base_url
raise NotImplementedError, 'Subclasses must define base_url'
end
# Get directory name for a size variant
# @abstract
# @param size [String] Image size variant
# @return [String] Directory name
def directory_for_size(size)
raise NotImplementedError, 'Subclasses must define directory_for_size'
end
# Build complete URL for a size variant
# @param size [String] Image size variant
# @return [String] Complete download URL
def build_url(size)
directory = directory_for_size(size)
"#{@base_url}/#{directory}/#{@id}.jpg"

View file

@ -2,7 +2,23 @@
module Granblue
module Downloaders
# Downloads character image assets from the game server in different sizes and variants.
# Handles character-specific variants like base art, uncap art, and transcendence art.
#
# @example Download images for a specific character
# downloader = CharacterDownloader.new("3040001000", storage: :both)
# downloader.download
#
# @note Character images come in multiple variants (_01, _02, etc.) based on uncap status
# @note Supports FLB (5★) and ULB (6★) art variants when available
class CharacterDownloader < BaseDownloader
# Downloads images for all variants of a character based on their uncap status.
# Overrides {BaseDownloader#download} to handle character-specific variants.
#
# @return [void]
# @note Skips download if character is not found in database
# @note Downloads FLB/ULB variants only if character has those uncaps
# @see #download_variants
def download
character = Character.find_by(granblue_id: @id)
return unless character
@ -12,9 +28,13 @@ module Granblue
private
# Downloads all variants of a character's images
# @param character [Character] Character model instance to download images for
# @return [void]
# @note Only downloads variants that should exist based on character uncap status
def download_variants(character)
# All characters have 01 and 02 variants
variants = ["#{@id}_01", "#{@id}_02"]
variants = %W[#{@id}_01 #{@id}_02]
# Add FLB variant if available
variants << "#{@id}_03" if character.flb
@ -29,6 +49,9 @@ module Granblue
end
end
# Downloads a specific variant's images in all sizes
# @param variant_id [String] Character variant ID (e.g., "3040001000_01")
# @return [void]
def download_variant(variant_id)
log_info "-> #{variant_id}" if @verbose
return if @test_mode
@ -40,19 +63,31 @@ module Granblue
end
end
# Builds URL for a specific variant and size
# @param variant_id [String] Character variant ID
# @param size [String] Image size variant ("main", "grid", or "square")
# @return [String] Complete URL for downloading the image
def build_variant_url(variant_id, size)
directory = directory_for_size(size)
"#{@base_url}/#{directory}/#{variant_id}.jpg"
end
# Gets object type for file paths and storage keys
# @return [String] Returns "character"
def object_type
'character'
end
# Gets base URL for character assets
# @return [String] Base URL for character images
def base_url
'http://gbf.game-a.mbga.jp/assets/img/sp/assets/npc'
end
# Gets directory name for a size variant
# @param size [String] Image size variant
# @return [String] Directory name in game asset URL structure
# @note Maps "main" -> "f", "grid" -> "m", "square" -> "s"
def directory_for_size(size)
case size.to_s
when 'main' then 'f'

View file

@ -2,8 +2,41 @@
module Granblue
module Downloaders
# Manages downloading of game assets by coordinating different downloader types.
# Provides a single interface for downloading any type of game asset.
#
# @example Download character images
# DownloadManager.download_for_object('character', '3040001000', storage: :s3)
#
# @example Download weapon images in test mode
# DownloadManager.download_for_object('weapon', '1040001000', test_mode: true, verbose: true)
#
# @note Automatically selects the appropriate downloader based on object type
# @note Handles configuration of downloader options consistently across types
class DownloadManager
class << self
# Downloads assets for a specific game object using the appropriate downloader
#
# @param type [String] Type of game object ('character', 'weapon', or 'summon')
# @param granblue_id [String] Game ID of the object to download assets for
# @param test_mode [Boolean] When true, simulates downloads without actually downloading
# @param verbose [Boolean] When true, enables detailed logging
# @param storage [Symbol] Storage mode to use (:local, :s3, or :both)
# @return [void]
#
# @example Download character images to S3
# DownloadManager.download_for_object('character', '3040001000', storage: :s3)
#
# @example Test weapon downloads with verbose logging
# DownloadManager.download_for_object('weapon', '1040001000',
# test_mode: true,
# verbose: true
# )
#
# @note Logs warning if object type is unknown
# @see CharacterDownloader
# @see WeaponDownloader
# @see SummonDownloader
def download_for_object(type, granblue_id, test_mode: false, verbose: false, storage: :both)
downloader_options = {
test_mode: test_mode,

View file

@ -4,13 +4,31 @@ require_relative 'weapon_downloader'
module Granblue
module Downloaders
# Specialized downloader for handling elemental weapon variants.
# Some weapons have different art for each element, requiring multiple downloads.
#
# @example Download all elemental variants
# downloader = ElementalWeaponDownloader.new(1040001000)
# downloader.download
#
# @note Handles weapons that have variants for all six elements
# @note Uses specific suffix mappings for element art variants
class ElementalWeaponDownloader < WeaponDownloader
# Element variant suffix mapping
# @return [Array<Integer>] Ordered list of suffixes for element variants
SUFFIXES = [2, 3, 4, 1, 6, 5].freeze
# Initialize downloader with base weapon ID
# @param id_base [Integer] Base ID for the elemental weapon series
# @return [void]
def initialize(id_base)
@id_base = id_base.to_i
end
# Downloads all elemental variants of the weapon
# @return [void]
# @note Downloads variants for all six elements
# @note Uses progress reporter to show download status
def download
(1..6).each do |i|
id = @id_base + (i - 1) * 100

View file

@ -2,7 +2,23 @@
module Granblue
module Downloaders
# Downloads summon image assets from the game server in different sizes and variants.
# Handles summon-specific variants like base art, ULB art, and transcendence art.
#
# @example Download images for a specific summon
# downloader = SummonDownloader.new("2040001000", storage: :both)
# downloader.download
#
# @note Summon images come in multiple variants based on uncap status
# @note Supports ULB (5★) and transcendence variants when available
class SummonDownloader < BaseDownloader
# Downloads images for all variants of a summon based on their uncap status.
# Overrides {BaseDownloader#download} to handle summon-specific variants.
#
# @return [void]
# @note Skips download if summon is not found in database
# @note Downloads ULB and transcendence variants only if summon has those uncaps
# @see #download_variants
def download
summon = Summon.find_by(granblue_id: @id)
return unless summon
@ -12,6 +28,11 @@ module Granblue
private
# Downloads all variants of a summon's images
# @param summon [Summon] Summon model instance to download images for
# @return [void]
# @note Only downloads variants that should exist based on summon uncap status
# @note Handles special transcendence art variants for 6★ summons
def download_variants(summon)
# All summons have base variant
variants = [@id]
@ -31,6 +52,10 @@ module Granblue
end
end
# Downloads a specific variant's images in all sizes
# @param variant_id [String] Summon variant ID (e.g., "2040001000_02")
# @return [void]
# @note Downloads all size variants (main/grid/square) for the given variant
def download_variant(variant_id)
log_info "-> #{variant_id}" if @verbose
return if @test_mode
@ -42,19 +67,31 @@ module Granblue
end
end
# Builds URL for a specific variant and size
# @param variant_id [String] Summon variant ID
# @param size [String] Image size variant ("main", "grid", or "square")
# @return [String] Complete URL for downloading the image
def build_variant_url(variant_id, size)
directory = directory_for_size(size)
"#{@base_url}/#{directory}/#{variant_id}.jpg"
end
# Gets object type for file paths and storage keys
# @return [String] Returns "summon"
def object_type
'summon'
end
# Gets base URL for summon assets
# @return [String] Base URL for summon images
def base_url
'http://gbf.game-a.mbga.jp/assets/img/sp/assets/summon'
end
# Gets directory name for a size variant
# @param size [String] Image size variant
# @return [String] Directory name in game asset URL structure
# @note Maps "main" -> "party_main", "grid" -> "party_sub", "square" -> "s"
def directory_for_size(size)
case size.to_s
when 'main' then 'party_main'

View file

@ -2,7 +2,24 @@
module Granblue
module Downloaders
# Downloads weapon image assets from the game server in different sizes and variants.
# Handles weapon-specific variants like base art, transcendence art, and elemental variants.
#
# @example Download images for a specific weapon
# downloader = WeaponDownloader.new("1040001000", storage: :both)
# downloader.download
#
# @note Weapon images come in multiple variants based on uncap and element status
# @note Supports transcendence variants and element-specific variants
# @see ElementalWeaponDownloader for handling multi-element weapons
class WeaponDownloader < BaseDownloader
# Downloads images for all variants of a weapon based on their uncap status.
# Overrides {BaseDownloader#download} to handle weapon-specific variants.
#
# @return [void]
# @note Skips download if weapon is not found in database
# @note Downloads transcendence variants only if weapon has those uncaps
# @see #download_variants
def download
weapon = Weapon.find_by(granblue_id: @id)
return unless weapon
@ -12,6 +29,11 @@ module Granblue
private
# Downloads all variants of a weapon's images
# @param weapon [Weapon] Weapon model instance to download images for
# @return [void]
# @note Only downloads variants that should exist based on weapon uncap status
# @note Handles special transcendence art variants for transcendable weapons
def download_variants(weapon)
# All weapons have base variant
variants = [@id]
@ -28,6 +50,10 @@ module Granblue
end
end
# Downloads a specific variant's images in all sizes
# @param variant_id [String] Weapon variant ID (e.g., "1040001000_02")
# @return [void]
# @note Downloads all size variants (main/grid/square) for the given variant
def download_variant(variant_id)
log_info "-> #{variant_id}" if @verbose
return if @test_mode
@ -39,19 +65,31 @@ module Granblue
end
end
# Builds URL for a specific variant and size
# @param variant_id [String] Weapon variant ID
# @param size [String] Image size variant ("main", "grid", or "square")
# @return [String] Complete URL for downloading the image
def build_variant_url(variant_id, size)
directory = directory_for_size(size)
"#{@base_url}/#{directory}/#{variant_id}.jpg"
end
# Gets object type for file paths and storage keys
# @return [String] Returns "weapon"
def object_type
'weapon'
end
# Gets base URL for weapon assets
# @return [String] Base URL for weapon images
def base_url
'http://gbf.game-a.mbga.jp/assets/img/sp/assets/weapon'
end
# Gets directory name for a size variant
# @param size [String] Image size variant
# @return [String] Directory name in game asset URL structure
# @note Maps "main" -> "ls", "grid" -> "m", "square" -> "s"
def directory_for_size(size)
case size.to_s
when 'main' then 'ls'

View file

@ -4,9 +4,40 @@ require_relative 'import_error'
module Granblue
module Importers
# Abstract base class for importing game data from CSV files.
# Handles the core import logic including test mode, validation, and error handling.
#
# @abstract Subclass must implement {#model_class} and {#build_attributes}
#
# @example Implementing a subclass
# class WeaponImporter < BaseImporter
# private
# def model_class
# Weapon
# end
#
# def build_attributes(row)
# {
# name_en: parse_value(row['name_en']),
# granblue_id: parse_value(row['granblue_id'])
# }
# end
# end
#
# @note Tracks both new and updated records during import
# @note Supports test mode for simulating imports
class BaseImporter
attr_reader :new_records, :updated_records
# @return [Hash<String, Array<Hash>>] New records created during import
attr_reader :new_records
# @return [Hash<String, Array<Hash>>] Existing records updated during import
attr_reader :updated_records
# Initialize a new importer instance
# @param file_path [String] Path to CSV file to import
# @param test_mode [Boolean] When true, simulates import without making changes
# @param verbose [Boolean] When true, enables detailed logging
# @param logger [#log_step, #log_verbose] Optional logger instance
def initialize(file_path, test_mode: false, verbose: false, logger: nil)
@file_path = file_path
@test_mode = test_mode
@ -16,6 +47,12 @@ module Granblue
@updated_records = Hash.new { |h, k| h[k] = [] }
end
# Perform the actual import of CSV data
# @return [Hash] Results containing new and updated records
# @example
# importer = WeaponImporter.new("weapons.csv")
# results = importer.import
# results[:new][:weapon].each { |w| puts w[:name_en] }
def import
CSV.foreach(@file_path, headers: true) do |row|
import_row(row)
@ -23,6 +60,9 @@ module Granblue
{ new: @new_records, updated: @updated_records }
end
# Simulate an import without making changes
# @return [Hash] Results that would be created/updated
# @raise [ImportError] If validation fails
def simulate_import
simulated_new = Hash.new { |h, k| h[k] = [] }
simulated_updated = Hash.new { |h, k| h[k] = [] }
@ -49,12 +89,18 @@ module Granblue
private
# Import a single row from the CSV
# @param row [CSV::Row] Row to import
# @return [void]
def import_row(row)
attributes = build_attributes(row)
record = find_or_create_record(attributes)
track_record(record) if record
end
# Find existing record or create new one
# @param attributes [Hash] Attributes for the record
# @return [Array<ActiveRecord::Base, Boolean>, nil] Record and update flag, or nil in test mode
def find_or_create_record(attributes)
existing_record = model_class.find_by(granblue_id: attributes[:granblue_id])
@ -78,6 +124,11 @@ module Granblue
end
end
# Simulate creating a new record
# @param attributes [Hash] Attributes for the record
# @param simulated_new [Hash] Collection of simulated new records
# @param type [String] Model type being simulated
# @return [void]
def simulate_create(attributes, simulated_new, type)
test_record = model_class.new(attributes)
validate_record(test_record)
@ -91,6 +142,12 @@ module Granblue
}
end
# Simulate updating an existing record
# @param existing_record [ActiveRecord::Base] Record to update
# @param attributes [Hash] New attributes
# @param simulated_updated [Hash] Collection of simulated updates
# @param type [String] Model type being simulated
# @return [void]
def simulate_update(existing_record, attributes, simulated_updated, type)
update_attributes = attributes.compact
would_update = update_attributes.any? { |key, value| existing_record[key] != value }
@ -116,6 +173,9 @@ module Granblue
end
end
# Validate that all required attributes are present
# @param attributes [Hash] Attributes to validate
# @raise [ImportError] If required attributes are missing
def validate_required_attributes(attributes)
required_columns = model_class.columns.select { |c| !c.null }.map(&:name)
@ -140,6 +200,9 @@ module Granblue
end
end
# Validate attributes for an update
# @param update_attributes [Hash] Attributes being updated
# @raise [ImportError] If required attributes are missing
def validate_update_attributes(update_attributes)
# Get the list of columns that cannot be null in the database
required_columns = model_class.columns.select { |c| !c.null }.map(&:name)
@ -170,6 +233,9 @@ module Granblue
end
end
# Validate a record can be saved
# @param record [ActiveRecord::Base] Record to validate
# @raise [ImportError] If validation fails
def validate_record(record)
unless record.valid?
raise ImportError.new(
@ -205,6 +271,9 @@ module Granblue
end
end
# Track a processed record in results
# @param result [Array(ActiveRecord::Base, Boolean)] Record and whether it was updated
# @return [void]
def track_record(result)
record, was_updated = result
type = model_class.name.demodulize.downcase
@ -223,6 +292,9 @@ module Granblue
end
end
# Format attributes for logging
# @param attributes [Hash] Attributes to format
# @return [String] Formatted attribute string
def format_attributes(attributes)
attributes.map do |key, value|
formatted_value = case value
@ -235,6 +307,10 @@ module Granblue
end.join("\n")
end
# Log a test mode update
# @param record [ActiveRecord::Base] Record being updated
# @param attributes [Hash] New attributes
# @return [void]
def log_test_update(record, attributes)
update_attributes = attributes.compact
@logger&.log_step("\nUpdate #{model_class.name} #{record.granblue_id}:")
@ -245,68 +321,108 @@ module Granblue
@logger&.log_step("\n")
end
# Log a test mode creation
# @param attributes [Hash] Attributes for new record
# @return [void]
def log_test_creation(attributes)
@logger&.log_step("\nCreate #{model_class.name}:")
@logger&.log_verbose(format_attributes(attributes))
@logger&.log_step("\n")
end
# Log creation of a new record
# @param record [ActiveRecord::Base] Created record
# @return [void]
def log_new_record(record)
@logger&.log_verbose("Created #{model_class.name} with ID: #{record.granblue_id}\n")
end
# Log update of existing record
# @param record [ActiveRecord::Base] Updated record
# @return [void]
def log_updated_record(record)
@logger&.log_verbose("Updated #{model_class.name} with ID: #{record.granblue_id}\n")
end
# Parse a string value, returning nil if empty
# @param value [String, nil] Value to parse
# @return [String, nil] Parsed value
def parse_value(value)
return nil if value.nil? || value.strip.empty?
value
end
# Parse an integer value
# @param value [String, nil] Value to parse
# @return [Integer, nil] Parsed value
def parse_integer(value)
return nil if value.nil? || value.strip.empty?
value.to_i
end
# Parse a float value
# @param value [String, nil] Value to parse
# @return [Float, nil] Parsed value
def parse_float(value)
return nil if value.nil? || value.strip.empty?
value.to_f
end
# Parse a boolean value
# @param value [String, nil] Value to parse
# @return [Boolean, nil] Parsed value
def parse_boolean(value)
return nil if value.nil? || value.strip.empty?
value == 'true'
end
# Parse a date string
# @param date_str [String, nil] Date string to parse
# @return [Date, nil] Parsed date
def parse_date(date_str)
return nil if date_str.nil? || date_str.strip.empty?
Date.parse(date_str) rescue nil
end
# Parse a string array
# @param array_str [String, nil] Array string to parse
# @return [Array<String>] Parsed array
def parse_array(array_str)
return [] if array_str.nil? || array_str.strip.empty?
array_str.tr('{}', '').split(',')
end
# Parse an integer array
# @param array_str [String, nil] Array string to parse
# @return [Array<Integer>] Parsed array
def parse_integer_array(array_str)
parse_array(array_str).map(&:to_i)
end
# Get the model class for this importer
# @abstract Implement in subclass
# @return [Class] ActiveRecord model class
def model_class
raise NotImplementedError, 'Subclasses must define model_class'
end
# Build attributes hash from CSV row
# @abstract Implement in subclass
# @param row [CSV::Row] Row to build attributes from
# @return [Hash] Attributes for record
def build_attributes(row)
raise NotImplementedError, 'Subclasses must define build_attributes'
end
# Handle an import error
# @param error [StandardError] Error that occurred
# @raise [ImportError] Wrapped error with details
def handle_error(error)
details = case error
when ActiveRecord::RecordInvalid
@ -321,6 +437,9 @@ module Granblue
)
end
# Format a validation error for display
# @param error [ActiveRecord::RecordInvalid] Validation error
# @return [String] Formatted error message
def format_validation_error(error)
[
"Validation failed:",
@ -331,6 +450,9 @@ module Granblue
].flatten.join("\n")
end
# Format a standard error for display
# @param error [StandardError] Error to format
# @return [String] Formatted error message
def format_standard_error(error)
if @verbose && error.respond_to?(:backtrace)
[

View file

@ -2,13 +2,65 @@
module Granblue
module Importers
# Imports character data from CSV files into the Character model
#
# @example Importing character data
# importer = CharacterImporter.new("characters.csv")
# results = importer.import
#
# @see BaseImporter Base class with core import logic
class CharacterImporter < BaseImporter
private
# Returns the model class for character records
#
# @return [Class] The Character model class
# @note Overrides the abstract method from BaseImporter
def model_class
Character
end
# Builds attribute hash from a CSV row for character import
#
# @param row [CSV::Row] A single row from the character CSV file
# @return [Hash] A hash of attributes ready for model creation/update
# @option attributes [String] :name_en English name of the character
# @option attributes [String] :name_jp Japanese name of the character
# @option attributes [String] :granblue_id Unique identifier for the character
# @option attributes [Array<Integer>] :character_id Array of character IDs
# @option attributes [Integer] :rarity Character's rarity level
# @option attributes [Integer] :element Character's elemental affinity
# @option attributes [Integer] :proficiency1 First weapon proficiency
# @option attributes [Integer] :proficiency2 Second weapon proficiency
# @option attributes [Integer] :gender Character's gender
# @option attributes [Integer] :race1 First character race
# @option attributes [Integer] :race2 Second character race
# @option attributes [Boolean] :flb Flag for FLB
# @option attributes [Boolean] :ulb Flag for ULB
# @option attributes [Boolean] :special Flag for characters with special uncap patterns
# @option attributes [Integer] :min_hp Minimum HP
# @option attributes [Integer] :max_hp Maximum HP
# @option attributes [Integer] :max_hp_flb Maximum HP after FLB
# @option attributes [Integer] :max_hp_ulb Maximum HP after ULB
# @option attributes [Integer] :min_atk Minimum attack
# @option attributes [Integer] :max_atk Maximum attack
# @option attributes [Integer] :max_atk_flb Maximum attack after FLB
# @option attributes [Integer] :max_atk_ulb Maximum attack after ULB
# @option attributes [Integer] :base_da Base double attack rate
# @option attributes [Integer] :base_ta Base triple attack rate
# @option attributes [Float] :ougi_ratio Original ougi (charge attack) ratio
# @option attributes [Float] :ougi_ratio_flb Ougi ratio after FLB
# @option attributes [String] :release_date Character release date
# @option attributes [String] :flb_date Date FLB was implemented
# @option attributes [String] :ulb_date Date ULB was implemented
# @option attributes [String] :wiki_en English wiki link
# @option attributes [String] :wiki_ja Japanese wiki link
# @option attributes [String] :gamewith Gamewith link
# @option attributes [String] :kamigame Kamigame link
# @option attributes [Array<String>] :nicknames_en English nicknames
# @option attributes [Array<String>] :nicknames_jp Japanese nicknames
#
# @raise [ImportError] If required attributes are missing or invalid
def build_attributes(row)
{
name_en: parse_value(row['name_en']),

View file

@ -1,8 +1,30 @@
module Granblue
module Importers
# Custom error class for handling import-related exceptions
#
# @example Raising an import error
# raise ImportError.new(
# file_name: 'characters.csv',
# details: 'Missing required column: name_en'
# )
#
# @note This error provides detailed information about import failures
class ImportError < StandardError
attr_reader :file_name, :details
# @return [String] The name of the file that caused the import error
attr_reader :file_name
# @return [String] Detailed information about the error
attr_reader :details
# Create a new ImportError instance
#
# @param file_name [String] The name of the file that caused the import error
# @param details [String] Detailed information about the error
# @example
# ImportError.new(
# file_name: 'weapons.csv',
# details: 'Invalid data in rarity column'
# )
def initialize(file_name:, details:)
@file_name = file_name
@details = details
@ -11,11 +33,33 @@ module Granblue
private
# Constructs a comprehensive error message
#
# @return [String] Formatted error message combining file name and details
# @example
# # Returns "Error importing weapons.csv: Invalid data in rarity column"
# build_message
def build_message
"Error importing #{file_name}: #{details}"
end
end
# Formats attributes into a human-readable string representation
#
# @param attributes [Hash] A hash of attributes to format
# @return [String] A formatted string with each attribute on a new line
# @example
# attributes = {
# name: 'Example Weapon',
# rarity: 5,
# elements: ['fire', 'water']
# }
# format_attributes(attributes)
# # Returns:
# # name: "Example Weapon"
# # rarity: 5
# # elements: ["fire", "water"]
# @note Handles various attribute types including arrays and nil values
def format_attributes(attributes)
attributes.map do |key, value|
formatted_value = case value

View file

@ -2,13 +2,63 @@
module Granblue
module Importers
# Imports summon data from CSV files into the Summon model
#
# @example Importing summon data
# importer = SummonImporter.new("summons.csv")
# results = importer.import
#
# @see BaseImporter Base class with core import logic
class SummonImporter < BaseImporter
private
# Returns the model class for summon records
#
# @return [Class] The Summon model class
# @note Overrides the abstract method from BaseImporter
def model_class
Summon
end
# Builds attribute hash from a CSV row for summon import
#
# @param row [CSV::Row] A single row from the summon CSV file
# @return [Hash] A hash of attributes ready for model creation/update
# @option attributes [String] :name_en English name of the summon
# @option attributes [String] :name_jp Japanese name of the summon
# @option attributes [String] :granblue_id Unique identifier for the summon
# @option attributes [Integer] :summon_id Specific summon identifier
# @option attributes [Integer] :rarity Summon's rarity level
# @option attributes [Integer] :element Summon's elemental affinity
# @option attributes [String] :series Summon's series or collection
# @option attributes [Boolean] :flb Flag for FLB
# @option attributes [Boolean] :ulb Flag for ULB
# @option attributes [Boolean] :subaura Flag indicating the presence of a subaura effect
# @option attributes [Boolean] :limit Flag indicating only one of this summon can be equipped at once
# @option attributes [Boolean] :transcendence Flag for transcendence status
# @option attributes [Integer] :max_level Maximum level of the summon
# @option attributes [Integer] :min_hp Minimum HP
# @option attributes [Integer] :max_hp Maximum HP
# @option attributes [Integer] :max_hp_flb Maximum HP after FLB
# @option attributes [Integer] :max_hp_ulb Maximum HP after ULB
# @option attributes [Integer] :max_hp_xlb Maximum HP after Transcendence
# @option attributes [Integer] :min_atk Minimum attack
# @option attributes [Integer] :max_atk Maximum attack
# @option attributes [Integer] :max_atk_flb Maximum attack after FLB
# @option attributes [Integer] :max_atk_ulb Maximum attack after ULB
# @option attributes [Integer] :max_atk_xlb Maximum attack after Transcendence
# @option attributes [String] :release_date Summon release date
# @option attributes [String] :flb_date Date FLB was implemented
# @option attributes [String] :ulb_date Date ULB was implemented
# @option attributes [String] :transcendence_date Date Transcendence was implemented
# @option attributes [String] :wiki_en English wiki link
# @option attributes [String] :wiki_ja Japanese wiki link
# @option attributes [String] :gamewith Gamewith link
# @option attributes [String] :kamigame Kamigame link
# @option attributes [Array<String>] :nicknames_en English nicknames
# @option attributes [Array<String>] :nicknames_jp Japanese nicknames
#
# @raise [ImportError] If required attributes are missing or invalid
def build_attributes(row)
{
name_en: parse_value(row['name_en']),

View file

@ -2,13 +2,66 @@
module Granblue
module Importers
# Imports weapon data from CSV files into the Weapon model
#
# @example Importing weapon data
# importer = WeaponImporter.new("weapons.csv")
# results = importer.import
#
# @see BaseImporter Base class with core import logic
class WeaponImporter < BaseImporter
private
# Returns the model class for weapon records
#
# @return [Class] The Weapon model class
# @note Overrides the abstract method from BaseImporter
def model_class
Weapon
end
# Builds attribute hash from a CSV row for weapon import
#
# @param row [CSV::Row] A single row from the weapon CSV file
# @return [Hash] A hash of attributes ready for model creation/update
# @option attributes [String] :name_en English name of the weapon
# @option attributes [String] :name_jp Japanese name of the weapon
# @option attributes [String] :granblue_id Unique identifier for the weapon
# @option attributes [Integer] :rarity Weapon's rarity level
# @option attributes [Integer] :element Weapon's elemental affinity
# @option attributes [Integer] :proficiency Weapon proficiency type
# @option attributes [Integer] :series Weapon series or collection
# @option attributes [Boolean] :flb Flag for FLB status
# @option attributes [Boolean] :ulb Flag for ULB status
# @option attributes [Boolean] :extra Flag indicating whether weapon can be slotted in Extra slots
# @option attributes [Boolean] :limit Flag indicating only one of this weapon can be equipped at once
# @option attributes [Boolean] :ax Flag indicating whether weapon supports AX skills
# @option attributes [Boolean] :transcendence Flag for transcendence status
# @option attributes [Integer] :max_level Maximum level of the weapon
# @option attributes [Integer] :max_skill_level Maximum skill level
# @option attributes [Integer] :max_awakening_level Maximum awakening level
# @option attributes [Integer] :ax_type AX type classification
# @option attributes [Integer] :min_hp Minimum HP
# @option attributes [Integer] :max_hp Maximum HP
# @option attributes [Integer] :max_hp_flb Maximum HP after FLB
# @option attributes [Integer] :max_hp_ulb Maximum HP after ULB
# @option attributes [Integer] :min_atk Minimum attack
# @option attributes [Integer] :max_atk Maximum attack
# @option attributes [Integer] :max_atk_flb Maximum attack after FLB
# @option attributes [Integer] :max_atk_ulb Maximum attack after ULB
# @option attributes [String] :recruits The granblue_id of the character this weapon recruits, if any
# @option attributes [String] :release_date Weapon release date
# @option attributes [String] :flb_date Date FLB was implemented
# @option attributes [String] :ulb_date Date ULB was implemented
# @option attributes [String] :transcendence_date Date transcendence was implemented
# @option attributes [String] :wiki_en English wiki link
# @option attributes [String] :wiki_ja Japanese wiki link
# @option attributes [String] :gamewith Gamewith link
# @option attributes [String] :kamigame Kamigame link
# @option attributes [Array<String>] :nicknames_en English nicknames
# @option attributes [Array<String>] :nicknames_jp Japanese nicknames
#
# @raise [ImportError] If required attributes are missing or invalid
def build_attributes(row)
{
name_en: parse_value(row['name_en']),

View file

@ -11,7 +11,11 @@ module Granblue
end
end
# Base class for transforming game data into standardized format
# @abstract
class BaseTransformer
# Mapping of game element IDs to internal element IDs
# @return [Hash<Integer, Integer?>]
ELEMENT_MAPPING = {
0 => nil,
1 => 4, # Wind -> Earth
@ -22,6 +26,12 @@ module Granblue
6 => 5 # Light -> Dark
}.freeze
# Initialize a new transformer
# @param data [Object] Raw game data to transform
# @param options [Hash<Symbol, Object>] Optional configuration settings
# @option options [String] :language ('en') Language code for transformations
# @option options [Boolean] :debug (false) Enable debug logging
# @return [void]
def initialize(data, options = {})
@data = data
@options = options
@ -30,6 +40,9 @@ module Granblue
validate_data
end
# Transform the raw data into standardized format
# @abstract Subclasses must implement this method
# @return [Object] Transformed data
def transform
raise NotImplementedError, "#{self.class} must implement #transform"
end
@ -38,6 +51,8 @@ module Granblue
attr_reader :data, :options, :language
# Validate the input data structure
# @return [Boolean] true if valid, false otherwise
def validate_data
Rails.logger.info "[TRANSFORM] Validating data: #{data.inspect[0..100]}..."
@ -55,6 +70,9 @@ module Granblue
true
end
# Extract master and parameter data from an object
# @param obj [Hash<String, Object>] Object containing master/param data
# @return [Array<(Hash?, Hash?)>] Array containing master and param data
def get_master_param(obj)
return [nil, nil] unless obj.is_a?(Hash)
@ -65,6 +83,9 @@ module Granblue
[master, param]
end
# Log a debug message if debug mode is enabled
# @param message [String] Message to log
# @return [void]
def log_debug(message)
return unless options[:debug]
Rails.logger.debug "[TRANSFORM-DEBUG] #{self.class.name}: #{message}"

View file

@ -1,16 +1,41 @@
module Granblue
module Transformers
# Transforms raw game character data into standardized format for database import.
# Handles character stats, uncap levels, transcendence, and perpetuity rings.
#
# @example Transforming character data
# data = { "master" => { "name" => "Katalina", "id" => "3040001000" },
# "param" => { "evolution" => 3, "phase" => 1 } }
# transformer = CharacterTransformer.new(data)
# result = transformer.transform
# # => [{ name: "Katalina", id: "3040001000", uncap: 3, transcend: 1 }]
#
# @note Expects data with "master" and "param" nested objects for each character
# @note Will filter out characters with missing or invalid required attributes
#
# @see BaseTransformer For base transformation functionality
class CharacterTransformer < BaseTransformer
# Transforms raw game character data into a standardized format
# @return [Array<Hash>] Array of character hashes with standardized attributes:
# @option character [String] :name Character's name
# @option character [String] :id Character's ID
# @option character [Integer] :uncap Character's uncap level
# @option character [Boolean] :ringed Whether character has perpetuity rings
# @option character [Integer] :transcend Character's transcendence phase level
def transform
# Log start of transformation process
Rails.logger.info "[TRANSFORM] Starting CharacterTransformer#transform"
# Validate that the input data is a Hash
unless data.is_a?(Hash)
Rails.logger.error "[TRANSFORM] Invalid character data structure"
return []
end
characters = []
# Iterate through each character data entry
data.each_value do |char_data|
# Skip entries missing required master/param data
next unless char_data['master'] && char_data['param']
master = char_data['master']
@ -18,27 +43,29 @@ module Granblue
Rails.logger.debug "[TRANSFORM] Processing character: #{master['name']}"
# Build base character hash with required attributes
character = {
name: master['name'],
id: master['id'],
uncap: param['evolution'].to_i
name: master['name'], # Character's display name
id: master['id'], # Unique identifier
uncap: param['evolution'].to_i # Current uncap level
}
Rails.logger.debug "[TRANSFORM] Base character data: #{character}"
# Add perpetuity (rings) if present
# Add perpetuity ring status if present
if param['has_npcaugment_constant']
character[:ringed] = true
Rails.logger.debug "[TRANSFORM] Character is ringed"
end
# Add transcendence if present
# Add transcendence level if present (stored as 'phase' in raw data)
phase = param['phase'].to_i
if phase && phase.positive?
if phase&.positive?
character[:transcend] = phase
Rails.logger.debug "[TRANSFORM] Character has transcendence: #{phase}"
end
# Only add characters with valid IDs to result set
characters << character unless master['id'].nil?
Rails.logger.info "[TRANSFORM] Successfully processed character #{character[:name]}"
end

View file

@ -2,18 +2,45 @@
module Granblue
module Transformers
# Transforms raw game summon data into standardized format for database import.
# Handles summon stats, uncap levels, transcendence, and quick summon status.
#
# @example Transforming summon data
# data = {
# "master" => { "name" => "Bahamut", "id" => "2040003000" },
# "param" => { "evolution" => 5, "level" => 200 }
# }
# transformer = SummonTransformer.new(data, "2040003000")
# result = transformer.transform
# # => [{ name: "Bahamut", id: "2040003000", uncap: 5, transcend: 1, qs: true }]
#
# @note Expects data with "master" and "param" nested objects for each summon
# @note Handles quick summon status if ID matches provided quick_summon_id
#
# @see BaseTransformer For base transformation functionality
class SummonTransformer < BaseTransformer
# @return [Array<Integer>] Level thresholds for determining transcendence level
TRANSCENDENCE_LEVELS = [210, 220, 230, 240].freeze
# Creates a new summon transformer
# @param data [Object] Raw summon data to transform
# @param quick_summon_id [String, nil] ID of the current quick summon
# @param options [Hash] Additional transformation options
# @option options [String] :language ('en') Language for names
# @option options [Boolean] :debug (false) Enable debug logging
# @return [void]
def initialize(data, quick_summon_id = nil, options = {})
super(data, options)
@quick_summon_id = quick_summon_id
Rails.logger.info "[TRANSFORM] Initializing SummonTransformer with quick_summon_id: #{quick_summon_id}"
end
# Transform raw summon data into standardized format
# @return [Array<Hash>] Array of transformed summon data
def transform
Rails.logger.info "[TRANSFORM] Starting SummonTransformer#transform"
# Validate that input data is a Hash
unless data.is_a?(Hash)
Rails.logger.error "[TRANSFORM] Invalid summon data structure"
Rails.logger.error "[TRANSFORM] Data class: #{data.class}"
@ -21,24 +48,27 @@ module Granblue
end
summons = []
# Process each summon in the data
data.each_value do |summon_data|
Rails.logger.debug "[TRANSFORM] Processing summon: #{summon_data['master']['name'] if summon_data['master']}"
# Extract master and parameter data
master, param = get_master_param(summon_data)
unless master && param
Rails.logger.debug "[TRANSFORM] Skipping summon - missing master or param data"
next
end
# Build base summon hash with required attributes
summon = {
name: master['name'],
id: master['id'],
uncap: param['evolution'].to_i
name: master['name'], # Summon's display name
id: master['id'], # Unique identifier
uncap: param['evolution'].to_i # Current uncap level
}
Rails.logger.debug "[TRANSFORM] Base summon data: #{summon}"
# Add transcendence if applicable
# Add transcendence level for highly uncapped summons
if summon[:uncap] > 5
level = param['level'].to_i
trans = calculate_transcendence_level(level)
@ -46,7 +76,7 @@ module Granblue
Rails.logger.debug "[TRANSFORM] Added transcendence level: #{trans}"
end
# Mark quick summon if applicable
# Mark quick summon status if this summon matches quick_summon_id
if @quick_summon_id && param['id'].to_s == @quick_summon_id.to_s
summon[:qs] = true
Rails.logger.debug "[TRANSFORM] Marked as quick summon"
@ -62,6 +92,9 @@ module Granblue
private
# Calculates transcendence level based on summon level
# @param level [Integer, nil] Current summon level
# @return [Integer] Calculated transcendence level (1-5)
def calculate_transcendence_level(level)
return 1 unless level
level = 1 + TRANSCENDENCE_LEVELS.count { |cutoff| level > cutoff }

View file

@ -1,20 +1,47 @@
module Granblue
module Transformers
# Transforms raw game weapon data into standardized format for database import.
# Handles weapon stats, uncap levels, transcendence, awakening, AX skills, and weapon keys.
#
# @example Transforming weapon data
# data = {
# "master" => { "name" => "Luminiera Sword Omega", "id" => "1040007100", "series_id" => 1 },
# "param" => { "level" => 150, "arousal" => { "is_arousal_weapon" => true } }
# }
# transformer = WeaponTransformer.new(data)
# result = transformer.transform
# # => [{ name: "Luminiera Sword Omega", id: "1040007100", uncap: 4, ... }]
#
# @note Expects data with "master" and "param" nested objects for each weapon
# @note Special handling for multi-element weapons from specific series
#
# @see BaseTransformer For base transformation functionality
class WeaponTransformer < BaseTransformer
# @return [Array<Integer>] Level thresholds for determining uncap level
UNCAP_LEVELS = [40, 60, 80, 100, 150, 200].freeze
# @return [Array<Integer>] Level thresholds for determining transcendence level
TRANSCENDENCE_LEVELS = [210, 220, 230, 240].freeze
# @return [Array<Integer>] Weapon series IDs that can have multiple elements
MULTIELEMENT_SERIES = [13, 17, 19].freeze
# Transform raw weapon data into standardized format
# @return [Array<Hash>] Array of transformed weapon data
def transform
# Log start of transformation process
Rails.logger.info "[TRANSFORM] Starting WeaponTransformer#transform"
# Validate that the input data is a Hash
unless data.is_a?(Hash)
Rails.logger.error "[TRANSFORM] Invalid weapon data structure"
return []
end
weapons = []
# Iterate through each weapon entry in the data
data.each_value do |weapon_data|
# Skip entries missing required master/param data
next unless weapon_data['master'] && weapon_data['param']
master = weapon_data['master']
@ -22,18 +49,23 @@ module Granblue
Rails.logger.debug "[TRANSFORM] Processing weapon: #{master['name']}"
# Transform base weapon attributes (ID, name, uncap level, etc)
weapon = transform_base_attributes(master, param)
Rails.logger.debug "[TRANSFORM] Base weapon attributes: #{weapon}"
# Add awakening data if present
weapon.merge!(transform_awakening(param))
Rails.logger.debug "[TRANSFORM] After awakening: #{weapon[:awakening] if weapon[:awakening]}"
# Add AX skills if present
weapon.merge!(transform_ax_skills(param))
Rails.logger.debug "[TRANSFORM] After AX skills: #{weapon[:ax] if weapon[:ax]}"
# Add weapon keys if present
weapon.merge!(transform_weapon_keys(weapon_data))
Rails.logger.debug "[TRANSFORM] After weapon keys: #{weapon[:keys] if weapon[:keys]}"
# Only add weapons with valid IDs
weapons << weapon unless master['id'].nil?
Rails.logger.info "[TRANSFORM] Successfully processed weapon #{weapon[:name]}"
end
@ -44,6 +76,10 @@ module Granblue
private
# Transforms the core weapon attributes from master and param data
# @param master [Hash] Master data containing basic weapon information
# @param param [Hash] Parameter data containing weapon's current state
# @return [Hash] Base weapon attributes including ID, name, uncap level, etc
def transform_base_attributes(master, param)
Rails.logger.debug "[TRANSFORM] Processing base attributes for weapon"
@ -77,6 +113,9 @@ module Granblue
weapon
end
# Transforms weapon awakening data if present
# @param param [Hash] Parameter data containing awakening information
# @return [Hash] Awakening type and level if weapon is awakened
def transform_awakening(param)
return {} unless param['arousal']&.[]('is_arousal_weapon')
@ -89,6 +128,9 @@ module Granblue
}
end
# Transforms AX skill data if present
# @param param [Hash] Parameter data containing AX skill information
# @return [Hash] Array of AX skills with IDs and values
def transform_ax_skills(param)
augments = param['augment_skill_info']
return {} unless augments&.first&.any?
@ -107,6 +149,9 @@ module Granblue
{ ax: ax }
end
# Transforms weapon key data if present
# @param weapon_data [Hash] Full weapon data containing key information
# @return [Hash] Array of weapon key IDs
def transform_weapon_keys(weapon_data)
Rails.logger.debug "[TRANSFORM] Processing weapon keys"
keys = []
@ -122,11 +167,17 @@ module Granblue
keys.any? ? { keys: keys } : {}
end
# Calculates uncap level based on weapon level
# @param level [Integer, nil] Current weapon level
# @return [Integer] Calculated uncap level
def calculate_uncap_level(level)
return 0 unless level
UNCAP_LEVELS.count { |cutoff| level.to_i > cutoff }
end
# Calculates transcendence level based on weapon level
# @param level [Integer, nil] Current weapon level
# @return [Integer] Calculated transcendence level
def calculate_transcendence_level(level)
return 1 unless level
1 + TRANSCENDENCE_LEVELS.count { |cutoff| level.to_i > cutoff }

View file

@ -0,0 +1,53 @@
module Granblue
module Downloaders
class BaseDownloader
SIZES: Array[String]
# Define allowed storage types
type storage = :local | :s3 | :both
@id: String
@base_url: String
@test_mode: bool
@verbose: bool
@storage: storage
@aws_service: AwsService
def initialize: (String id, ?test_mode: bool, ?verbose: bool, ?storage: storage) -> void
def download: -> void
private
def process_download: (String url, String size, String path, ?last: bool) -> void
def download_to_local: (String url, String download_uri) -> void
def stream_to_s3: (String url, String s3_key) -> void
def download_to_both: (String url, String download_uri, String s3_key) -> void
def should_download?: (String local_path, String s3_key) -> bool
def ensure_directories_exist: -> void
def store_locally?: -> bool
def download_path: (String size) -> String
def build_s3_key: (String size, String filename) -> String
def log_info: (String message) -> void
def download_elemental_image: (String url, String size, String path, String filename) -> void
def object_type: -> String
def base_url: -> String
def directory_for_size: (String size) -> String
def build_url: (String size) -> String
end
end
end

View file

@ -0,0 +1,28 @@
module Granblue
module Downloaders
class CharacterDownloader < BaseDownloader
private
def download_variants: (Character character) -> void
def download_variant: (String variant_id) -> void
def build_variant_url: (String variant_id, String size) -> String
def object_type: -> String
def base_url: -> String
def directory_for_size: (String size) -> String
private
@id: String
@base_url: String
@test_mode: bool
@verbose: bool
@storage: Symbol
@aws_service: AwsService
end
end
end

View file

@ -0,0 +1,15 @@
module Granblue
module Downloaders
class DownloadManager
def self.download_for_object: (
String type,
String granblue_id,
?test_mode: bool,
?verbose: bool,
?storage: Symbol
) -> void
private
end
end
end

View file

@ -0,0 +1,30 @@
module Granblue
module Downloaders
class SummonDownloader < BaseDownloader
def download: -> void
private
def download_variants: (Summon summon) -> void
def download_variant: (String variant_id) -> void
def build_variant_url: (String variant_id, String size) -> String
def object_type: -> String
def base_url: -> String
def directory_for_size: (String size) -> String
private
@id: String
@base_url: String
@test_mode: bool
@verbose: bool
@storage: Symbol
@aws_service: AwsService
end
end
end

View file

@ -0,0 +1,48 @@
module Granblue
module Downloaders
class WeaponDownloader < BaseDownloader
def download: -> void
private
def download_variants: (Weapon weapon) -> void
def download_variant: (String variant_id) -> void
def build_variant_url: (String variant_id, String size) -> String
def object_type: -> String
def base_url: -> String
def directory_for_size: (String size) -> String
def build_url_for_id: (String id, String size) -> String
# Track progress of elemental weapon downloads
def progress_reporter: (count: Integer, total: Integer, result: String, ?bar_len: Integer) -> void
private
@id: String
@base_url: String
@test_mode: bool
@verbose: bool
@storage: Symbol
@aws_service: AwsService
end
# Special downloader for handling elemental weapon variants
class ElementalWeaponDownloader < WeaponDownloader
SUFFIXES: Array[Integer]
def initialize: (Integer id_base) -> void
def download: -> void
private
@id_base: Integer
end
end
end

View file

@ -0,0 +1,80 @@
module Granblue
module Importers
class BaseImporter
attr_reader new_records: Hash[String, Array[Hash[Symbol, untyped]]]
attr_reader updated_records: Hash[String, Array[Hash[Symbol, untyped]]]
def initialize: (
String file_path,
?test_mode: bool,
?verbose: bool,
?logger: untyped
) -> void
def import: -> Hash[Symbol, Hash[String, Array[Hash[Symbol, untyped]]]]
def simulate_import: -> Hash[Symbol, Hash[String, Array[Hash[Symbol, untyped]]]]
private
def import_row: (CSV::Row row) -> void
def find_or_create_record: (Hash[Symbol, untyped] attributes) -> [untyped, bool]?
def simulate_create: (
Hash[Symbol, untyped] attributes,
Hash[String, Array[Hash[Symbol, untyped]]] simulated_new,
String type
) -> void
def simulate_update: (
untyped existing_record,
Hash[Symbol, untyped] attributes,
Hash[String, Array[Hash[Symbol, untyped]]] simulated_updated,
String type
) -> void
def validate_required_attributes: (Hash[Symbol, untyped] attributes) -> void
def validate_update_attributes: (Hash[Symbol, untyped] update_attributes) -> void
def validate_record: (untyped record) -> void
def track_record: ([untyped, bool] result) -> void
def format_attributes: (Hash[Symbol, untyped] attributes) -> String
def log_test_update: (untyped record, Hash[Symbol, untyped] attributes) -> void
def log_test_creation: (Hash[Symbol, untyped] attributes) -> void
def log_new_record: (untyped record) -> void
def log_updated_record: (untyped record) -> void
def parse_value: (String? value) -> String?
def parse_integer: (String? value) -> Integer?
def parse_float: (String? value) -> Float?
def parse_boolean: (String? value) -> bool?
def parse_date: (String? date_str) -> Date?
def parse_array: (String? array_str) -> Array[String]
def parse_integer_array: (String? array_str) -> Array[Integer]
def model_class: -> singleton(ActiveRecord::Base)
def build_attributes: (CSV::Row row) -> Hash[Symbol, untyped]
def handle_error: (StandardError error) -> void
def format_validation_error: (ActiveRecord::RecordInvalid error) -> String
def format_standard_error: (StandardError error) -> String
end
end
end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
module Granblue
module Importers
class ImportError
attr_reader file_name: String
attr_reader details: String
def initialize: (file_name: String, details: String) -> void
private
def build_message: () -> String
end
def format_attributes: (
attributes: Hash[Symbol, String | Integer | Float | Boolean | Array[untyped] | nil]
) -> String
end
end

View file

@ -0,0 +1,31 @@
module Granblue
module Transformers
class TransformerError < StandardError
attr_reader details: untyped
def initialize: (String message, ?untyped details) -> void
end
class BaseTransformer
ELEMENT_MAPPING: Hash[Integer, Integer?]
@data: untyped
@options: Hash[Symbol, untyped]
@language: String
attr_reader data: untyped
attr_reader options: Hash[Symbol, untyped]
attr_reader language: String
def initialize: (untyped data, ?Hash[Symbol, untyped] options) -> void
def transform: -> untyped
def validate_data: -> bool
def get_master_param: (Hash[String, untyped] obj) -> [Hash[String, untyped]?, Hash[String, untyped]?]
def log_debug: (String message) -> void
end
end
end

View file

@ -0,0 +1,17 @@
module Granblue
module Transformers
class SummonTransformer < BaseTransformer
TRANSCENDENCE_LEVELS: Array[Integer]
@quick_summon_id: String?
def initialize: (untyped data, ?String? quick_summon_id, ?Hash[Symbol, untyped] options) -> void
def transform: -> Array[Hash[Symbol, untyped]]
private
def calculate_transcendence_level: (Integer? level) -> Integer
end
end
end

View file

@ -0,0 +1,27 @@
module Granblue
module Transformers
class WeaponTransformer < BaseTransformer
# Constants for level calculations
UNCAP_LEVELS: Array[Integer]
TRANSCENDENCE_LEVELS: Array[Integer]
MULTIELEMENT_SERIES: Array[Integer]
# Implements abstract method from BaseTransformer
def transform: -> Array[Hash[Symbol, untyped]]
private
def transform_base_attributes: (Hash[String, untyped] master, Hash[String, untyped] param) -> Hash[Symbol, untyped]
def transform_awakening: (Hash[String, untyped] param) -> Hash[Symbol, Hash[Symbol, untyped]]
def transform_ax_skills: (Hash[String, untyped] param) -> Hash[Symbol, Array[Hash[Symbol, untyped]]]
def transform_weapon_keys: (Hash[String, untyped] weapon_data) -> Hash[Symbol, Array[String]]
def calculate_uncap_level: (Integer? level) -> Integer
def calculate_transcendence_level: (Integer? level) -> Integer
end
end
end