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.
- data/lib/quickbooks/api.rb +123 -0
- data/lib/quickbooks/dtd_parser.rb +46 -0
- data/lib/quickbooks/qbxml_base.rb +212 -0
- data/lib/quickbooks/qbxml_parser.rb +96 -0
- data/lib/quickbooks/support/api.rb +88 -0
- data/lib/quickbooks/support/class_builder.rb +65 -0
- data/lib/quickbooks/support/logger.rb +56 -0
- data/lib/quickbooks/support/qbxml.rb +31 -0
- data/lib/quickbooks/support.rb +33 -0
- data/lib/quickbooks.rb +17 -0
- data/ruby_schema/qb/templates/.placeholder +0 -0
- data/ruby_schema/qbpos/templates/.placeholder +0 -0
- data/spec/quickbooks/api_spec.rb +34 -0
- data/spec/quickbooks/class_builder_spec.rb +17 -0
- data/spec/quickbooks/dtd_parser_spec.rb +20 -0
- data/spec/quickbooks/logger_spec.rb +20 -0
- data/spec/quickbooks/qbxml_parser_spec.rb +33 -0
- data/spec/quickbooks/qbxml_spec.rb +23 -0
- data/spec/quickbooks/spec_helper.rb +3 -0
- data/spec/quickbooks/support_spec.rb +118 -0
- data/xml_schema/qbposxmlops30.xml +8343 -0
- data/xml_schema/qbxmlops70.xml +26714 -0
- metadata +131 -0
@@ -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
|