quickbooks_api 0.0.7 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ require 'buffered_logger'
2
+
3
+ module Quickbooks::Logger
4
+ def log
5
+ Quickbooks::Log.log
6
+ end
7
+ end
8
+
9
+ class Quickbooks::Log
10
+ private_class_method :new
11
+ LOG_LEVEL = 1
12
+
13
+ def self.init(log_level)
14
+ @log = BufferedLogger.new(STDOUT, log_level || LOG_LEVEL)
15
+ end
16
+
17
+ def self.log
18
+ @log ||= BufferedLogger.new(STDOUT, LOG_LEVEL)
19
+ end
20
+ end
@@ -1,4 +1,4 @@
1
- module Quickbooks::Support::ClassBuilder
1
+ module Quickbooks::Parser::ClassBuilder
2
2
 
3
3
  private
4
4
 
@@ -10,13 +10,13 @@ def add_strict_attribute(klass, attr_name, type)
10
10
  attr_accessor :#{attr_name}
11
11
 
12
12
  def #{attr_name}=(obj)
13
- expected_type = self.class.#{attr_name}_type
14
- case obj
15
- when expected_type, Array
16
- @#{attr_name} = obj
17
- else
18
- raise(TypeError, "expecting an object of type \#{expected_type}")
13
+ if self.respond_to?("#{attr_name}_type")
14
+ expected_type = self.class.#{attr_name}_type
15
+ unless obj.is_a?(expected_type) || obj.is_a?(Array)
16
+ raise(TypeError, "expecting an object of type \#{expected_type}")
17
+ end
19
18
  end
19
+ @#{attr_name} = obj
20
20
  end
21
21
  end
22
22
  class_body
