Adds sigs and docs to importers

This commit is contained in:
Justin Edmund 2025-01-18 03:07:29 -08:00
parent 2d41b24896
commit 19c3b7dbc6
7 changed files with 423 additions and 2 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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