Parsing the WoW Armory with Rails

Comments

So, in the not-too-distant future, I'm going to need to tie this app I just deployed into a third-party API that exposes data via XML. In the interest of familiarizing myself with working with XML output from non-ActiveResource sources in Rails, I decided to use a publicly-accessible resource many of you may be familiar with: The World of Warcraft Armory. Since my actual source API isn't available yet, seems as good a choice as any. Plus, given the notorious unreliability of the armory, it also gave me a good chance to see how exception handling needs to be set up.

We want to be able to make a call like this:

    my_character = Character.new(:realm => "Realm Name",
                                 :name => "Charname",
                                 :armory_server => "us")

and then have any number of methods for getting at this data in different ways. Let's first have a look at the model.

app/models/character.rb:

    require 'net/http'

    class Character
      TAB_URLS = ["/character-sheet.xml", "/character-reputation.xml",
                  "/character-skills.xml", "/character-talents.xml",
                  "/character-arenateams.xml"]
      ARMORY_SERVERS = { "us" => "www.wowarmory.com",
                         "eu" => "eu.wowarmory.com" }
      attr_reader :rawdata

      def initialize(options)
        return nil unless options.has_key?(:realm) and options.has_key?(:name)
        options[:armory_server] ||= "us"
        @rawdata = Hash.new
        http = Net::HTTP.new(ARMORY_SERVERS[options[:armory_server]])
        http.open_timeout = 5
        http.read_timeout = 5
        realm = URI.escape(options[:realm])
        name = URI.escape(options[:name])
        TAB_URLS.each do |url|
          result = http.get("#{url}?r=#{realm}&n;=#{name}",
                            "User-Agent" => "Mozilla/5.0 Gecko/20070219 Firefox/2.0.0.2")
          @rawdata.merge! Hash.from_xml(result.body)['page']['characterInfo']
        end

      rescue Timeout::Error
        @rawdata["errCode"] = "connectionTimeout"

      rescue => e
        @rawdata["errCode"] = "armoryError: #{e.message}"

      ensure @rawdata.freeze
      end
    end

For brevity's sake, only the initialize method has been implemented, with one reader method to get at the collected data, which gets stored in an instance variable, @rawdata. It's pretty trivial to add additional readers from here, and any other methods you might want to to in order to get at different parts of the data. Anyway, here's the breakdown:

  • The armory only exposes certain parts of a character's data depending on the "tab" we're accessing, so we go ahead and preload them all, merging them into one, by iterating through the TAB_URLS array.
  • We set up a reasonable timeout for HTTP sessions, so that we don't end up with ridiculously long queries during the inevitable armory downtime.
  • We make sure to use URI.escape to ensure that server names like "Argent Dawn" get handled properly, along with all those funky accented characters that europeans and people on US servers who think they're extra clever (tip: they're not) use.
  • The armory is sometimes too smart for its own good. If you don't specify a User-Agent that it knows can interpret styled XML, it won't send results in XML format. We're spoofing Firefox 2, to make sure we get XML output.
  • Getting this data into a hash is really easy, thanks to the Hash.from_xml mixin provided by Rails. We pull from a couple levels deep in the data structure because "page" and "characterInfo" are common to all of the results, to strip those out of our merged hash.
  • IMPORTANT: If we don't specifically rescue Timeout::Error, an HTTP timeout will kill our process, booting us to a command prompt even if testing from an irb session, because Timeout::Error is an Interrupt, not a StandardError, which a normal rescue will catch.
  • Since the armory stores error codes (such as noCharacter, for when the character doesn't exist) in a standard location in the data structure already, we're opting to store our errors there in the event that we rescue one. Yes, this would overwrite the armory-generated error message, but in the event that we have errors thrown in our code, I'd rather see those anyway.

It's that easy! We can now do something like the call above, and once our code pulls down the character data, we can do stuff like:

    >> my_character.rawdata["character"]["name"]
    => "Character"
    >> my_character.rawdata["character"]["level"]
    => "70"
    >> my_character.rawdata["character"]["race"]
    => "Orc"
    >> my_character.rawdata["character"]["gender"]
    => "Male"
    >> my_character.rawdata["characterTab"]["pvp"]
    => {"lifetimehonorablekills"=>{"value"=>"19662"},
       "arenacurrency"=>{"value"=>"2066"}}
    >> my_character.rawdata["skillTab"]["skillCategory"][0]
    => {"name"=>"Professions", "skill"=>[
       {"name"=>"Enchanting", "max"=>"375", "value"=>"363", "key"=>"enchanting"},
       {"name"=>"Tailoring", "max"=>"375", "value"=>"375", "key"=>"tailoring"}
       ], "key"=>"professions"}

You get the idea. Anyway, from here, the sky's the limit. :)

Hope this helps someone out there!

comments powered by Disqus