Implement JobProcessor

Process job, job skill, and job accessory data from deck
This commit is contained in:
Justin Edmund 2025-02-17 22:42:13 -08:00
parent e208e7daa9
commit 1dd8ae6159
2 changed files with 213 additions and 0 deletions

View file

@ -0,0 +1,127 @@
# frozen_string_literal: true
module Processors
##
# JobProcessor is responsible for processing job data from the transformed deck data.
# It finds a Job record by the masters id and assigns it (and its job skills) to the Party.
#
# @example
# raw_data = { 'job' => { "master": { "id": '130401', ... }, ... }, 'set_action': [ ... ] }
# processor = Processors::JobProcessor.new(party, raw_data, language: 'en')
# processor.process
class JobProcessor < BaseProcessor
##
# Initializes a new JobProcessor.
#
# @param party [Party] the Party record.
# @param data [Hash] the raw JSON data.
# @param options [Hash] options hash; e.g. expects :language.
def initialize(party, data, options = {})
super(party, options)
@party = party
@data = data
@language = options[:language] || 'en'
end
##
# Processes job data.
#
# Finds a Job record using a caseinsensitive search on +name_en+ or +name_jp+.
# If found, it assigns the job to the party and (if provided) assigns subskills.
#
# @return [void]
def process
if @data.is_a?(Hash)
@data = @data.with_indifferent_access
else
Rails.logger.error "[JOB] Invalid data format: expected a Hash, got #{@data.class}"
return
end
unless @data.key?('deck') && @data['deck'].key?('pc') && @data['deck']['pc'].key?('job')
Rails.logger.error '[JOB] Missing job data in deck JSON'
return
end
# Extract job data
job_data = @data.dig('deck', 'pc', 'job', 'master')
job_skills = @data.dig('deck', 'pc', 'set_action')
job_accessory_id = @data.dig('deck', 'pc', 'familiar_id') || @data.dig('deck', 'pc', 'shield_id')
# Look up and set the Job and its main skill
process_core_job(job_data)
# Look up and set the job skills.
if job_skills.present?
skills = process_job_skills(job_skills)
party.update(skill1: skills[0], skill2: skills[1], skill3: skills[2])
end
# Look up and set the job accessory.
accessory = process_job_accessory(job_accessory_id)
party.update(accessory: accessory)
rescue StandardError => e
Rails.logger.error "[JOB] Exception during job processing: #{e.message}"
raise e
end
private
##
# Updates the party with the corresponding job and its main skill.
#
# This method attempts to locate a Job using the provided job_data's 'id' (which represents
# the granblue_id). If the job is found, it retrieves the job's main
# skill (i.e. the JobSkill record where `main` is true) and updates the party with the job
# and its main skill. If no job is found, the method returns without updating.
#
# @param [Hash] job_data A hash containing job information.
# It must include the key 'id', which holds the granblue_id for the job.
# @return [void]
#
# @example
# job_data = { 'id' => 42 }
# process_core_job(job_data)
def process_core_job(job_data)
# Look up the Job by granblue_id (the job master id).
job = Job.find_by(granblue_id: job_data['id'])
return unless job
main_skill = JobSkill.find_by(job_id: job.id, main: true)
party.update(job: job, skill0: main_skill)
end
##
# Processes and associates job skills with a given job.
#
# This method first removes any existing skills from the job. It then iterates over the provided
# array of skill names, attempting to find a matching JobSkill record by comparing the provided
# name against both the English and Japanese name fields. Any found JobSkill records are then
# associated with the job. Finally, the method logs the processed job skill names.
#
# @param job_skills [Array<String>] an array of job skill names.
# @return [Array<JobSkill>] an array of JobSkill records that were associated with the job.
def process_job_skills(job_skills)
job_skills.map do |skill|
name = skill['name']
JobSkill.find_by(name_en: name)
end
end
##
# Processes raw data to find the currently set job accessory
#
# Searches JobAccessories for the given `granblue_id`
#
# @param accessory_id [String] the granblue_id of the accessory
def process_job_accessory(accessory_id)
JobAccessory.find_by(granblue_id: accessory_id)
end
# Converts a value (string or boolean) to a boolean.
def to_boolean(val)
val.to_s.downcase == 'true'
end
end
end

View file

@ -0,0 +1,86 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Processors::JobProcessor, type: :model do
let(:party) { create(:party) }
# Use a job that has associated job skills.
# In our seed/canonical data this job (by its ID) has several associated skills.
let!(:job_record) { Job.find_by!(granblue_id: '130401') }
# Build the raw data hash that mimics the transformed structure.
# The master section includes the job's basic information.
# The param section includes level data and the subskills derived from the job's associated job skills.
let(:deck_data) do
file_path = Rails.root.join('spec', 'fixtures', 'deck_sample2.json')
JSON.parse(File.read(file_path))
end
subject { described_class.new(party, deck_data, language: 'en') }
context 'with valid job data' do
it 'assigns the job to the party' do
# Before processing, the party should not have a job.
expect(party.job).to be_nil
# Process the job data.
subject.process
party.reload
# The party's job should now be set to the job_record.
expect(party.job).to eq(job_record)
end
it 'assigns the correct main skill to the party' do
# Before processing, the party should not have a job.
expect(party.job).to be_nil
# Process the job data.
subject.process
party.reload
main_skill = party.job.skills.where(main: true).first
expect(party.skill0.id).to eq(main_skill.id)
end
it 'associates the correct job skills' do
# Before processing, the party should not have a job.
expect(party.job).to be_nil
# Process the job data.
subject.process
party.reload
# We assume that the processor assigns up to four subskills to party attributes,
# for example, party.skill0, party.skill1, etc.
# Get the expected subskills (using order and taking the first four).
data = deck_data.with_indifferent_access
expected_subskills = data.dig('deck', 'pc', 'set_action').pluck(:name)
actual_subskills = [party.skill1.name_en, party.skill2.name_en, party.skill3.name_en]
expect(actual_subskills).to eq(expected_subskills)
end
it 'assigns the correct accessory to the party' do
# Process the job data.
subject.process
party.reload
expect(party.accessory.granblue_id).to eq(1.to_s)
end
end
context 'with invalid job data' do
let(:invalid_data) { 'invalid data' }
subject { described_class.new(party, invalid_data, language: 'en') }
it 'logs an error and does not assign a job' do
expect { subject.process }.not_to(change { party.reload.job })
end
end
after(:each) do |example|
if example.exception
puts "\nDEBUG [JobProcessor]: #{example.full_description} failed with error: #{example.exception.message}"
end
end
end