quickbooks_api 0.0.2

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,123 @@
1
+ class Quickbooks::API
2
+ include Quickbooks::Support
3
+ include Quickbooks::Support::API
4
+ include Quickbooks::Support::QBXML
5
+
6
+ attr_reader :dtd_parser, :qbxml_parser, :schema_type
7
+
8
+ def initialize(schema_type = nil, opts = {})
9
+ @schema_type = schema_type
10
+ use_disk_cache, log_level = opts.values_at(:use_disk_cache, :log_level)
11
+
12
+ unless valid_schema_type?
13
+ raise(ArgumentError, "schema type required: #{valid_schema_types.inspect}")
14
+ end
15
+
16
+ @dtd_file = get_dtd_file
17
+ @dtd_parser = DtdParser.new(schema_type)
18
+ @qbxml_parser = QbxmlParser.new(schema_type)
19
+
20
+ load_qb_classes(use_disk_cache)
21
+
22
+ # load the container class template into memory (significantly speeds up wrapping of partial data hashes)
23
+ get_container_class.template(true, use_disk_cache)
24
+ end
25
+
26
+ def container
27
+ get_container_class
28
+ end
29
+
30
+ def qbxml_classes
31
+ cached_classes
32
+ end
33
+
34
+ # QBXML 2 RUBY
35
+
36
+ def qbxml_to_obj(qbxml)
37
+ case qbxml
38
+ when IO
39
+ qbxml_parser.parse_file(qbxml)
40
+ else
41
+ qbxml_parser.parse(qbxml)
42
+ end
43
+ end
44
+
45
+ def qbxml_to_hash(qbxml, include_container = false)
46
+ qb_obj = qbxml_to_obj(qbxml)
47
+ unless include_container
48
+ qb_obj.inner_attributes
49
+ else
50
+ qb_obj.attributes
51
+ end
52
+ end
53
+
54
+
55
+ # RUBY 2 QBXML
56
+
57
+ def hash_to_obj(data)
58
+ key = data.keys.first
59
+ value = data[key]
60
+
61
+ key_path = find_nested_key(container.template(true), key)
62
+ raise(RuntimeError, "#{key} class not found in api template") unless key_path
63
+
64
+ wrapped_data = build_hash_wrapper(key_path, value)
65
+ container.new(wrapped_data)
66
+ end
67
+
68
+ def hash_to_qbxml(data)
69
+ hash_to_obj(data).to_qbxml.to_s
70
+ end
71
+
72
+ # Disk Cache
73
+
74
+ def clear_disk_cache(rebuild = false)
75
+ qbxml_cache = Dir["#{get_disk_cache_path}/*.rb"]
76
+ template_cache = Dir["#{get_template_cache_path}/*.yml"]
77
+ File.delete(*(qbxml_cache + template_cache))
78
+ load_qb_classes(rebuild)
79
+ end
80
+
81
+
82
+ private
83
+
84
+
85
+ def load_qb_classes(use_disk_cache = false)
86
+ if use_disk_cache
87
+ disk_cache = Dir["#{get_disk_cache_path}/*.rb"]
88
+ if disk_cache.empty?
89
+ log.info "Warning: on disk schema cache is empty, rebuilding..."
90
+ rebuild_schema_cache(false, true)
91
+ else
92
+ disk_cache.each {|file| require file }
93
+ end
94
+ else
95
+ rebuild_schema_cache(false, false)
96
+ end
97
+ end
98
+
99
+ # rebuilds schema cache in memory and writes to disk if desired
100
+ #
101
+ def rebuild_schema_cache(force = false, write_to_disk = false)
102
+ dtd_parser.parse_file(@dtd_file) if (cached_classes.empty? || force)
103
+ dump_cached_classes if write_to_disk
104
+ end
105
+
106
+ # writes dynamically generated api classes to disk
107
+ #
108
+ def dump_cached_classes
109
+ cached_classes.each do |c|
110
+ File.open("#{get_disk_cache_path}/#{to_attribute_name(c)}.rb", 'w') do |f|
111
+ f << Ruby2Ruby.translate(c)
112
+ end
113
+ end
114
+ end
115
+
116
+ # class methods
117
+
118
+ def self.log
119
+ @@log ||= Logger.new(STDOUT, DEFAULT_LOG_LEVEL)
120
+ end
121
+
122
+
123
+ end
@@ -0,0 +1,46 @@
1
+ class Quickbooks::DtdParser < Quickbooks::QbxmlParser
2
+ include Quickbooks::Support::ClassBuilder
3
+
4
+ def parse_file(qbxml_file)
5
+ parse(
6
+ cleanup_qbxml(
7
+ File.read(qbxml_file)))
8
+ end
9
+
10
+ private
11
+
12
+ def process_leaf_node(xml_obj, parent_class)
13
+ attr_name, qb_type = parse_leaf_node_data(xml_obj)
14
+ if parent_class
15
+ add_casting_attribute(parent_class, attr_name, qb_type)
16
+ end
17
+ end
18
+
19
+ def process_non_leaf_node(xml_obj, parent_class)
20
+ klass = build_qbxml_class(xml_obj)
21
+ attr_name = to_attribute_name(xml_obj)
22
+ if parent_class
23
+ add_strict_attribute(parent_class, attr_name, klass)
24
+ end
25
+ klass
26
+ end
27
+
28
+ def process_comment_node(xml_obj, parent_class)
29
+ parent_class
30
+ end
31
+
32
+ # helpers
33
+
34
+ def build_qbxml_class(xml_obj)
35
+ obj_name = xml_obj.name
36
+ unless qbxml_class_defined?(obj_name)
37
+ klass = Class.new(QbxmlBase)
38
+ get_schema_namespace.const_set(obj_name, klass)
39
+ add_xml_template(klass, xml_obj.to_xml)
40
+ else
41
+ klass = get_schema_namespace.const_get(obj_name)
42
+ end
43
+ klass
44
+ end
45
+
46
+ end
@@ -0,0 +1,212 @@
1
+ # inheritance base for schema classes
2
+ class Quickbooks::QbxmlBase
3
+ include Quickbooks::Support
4
+ include Quickbooks::Support::QBXML
5
+
6
+ extend Quickbooks::Support
7
+ extend Quickbooks::Support::API
8
+
9
+ #QB_TYPE_CONVERSION_MAP= {
10
+ #"AMTTYPE" => lambda {|d| String(d)},
11
+ #"BOOLTYPE" => lambda {|d| String(d)},
12
+ #"DATETIMETYPE" => lambda {|d| Date.parse(d)},
13
+ #"DATETYPE" => lambda {|d| Date.parse(d)},
14
+ #"ENUMTYPE" => lambda {|d| String(d)},
15
+ #"FLOATTYPE" => lambda {|d| String(d)},
16
+ #"GUIDTYPE" => lambda {|d| String(d)},
17
+ #"IDTYPE" => lambda {|d| String(d)},
18
+ #"INTTYPE" => lambda {|d| Integer(d)},
19
+ #"PERCENTTYPE" => lambda {|d| Float(d)},
20
+ #"PRICETYPE" => lambda {|d| Float(d)},
21
+ #"QUANTYPE" => lambda {|d| Integer(d)},
22
+ #"STRTYPE" => lambda {|d| String(d)},
23
+ #"TIMEINTERVALTYPE" => lambda {|d| String(d)}
24
+ #}
25
+
26
+ QB_TYPE_CONVERSION_MAP= {
27
+ "AMTTYPE" => lambda {|d| String(d)},
28
+ "BOOLTYPE" => lambda {|d| String(d)},
29
+ "DATETIMETYPE" => lambda {|d| String(d)},
30
+ "DATETYPE" => lambda {|d| String(d)},
31
+ "ENUMTYPE" => lambda {|d| String(d)},
32
+ "FLOATTYPE" => lambda {|d| String(d)},
33
+ "GUIDTYPE" => lambda {|d| String(d)},
34
+ "IDTYPE" => lambda {|d| String(d)},
35
+ "INTTYPE" => lambda {|d| String(d)},
36
+ "PERCENTTYPE" => lambda {|d| String(d)},
37
+ "PRICETYPE" => lambda {|d| String(d)},
38
+ "QUANTYPE" => lambda {|d| String(d)},
39
+ "STRTYPE" => lambda {|d| String(d)},
40
+ "TIMEINTERVALTYPE" => lambda {|d| String(d)}
41
+ }
42
+
43
+
44
+ def initialize(params = nil)
45
+ return unless params.is_a?(Hash)
46
+ params.each do |k,v|
47
+ if self.respond_to?(k)
48
+ self.send("#{k}=", v)
49
+ end
50
+ end
51
+ end
52
+
53
+
54
+ def to_qbxml
55
+ xml_doc = Nokogiri::XML(self.class.xml_template)
56
+ root = xml_doc.root
57
+ log.debug "to_qbxml#nodes_size: #{root.children.size}"
58
+
59
+ # replace all children nodes of the template with populated data nodes
60
+ xml_nodes = []
61
+ root.children.each do |xml_template|
62
+ next unless xml_template.is_a? XML_ELEMENT
63
+ attr_name = to_attribute_name(xml_template)
64
+ log.debug "to_qbxml#attr_name: #{attr_name}"
65
+
66
+ val = self.send(attr_name)
67
+ next unless val
68
+
69
+ case val
70
+ when Array
71
+ xml_nodes += build_qbxml_nodes(xml_template, val)
72
+ else
73
+ xml_nodes << build_qbxml_node(xml_template, val)
74
+ end
75
+ log.debug "to_qbxml#val: #{val}"
76
+ end
77
+
78
+ log.debug "to_qbxml#xml_nodes_size: #{xml_nodes.size}"
79
+ root.children = xml_nodes.join('')
80
+ root
81
+ end
82
+
83
+
84
+ def self.template(recursive = false, use_disk_cache = false)
85
+ if recursive
86
+ @template ||= load_template(true, use_disk_cache)
87
+ else build_template(false)
88
+ end
89
+ end
90
+
91
+
92
+ def self.attribute_names
93
+ instance_methods(false).reject { |m| m[-1..-1] == '=' || m =~ /_xml_class/}
94
+ end
95
+
96
+
97
+ def inner_attributes
98
+ top_level_attrs = \
99
+ self.class.attribute_names.inject({}) do |h, m|
100
+ h[m] = self.send(m); h
101
+ end
102
+
103
+ values = top_level_attrs.values.compact
104
+ if values.size > 1 || values.first.is_a?(Array)
105
+ attributes
106
+ else
107
+ values.first.inner_attributes
108
+ end
109
+ end
110
+
111
+
112
+ def attributes(recursive = true)
113
+ self.class.attribute_names.inject({}) do |h, m|
114
+ val = self.send(m)
115
+ if val
116
+ unless recursive
117
+ h[m] = val
118
+ else
119
+ h[m] = nested_attributes(val)
120
+ end
121
+ end; h
122
+ end
123
+ end
124
+
125
+
126
+ private
127
+
128
+ # qbxml conversion
129
+
130
+ def nested_attributes(val)
131
+ case val
132
+ when Quickbooks::QbxmlBase
133
+ val.attributes
134
+ when Array
135
+ val.inject([]) do |a, obj|
136
+ case obj
137
+ when Quickbooks::QbxmlBase
138
+ a << obj.attributes
139
+ else a << obj
140
+ end
141
+ end
142
+ else val
143
+ end
144
+ end
145
+
146
+ def build_qbxml_node(node, val)
147
+ case val
148
+ when Quickbooks::QbxmlBase
149
+ val.to_qbxml
150
+ else
151
+ node.children = val.to_s
152
+ node
153
+ end
154
+ end
155
+
156
+ def build_qbxml_nodes(node, val)
157
+ val.inject([]) do |a, v|
158
+ n = clone_qbxml_node(node,v)
159
+ a << n
160
+ end
161
+ end
162
+
163
+ def clone_qbxml_node(node, val)
164
+ n = node.clone
165
+ n.children = \
166
+ case val
167
+ when Quickbooks::QbxmlBase
168
+ val.to_qbxml
169
+ else
170
+ val.to_s
171
+ end; n
172
+ end
173
+
174
+ # qbxml class templates
175
+
176
+ def self.load_template(recursive = false, use_disk_cache = false)
177
+ if use_disk_cache && File.exist?(template_cache_path)
178
+ YAML.load(File.read(template_cache_path))
179
+ else
180
+ log.info "Warning: on disk template is missing, rebuilding..." if use_disk_cache
181
+ template = build_template(recursive)
182
+ dump_template(template) if use_disk_cache
183
+ template
184
+ end
185
+ end
186
+
187
+ def self.build_template(recursive = false)
188
+ attribute_names.inject({}) do |h, a|
189
+ attr_type = self.send("#{a}_type")
190
+ h[a] = (is_cached_class?(attr_type) && recursive) ? attr_type.build_template(true): attr_type.to_s; h
191
+ end
192
+ end
193
+
194
+ def self.dump_template(template)
195
+ File.open(template_cache_path, 'w') do |f|
196
+ f << template.to_yaml
197
+ end
198
+ end
199
+
200
+ def self.template_cache_path
201
+ "#{get_template_cache_path}/#{to_attribute_name(self)}.yml"
202
+ end
203
+
204
+ def self.schema_type
205
+ namespace = self.to_s.split("::")[1]
206
+ API::SCHEMA_MAP.find do |k,v|
207
+ simple_class_name(v[:namespace]) == namespace
208
+ end.first
209
+ end
210
+
211
+
212
+ end
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ class Quickbooks::QbxmlParser
4
+ include Quickbooks::Support
5
+ include Quickbooks::Support::API
6
+ include Quickbooks::Support::QBXML
7
+
8
+ attr_accessor :schema_type
9
+
10
+ def initialize(schema_type)
11
+ @schema_type = schema_type
12
+ end
13
+
14
+ def parse_file(qbxml_file)
15
+ parse(qbxml_file.read)
16
+ end
17
+
18
+ def parse(qbxml)
19
+ xml_doc = Nokogiri::XML(qbxml)
20
+ process_xml_obj(xml_doc, nil)
21
+ end
22
+
23
+ private
24
+
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
41
+ end
42
+ when XML_COMMENT
43
+ process_comment_node(xml_obj, parent)
44
+ end
45
+ end
46
+
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)
51
+ end
52
+ parent_instance
53
+ end
54
+
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)
60
+ end
61
+ instance
62
+ end
63
+
64
+ def process_comment_node(xml_obj, parent_instance)
65
+ parent_instance
66
+ end
67
+
68
+ # helpers
69
+
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
+ instance = get_schema_namespace.const_get(xml_obj.name).new
78
+ instance
79
+ end
80
+
81
+ def set_attribute_value(instance, attr_name, data)
82
+ if instance.respond_to?(attr_name)
83
+ cur_val = instance.send(attr_name)
84
+ case cur_val
85
+ when nil
86
+ instance.send("#{attr_name}=", data)
87
+ when Array
88
+ cur_val << data
89
+ else
90
+ instance.send("#{attr_name}=", [cur_val, data])
91
+ end
92
+ end
93
+ end
94
+
95
+
96
+ end
@@ -0,0 +1,88 @@
1
+ module Quickbooks::Support::API
2
+ include Quickbooks
3
+
4
+ API_ROOT = File.join(File.dirname(__FILE__), '..', '..', '..').freeze
5
+ XML_SCHEMA_PATH = File.join(API_ROOT, 'xml_schema').freeze
6
+ RUBY_SCHEMA_PATH = File.join(API_ROOT, 'ruby_schema').freeze
7
+
8
+ SCHEMA_MAP = {
9
+ :qb => {:dtd_file => "qbxmlops70.xml",
10
+ :namespace => QBXML,
11
+ :container_class => lambda { Quickbooks::QBXML::QBXML },
12
+ }.freeze,
13
+ :qbpos => {:dtd_file => "qbposxmlops30.xml",
14
+ :namespace => QBPOSXML,
15
+ :container_class => lambda { Quickbooks::QBPOSXML::QBPOSXML },
16
+ }.freeze,
17
+ }.freeze
18
+
19
+ DEFAULT_LOG_LEVEL = 1
20
+
21
+ private
22
+
23
+ def valid_schema_types
24
+ SCHEMA_MAP.keys
25
+ end
26
+
27
+ def valid_schema_type?
28
+ SCHEMA_MAP.include?(schema_type)
29
+ end
30
+
31
+ def get_dtd_file
32
+ "#{XML_SCHEMA_PATH}/#{SCHEMA_MAP[schema_type][:dtd_file]}"
33
+ end
34
+
35
+ def get_schema_namespace
36
+ SCHEMA_MAP[schema_type][:namespace]
37
+ end
38
+
39
+ def get_container_class
40
+ SCHEMA_MAP[schema_type][:container_class].call
41
+ end
42
+
43
+ def get_disk_cache_path
44
+ "#{RUBY_SCHEMA_PATH}/#{schema_type.to_s}"
45
+ end
46
+
47
+ def get_template_cache_path
48
+ "#{RUBY_SCHEMA_PATH}/#{schema_type.to_s}/templates"
49
+ end
50
+
51
+ # fetches all the dynamically generated schema classes
52
+ def cached_classes
53
+ cached_classes = SCHEMA_MAP.inject({}) do |h, (schema_type, opts)|
54
+ namespace = opts[:namespace]
55
+ h[schema_type] = namespace.constants.map { |klass| namespace.const_get(klass) }; h
56
+ end
57
+ cached_classes[schema_type]
58
+ end
59
+
60
+ def is_cached_class?(klass)
61
+ SCHEMA_MAP.any? do |schema_type, opts|
62
+ namespace = opts[:namespace]
63
+ namespace.constants.include?(simple_class_name(klass))
64
+ end
65
+ end
66
+
67
+ def find_nested_key(hash, key)
68
+ hash.each do |k,v|
69
+ path = [k]
70
+ if k == key
71
+ return path
72
+ elsif v.is_a? Hash
73
+ nested_val = find_nested_key(v, key)
74
+ nested_val ? (return path + nested_val) : nil
75
+ end
76
+ end
77
+ return nil
78
+ end
79
+
80
+ def build_hash_wrapper(path, value)
81
+ hash_constructor = lambda { |h, k| h[k] = Hash.new(&hash_constructor) }
82
+
83
+ wrapped_data = Hash.new(&hash_constructor)
84
+ path.inject(wrapped_data) { |h, k| k == path.last ? h[k] = value: h[k] }
85
+ wrapped_data
86
+ end
87
+
88
+ end
@@ -0,0 +1,65 @@
1
+ module Quickbooks::Support::ClassBuilder
2
+
3
+ private
4
+
5
+ def add_strict_attribute(klass, attr_name, type)
6
+ add_attribute_type(klass, attr_name, type)
7
+
8
+ eval <<-class_body
9
+ class #{klass}
10
+ attr_accessor :#{attr_name}
11
+
12
+ def #{attr_name}=(obj)
13
+ expected_type = self.class.#{attr_name}_type
14
+ if obj.class == expected_type
15
+ @#{attr_name} = obj
16
+ elsif obj.is_a?(Hash)
17
+ @#{attr_name} = #{type}.new(obj)
18
+ else
19
+ raise(TypeError, "expecting an object of type \#{expected_type}")
20
+ end
21
+ end
22
+ end
23
+ class_body
24
+ end
25
+
26
+ def add_casting_attribute(klass, attr_name, type)
27
+ type_casting_proc = klass::QB_TYPE_CONVERSION_MAP[type]
28
+ type = type_casting_proc.call(nil).class
29
+ add_attribute_type(klass, attr_name, type)
30
+
31
+ eval <<-class_body
32
+ class #{klass}
33
+ attr_accessor :#{attr_name}
34
+
35
+ def #{attr_name}=(obj)
36
+ type_casting_proc = QB_TYPE_CONVERSION_MAP["#{type}"]
37
+ @#{attr_name} = type_casting_proc ? type_casting_proc.call(obj) : obj
38
+ end
39
+ end
40
+ class_body
41
+ end
42
+
43
+ def add_attribute_type(klass, attr_name, type)
44
+ eval <<-class_body
45
+ class #{klass}
46
+ @@#{attr_name}_type = #{type}
47
+ def self.#{attr_name}_type
48
+ #{type}
49
+ end
50
+ end
51
+ class_body
52
+ end
53
+
54
+ def add_xml_template(klass, xml_template)
55
+ eval <<-class_body
56
+ class #{klass}
57
+ def self.xml_template
58
+ #{xml_template.dump}
59
+ end
60
+ end
61
+ class_body
62
+ end
63
+
64
+
65
+ end
@@ -0,0 +1,56 @@
1
+ require 'forwardable'
2
+
3
+ class Quickbooks::Support::Logger
4
+ extend Forwardable
5
+
6
+ def_delegators :@logger, :level, :flush, :auto_flushing=
7
+
8
+ DEFAULT_FORMATTER = "%s"
9
+ DEFAULT_PADDING = ""
10
+ PADDING_CHAR = " "
11
+
12
+ def initialize(log_file, log_level, log_count = nil, log_size = nil)
13
+ @logger = ActiveSupport::BufferedLogger.new(log_file, log_level)
14
+ @padding, @formatter = {}, {}
15
+ end
16
+
17
+ def buffer
18
+ buf = @logger.send(:buffer)
19
+ buf && buf.join('')
20
+ end
21
+
22
+ # overwrite all the logging methods
23
+ class_eval do
24
+ [:debug, :info, :warn, :error, :fatal, :unknown].each do |method|
25
+ define_method(method) do |message|
26
+ @logger.send(method, (padding + formatter) % message.to_s)
27
+ end
28
+ end
29
+ end
30
+
31
+ def indent(indent_level)
32
+ @padding[Thread.current] = \
33
+ if indent_level == :reset
34
+ ""
35
+ elsif indent_level > 0
36
+ padding + (PADDING_CHAR * indent_level)
37
+ else
38
+ padding[0..(-1+indent_level)]
39
+ end
40
+ end
41
+
42
+ def formatter=(format)
43
+ @formatter[Thread.current] = format
44
+ end
45
+
46
+ protected
47
+
48
+ def padding
49
+ @padding[Thread.current] ||= DEFAULT_PADDING
50
+ end
51
+
52
+ def formatter
53
+ @formatter[Thread.current] ||= DEFAULT_FORMATTER
54
+ end
55
+
56
+ end
@@ -0,0 +1,31 @@
1
+ module Quickbooks::Support::QBXML
2
+
3
+ XML_DOCUMENT = Nokogiri::XML::Document
4
+ XML_NODE_SET = Nokogiri::XML::NodeSet
5
+ XML_NODE = Nokogiri::XML::Node
6
+ XML_ELEMENT = Nokogiri::XML::Element
7
+ XML_COMMENT= Nokogiri::XML::Comment
8
+ XML_TEXT = Nokogiri::XML::Text
9
+
10
+ COMMENT_START = "<!--"
11
+ COMMENT_END = "-->"
12
+ COMMENT_MATCHER = /\A#{COMMENT_START}.*#{COMMENT_END}\z/
13
+
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
+ # remove all comment lines and empty nodes
24
+ def cleanup_qbxml(qbxml)
25
+ qbxml = qbxml.split('\n')
26
+ qbxml.map! { |l| l.strip }
27
+ qbxml.reject! { |l| l =~ COMMENT_MATCHER }
28
+ qbxml.join('')
29
+ end
30
+
31
+ end