@@ -0,0 +1,128 @@
1
+ # inheritance base for schema classes
2
+ class Quickbooks::Parser::QbxmlBase
3
+ include Quickbooks::Logger
4
+ extend Quickbooks::Logger
5
+ include Quickbooks::Parser::XMLGeneration
6
+
7
+ QBXML_BASE = Quickbooks::Parser::QbxmlBase
8
+
9
+ FLOAT_CAST = lambda {|d| d ? Float(d) : 0.0}
10
+ BOOL_CAST = lambda {|d| d ? (d == 'True' ? true : false) : false }
11
+ DATE_CAST = lambda {|d| d ? Date.parse(d).xmlschema : Date.today.xmlschema }
12
+ TIME_CAST = lambda {|d| d ? Time.parse(d).xmlschema : Time.now.xmlschema }
13
+ INT_CAST = lambda {|d| d ? Integer(d.to_i) : 0 }
14
+ STR_CAST = lambda {|d| d ? String(d) : ''}
15
+
16
+ QB_TYPE_CONVERSION_MAP= {
17
+ "AMTTYPE" => FLOAT_CAST,
18
+ #"BOOLTYPE" => BOOL_CAST,
19
+ "BOOLTYPE" => STR_CAST,
20
+ "DATETIMETYPE" => TIME_CAST,
21
+ "DATETYPE" => DATE_CAST,
22
+ "ENUMTYPE" => STR_CAST,
23
+ "FLOATTYPE" => FLOAT_CAST,
24
+ "GUIDTYPE" => STR_CAST,
25
+ "IDTYPE" => STR_CAST,
26
+ "INTTYPE" => INT_CAST,
27
+ "PERCENTTYPE" => FLOAT_CAST,
28
+ "PRICETYPE" => FLOAT_CAST,
29
+ "QUANTYPE" => INT_CAST,
30
+ "STRTYPE" => STR_CAST,
31
+ "TIMEINTERVALTYPE" => STR_CAST
32
+ }
33
+
34
+ attr_accessor :xml_attributes
35
+ class << self
36
+ attr_accessor :xml_attributes
37
+ end
38
+
39
+ def initialize(params = nil)
40
+ return unless params.is_a?(Hash)
41
+ @xml_attributes = params[:xml_attributes] || {}
42
+ params.delete(:xml_attributes)
43
+
44
+ params.each do |attr, value|
45
+ if self.respond_to?(attr)
46
+ expected_attr_type = self.class.send("#{attr}_type")
47
+ value = \
48
+ case value
49
+ when Hash
50
+ expected_attr_type.new(value)
51
+ when Array
52
+ value.inject([]) { |a,i| a << expected_attr_type.new(i) }
53
+ else value
54
+ end
55
+ self.send("#{attr}=", value)
56
+ else
57
+ log.info "Warning: instance #{self} does not respond to attribute #{attr}"
58
+ end
59
+ end
60
+ end
61
+
62
+ def self.attribute_names
63
+ instance_methods(false).reject { |m| m == "xml_attributes" || m[-1..-1] == '=' || m =~ /_xml_class/}
64
+ end
65
+
66
+ # returns innermost attributes without outer layers of the hash
67
+ #
68
+ def inner_attributes(parent = nil)
69
+ attrs = attributes(false)
70
+ attrs.delete(:xml_attributes)
71
+ values = attrs.values.compact
72
+
73
+ if values.empty?
74
+ attributes
75
+ elsif values.first.is_a?(Array)
76
+ attributes
77
+ elsif values.size > 1
78
+ parent.attributes
79
+ else
80
+ first_val = values.first
81
+ if first_val.respond_to?(:inner_attributes)
82
+ first_val.inner_attributes(self)
83
+ else
84
+ parent.attributes
85
+ end
86
+ end
87
+ end
88
+
89
+ def attributes(recursive = true)
90
+ attrs = {}
91
+ attrs[:xml_attributes] = xml_attributes
92
+ self.class.attribute_names.inject(attrs) do |h, m|
93
+ val = self.send(m)
94
+ if val
95
+ if recursive
96
+ h[m] = case val
97
+ when QBXML_BASE
98
+ val.attributes
99
+ when Array
100
+ val.inject([]) { |a, obj| obj.is_a?(QBXML_BASE) ? a << obj.attributes : a << obj }
101
+ else val
102
+ end
103
+ else
104
+ h[m] = val
105
+ end
106
+ end; h
107
+ end
108
+ end
109
+
110
+ # returns a type map of the object's attributes
111
+ #
112
+ def self.template(recursive = false, reload = false)
113
+ if recursive
114
+ @template = (!reload && @template) || build_template(true)
115
+ else build_template(false)
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def self.build_template(recursive = false)
122
+ attribute_names.inject({}) do |h, a|
123
+ attr_type = self.send("#{a}_type")
124
+ h[a] = ((attr_type < QBXML_BASE) && recursive) ? attr_type.build_template(true): attr_type.to_s; h
125
+ end
126
+ end
127
+
128
+ end
@@ -0,0 +1,52 @@
1
+ module Quickbooks::Parser::XMLGeneration
2
+ include Quickbooks::Parser
3
+ include Quickbooks::Parser::XMLParsing
4
+ include Quickbooks::Support::Inflection
5
+
6
+ def to_qbxml
7
+ xml_doc = Nokogiri::XML(self.class.xml_template)
8
+ root = xml_doc.root
9
+ log.debug "to_qbxml#nodes_size: #{root.children.size}"
10
+
11
+ # replace all children nodes of the template with populated data nodes
12
+ xml_nodes = []
13
+ root.children.each do |xml_template|
14
+ next unless xml_template.is_a? XML_ELEMENT
15
+ attr_name = underscore(xml_template)
16
+ log.debug "to_qbxml#attr_name: #{attr_name}"
17
+
18
+ val = self.send(attr_name)
19
+ next unless val && val.not_blank?
20
+
21
+ xml_nodes += build_qbxml_nodes(xml_template, val)
22
+ log.debug "to_qbxml#val: #{val}"
23
+ end
24
+
25
+ log.debug "to_qbxml#xml_nodes_size: #{xml_nodes.size}"
26
+ root.children = xml_nodes.join('')
27
+ set_xml_attributes!(root)
28
+ root.to_s
29
+ end
30
+
31
+ private
32
+
33
+ def build_qbxml_nodes(node, val)
34
+ val = [val].flatten
35
+ val.inject([]) do |a, v|
36
+ a << case v
37
+ when QbxmlBase
38
+ v.to_qbxml
39
+ else
40
+ n = node.clone
41
+ n.children = val.to_s
42
+ n
43
+ end
44
+ end
45
+ end
46
+
47
+ def set_xml_attributes!(node)
48
+ node.attributes.each { |name, value| node.remove_attribute(name) }
49
+ self.xml_attributes.each { |a,v| node.set_attribute(a, v) }
50
+ end
51
+
52
+ end
@@ -1,4 +1,6 @@
1
- module Quickbooks::Support::QBXML
1
+ require 'nokogiri'
2
+
3
+ module Quickbooks::Parser::XMLParsing
2
4
 
