Create endpoint to show all species appearances for a given color

Building toward replacing more of the 2020 data sources! I think this is
an endpoint that benefits from bulk loading, esp with the way the item
page previews work. I also like taking the concept of "canonical" out of
the GQL interface, and instead just loading for each of the 50 species
and letting the client decide. (And then it can fast-swap between them!)
This commit is contained in:
Emi Matchu 2023-12-05 18:04:54 -08:00
parent c8de3dae63
commit a656cd511a
6 changed files with 70 additions and 91 deletions

View file

@ -1,4 +1,37 @@
class PetTypesController < ApplicationController class PetTypesController < ApplicationController
def index
color = Color.find params[:color_id]
pet_types = color.pet_types.includes(pet_states: [:swf_assets]).
includes(species: [:translations])
# This is a relatively big request, for relatively static data. Let's
# consider it fresh for 10min (so new pet releases show up quickly), but
# it's also okay for the client to quickly serve the cached copy then
# reload in the background, if it's been less than a day.
expires_in 10.minutes, stale_while_revalidate: 1.day, public: true
# We dive deep to get all the right fields for appearance data, cuz this
# endpoint is primarily used to power the preview on the item page!
render json: pet_types.map { |pt|
pt.as_json(
only: [:id, :body_id],
include: {
species: {only: [:id], methods: [:name, :human_name]},
canonical_pet_state: {
only: [:id],
methods: [:pose],
include: {
swf_assets: {
only: [:id, :known_glitches],
methods: [:zone, :restricted_zones, :urls],
},
},
},
},
)
}
end
def show def show
@pet_type = PetType. @pet_type = PetType.
where(species_id: params[:species_id]). where(species_id: params[:species_id]).

View file

