hensei-api/app/controllers/api/v1/import_controller.rb
Justin Edmund 7880ac76cc
Add data ingestion endpoints (#195)
* Update schema.rb

* Add endpoints for importing game data

This lets privileged users import canonical data for characters, weapons and summons directly from the game
2025-03-02 17:48:35 -08:00

216 lines
7.4 KiB
Ruby

# frozen_string_literal: true
module Api
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
ELEMENT_MAPPING = {
0 => nil,
1 => 4,
2 => 2,
3 => 3,
4 => 1,
5 => 6,
6 => 5
}.freeze
before_action :ensure_admin_role, only: %i[weapons summons characters]
##
# 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
Rails.logger.info '[IMPORT] Checking input...'
body = parse_request_body
return unless body
raw_params = body['import']
unless raw_params.is_a?(Hash)
Rails.logger.error "[IMPORT] 'import' key is missing or not a hash."
return render json: { error: 'Invalid JSON data' }, status: :unprocessable_content
end
unless raw_params['deck'].is_a?(Hash) &&
raw_params['deck'].key?('pc') &&
raw_params['deck'].key?('npc')
Rails.logger.error '[IMPORT] Deck data incomplete or missing.'
return render json: { error: 'Invalid deck data' }, status: :unprocessable_content
end
Rails.logger.info '[IMPORT] Starting import...'
return if performed? # Rendered an error response already
party = Party.create(user: current_user)
deck_data = raw_params['import']
process_data(party, deck_data)
render json: { shortcode: party.shortcode }, status: :created
rescue StandardError => e
render json: { error: e.message }, status: :unprocessable_content
end
def weapons
Rails.logger.info '[IMPORT] Checking weapon gamedata input...'
body = parse_request_body
return unless body
weapon = Weapon.find_by(granblue_id: body['id'])
unless weapon
Rails.logger.error "[IMPORT] Weapon not found with ID: #{body['id']}"
return render json: { error: 'Weapon not found' }, status: :not_found
end
lang = params[:lang]
unless %w[en jp].include?(lang)
Rails.logger.error "[IMPORT] Invalid language: #{lang}"
return render json: { error: 'Invalid language' }, status: :unprocessable_content
end
begin
weapon.update!(
"game_raw_#{lang}" => body.to_json
)
render json: { message: 'Weapon gamedata updated successfully' }, status: :ok
rescue StandardError => e
Rails.logger.error "[IMPORT] Failed to update weapon gamedata: #{e.message}"
render json: { error: e.message }, status: :unprocessable_content
end
end
def summons
Rails.logger.info '[IMPORT] Checking summon gamedata input...'
body = parse_request_body
return unless body
summon = Summon.find_by(granblue_id: body['id'])
unless summon
Rails.logger.error "[IMPORT] Summon not found with ID: #{body['id']}"
return render json: { error: 'Summon not found' }, status: :not_found
end
lang = params[:lang]
unless %w[en jp].include?(lang)
Rails.logger.error "[IMPORT] Invalid language: #{lang}"
return render json: { error: 'Invalid language' }, status: :unprocessable_content
end
begin
summon.update!(
"game_raw_#{lang}" => body.to_json
)
render json: { message: 'Summon gamedata updated successfully' }, status: :ok
rescue StandardError => e
Rails.logger.error "[IMPORT] Failed to update summon gamedata: #{e.message}"
render json: { error: e.message }, status: :unprocessable_content
end
end
##
# Updates character gamedata from JSON blob.
#
# @return [void] Renders JSON response with success or error message.
def characters
Rails.logger.info '[IMPORT] Checking character gamedata input...'
body = parse_request_body
return unless body
character = Character.find_by(granblue_id: body['id'])
unless character
Rails.logger.error "[IMPORT] Character not found with ID: #{body['id']}"
return render json: { error: 'Character not found' }, status: :not_found
end
lang = params[:lang]
unless %w[en jp].include?(lang)
Rails.logger.error "[IMPORT] Invalid language: #{lang}"
return render json: { error: 'Invalid language' }, status: :unprocessable_content
end
begin
character.update!(
"game_raw_#{lang}" => body.to_json
)
render json: { message: 'Character gamedata updated successfully' }, status: :ok
rescue StandardError => e
Rails.logger.error "[IMPORT] Failed to update character gamedata: #{e.message}"
render json: { error: e.message }, status: :unprocessable_content
end
end
private
##
# Ensures the current user has admin role (role 9).
# Renders an error if the user is not an admin.
#
# @return [void]
def ensure_admin_role
return if current_user&.role == 9
Rails.logger.error "[IMPORT] Unauthorized access attempt by user #{current_user&.id}"
render json: { error: 'Unauthorized' }, status: :unauthorized
end
##
# Reads and parses the raw JSON request body.
#
# @return [Hash] Parsed JSON data.
# @raise [JSON::ParserError] If the JSON is invalid.
def parse_request_body
raw_body = request.raw_post
JSON.parse(raw_body)
rescue JSON::ParserError => e
Rails.logger.error "[IMPORT] Invalid JSON: #{e.message}"
render json: { error: 'Invalid JSON data' }, status: :bad_request and return
end
##
# Ensures that the provided data is wrapped under an "import" key.
#
# @param data [Hash] The parsed JSON data.
# @return [Hash] Data wrapped under the "import" key.
def wrap_import_data(data)
data.key?('import') ? data : { 'import' => data }
end
##
# Processes the deck data using processors.
#
# @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'
Processors::JobProcessor.new(party, data).process
Processors::CharacterProcessor.new(party, data).process
Processors::SummonProcessor.new(party, data).process
Processors::WeaponProcessor.new(party, data).process
end
end
end
end