From 7d164b540cbd320123c74b58f947f716b2e35dfa Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 18 Jan 2025 03:09:29 -0800 Subject: [PATCH] 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 --- lib/granblue/downloaders/base_downloader.rb | 109 ++++++++++++++- .../downloaders/character_downloader.rb | 37 +++++- lib/granblue/downloaders/download_manager.rb | 33 +++++ .../elemental_weapon_downloader.rb | 18 +++ lib/granblue/downloaders/summon_downloader.rb | 37 ++++++ lib/granblue/downloaders/weapon_downloader.rb | 38 ++++++ lib/granblue/importers/base_importer.rb | 124 +++++++++++++++++- lib/granblue/importers/character_importer.rb | 52 ++++++++ lib/granblue/importers/import_error.rb | 46 ++++++- lib/granblue/importers/summon_importer.rb | 50 +++++++ lib/granblue/importers/weapon_importer.rb | 53 ++++++++ lib/granblue/transformers/base_transformer.rb | 21 +++ .../transformers/character_transformer.rb | 39 +++++- .../transformers/summon_transformer.rb | 43 +++++- .../transformers/weapon_transformer.rb | 51 +++++++ sig/granblue/downloaders/base_downloader.rbs | 53 ++++++++ .../downloaders/character_downloader.rbs | 28 ++++ sig/granblue/downloaders/download_manager.rbs | 15 +++ .../downloaders/summon_downloader.rbs | 30 +++++ .../downloaders/weapon_downloader.rbs | 48 +++++++ sig/granblue/importers/base_importer.rbs | 80 +++++++++++ sig/granblue/importers/import_error.rbs | 20 +++ .../transformers/base_transformer.rbs | 31 +++++ .../transformers/summon_transformer.rbs | 17 +++ .../transformers/weapon_transformer.rbs | 27 ++++ 25 files changed, 1080 insertions(+), 20 deletions(-) create mode 100644 sig/granblue/downloaders/base_downloader.rbs create mode 100644 sig/granblue/downloaders/character_downloader.rbs create mode 100644 sig/granblue/downloaders/download_manager.rbs create mode 100644 sig/granblue/downloaders/summon_downloader.rbs create mode 100644 sig/granblue/downloaders/weapon_downloader.rbs create mode 100644 sig/granblue/importers/base_importer.rbs create mode 100644 sig/granblue/importers/import_error.rbs create mode 100644 sig/granblue/transformers/base_transformer.rbs create mode 100644 sig/granblue/transformers/summon_transformer.rbs create mode 100644 sig/granblue/transformers/weapon_transformer.rbs diff --git a/lib/granblue/downloaders/base_downloader.rb b/lib/granblue/downloaders/base_downloader.rb index 09aaac8..a740adc 100644 --- a/lib/granblue/downloaders/base_downloader.rb +++ b/lib/granblue/downloaders/base_downloader.rb @@ -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] 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" diff --git a/lib/granblue/downloaders/character_downloader.rb b/lib/granblue/downloaders/character_downloader.rb index 825b1d8..5c45c3c 100644 --- a/lib/granblue/downloaders/character_downloader.rb +++ b/lib/granblue/downloaders/character_downloader.rb @@ -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' diff --git a/lib/granblue/downloaders/download_manager.rb b/lib/granblue/downloaders/download_manager.rb index db2cfe8..f3edde2 100644 --- a/lib/granblue/downloaders/download_manager.rb +++ b/lib/granblue/downloaders/download_manager.rb @@ -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, diff --git a/lib/granblue/downloaders/elemental_weapon_downloader.rb b/lib/granblue/downloaders/elemental_weapon_downloader.rb index 2525c3d..8e3f2b0 100644 --- a/lib/granblue/downloaders/elemental_weapon_downloader.rb +++ b/lib/granblue/downloaders/elemental_weapon_downloader.rb @@ -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] 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 diff --git a/lib/granblue/downloaders/summon_downloader.rb b/lib/granblue/downloaders/summon_downloader.rb index 20fcfc3..0ac6493 100644 --- a/lib/granblue/downloaders/summon_downloader.rb +++ b/lib/granblue/downloaders/summon_downloader.rb @@ -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' diff --git a/lib/granblue/downloaders/weapon_downloader.rb b/lib/granblue/downloaders/weapon_downloader.rb index 66aeae2..d380a32 100644 --- a/lib/granblue/downloaders/weapon_downloader.rb +++ b/lib/granblue/downloaders/weapon_downloader.rb @@ -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' diff --git a/lib/granblue/importers/base_importer.rb b/lib/granblue/importers/base_importer.rb index d72143f..3cb1de5 100644 --- a/lib/granblue/importers/base_importer.rb +++ b/lib/granblue/importers/base_importer.rb @@ -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>] New records created during import + attr_reader :new_records + # @return [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, 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] 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] 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) [ diff --git a/lib/granblue/importers/character_importer.rb b/lib/granblue/importers/character_importer.rb index 2343fa0..c1743e5 100644 --- a/lib/granblue/importers/character_importer.rb +++ b/lib/granblue/importers/character_importer.rb @@ -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] :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] :nicknames_en English nicknames + # @option attributes [Array] :nicknames_jp Japanese nicknames + # + # @raise [ImportError] If required attributes are missing or invalid def build_attributes(row) { name_en: parse_value(row['name_en']), diff --git a/lib/granblue/importers/import_error.rb b/lib/granblue/importers/import_error.rb index f036bdc..6579aed 100644 --- a/lib/granblue/importers/import_error.rb +++ b/lib/granblue/importers/import_error.rb @@ -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 diff --git a/lib/granblue/importers/summon_importer.rb b/lib/granblue/importers/summon_importer.rb index c68ab67..c6b091b 100644 --- a/lib/granblue/importers/summon_importer.rb +++ b/lib/granblue/importers/summon_importer.rb @@ -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] :nicknames_en English nicknames + # @option attributes [Array] :nicknames_jp Japanese nicknames + # + # @raise [ImportError] If required attributes are missing or invalid def build_attributes(row) { name_en: parse_value(row['name_en']), diff --git a/lib/granblue/importers/weapon_importer.rb b/lib/granblue/importers/weapon_importer.rb index 370c954..6a5dd77 100644 --- a/lib/granblue/importers/weapon_importer.rb +++ b/lib/granblue/importers/weapon_importer.rb @@ -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] :nicknames_en English nicknames + # @option attributes [Array] :nicknames_jp Japanese nicknames + # + # @raise [ImportError] If required attributes are missing or invalid def build_attributes(row) { name_en: parse_value(row['name_en']), diff --git a/lib/granblue/transformers/base_transformer.rb b/lib/granblue/transformers/base_transformer.rb index e49a3df..581625d 100644 --- a/lib/granblue/transformers/base_transformer.rb +++ b/lib/granblue/transformers/base_transformer.rb @@ -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] 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] 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] 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}" diff --git a/lib/granblue/transformers/character_transformer.rb b/lib/granblue/transformers/character_transformer.rb index 210228a..573da04 100644 --- a/lib/granblue/transformers/character_transformer.rb +++ b/lib/granblue/transformers/character_transformer.rb @@ -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] 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 diff --git a/lib/granblue/transformers/summon_transformer.rb b/lib/granblue/transformers/summon_transformer.rb index f149c87..eefb380 100644 --- a/lib/granblue/transformers/summon_transformer.rb +++ b/lib/granblue/transformers/summon_transformer.rb @@ -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] 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] 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 } diff --git a/lib/granblue/transformers/weapon_transformer.rb b/lib/granblue/transformers/weapon_transformer.rb index db5e964..0bb38b0 100644 --- a/lib/granblue/transformers/weapon_transformer.rb +++ b/lib/granblue/transformers/weapon_transformer.rb @@ -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] Level thresholds for determining uncap level UNCAP_LEVELS = [40, 60, 80, 100, 150, 200].freeze + + # @return [Array] Level thresholds for determining transcendence level TRANSCENDENCE_LEVELS = [210, 220, 230, 240].freeze + + # @return [Array] Weapon series IDs that can have multiple elements MULTIELEMENT_SERIES = [13, 17, 19].freeze + # Transform raw weapon data into standardized format + # @return [Array] 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 } diff --git a/sig/granblue/downloaders/base_downloader.rbs b/sig/granblue/downloaders/base_downloader.rbs new file mode 100644 index 0000000..9174c1b --- /dev/null +++ b/sig/granblue/downloaders/base_downloader.rbs @@ -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 diff --git a/sig/granblue/downloaders/character_downloader.rbs b/sig/granblue/downloaders/character_downloader.rbs new file mode 100644 index 0000000..b6dc2b7 --- /dev/null +++ b/sig/granblue/downloaders/character_downloader.rbs @@ -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 diff --git a/sig/granblue/downloaders/download_manager.rbs b/sig/granblue/downloaders/download_manager.rbs new file mode 100644 index 0000000..52d92c2 --- /dev/null +++ b/sig/granblue/downloaders/download_manager.rbs @@ -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 diff --git a/sig/granblue/downloaders/summon_downloader.rbs b/sig/granblue/downloaders/summon_downloader.rbs new file mode 100644 index 0000000..7cac4eb --- /dev/null +++ b/sig/granblue/downloaders/summon_downloader.rbs @@ -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 diff --git a/sig/granblue/downloaders/weapon_downloader.rbs b/sig/granblue/downloaders/weapon_downloader.rbs new file mode 100644 index 0000000..878deeb --- /dev/null +++ b/sig/granblue/downloaders/weapon_downloader.rbs @@ -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 diff --git a/sig/granblue/importers/base_importer.rbs b/sig/granblue/importers/base_importer.rbs new file mode 100644 index 0000000..3627c17 --- /dev/null +++ b/sig/granblue/importers/base_importer.rbs @@ -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 diff --git a/sig/granblue/importers/import_error.rbs b/sig/granblue/importers/import_error.rbs new file mode 100644 index 0000000..50f6fc1 --- /dev/null +++ b/sig/granblue/importers/import_error.rbs @@ -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 diff --git a/sig/granblue/transformers/base_transformer.rbs b/sig/granblue/transformers/base_transformer.rbs new file mode 100644 index 0000000..6eca1ba --- /dev/null +++ b/sig/granblue/transformers/base_transformer.rbs @@ -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 diff --git a/sig/granblue/transformers/summon_transformer.rbs b/sig/granblue/transformers/summon_transformer.rbs new file mode 100644 index 0000000..b6199ea --- /dev/null +++ b/sig/granblue/transformers/summon_transformer.rbs @@ -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 diff --git a/sig/granblue/transformers/weapon_transformer.rbs b/sig/granblue/transformers/weapon_transformer.rbs new file mode 100644 index 0000000..b4402d3 --- /dev/null +++ b/sig/granblue/transformers/weapon_transformer.rbs @@ -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