11 KiB
11 KiB
Data Transformers Documentation
The transformer system converts game data between different formats and structures. It handles element mapping, data normalization, and format conversions for characters, weapons, and summons.
Architecture
Base Transformer
All transformers inherit from BaseTransformer which provides:
- Data validation
- Element mapping (game ↔ internal)
- Error handling with detailed context
- Debug logging
- Common transformation utilities
Element Mapping
The system uses different element IDs internally vs the game:
ELEMENT_MAPPING = {
0 => nil, # Null/None
1 => 4, # Wind → Earth
2 => 2, # Fire → Fire
3 => 3, # Water → Water
4 => 1, # Earth → Wind
5 => 6, # Dark → Light
6 => 5 # Light → Dark
}
Available Transformers
CharacterTransformer
Transforms character data for different contexts.
Transformations:
- Game format → Database format
- Database format → API response
- Wiki data → Database format
- Legacy format → Current format
Usage:
# Transform game data to database format
game_data = {
id: "3040001000",
name: "Katalina",
element: 3, # Water in game format
hp: 1680,
atk: 7200
}
transformer = Granblue::Transformers::CharacterTransformer.new(game_data)
db_data = transformer.transform
# => { granblue_id: "3040001000", name_en: "Katalina", element: 3, ... }
# Transform for API response
transformer = Granblue::Transformers::CharacterTransformer.new(
character,
format: :api
)
api_data = transformer.transform
WeaponTransformer
Transforms weapon data between formats.
Transformations:
- Skill format conversions
- Awakening data mapping
- Element transformations
- Legacy skill migrations
Usage:
weapon_data = {
id: "1040001000",
name: "Murgleis",
element: 3,
skills: [{ name: "Hoarfrost's Might", level: 10 }]
}
transformer = Granblue::Transformers::WeaponTransformer.new(weapon_data)
transformed = transformer.transform
SummonTransformer
Transforms summon data between formats.
Transformations:
- Call effect formatting
- Aura data structuring
- Sub-aura conversions
- Cooldown normalization
Usage:
summon_data = {
id: "2040001000",
name: "Bahamut",
element: 0,
call_effect: "120% Dark damage",
initial_cd: 9,
recast: 10
}
transformer = Granblue::Transformers::SummonTransformer.new(summon_data)
transformed = transformer.transform
BaseDeckTransformer
Transforms party/deck configurations.
Transformations:
- Party format → Deck format
- Grid positions mapping
- Equipment slot conversions
- Skill selection formatting
Usage:
party_data = {
characters: [char1, char2, char3],
weapons: [weapon1, weapon2, ...],
summons: [summon1, summon2, ...]
}
transformer = Granblue::Transformers::BaseDeckTransformer.new(party_data)
deck = transformer.transform
Transformation Patterns
Input Validation
class CustomTransformer < BaseTransformer
def transform
validate_data
# Transformation logic
{
id: @data[:id],
name: transform_name(@data[:name]),
element: transform_element(@data[:element])
}
end
private
def validate_data
raise TransformerError.new("Missing ID") if @data[:id].blank?
raise TransformerError.new("Invalid element") unless valid_element?
end
def valid_element?
(0..6).include?(@data[:element].to_i)
end
end
Element Transformation
# Game to internal
def transform_element_to_internal(game_element)
ELEMENT_MAPPING[game_element] || 0
end
# Internal to game
def transform_element_to_game(internal_element)
ELEMENT_MAPPING.invert[internal_element] || 0
end
# Element name
def element_name(element_id)
%w[Null Wind Fire Water Earth Dark Light][element_id]
end
Safe Value Extraction
def safe_integer(value, default = 0)
Integer(value.to_s)
rescue ArgumentError, TypeError
default
end
def safe_string(value, default = "")
value.to_s.presence || default
end
def safe_boolean(value, default = false)
return default if value.nil?
ActiveModel::Type::Boolean.new.cast(value)
end
Nested Data Transformation
def transform_skills(skills)
return [] if skills.blank?
skills.map do |skill|
{
name: safe_string(skill[:name]),
description: safe_string(skill[:description]),
cooldown: safe_integer(skill[:cd]),
effects: transform_skill_effects(skill[:effects])
}
end
end
def transform_skill_effects(effects)
return [] if effects.blank?
effects.map do |effect|
{
type: effect[:type].to_s.underscore,
value: safe_integer(effect[:value]),
target: effect[:target] || "self"
}
end
end
Error Handling
TransformerError
Custom error class with context:
class TransformerError < StandardError
attr_reader :details
def initialize(message, details = nil)
@details = details
super(message)
end
end
# Usage
raise TransformerError.new(
"Invalid skill format",
{ skill: skill_data, index: index }
)
Error Recovery
def transform_with_fallback
begin
primary_transform
rescue TransformerError => e
Rails.logger.warn "Transform failed: #{e.message}"
fallback_transform
end
end
Validation Errors
def validate_and_transform
errors = []
errors << "Missing name" if @data[:name].blank?
errors << "Invalid HP" if @data[:hp].to_i <= 0
errors << "Invalid element" unless valid_element?
if errors.any?
raise TransformerError.new(
"Validation failed",
{ errors: errors, data: @data }
)
end
perform_transform
end
Format Specifications
API Format
class ApiTransformer < BaseTransformer
def transform
{
id: @data.granblue_id,
name: {
en: @data.name_en,
jp: @data.name_jp
},
element: element_name(@data.element),
rarity: rarity_string(@data.rarity),
stats: {
hp: @data.hp,
atk: @data.atk
},
skills: transform_skills(@data.skills)
}
end
end
Database Format
class DatabaseTransformer < BaseTransformer
def transform
{
granblue_id: @data[:id].to_s,
name_en: @data[:name][:en],
name_jp: @data[:name][:jp],
element: parse_element(@data[:element]),
rarity: parse_rarity(@data[:rarity]),
hp: @data[:stats][:hp].to_i,
atk: @data[:stats][:atk].to_i
}
end
end
Legacy Format Migration
class LegacyTransformer < BaseTransformer
def transform
# Map old field names to new
{
granblue_id: @data[:char_id] || @data[:id],
name_en: @data[:name_english] || @data[:name],
element: map_legacy_element(@data[:elem]),
# Handle removed fields
deprecated_field: nil
}
end
private
def map_legacy_element(elem)
# Old system used different IDs
legacy_mapping = {
"wind" => 1,
"fire" => 2,
"water" => 3,
"earth" => 4
}
legacy_mapping[elem.to_s.downcase] || 0
end
end
Best Practices
1. Always Validate Input
def transform
validate_required_fields
validate_data_types
validate_ranges
perform_transform
end
2. Use Safe Extraction Methods
# Good
name = safe_string(@data[:name], "Unknown")
# Bad
name = @data[:name] # Could be nil
3. Provide Clear Error Messages
raise TransformerError.new(
"Element value '#{element}' is not valid. Expected 0-6.",
{ element: element, valid_range: (0..6) }
)
4. Log Transformations
def transform
Rails.logger.info "[TRANSFORM] Starting #{self.class.name}"
result = perform_transform
Rails.logger.info "[TRANSFORM] Completed with #{result.keys.count} fields"
result
rescue => e
Rails.logger.error "[TRANSFORM] Failed: #{e.message}"
raise
end
5. Handle Missing Data Gracefully
def transform_optional_field(value)
return nil if value.blank?
# Transform only if present
value.to_s.upcase
end
Custom Transformer Implementation
module Granblue
module Transformers
class CustomTransformer < BaseTransformer
# Define transformation options
OPTIONS = {
format: :database,
include_metadata: false,
validate_strict: true
}.freeze
def initialize(data, options = {})
super(data, OPTIONS.merge(options))
end
def transform
validate_data if @options[:validate_strict]
base_transform.tap do |result|
result[:metadata] = metadata if @options[:include_metadata]
end
end
private
def base_transform
{
id: @data[:id],
type: determine_type,
attributes: transform_attributes,
relationships: transform_relationships
}
end
def transform_attributes
{
name: safe_string(@data[:name]),
description: safe_string(@data[:desc]),
stats: transform_stats
}
end
def transform_stats
return {} unless @data[:stats]
@data[:stats].transform_values { |v| safe_integer(v) }
end
def transform_relationships
{
parent_id: @data[:parent],
child_ids: Array(@data[:children])
}
end
def metadata
{
transformed_at: Time.current,
transformer: self.class.name,
version: "1.0"
}
end
end
end
end
Testing Transformers
Unit Tests
RSpec.describe Granblue::Transformers::CharacterTransformer do
let(:input_data) do
{
id: "3040001000",
name: "Test Character",
element: 3,
hp: 1000,
atk: 500
}
end
subject { described_class.new(input_data) }
describe "#transform" do
it "transforms game data to database format" do
result = subject.transform
expect(result[:granblue_id]).to eq("3040001000")
expect(result[:name_en]).to eq("Test Character")
expect(result[:element]).to eq(3)
end
it "handles missing optional fields" do
input_data.delete(:hp)
expect { subject.transform }.not_to raise_error
end
it "raises error for invalid element" do
input_data[:element] = 99
expect { subject.transform }.to raise_error(TransformerError)
end
end
end
Integration Tests
# Test full pipeline
character_data = fetch_from_api
transformer = CharacterTransformer.new(character_data)
transformed = transformer.transform
character = Character.create!(transformed)
expect(character.persisted?).to be true
expect(character.element).to eq(transformed[:element])
Troubleshooting
Transformation Returns Nil
- Check input data is not nil
- Verify required fields are present
- Enable debug logging
- Check for silent rescue blocks
Wrong Element Mapping
- Verify using correct mapping direction
- Check for element ID vs name confusion
- Ensure consistent element system
Data Loss During Transform
- Check all fields are mapped
- Verify no fields silently dropped
- Add logging for each field
- Compare input and output keys
Performance Issues
- Cache repeated transformations
- Use batch transformations
- Avoid N+1 queries in transformers
- Profile transformation methods