hensei-api/docs/transformers.md

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

  1. Check input data is not nil
  2. Verify required fields are present
  3. Enable debug logging
  4. Check for silent rescue blocks

Wrong Element Mapping

  1. Verify using correct mapping direction
  2. Check for element ID vs name confusion
  3. Ensure consistent element system

Data Loss During Transform

  1. Check all fields are mapped
  2. Verify no fields silently dropped
  3. Add logging for each field
  4. Compare input and output keys

Performance Issues

  1. Cache repeated transformations
  2. Use batch transformations
  3. Avoid N+1 queries in transformers
  4. Profile transformation methods