3
5
  XML_DOCUMENT = Nokogiri::XML::Document
4
6
  XML_NODE_SET = Nokogiri::XML::NodeSet
@@ -11,15 +13,6 @@ module Quickbooks::Support::QBXML
11
13
  COMMENT_END = "-->"
12
14
  COMMENT_MATCHER = /\A#{COMMENT_START}.*#{COMMENT_END}\z/
13
15
 
14
-
15
- def is_leaf_node?(xml_obj)
16
- xml_obj.children.size == 1 && xml_obj.children.first.class == XML_TEXT
17
- end
18
-
19
- def qbxml_class_defined?(name)
20
- get_schema_namespace.constants.include?(name)
21
- end
22
-
23
16
  # remove all comment lines and empty nodes
24
17
  def cleanup_qbxml(qbxml)
25
18
  qbxml = qbxml.split('\n')
@@ -28,16 +21,19 @@ module Quickbooks::Support::QBXML
28
21
  qbxml.join('')
29
22
  end
30
23
 
31
- def set_required_attributes(xml_obj)
32
- required_attributes = get_required_xml_attributes
33
- xml_obj.attributes.each do |a,v|
34
- if required_attributes.keys.include?(a)
35
- xml_obj.set_attribute(a, required_attributes[a])
36
- else
37
- xml_obj.remove_attribute(a)
38
- end
39
- end
40
- xml_obj
24
+ def leaf_node?(xml_obj)
25
+ xml_obj.children.size == 1 && xml_obj.children.first.class == XML_TEXT
26
+ end
27
+
28
+ def parse_leaf_node_data(xml_obj)
29
+ attr_name = underscore(xml_obj)
30
+ text_node = xml_obj.children.first
31
+ [attr_name, text_node.text]
32
+ end
33
+
34
+ def parse_xml_attributes(xml_obj)
35
+ attrs = xml_obj.attributes
36
+ attrs.inject({}) { |h, (n,v)| h[n] = v.value; h }
41
37
  end
42
38
 
43
39
  end
@@ -1,94 +1,94 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  class Quickbooks::QbxmlParser
4
- include Quickbooks::Support
5
- include Quickbooks::Support::API
6
- include Quickbooks::Support::QBXML
4
+ include Quickbooks::Config
5
+ include Quickbooks::Logger
6
+ include Quickbooks::Parser::XMLParsing
7
+ include Quickbooks::Support::Inflection
7
8
 
8
- attr_accessor :schema_type
9
+ attr_accessor :schema_type
9
10
 
10
- def initialize(schema_type)
11
- @schema_type = schema_type
12
- end
11
+ def initialize(schema_type)
12
+ @schema_type = schema_type
13
+ end
13
14
 
14
- def parse_file(qbxml_file)
15
- parse(qbxml_file.read)
16
- end
15
+ def parse_file(qbxml_file)
16
+ parse( cleanup_qbxml( File.read_from_unknown(qbxml_file) ) )
17
+ end
17
18
 
18
- def parse(qbxml)
19
- xml_doc = Nokogiri::XML(qbxml)
20
- process_xml_obj(xml_doc, nil)
21
- end
19
+ def parse(qbxml)
20
+ xml_doc = Nokogiri::XML(qbxml)
21
+ process_xml_obj(xml_doc, nil)
22
+ end
22
23
 
23
24
  private
24
25
 
25
- def process_xml_obj(xml_obj, parent)
26
- case xml_obj
27
- when XML_DOCUMENT
28
- process_xml_obj(xml_obj.root, parent)
29
- when XML_NODE_SET
30
- if !xml_obj.empty?
31
- process_xml_obj(xml_obj.shift, parent)
32
- process_xml_obj(xml_obj, parent)
33
- end
34
- when XML_ELEMENT
35
- if is_leaf_node?(xml_obj)
36
- process_leaf_node(xml_obj, parent)
37
- else
38
- obj = process_non_leaf_node(xml_obj, parent)
39
- process_xml_obj(xml_obj.children, obj)
40
- obj
26
+ def process_xml_obj(xml_obj, parent)
27
+ case xml_obj
28
+ when XML_DOCUMENT
29
+ process_xml_obj(xml_obj.root, parent)
30
+ when XML_NODE_SET
31
+ if !xml_obj.empty?
32
+ process_xml_obj(xml_obj.shift, parent)
33
+ process_xml_obj(xml_obj, parent)
34
+ end
35
+ when XML_ELEMENT
36
+ if leaf_node?(xml_obj)
37
+ process_leaf_node(xml_obj, parent)
38
+ else
39
+ obj = process_non_leaf_node(xml_obj, parent)
40
+ process_xml_obj(xml_obj.children, obj)
41
+ obj
42
+ end
43
+ when XML_COMMENT
44
+ process_comment_node(xml_obj, parent)
41
45
  end