@ -1,5 +1,6 @@
class Color < ApplicationRecord class Color < ApplicationRecord
translates :name translates :name
has_many :pet_types
scope :alphabetical, -> { scope :alphabetical, -> {
ct = Color::Translation.arel_table ct = Color::Translation.arel_table
@ -24,7 +25,7 @@ class Color < ApplicationRecord
end end
def as_json(options={}) def as_json(options={})
{id: id, name: human_name, unfunny_name: unfunny_human_name, prank: prank?} {id: id, name: name, human_name: human_name}
end end
def human_name def human_name

View file

@ -25,7 +25,7 @@ class PetState < ApplicationRecord
} }
# Filter pet states using the "pose" concept we use in the editor. # Filter pet states using the "pose" concept we use in the editor.
scope :with_pose, ->(pose) { scope :with_pose, -> pose {
case pose case pose
when "UNCONVERTED" when "UNCONVERTED"
where(unconverted: true) where(unconverted: true)
@ -71,16 +71,6 @@ class PetState < ApplicationRecord
end end
end end
def as_json(options={})
{
id: id,
gender_mood_description: gender_mood_description,
swf_asset_ids: swf_asset_ids_array,
artist_name: artist_name,
artist_url: artist_url
}
end
def reassign_children_to!(main_pet_state) def reassign_children_to!(main_pet_state)
self.contributions.each do |contribution| self.contributions.each do |contribution|
contribution.contributed = main_pet_state contribution.contributed = main_pet_state
@ -127,47 +117,6 @@ class PetState < ApplicationRecord
rel.save! rel.save!
end end
end end
def mood
Mood.find(self.mood_id)
end
def gender_name
if female?
I18n.translate("pet_states.description.gender.female")
else
I18n.translate("pet_states.description.gender.male")
end
end
def mood_name
I18n.translate("pet_states.description.mood.#{mood.name}")
end
def gender_mood_description
if glitched?
I18n.translate('pet_states.description.glitched')
elsif unconverted?
I18n.translate('pet_states.description.unconverted')
elsif labeled?
I18n.translate('pet_states.description.main', :gender => gender_name,
:mood => mood_name)
else
I18n.translate('pet_states.description.unlabeled')
end
end
def artist_name
artist_neopets_username || I18n.translate("pet_states.default_artist_name")
end
def artist_url
if artist_neopets_username
"https://www.neopets.com/userlookup.phtml?user=#{artist_neopets_username}"
else
nil
end
end
def self.from_pet_type_and_biology_info(pet_type, info) def self.from_pet_type_and_biology_info(pet_type, info)
swf_asset_ids = [] swf_asset_ids = []
@ -223,31 +172,5 @@ class PetState < ApplicationRecord
pet_state.unconverted = (relationships.size == 1) pet_state.unconverted = (relationships.size == 1)
pet_state pet_state
end end
# Copied from https://github.com/matchu/neopets/blob/5d13a720b616ba57fbbd54541f3e5daf02b3fedc/lib/neopets/pet/mood.rb
class Mood
attr_reader :id, :name
def initialize(options)
@id = options[:id]
@name = options[:name]
end
def self.find(id)
self.all_by_id[id.to_i]
end
def self.all
@all ||= [
Mood.new(:id => 1, :name => :happy),
Mood.new(:id => 2, :name => :sad),
Mood.new(:id => 4, :name => :sick)
]
end
def self.all_by_id
@all_by_id ||= self.all.inject({}) { |h, m| h[m.id] = m; h }
end
end
end end

View file

@ -59,15 +59,10 @@ class PetType < ApplicationRecord
end end
def as_json(options={}) def as_json(options={})
if options[:for] == 'wardrobe' super({
{ only: [:id],
:id => id, methods: [:color, :species]
:body_id => body_id, }.merge(options))
:pet_states => pet_states.emotion_order.as_json
}
else
{:image_hash => image_hash}
end
end end
def image_hash def image_hash
@ -157,6 +152,31 @@ class PetType < ApplicationRecord
end end
end end
def canonical_pet_state
# For consistency (randomness is always scary!), we use the PetType ID to
# determine which gender to prefer. That way, it'll be stable, but we'll
# still get the *vibes* of uniform randomness.
preferred_gender = id % 2 == 0 ? :fem : :masc
# NOTE: If this were only being called on one pet type at a time, it would
# be more efficient to send this as a single query with an `order` part and
# just get the first record. But we most importantly call this on all the
# pet types for a single color at once, in which case it's better for the
# caller to use `includes(:pet_states)` to preload the pet states then sort
# then in Ruby here, rather than send ~50 queries. Also, there's generally
# very few pet states per pet type, so the perf difference is negligible.
pet_states.sort_by { |pet_state|
gender = pet_state.female? ? :fem : :masc
[
pet_state.mood_id.present? ? -1 : 1, # Prefer mood is labeled
pet_state.mood_id, # Prefer mood is happy, then sad, then sick
gender == preferred_gender ? -1 : 1, # Prefer our "random" gender
-pet_state.id, # Prefer newer pet states
!pet_state.glitched? ? -1 : 1, # Prefer is not glitched
]
}.first
end
def self.all_by_ids_or_children(ids, pet_states) def self.all_by_ids_or_children(ids, pet_states)
pet_states_by_pet_type_id = {} pet_states_by_pet_type_id = {}
pet_states.each do |pet_state| pet_states.each do |pet_state|

View file

@ -715,7 +715,6 @@ en:
glitched: Glitched glitched: Glitched
unconverted: Unconverted unconverted: Unconverted
unlabeled: Unlabeled unlabeled: Unlabeled
default_artist_name: the OpenNeo team
pet_types: pet_types:
human_name: "%{color_human_name} %{species_human_name}" human_name: "%{color_human_name} %{species_human_name}"

View file

@ -23,11 +23,14 @@ OpenneoImpressItems::Application.routes.draw do
get :needed get :needed
end end
end end
resources :species do resources :species, only: [] do
resources :colors do resources :colors, only: [] do
get :pet_type, to: 'pet_types#show' get :pet_type, to: 'pet_types#show'
end end
end end
resources :colors, only: [] do
resources :pet_types, only: [:index]
end
# Loading and modeling pets! # Loading and modeling pets!
post '/pets/load' => 'pets#load', :as => :load_pet post '/pets/load' => 'pets#load', :as => :load_pet