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.
- data/README +74 -0
- data/TODO +5 -0
- data/lib/quickbooks.rb +12 -10
- data/lib/quickbooks/api.rb +72 -112
- data/lib/quickbooks/config.rb +59 -0
- data/lib/quickbooks/dtd_parser.rb +30 -35
- data/lib/quickbooks/logger.rb +20 -0
- data/lib/quickbooks/{support → parser}/class_builder.rb +7 -7
- data/lib/quickbooks/parser/qbxml_base.rb +128 -0
- data/lib/quickbooks/parser/xml_generation.rb +52 -0
- data/lib/quickbooks/{support/qbxml.rb → parser/xml_parsing.rb} +16 -20
- data/lib/quickbooks/qbxml_parser.rb +68 -68
- data/lib/quickbooks/support/inflection.rb +18 -0
- data/lib/quickbooks/support/monkey_patches.rb +48 -0
- data/spec/quickbooks/api_spec.rb +13 -10
- data/spec/quickbooks/class_builder_spec.rb +1 -1
- data/spec/quickbooks/config_spec.rb +72 -0
- data/spec/quickbooks/inflection_spec.rb +18 -0
- data/spec/quickbooks/logger_spec.rb +1 -13
- data/spec/quickbooks/monkey_patches_spec.rb +11 -0
- data/spec/quickbooks/qbxml_base_spec.rb +23 -0
- data/spec/quickbooks/xml_generation_spec.rb +5 -0
- data/spec/quickbooks/xml_parsing_spec.rb +36 -0
- metadata +20 -30
- data/lib/quickbooks/qbxml_base.rb +0 -212
- data/lib/quickbooks/support.rb +0 -33
- data/lib/quickbooks/support/api.rb +0 -98
- data/lib/quickbooks/support/logger.rb +0 -56
- data/ruby_schema/qb/templates/.placeholder +0 -0
- data/ruby_schema/qbpos/templates/.placeholder +0 -0
- data/spec/quickbooks/qbxml_spec.rb +0 -23
- data/spec/quickbooks/support_spec.rb +0 -118
@@ -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::
|
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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
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
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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::
|
5
|
-
include Quickbooks::
|
6
|
-
include Quickbooks::
|
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
|
-
|
12
|
-
end
|
11
|
+
def initialize(schema_type)
|
12
|
+
@schema_type = schema_type
|
13
|
+
end
|
13
14
|
|
14
|
-
def parse_file(qbxml_file)
|
15
|
-
|
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
|
-
|
20
|
-
|
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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
65
|
-
parent_instance
|
66
|
-
|
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
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
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
|