42
- when XML_COMMENT
43
- process_comment_node(xml_obj, parent)
44
46
  end
45
- end
46
47
 
47
- def process_leaf_node(xml_obj, parent_instance)
48
- attr_name, data = parse_leaf_node_data(xml_obj)
49
- if parent_instance
50
- set_attribute_value(parent_instance, attr_name, data)
48
+ def process_leaf_node(xml_obj, parent_instance)
49
+ attr_name, data = parse_leaf_node_data(xml_obj)
50
+ if parent_instance
51
+ set_attribute_value(parent_instance, attr_name, data)
52
+ end
53
+ parent_instance
51
54
  end
52
- parent_instance
53
- end
54
55
 
55
- def process_non_leaf_node(xml_obj, parent_instance)
56
- instance = fetch_qbxml_class_instance(xml_obj)
57
- attr_name = to_attribute_name(instance.class)
58
- if parent_instance
59
- set_attribute_value(parent_instance, attr_name, instance)
56
+ def process_non_leaf_node(xml_obj, parent_instance)
57
+ instance = fetch_qbxml_class_instance(xml_obj)
58
+ attr_name = underscore(instance.class)
59
+ if parent_instance
60
+ set_attribute_value(parent_instance, attr_name, instance)
61
+ end
62
+ instance
60
63
  end
61
- instance
62
- end
63
64
 
64
- def process_comment_node(xml_obj, parent_instance)
65
- parent_instance
66
- end
65
+ #TODO: stub
66
+ def process_comment_node(xml_obj, parent_instance)
67
+ parent_instance
68
+ end
67
69
 
68
70
  # helpers
69
71
 
70
- def parse_leaf_node_data(xml_obj)
71
- attr_name = to_attribute_name(xml_obj)
72
- text_node = xml_obj.children.first
73
- [attr_name, text_node.text]
74
- end
75
-
76
- def fetch_qbxml_class_instance(xml_obj)
77
- get_schema_namespace.const_get(xml_obj.name).new
78
- end
72
+ def fetch_qbxml_class_instance(xml_obj)
73
+ obj = schema_namespace.const_get(xml_obj.name).new
74
+ obj.xml_attributes = parse_xml_attributes(xml_obj)
75
+ obj
76
+ end
79
77
 
80
- def set_attribute_value(instance, attr_name, data)
81
- if instance.respond_to?(attr_name)
82
- cur_val = instance.send(attr_name)
83
- case cur_val
84
- when nil
85
- instance.send("#{attr_name}=", data)
86
- when Array
87
- cur_val << data
78
+ def set_attribute_value(instance, attr_name, data)
79
+ if instance.respond_to?(attr_name)
80
+ cur_val = instance.send(attr_name)
81
+ case cur_val
82
+ when nil
83
+ instance.send("#{attr_name}=", data)
84
+ when Array
85
+ cur_val << data
86
+ else
87
+ instance.send("#{attr_name}=", [cur_val, data])
88
+ end
88
89
  else
89
- instance.send("#{attr_name}=", [cur_val, data])
90
+ log.info "Warning: instance #{instance} does not respond to attribute #{attr_name}"
90
91
  end
91
92
  end
92
- end
93
93
 
94
94
  end
@@ -0,0 +1,18 @@
1
+ require 'active_support/core_ext'
2
+
3
+ module Quickbooks::Support::Inflection
4
+
5
+ def underscore(obj)
6
+ name = \
7
+ case obj
8
+ when Class
9
+ obj.simple_name
10
+ when Nokogiri::XML::Element
11
+ obj.name
12
+ else
13
+ obj.to_s
14
+ end
15
+ name.underscore
16
+ end
17
+
18
+ end