quickbooks_api 0.0.2

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