From 19c3b7dbc694c454c5a8d8a7ff9871f10636d6de Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 18 Jan 2025 03:07:29 -0800 Subject: [PATCH] Adds sigs and docs to importers --- 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 ++++++++ sig/granblue/importers/base_importer.rbs | 80 ++++++++++++ sig/granblue/importers/import_error.rbs | 20 +++ 7 files changed, 423 insertions(+), 2 deletions(-) create mode 100644 sig/granblue/importers/base_importer.rbs create mode 100644 sig/granblue/importers/import_error.rbs 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/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