Confounding URL typists since 2007.
At the company where I work, we use billing software called ICOMS that has been around for ages. This is, of course, because of the commonly-held belief that only old software can possibly be enterprise-class. Well, that, and the fact that once you get yourself tied to a billing vendor it is extremely expensive to migrate to another one. Anyway, this particular billing software does have an API. Well, that’s what they call it anyway. I’d call it something between a trip to the dentist’s office and having a live badger loosed in my pants. Oh, right. The title said something about customizing your XML serialization in Rails. Click the link.
So, consider for a moment the following XML representing an actual request to the existing API, to get the current bill for a customer.
version="1.0" encoding="UTF-8"?>
KEY="0000" USERID="username"
PASSWORD="password" ENVIRONMENT="ENV">
SLCTSITEID="001" SLCTACNTNMBR="123456789">
SLCTPRCSCNTRLNMBR="-1" SLCTSTMNTCODE="1">
/>
STMNTLINETYPE="TS"/>
TRVRSLEVL="3" SLCTTRNSCTNTYPE="A"/>
TRVRSLEVL="3" SLCTTRNSCTNTYPE="P"/>
TRVRSLEVL="3" SLCTTRNSCTNTYPE="R"/>
TRVRSLEVL="3" SLCTTRNSCTNTYPE="V"/>
TRVRSLEVL="3" SLCTTRNSCTNTYPE="Q"/>
TRVRSLEVL="3" SLCTTRNSCTNTYPE="O"/>
TRVRSLEVL="3" SLCTTRNSCTNTYPE="I"/>
/>
>
>
>
See, the ICOMS API works like this: There’s an API gateway server, and it takes these XML requests, which include login credentials and the intended ICOMS environment (large companies will use more than one to cover various areas where they provide service) that the request should be processed against. You can see those as attributes on the ICOMS element above. The ICOMS element will include a “macro,” which can contain one or more “inlines,” which can themselves contain one or more inlines, and so on. All of these elements will be passed any relevant parameters by way of attributes on the element. With me so far? Good.
So, as you can see, those MACnnnnn and INLnnnnn bits above seem awfully arbitrary, like something I may have just made up as a placeholder. I didn’t. That’s the actual way you make requests to the API gateway. Aside from a handful of now deprecated sanely-named elements, any useful naming convention has been abandoned and replaced by these TB (or “trouser badger”) elements, which must be cross-referenced in a 518-page PDF document to determine their intended function, what parameters they take, what elements they can contain, what elements they can return… You get the idea. You can see why I might want to create a simpler way to interact with the billing system.
So, the end goal is to write a RESTful API that actually accepts a request to something like http://billing-api-sans-badgers/customers/001-123456789/current_bill and takes care of the rest, returning a reasonable representation of a bill.
Step one in this master plan is to write something that will let us build up a request to the existing API gateway, then serialize to XML in a way that the gateway understands when we need to actually send the request. A first instinct about tackling this problem might be to create a nested hash structure that represents the data and then just use the handy Rails Hash#to_xml mixin. However, Hash#to_xml will just end up serializing all keys as elements, and we need most keys to be treated as attributes. This is a perfect time to get our hands dirty with the guts of to_xml, Builder::XmlMarkup.
So, we know we have three basic building blocks to create a request our API gateway will understand: the main ICOMS element, Macros, and Inlines. Following the Rails to_xml convention, it might make sense to create three classes to represent these elements, and give each its own to_xml method. The nice thing about handling things this way is that later, if we want to create some convenience methods for working with this data, they have their own namespace to exist in already, and we can use writer methods to enforce constraints as we build the request if we wish to. None of that’s necessary to illustrate today’s topic, though, so let’s keep it simple.
First, we’ll create a class that models the ICOMS element that wraps the request and contains our macros and inlines. We’ll call it Request.
class Request
attr_accessor :environment, :key, :userid, :password, :macro
def initialize(attributes)
= attributes[:environment]
= attributes[:key]
= attributes[:userid]
= attributes[:password]
if attributes.has_key?(:macro)
= Macro.new(*attributes.delete(:macro))
end
end
end
You’ll notice I added a convenience feature for initialization of the Request class, so that if a :macro “attribute” is passed in on initialization, an object of class Macro will be created with the contents of that “attribute” as initialization parameters. Of course, we now have to model the Macro element:
class Macro
attr_accessor :element, :attributes, :inlines
def initialize(element, attributes = {})
= element
= attributes || {}
= []
if att_inlines = self.attributes.delete(:inlines)
att_inlines.to_a.each do |inline|
<< Inline.new(*inline)
end
end
end
end
The only interesting deviation from our Request class is that we have an array called inlines, since a Macro can contain more than one inline. If we’re passed :inlines as an attribute, we delete it and instead populate the inlines array with Inline objects. Which brings us to the Inline class:
# Inlines can contain other inlines.
class Inline
attr_accessor :element, :attributes, :inlines
def initialize(element, attributes = {})
= element
= attributes || {}
= []
if att_inlines = self.attributes.delete(:inlines)
att_inlines.to_a.each do |inline|
<< Inline.new(*inline)
end
end
end
end
Lovely. We have now modeled what we know about a valid TB ICOMS API request. Of course, that doesn’t get us an XML version just yet.
Earlier, I mentioned we’d be using Builder::XmlMarkup to create our own custom to_xml methods for these spiffy new classes we’ve put together. If you’re not familiar with Builder::XmlMarkup, go ahead and check it out in the Rails API documentation now. While you’re at it, you may want to look at the example for overriding ActiveRecord’s to_xml method (it’s at the end). I’ll be here when you get back.
Back already? Great. Let’s get started.
First up, we’ll add a to_xml method to Request:
class Request
(...)
def to_xml(options = {})
options[:indent] ||= 2
xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
xml.instruct! unless options[:skip_instruct]
xml.ICOMS(:ENVIRONMENT => self.environment,
:KEY => self.key,
:USERID => self.userid,
:PASSWORD => self.password) {
self.macro.to_xml(options.merge!(:dasherize => false, :skip_instruct => true))
}
end
end
In Request, we know what attributes we have ahead of time, because there’s only one set of possibilities to deal with, so we call them by name. We build the ICOMS element along with those attributes, then ask our Macro to serialize itself inside the ICOMS element, being sure to add a couple of extra builder options, to prevent underscores from becoming dashes, and avoid a second XML instruction being added:
class Macro
(...)
def to_xml(options = {})
options[:indent] ||= 2
xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
xml.instruct! unless options[:skip_instruct]
if self.inlines.empty?
xml.tag!(self.element, self.attributes)
else
xml.tag!(self.element, self.attributes) {
self.inlines.each do |inline|
inline.to_xml(options.merge!(:dasherize => false, :skip_instruct => true))
end
}
end
end
end
With a Macro, we have one new thing to deal with. We may or may not have Inlines in the Macro, and we’d prefer a self-closing element if there are no Inlines to serialize. Builder::XmlMarkup will self-close the element if no block is passed, so that solves that.
Now, at this point, you might think we’d be going ahead with writing a to_xml for our Inline class. You’d be wrong, though. Why? Well, those readers who are more advanced in code fu and probably don’t need to read this write-up (but are doing so anyway because it beats actually working and reading about coding sort of counts as self-education even if you already knew what you’re reading about… right?) would have likely noticed a way to make this implementation more DRY way back when I described what a TB ICOMS API request looks like.
To recap:
Wait a minute. So, in an abstract sense, Macros are the same as Inlines! That would explain why the code for modeling and serializing them is exactly the same. So, rather than continue down this rather silly and repetitive path, let’s quickly refactor our code, and we’ll end up with an InlineContainer class, which both Macro and Inline derive from. This reduces our Macro and Inline classes down to:
class Macro < InlineContainer
end
class Inline < InlineContainer
end
Wow. That’s so zen. We’ll bundle it up into an Icoms namespace with a module, and here we have the finished product: ICOMS Request Generator
Now, if we decide to add methods to generate different types of Macros and Inlines with a more convenient syntax, we have a nice blank canvas to work with. Or, we might (I did, actually) choose to create an API model within our Icoms namespace to do the heavy lifting of creating different types of requests, tracking what server IP they should be submitted to, sending the HTTP request, and so on.
Either way, now that we’ve gotten a representation of the insane vendor-supplied API request out of the way that knows how to serialize itself, we’ve got one less badger in our pants.