quickbooks_api 0.0.7 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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