ImportController should use processors
This commit is contained in:
parent
ebb3218c29
commit
5b8fcdcfba
2 changed files with 144 additions and 237 deletions
|
|
@ -2,6 +2,20 @@
|
||||||
|
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
|
##
|
||||||
|
# ImportController is responsible for importing game data (e.g. deck data)
|
||||||
|
# and creating a new Party along with associated records (job, characters, weapons, summons, etc.).
|
||||||
|
#
|
||||||
|
# The controller expects a JSON payload whose top-level key is "import". If not wrapped,
|
||||||
|
# the controller will wrap the raw data automatically.
|
||||||
|
#
|
||||||
|
# @example Valid payload structure
|
||||||
|
# {
|
||||||
|
# "import": {
|
||||||
|
# "deck": { "name": "My Party", ... },
|
||||||
|
# "pc": { "job": { "master": { "name": "Warrior" } }, ... }
|
||||||
|
# }
|
||||||
|
# }
|
||||||
class ImportController < Api::V1::ApiController
|
class ImportController < Api::V1::ApiController
|
||||||
ELEMENT_MAPPING = {
|
ELEMENT_MAPPING = {
|
||||||
0 => nil,
|
0 => nil,
|
||||||
|
|
@ -13,263 +27,84 @@ module Api
|
||||||
6 => 5
|
6 => 5
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
##
|
||||||
|
# Processes an import request.
|
||||||
|
#
|
||||||
|
# It reads and parses the raw JSON, wraps the data under the "import" key if necessary,
|
||||||
|
# transforms the deck data using BaseDeckTransformer, validates that the transformed data
|
||||||
|
# contains required fields, and then creates a new Party record (and its associated objects)
|
||||||
|
# inside a transaction.
|
||||||
|
#
|
||||||
|
# @return [void] Renders JSON response with a party shortcode or an error message.
|
||||||
def create
|
def create
|
||||||
Rails.logger.info "[IMPORT] Starting import..."
|
Rails.logger.info '[IMPORT] Checking input...'
|
||||||
|
|
||||||
# Parse JSON request body
|
body = parse_request_body
|
||||||
raw_body = request.raw_post
|
return unless body
|
||||||
begin
|
|
||||||
raw_params = JSON.parse(raw_body) if raw_body.present?
|
raw_params = body['import']
|
||||||
Rails.logger.info "[IMPORT] Raw game data: #{raw_params.inspect}"
|
unless raw_params.is_a?(Hash)
|
||||||
rescue JSON::ParserError => e
|
Rails.logger.error "[IMPORT] 'import' key is missing or not a hash."
|
||||||
Rails.logger.error "[IMPORT] Invalid JSON in request body: #{e.message}"
|
return render json: { error: 'Invalid JSON data' }, status: :unprocessable_content
|
||||||
render json: { error: 'Invalid JSON data' }, status: :bad_request
|
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if raw_params.nil? || !raw_params.is_a?(Hash)
|
unless raw_params['deck'].is_a?(Hash) &&
|
||||||
Rails.logger.error "[IMPORT] Missing or invalid game data"
|
raw_params['deck'].key?('pc') &&
|
||||||
render json: { error: 'Missing or invalid game data' }, status: :bad_request
|
raw_params['deck'].key?('npc')
|
||||||
return
|
Rails.logger.error "[IMPORT] Deck data incomplete or missing."
|
||||||
|
return render json: { error: 'Invalid deck data' }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
# Transform game data
|
Rails.logger.info '[IMPORT] Starting import...'
|
||||||
transformer = ::Granblue::Transformers::BaseDeckTransformer.new(raw_params)
|
|
||||||
transformed_data = transformer.transform
|
|
||||||
Rails.logger.info "[IMPORT] Transformed data: #{transformed_data.inspect}"
|
|
||||||
|
|
||||||
# Validate transformed data
|
return if performed? # Rendered an error response already
|
||||||
unless transformed_data[:name].present? && transformed_data[:lang].present?
|
|
||||||
Rails.logger.error "[IMPORT] Missing required fields in transformed data"
|
|
||||||
render json: { error: 'Missing required fields name or lang' }, status: :unprocessable_entity
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
# Create party
|
party = Party.create(user: current_user)
|
||||||
party = Party.new(user: current_user)
|
deck_data = raw_params['import']
|
||||||
|
process_data(party, deck_data)
|
||||||
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
# Basic party data
|
|
||||||
party.name = transformed_data[:name]
|
|
||||||
party.extra = transformed_data[:extra]
|
|
||||||
party.save!
|
|
||||||
|
|
||||||
# Process job and skills
|
|
||||||
if transformed_data[:class].present?
|
|
||||||
process_job(party, transformed_data[:class], transformed_data[:subskills])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Process characters
|
|
||||||
if transformed_data[:characters].present?
|
|
||||||
process_characters(party, transformed_data[:characters])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Process weapons
|
|
||||||
if transformed_data[:weapons].present?
|
|
||||||
process_weapons(party, transformed_data[:weapons])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Process summons
|
|
||||||
if transformed_data[:summons].present?
|
|
||||||
process_summons(party, transformed_data[:summons], transformed_data[:friend_summon])
|
|
||||||
end
|
|
||||||
|
|
||||||
# Process sub summons
|
|
||||||
if transformed_data[:sub_summons].present?
|
|
||||||
process_sub_summons(party, transformed_data[:sub_summons])
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Return shortcode for redirection
|
|
||||||
render json: { shortcode: party.shortcode }, status: :created
|
render json: { shortcode: party.shortcode }, status: :created
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.error "[IMPORT] Error processing import: #{e.message}"
|
render json: { error: e.message }, status: :unprocessable_content
|
||||||
Rails.logger.error "[IMPORT] Backtrace: #{e.backtrace.join("\n")}"
|
|
||||||
render json: { error: 'Error processing import' }, status: :unprocessable_entity
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def process_job(party, job_name, subskills)
|
##
|
||||||
return unless job_name
|
# Reads and parses the raw JSON request body.
|
||||||
job = Job.find_by("name_en = ? OR name_jp = ?", job_name, job_name)
|
#
|
||||||
unless job
|
# @return [Hash] Parsed JSON data.
|
||||||
Rails.logger.warn "[IMPORT] Could not find job: #{job_name}"
|
# @raise [JSON::ParserError] If the JSON is invalid.
|
||||||
return
|
def parse_request_body
|
||||||
end
|
raw_body = request.raw_post
|
||||||
|
JSON.parse(raw_body)
|
||||||
party.job = job
|
rescue JSON::ParserError => e
|
||||||
party.save!
|
Rails.logger.error "[IMPORT] Invalid JSON: #{e.message}"
|
||||||
Rails.logger.info "[IMPORT] Assigned job=#{job_name} to party_id=#{party.id}"
|
render json: { error: 'Invalid JSON data' }, status: :bad_request and return
|
||||||
|
|
||||||
return unless subskills&.any?
|
|
||||||
subskills.each_with_index do |skill_name, idx|
|
|
||||||
next if skill_name.blank?
|
|
||||||
skill = JobSkill.find_by("(name_en = ? OR name_jp = ?) AND job_id = ?", skill_name, skill_name, job.id)
|
|
||||||
unless skill
|
|
||||||
Rails.logger.warn "[IMPORT] Could not find skill=#{skill_name} for job_id=#{job.id}"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
party["skill#{idx + 1}_id"] = skill.id
|
|
||||||
Rails.logger.info "[IMPORT] Assigned skill=#{skill_name} at position #{idx + 1}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_characters(party, characters)
|
##
|
||||||
return unless characters&.any?
|
# Ensures that the provided data is wrapped under an "import" key.
|
||||||
Rails.logger.info "[IMPORT] Processing #{characters.length} characters"
|
#
|
||||||
|
# @param data [Hash] The parsed JSON data.
|
||||||
characters.each_with_index do |char_data, idx|
|
# @return [Hash] Data wrapped under the "import" key.
|
||||||
character = Character.find_by(granblue_id: char_data[:id])
|
def wrap_import_data(data)
|
||||||
unless character
|
data.key?('import') ? data : { 'import' => data }
|
||||||
Rails.logger.warn "[IMPORT] Character not found: #{char_data[:id]}"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
GridCharacter.create!(
|
|
||||||
party: party,
|
|
||||||
character_id: character.id,
|
|
||||||
position: idx,
|
|
||||||
uncap_level: char_data[:uncap],
|
|
||||||
perpetuity: char_data[:ringed] || false,
|
|
||||||
transcendence_step: char_data[:transcend] || 0
|
|
||||||
)
|
|
||||||
Rails.logger.info "[IMPORT] Added character: #{character.name_en} at position #{idx}"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def process_weapons(party, weapons)
|
##
|
||||||
return unless weapons&.any?
|
# Processes the deck data using processors.
|
||||||
Rails.logger.info "[IMPORT] Processing #{weapons.length} weapons"
|
#
|
||||||
|
# @param party [Party] The party to insert data into
|
||||||
|
# @param data [Hash] The wrapped data.
|
||||||
|
# @return [Hash] The transformed deck data.
|
||||||
|
def process_data(party, data)
|
||||||
|
Rails.logger.info '[IMPORT] Transforming deck data'
|
||||||
|
|
||||||
weapons.each_with_index do |weapon_data, idx|
|
Processors::JobProcessor.new(party, data).process
|
||||||
weapon = Weapon.find_by(granblue_id: weapon_data[:id])
|
Processors::CharacterProcessor.new(party, data).process
|
||||||
unless weapon
|
Processors::SummonProcessor.new(party, data).process
|
||||||
Rails.logger.warn "[IMPORT] Weapon not found: #{weapon_data[:id]}"
|
Processors::WeaponProcessor.new(party, data).process
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
grid_weapon = GridWeapon.create!(
|
|
||||||
party: party,
|
|
||||||
weapon_id: weapon.id,
|
|
||||||
position: idx - 1,
|
|
||||||
mainhand: idx.zero?,
|
|
||||||
uncap_level: weapon_data[:uncap],
|
|
||||||
transcendence_step: weapon_data[:transcend] || 0,
|
|
||||||
element: weapon_data[:attr] ? ELEMENT_MAPPING[weapon_data[:attr]] : nil
|
|
||||||
)
|
|
||||||
|
|
||||||
process_weapon_keys(grid_weapon, weapon_data[:keys]) if weapon_data[:keys]
|
|
||||||
process_weapon_ax(grid_weapon, weapon_data[:ax]) if weapon_data[:ax]
|
|
||||||
|
|
||||||
Rails.logger.info "[IMPORT] Added weapon: #{weapon.name_en} at position #{idx - 1}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_weapon_keys(grid_weapon, keys)
|
|
||||||
keys.each_with_index do |key_id, idx|
|
|
||||||
key = WeaponKey.find_by(granblue_id: key_id)
|
|
||||||
unless key
|
|
||||||
Rails.logger.warn "[IMPORT] WeaponKey not found: #{key_id}"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
grid_weapon["weapon_key#{idx + 1}_id"] = key.id
|
|
||||||
grid_weapon.save!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_weapon_ax(grid_weapon, ax_skills)
|
|
||||||
ax_skills.each_with_index do |ax, idx|
|
|
||||||
grid_weapon["ax_modifier#{idx + 1}"] = ax[:id].to_i
|
|
||||||
grid_weapon["ax_strength#{idx + 1}"] = ax[:val].to_s.gsub(/[+%]/, '').to_i
|
|
||||||
end
|
|
||||||
grid_weapon.save!
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_summons(party, summons, friend_summon = nil)
|
|
||||||
return unless summons&.any?
|
|
||||||
Rails.logger.info "[IMPORT] Processing #{summons.length} summons"
|
|
||||||
|
|
||||||
# Main and sub summons
|
|
||||||
summons.each_with_index do |summon_data, idx|
|
|
||||||
summon = Summon.find_by(granblue_id: summon_data[:id])
|
|
||||||
unless summon
|
|
||||||
Rails.logger.warn "[IMPORT] Summon not found: #{summon_data[:id]}"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
grid_summon = GridSummon.new(
|
|
||||||
party: party,
|
|
||||||
summon_id: summon.id,
|
|
||||||
position: idx,
|
|
||||||
main: idx.zero?,
|
|
||||||
friend: false,
|
|
||||||
uncap_level: summon_data[:uncap],
|
|
||||||
transcendence_step: summon_data[:transcend] || 0,
|
|
||||||
quick_summon: summon_data[:qs] || false
|
|
||||||
)
|
|
||||||
|
|
||||||
if grid_summon.save
|
|
||||||
Rails.logger.info "[IMPORT] Added summon: #{summon.name_en} at position #{idx}"
|
|
||||||
else
|
|
||||||
Rails.logger.error "[IMPORT] Failed to save summon: #{grid_summon.errors.full_messages}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Friend summon if provided
|
|
||||||
process_friend_summon(party, friend_summon) if friend_summon.present?
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_friend_summon(party, friend_summon)
|
|
||||||
friend = Summon.find_by("name_en = ? OR name_jp = ?", friend_summon, friend_summon)
|
|
||||||
unless friend
|
|
||||||
Rails.logger.warn "[IMPORT] Friend summon not found: #{friend_summon}"
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
grid_summon = GridSummon.new(
|
|
||||||
party: party,
|
|
||||||
summon_id: friend.id,
|
|
||||||
position: 6,
|
|
||||||
main: false,
|
|
||||||
friend: true,
|
|
||||||
uncap_level: friend.ulb ? 5 : (friend.flb ? 4 : 3)
|
|
||||||
)
|
|
||||||
|
|
||||||
if grid_summon.save
|
|
||||||
Rails.logger.info "[IMPORT] Added friend summon: #{friend.name_en}"
|
|
||||||
else
|
|
||||||
Rails.logger.error "[IMPORT] Failed to save friend summon: #{grid_summon.errors.full_messages}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def process_sub_summons(party, sub_summons)
|
|
||||||
return unless sub_summons&.any?
|
|
||||||
Rails.logger.info "[IMPORT] Processing #{sub_summons.length} sub summons"
|
|
||||||
|
|
||||||
sub_summons.each_with_index do |summon_data, idx|
|
|
||||||
summon = Summon.find_by(granblue_id: summon_data[:id])
|
|
||||||
unless summon
|
|
||||||
Rails.logger.warn "[IMPORT] Sub summon not found: #{summon_data[:id]}"
|
|
||||||
next
|
|
||||||
end
|
|
||||||
|
|
||||||
grid_summon = GridSummon.new(
|
|
||||||
party: party,
|
|
||||||
summon_id: summon.id,
|
|
||||||
position: idx + 5,
|
|
||||||
main: false,
|
|
||||||
friend: false,
|
|
||||||
uncap_level: summon_data[:uncap],
|
|
||||||
transcendence_step: summon_data[:transcend] || 0
|
|
||||||
)
|
|
||||||
|
|
||||||
if grid_summon.save
|
|
||||||
Rails.logger.info "[IMPORT] Added sub summon: #{summon.name_en} at position #{idx + 5}"
|
|
||||||
else
|
|
||||||
Rails.logger.error "[IMPORT] Failed to save sub summon: #{grid_summon.errors.full_messages}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
72
spec/requests/import_controller_spec.rb
Normal file
72
spec/requests/import_controller_spec.rb
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'ImportController', type: :request do
|
||||||
|
let(:user) { create(:user) }
|
||||||
|
let(:access_token) do
|
||||||
|
Doorkeeper::AccessToken.create!(resource_owner_id: user.id, expires_in: 30.days, scopes: 'public')
|
||||||
|
end
|
||||||
|
let(:headers) do
|
||||||
|
{ 'Authorization' => "Bearer #{access_token.token}", 'Content-Type' => 'application/json' }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Load raw deck JSON from fixture and wrap it under the "import" key.
|
||||||
|
let(:raw_deck_data) do
|
||||||
|
file_path = Rails.root.join('spec', 'fixtures', 'deck_sample.json')
|
||||||
|
JSON.parse(File.read(file_path))
|
||||||
|
end
|
||||||
|
let(:valid_deck_json) { { 'import' => raw_deck_data }.to_json }
|
||||||
|
|
||||||
|
describe 'POST /api/v1/import' do
|
||||||
|
context 'with valid deck data' do
|
||||||
|
it 'creates a new party and returns a shortcode' do
|
||||||
|
expect {
|
||||||
|
post '/api/v1/import', params: valid_deck_json, headers: headers
|
||||||
|
}.to change(Party, :count).by(1)
|
||||||
|
expect(response).to have_http_status(:created)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response).to have_key('shortcode')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid JSON' do
|
||||||
|
it 'returns a bad request error' do
|
||||||
|
post '/api/v1/import', params: 'this is not json', headers: headers
|
||||||
|
expect(response).to have_http_status(:bad_request)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['error']).to eq('Invalid JSON data')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with missing required fields in transformed data' do
|
||||||
|
it 'returns unprocessable entity error' do
|
||||||
|
# Here we simulate missing required fields by sending an import hash
|
||||||
|
# where the 'deck' key is present but missing required subkeys.
|
||||||
|
invalid_data = { 'import' => { 'deck' => { 'name' => '', 'pc' => nil } } }.to_json
|
||||||
|
post '/api/v1/import', params: invalid_data, headers: headers
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['error']).to eq('Invalid deck data')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when an error occurs during processing' do
|
||||||
|
it 'returns unprocessable entity status with error details' do
|
||||||
|
# Stub the transformer to raise an error when transform is called.
|
||||||
|
allow_any_instance_of(Processors::CharacterProcessor)
|
||||||
|
.to receive(:process).and_raise(StandardError.new('Error processing import'))
|
||||||
|
allow_any_instance_of(Processors::WeaponProcessor)
|
||||||
|
.to receive(:process).and_raise(StandardError.new('Error processing import'))
|
||||||
|
allow_any_instance_of(Processors::SummonProcessor)
|
||||||
|
.to receive(:process).and_raise(StandardError.new('Error processing import'))
|
||||||
|
allow_any_instance_of(Processors::JobProcessor)
|
||||||
|
.to receive(:process).and_raise(StandardError.new('Error processing import'))
|
||||||
|
post '/api/v1/import', params: valid_deck_json, headers: headers
|
||||||
|
expect(response).to have_http_status(:unprocessable_entity)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
expect(json_response['error']).to eq('Error processing import')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in a new issue