I've finished the major work on the Quickbooks gem, and the best part is that it was quite a fun feat in meta-programming. In fact, it's meta-programming writing itself to file with some extra salt. I like to call it meta-meta-programming, although that term is rather difficult to define. To explain the process briefly, I first 1) made a ruby script and supporting library to read and parse the DTD, then 2) Created ruby classes with properties, using them more as a smart data structure, then 3) told each class and module to write itself to file. Since there are so many classes, they are all set to autoload.
There are two reasons I decided to go about it this way:
1) It's the easiest way. The Quickbooks XML specifications (QBXML) in DTD form reveal that there are a huge number of object types in Quickbooks, and many different property and value types. There are 1000+ object classes. I could make a way to work with them without classes, but every single object type has its own property list and validations. To record real knowledge of how the API works for each object class, I have to record the property list the way the DTD defines it, which then includes the property order and how to validate the object type. Also, since there are so many classes, I really didn't want to write them all out by hand and have the likelihood of mistakes either.
2) It's the most comprehensive way. Because the QuickBooks API's xml DTD specification clearly defines precisely the way the API works, if I can read that into ruby, then I've got myself the real thing. The "real thing" needs to understand how the API works, not just be capable of working with it. For an illustration, instead of learning a few words of a language, we need the gem to actually learn the constructs and full vocabulary of the language. Example: validations. You can't have a good QuickBooks gem without good working validations. Because this gem has all the information from the DTD's baked in in some form or another, the gem will tell you if anything's askew in your data, because it understands what properties must be present in any object when it's created and when it's updated, and even if you have conflicting properties defined.
But now how to make it work smoothly and intuitively for a ruby programmer. With all those complexities, I determined that the most accurate way to write out the 1000+ object classes was to parse the DTD and generate them, then write them to file.
1) Parse the DTD
This isn't too hard, but it isn't easy either. There are primarily two parts to parsing a DTD: reading entities and elements into objects, and parsing the Parsing Expression Grammer (PEG) from their definitions. In the end of parsing, a DTD object holds a whole bunch of elements and macros and types. The next step is to iterate over those and create ruby classes and modules out of each of them.
(
What I'm talking about is a PEG, which looks something like this: "(CompanyFilePath , HostInfo , (ListEvent | TxnEvent) , LastRestoreTime? , LastCondenseTime? , DataEventRecoveryTime?)" According to that expression, the CompanyFilePath and HostInfo are required, as well as either ListEvent or TxnEvent, and the rest are optional. The first step was to create a parser for those, and some of them are pretty complicated!)
2) Generate Ruby classes and modules from the DTD content
I made four "containers" for the different types of classes: Elements, Macros, Properties, and Types. Inside of each of these plural-named modules resides all of each type: 127 Macros, 14 Types, 435 Properties, and 850 Element classes, each with their own unique name. There are 3 for most of the element classes, for example: SalesOrderAddRq, SalesOrderModRq, and SalesOrderDelRq; so there are really only around 270 different object models. Each Element class simply sets the PEG Expression for that class, which specifies the class's elements and properties, as well as their validations, all in one go. When the class is instantiated, that expression is parsed, each element or property class is in turn instantiated, and any Macros are included and expanded into the expression string. So again, this PEG expression functions as both a property list and a validation specification. Properties such as addresses have embedded properties which must also validate (an address element/property has sub-elements like street, state, postal code). Many of the object types have even more complex validations. ListItems require either a ListID OR a FullName. In many cases you must have one of several properties, but only one. In other cases there are two possible groups of attributes, and you may have zero or more of the attributes in only one of those groups. Furthermore, you might be allowed either [both attributes A and B] or [one or more of another set of attributes]. Talk about complicated validations!
3) Write those classes and modules to file
After all of the DTD is translated into real classes and modules, I proceed to write them all to files. I've made a couple helpers that take care of simple things like creating the directory structure, determining the filename for each class/module, and wrapping the insides in the correct module and class structure. Some specific modules or parent classes get some static behavior added, which I write in a separate file and which gets included when the class is written to file. With all these classes written to file, each class, in a sense, knew "what" it was, but none of them knew what they could do or how to behave themselves. So I wrote in behavior in their super classes. Elements inherit from the Element class, Properties from the Property class, and Types from the Type class. Each type of object has its own behavior. I had to deduce rules from the structuring of the elements in the DTD that dictate how to classify each xml element as an Element, Property, or Type. Once each of these had behavior defined, I had basically created a class structure that brought life to the DTD's definitions. This is basically what the "QuickBooks Foundation Class (QBFC) Library API" provides.
4) Add behavior that bridges how you use QuickBooks to how you use Ruby
Up to this point, to continue the illustration of language, we have only the constructs and vocabulary of the language. The gem is usable, and you don't have to think at all about the QBXML, but you still need to know how the QuickBooks API works. Fortunately for you, that is no longer the case. I've added a layer of behavior on top of all of those elements, object classes, etc that allows you to
work with the data the way you
think about the data, instead of having to think about
communicating about the data with QuickBooks. No other QuickBooks rubygem does this for you.
End Result
At the end I have a library of ruby classes and modules that work together exactly as the DTD dictates, and seasoned with some extra behavior such as to serialize the objects to xml and to connect with Quickbooks and operate the API communication. You don't need to know anything more about QuickBooks than you can pick up by simply using QuickBooks. It's actually quite fun to operate QuickBooks from the irb ruby console! After all the work I've done coding in an understanding of the API, 3 months later it still feels like magic!
The QuickBooks rubygem
Now it's time for a shameless plug. I've spent many an hour creating this QuickBooks rubygem, and it's now available for a fee. I ASSURE you it'll save countless hours of your own time trying to figure out QuickBooks, not only because there is a lot to the API, but because
there is a LOT to the API and it is hard to comprehend! And because this gem brings the workings of QuickBooks into the "ruby way" of doing things.
Some Example Code
# Changing the name of a Customer.
# QB is a synonym for QuickBooks::Models.
# FullName is a collection attribute -- you can include several values, so we must use an array.
tom = QB::Customer.first(:FullName => ["Bombardi, Thomas"])
tom[:FirstName] = "Tom"
tom[:FullName] = "Tom Bombardi"
tom.save
# Creating a new SalesOrder.
so = QB::SalesOrder.new(:TxnDate => Date.today, :RefNumber => '12345678901') # QB is a shortcut for Quickbooks::Models, which contains all the object classes
so[:CustomerRef] = {:FullName => "Tom Bombardi"}
so[:CustomerRef] = tom.to_ref # same as above
so[:BillAddress] = Quickbooks::Elements::BillAddress.new(:Addr1 => '999 Some Rd', :City => 'Other City', :State => 'TX', :PostalCode => '88888')
so[:BillAddress] = {:Addr1 => '999 Some Rd', :City => 'Other City', :State => 'TX', :PostalCode => '88888'} # same as above
so[:ShipAddress] = {:Addr1 => '999 Some Rd', :City => 'Other City', :State => 'TX', :PostalCode => '88888'}
so[:PONumber] = '99999-111-ABCDE-00000'
so.attributes = {:ShipDate => Date.today, :Memo => 'This is a test SalesOrder Add request.', :IsToBePrinted => true} # just a shortcut for setting each one individually
so.attributes = {:IsToBeEmailed => false, :CustomerSalesTaxCodeRef => {:ListID => '3987235'}}
# SalesOrderLine is a collection attribute. You can either use = to set an array of items, or just use << to append to an initially empty collection.
so[:SalesOrderLine] << {:ItemRef => {:ListID => '8887778'}, :Desc => 'Some odd item', :Quantity => 1, :Rate => '25.00', :Amount => '25.00'} # notice the embedded ItemRef object with its own properties.
so[:SalesOrderLine] << {:ItemRef => {:ListID => '8887779'}, :Desc => 'Some other odd item', :Quantity => 2, :Rate => '21.00', :Amount => '42.00'} # here we add a second line-item
so.save
To obtain this gem
http://www.behindlogic.com/, or email me:
gems [at] behindlogic.com