Skip to content

Commit

Permalink
New gem being tested.
Browse files Browse the repository at this point in the history
git-svn-id: http://quickbooks.rubyforge.org/svn@15 0eb1a872-461d-0410-a379-984e9e9c21cf
  • Loading branch information
dcparker committed Mar 17, 2008
1 parent 8ea5dae commit 83415a5
Show file tree
Hide file tree
Showing 17 changed files with 696 additions and 529 deletions.
2 changes: 1 addition & 1 deletion trunk/Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ require 'rake/gempackagetask'
require 'rake/contrib/rubyforgepublisher'

PKG_NAME = 'quickbooks'
PKG_VERSION = "0.0.5"
PKG_VERSION = "0.0.7"

PKG_FILES = FileList[
"lib/**/*", "rspec/**/*", "[A-Z]*", "Rakefile", "doc/**/*"
Expand Down
2 changes: 2 additions & 0 deletions trunk/lib/qbxml.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
require 'qbxml/request'
require 'qbxml/response'
164 changes: 164 additions & 0 deletions trunk/lib/qbxml/request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
require 'qbxml/support/core_ext'
gem 'builder', '=2.1.2'
require 'builder'

module Qbxml
VERSION = '6.0'

# module RequestSetArrayExt
# end

class RequestSet
include Enumerable
def set
unless @set.is_a?(Array)
@set = []
# @set.extend(Qbxml::RequestSetArrayExt)
end
@set
end
delegate_methods [:each, :length, :first, :last, :[], :map, :join] => :set
def <<(qbxml_request)
if qbxml_request.is_a?(Qbxml::Request)
set << qbxml_request
elsif qbxml_request.respond_to?(:each)
qbxml_request.each do |request|
self << request
end
else
raise ArgumentError, "Cannot add object of type #{qbxml_request.class.name} to a Qbxml::RequestSet"
end
end

def initialize(*requests)
self << requests
end

def to_xml
pre = <<-thequickbooks_qbxmlrequestsetxml
<?xml version="1.0" ?>
<?qbxml version="#{Qbxml::VERSION}" ?>
<QBXML>
<QBXMLMsgsRq onError="stopOnError">
thequickbooks_qbxmlrequestsetxml
requests = map {|x| x.to_xml(false)}.join
post = <<-thequickbooks_qbxmlrequestsetxml
</QBXMLMsgsRq>
</QBXML>
thequickbooks_qbxmlrequestsetxml
# puts pre + requests + post
pre + requests + post
end
end

class Request
# 1. List queries (:query)
# Request.new(object, :query) # <= will return the record matching the object supplied (automatically searches by list_id or txn_id)
# Request.new(Customer, :query, :limit => 1) # <= will return the first customer
# Request.new(Customer, :query, /some match/) # <= will return all customers matching the regexp
# 2. Object-specific transaction query (:transaction)
# Request.new(Transaction, :query) # <= will return the transaction matching the object supplied
# 3. Mod requests (:mod)
# Request.new(object, :mod) # <= will update the object
# 4. Delete requests (:delete)
# Request.new(object, :delete) # <= will delete the object
# We want the attributes of the object when we are updating, but no other time.
def initialize(object, type, options_or_regexp={})
options = options_or_regexp.is_a?(Regexp) ? {:matches => options_or_regexp} : options_or_regexp
@type = type
raise ArgumentError, "Qbxml::Requests can only be of one of the following types: :query, :transaction, :any_transaction, :mod, :add, :delete, or :report" unless @type.is_one_of?(:query, :transaction, :any_transaction, :mod, :report, :add, :delete)
@klass = object.is_a?(Class) ? object : object.class
@object = object
@options = options

# Return only specific properties: Request.new(Customer, :query, :only => [:list_id, :full_name]); Quickbooks::Customer.first(:only => :list_id)
@ret_elements = @options.delete(:only).to_a.only(@klass.properties).order!(@klass.properties).stringify_values.camelize_values!(Quickbooks::CAMELIZE_EXCEPTIONS) if @options.has_key?(:only)

# Includes only valid filters + aliases for valid filters, then transforms aliased filters to real filters, then camelizes keys to prepare for writing to XML, lastly orders the keys to a valid filter order.
# You may optionally have ListID OR FullName OR ( MaxReturned AND ActiveStatus AND FromModifiedDate AND ToModifiedDate AND ( NameFilter OR NameRangeFilter ) )
@filters = options.stringify_keys.only(@klass.valid_filters + @klass.filter_aliases.keys).transform_keys!(@klass.filter_aliases).camelize_keys!(Quickbooks::CAMELIZE_EXCEPTIONS).order!(@klass.camelized_valid_filters)

# Complain if:
# 1) type is :mod or :delete, and object supplied is not a valid model
raise ArgumentError, "A Quickbooks record object must be supplied to perform an add, mod or del action" if @type.is_one_of?(:add, :mod, :delete) && !@object.is_a?(Quickbooks::Base)
end

def self.next_request_id
@request_id ||= 0
@request_id += 1
end

# This is where the magic happens to convert a request object into xml worthy of quickbooks.
def to_xml(as_set=true)
return (RequestSet.new(self)).to_xml if as_set # Simple call yields xml as a single request in a request set. However, if the xml for the lone request is required, pass false.
req = Builder::XmlMarkup.new(:indent => 2)
request_root, container = case
when @type.is_one_of?(:query)
["#{@klass.class_leaf_name}QueryRq", nil]
when @type == :add
["#{@klass.class_leaf_name}AddRq", "#{@klass.class_leaf_name}Add"]
when @type == :mod
["#{@klass.class_leaf_name}ModRq", "#{@klass.class_leaf_name}Mod"]
when @type == :delete
["#{@klass.ListOrTxn}DelRq", nil]
else
raise RuntimeError, "Could not convert this request to qbxml!\n#{self.inspect}"
end
inner_stuff = lambda {
deep_tag = lambda {|k,v|
if v.is_a?(Hash)
if k == ''
v.each { |k,v|
deep_tag.call(k,v)
}
else
req.tag!(k.camelize) { v.each { |k,v| deep_tag.call(k,v) } }
end
else
req.tag!(k.camelize,uncast(v))
end
}

# Add the specific elements for the respective request type
if @type.is_one_of?(:add, :mod)
if @type == :mod
# First the ObjectId:
req.tag!(@klass.ListOrTxn + 'ID', @object.send("#{@klass.ListOrTxn}Id".underscore))
# Second the EditSequence
req.tag!('EditSequence', @object.send(:edit_sequence))
end
# Then, all the dirty_attributes
deep_tag.call('',@object.to_dirty_hash) # (this is an hash statically ordered to the model's qbxml attribute order)
elsif @type == :query && @object.class == @klass
# Sent an instance object for a query - we should include the ListId/TxnId (then other filters?)
req.tag!(@klass.ListOrTxn + 'ID', @object.send("#{@klass.ListOrTxn}Id".underscore))
deep_tag.call('', @filters)
elsif @type == :delete
req.tag!(@klass.ListOrTxn + 'DelType', @klass.class_leaf_name)
req.tag!(@klass.ListOrTxn + 'ID', @object.send("#{@klass.ListOrTxn}Id".underscore))
else
# just filters
deep_tag.call('', @filters)
end
# Lastly, specify the fields to return, if desired
@ret_elements.each { |r| req.tag!('IncludeRetElement', r) } if !@ret_elements.blank?
}
req.tag!(request_root, :requestID => self.class.next_request_id) {
if container
req.tag!(container) {
inner_stuff.call
}
else
inner_stuff.call
end
}
# puts req.target!
req.target!
end

private
def uncast(v)
v.is_a?(Time) ? v.xmlschema : v
end
end
end
185 changes: 185 additions & 0 deletions trunk/lib/qbxml/response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
require 'qbxml/support/core_ext'
gem 'formattedstring'
require 'formatted_string'

module Qbxml
# module ResponseSetArrayExt
# end

class ResponseSet
include Enumerable
def set
unless @set.is_a?(Array)
@set = []
# @set.extend(Qbxml::ResponseSetArrayExt)
end
@set
end
delegate_methods [:each, :length, :first, :last, :[], :map, :join] => :set
def <<(qbxml_response)
if qbxml_response.is_a?(Qbxml::Response)
set << qbxml_response
elsif qbxml_response.respond_to?(:each)
qbxml_response.each do |response|
self << response
end
else
raise ArgumentError, "Cannot add object of type #{qbxml_response.class.name} to a Qbxml::ResponseSet"
end
end

def initialize(xml_or_hash)
if(xml_or_hash.is_a?(Hash))
self.append_from_hash(xml_or_hash)
elsif(xml_or_hash.is_a?(String))
self.append_from_xml(xml_or_hash)
else
raise ArgumentError, "Qbxml::ResponseSet must be initialized with either a Hash or an xml-formatted String."
end
end

def append_from_xml(xml)
self.append_from_hash(xml.formatted(:xml).to_hash)
end
def append_from_hash(hsh)
to_append = []
hsh = hsh['QBXML'] if hsh.has_key?('QBXML')
hsh = hsh['QBXMLMsgsRs'] if hsh.has_key?('QBXMLMsgsRs')
# responses will contain one or more keys.
hsh.each_key do |name|
# response_type is either a single response object, or an array of response objects. Force it into an array:
responses = hsh[name].is_a?(Array) ? hsh[name] : [hsh[name]]
responses.each { |response| to_append << Response.new(name => response) }
end
self << to_append
end

class << self
def from_xml(xml)
new.append_from_xml(xml)
end
def from_hash(hsh)
new.append_from_hash(hsh)
end
end
end

class Response
attr_accessor :response_type, :status, :message, :severity, :ret_items
# For Development purposes:
attr_accessor :raw_response

def initialize(xml_or_hash)
if(xml_or_hash.is_a?(Hash))
self.import_from_hash(xml_or_hash)
elsif(xml_or_hash.is_a?(String))
self.import_from_xml(xml_or_hash)
else
raise ArgumentError, "Qbxml::ResponseSet must be initialized with either a Hash or an xml-formatted String."
end
end

def import_from_xml(xml)
self.import_from_hash(xml.formatted(:xml).to_hash)
self
end
def import_from_hash(hsh)
raise ArgumentError, "Hash passed to Qbxml::Response.from_hash must contain only one top-level key" unless hsh.keys.length == 1
name = hsh.keys.first
# for development
self.raw_response = hsh
# * * * *
hsh = hsh[name]

self.status = hsh['statusCode'].to_i
self.severity = hsh['statusSeverity']
self.message = hsh['statusMessage']
# if self.status == 0 # Status is good, proceed with eating the request.
if m = name.match(/^(List|Txn)Del(etedQuery)?Rs$/)
# (List|Txn)DelRs, or (List|Txn)DeletedQueryRs - both return just a few attributes, like ListID / TxnID and TimeDeleted
self.response_type = hsh.delete(m[1]+'DelType')
# self.ret_items = ResponseObject.new(self.response_type, hsh.dup)
self.ret_items = hsh.dup
elsif m = name.match(/^([A-Za-z][a-z]+)(Query|Mod|Add)Rs$/)
self.response_type = m[1]
self.ret_items = hsh[self.response_type+'Ret']
else
raise "Could not read this response:\n#{self.raw_response.inspect}"
end
# else # Status is bad.

# end
# puts self.inspect
self
end


class << self
def from_xml(xml)
new.import_from_xml(xml)
end
def from_hash(hsh)
new.import_from_hash(hsh)
end
end

def instantiatable? # Just checks to see if it can create an object of this type, and if there is actually data to instantiate
self.response_type ? (Quickbooks.const_defined?(self.response_type) && self.ret_items.is_a?(Hash) || (self.ret_items.is_a?(Array) && self.ret_items[0].is_a?(Hash))) : false
end
def success?
self.status == 0
end
def error?
!success?
end

def instantiate(reinstantiate=nil)
objs = []
(self.ret_items.is_a?(Array) ? self.ret_items : [self.ret_items]).each do |ret_item|
obj = reinstantiate if reinstantiate
if instantiatable?
if self.status == 0
if obj.nil?
obj = "Quickbooks::#{self.response_type}".constantize.instantiate(ret_item)
else
obj.class.instantiate(obj, ret_item)
end
else
# Instantiatable, but some error status. For any error status
if obj.nil? # Must be a new object. Just as well, create a new object WITHOUT any original_attributes.
obj = "Quickbooks::#{self.response_type}".constantize.new(ret_item)
else # An existing object. Must be an update or delete request. Update object's original_attributes.
updated = []
ret_item.each do |k,v|
k = k.underscore
if obj.original_values[k] != v
updated << k
if obj.class.instance_variable_get('@object_properties').has_key?(k.to_sym)
obj.original_values[k] = obj.class.instance_variable_get('@object_properties')[k.to_sym].new(v)
else
obj.original_values[k] = v
end
end
end
# Update object's 'read-only' attributes (as well as in the original_values):
obj.class.read_only.stringify_values.camelize_values.each do |k|
obj.send(k.underscore + '=', ret_item[k]) if ret_item.has_key?(k) && obj.respond_to?(k.underscore + '=')
end
self.message = self.message + " Other properties were out of date: [" + updated.join(', ') + '].'
end
obj.errors << {:code => self.status, :message => self.message, :response => self}
end
else
if self.status == 0
obj.errors << {:code => nil, :message => "Cannot instantiate object of type #{self.response_type}!", :response => self}
else
obj.errors << {:code => self.status, :message => self.message, :response => self}
end
end
obj.response_log << self unless obj.nil?
objs << obj
end
objs.length > 1 ? objs : objs[0] # Single or nil if not more than one.
end
end
end
Loading

0 comments on commit 83415a5

Please sign in to comment.