mrglass-qbxml 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/.travis.yml +26 -0
- data/Appraisals +23 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +86 -0
- data/Rakefile +7 -0
- data/gemfiles/rails_5.0.gemfile +7 -0
- data/gemfiles/rails_5.0.gemfile.lock +69 -0
- data/gemfiles/rails_5.1.gemfile +7 -0
- data/gemfiles/rails_5.1.gemfile.lock +69 -0
- data/gemfiles/rails_5.2.gemfile +7 -0
- data/gemfiles/rails_5.2.gemfile.lock +69 -0
- data/gemfiles/rails_6.0.gemfile +7 -0
- data/gemfiles/rails_6.0.gemfile.lock +71 -0
- data/gemfiles/rails_6.1.gemfile +7 -0
- data/gemfiles/rails_6.1.gemfile.lock +70 -0
- data/lib/qbxml/hash.rb +188 -0
- data/lib/qbxml/qbxml.rb +124 -0
- data/lib/qbxml/types.rb +45 -0
- data/lib/qbxml/version.rb +3 -0
- data/lib/qbxml.rb +11 -0
- data/qbxml.gemspec +29 -0
- data/schema/qbposxmlops30.xml +8343 -0
- data/schema/qbxmlops100.xml +30100 -0
- data/schema/qbxmlops110.xml +32241 -0
- data/schema/qbxmlops120.xml +34129 -0
- data/schema/qbxmlops130.xml +35788 -0
- data/schema/qbxmlops140.xml +35792 -0
- data/schema/qbxmlops20.xml +13140 -0
- data/schema/qbxmlops30.xml +18229 -0
- data/schema/qbxmlops40.xml +19811 -0
- data/schema/qbxmlops41.xml +19979 -0
- data/schema/qbxmlops50.xml +29931 -0
- data/schema/qbxmlops60.xml +24555 -0
- data/schema/qbxmlops70.xml +29844 -0
- data/schema/qbxmlops80.xml +28447 -0
- data/schema/qbxmlopsCA30.xml +18409 -0
- data/spec/backwards_compatibility.rb +26 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/requests/account_query_rq.xml +8 -0
- data/spec/support/requests/customer_add_rq.xml +28 -0
- data/spec/support/requests/customer_query_iterator_rq.xml +11 -0
- data/spec/support/requests/customer_query_rq.xml +9 -0
- data/spec/support/requests/estimate_add_rq.xml +45 -0
- data/spec/support/requests/invoice_add_rq.xml +40 -0
- data/spec/support/requests/item_inventory_add_rq.xml +22 -0
- data/spec/support/requests/purchase_order_query_rq.xml +14 -0
- data/spec/support/requests/receive_payment_add_rq.xml +35 -0
- data/spec/support/requests/receive_payment_query_rq.xml +9 -0
- data/spec/support/requests/sales_receipt_add_rq.xml +49 -0
- data/spec/support/responses/account_query_rs.xml +42 -0
- data/spec/support/responses/customer_add_rs.xml +37 -0
- data/spec/support/responses/customer_query_rs.xml +49 -0
- data/spec/support/responses/customer_query_terator_rs.xml +34 -0
- data/spec/support/responses/item_inventory_add_rs.xml +40 -0
- data/spec/support/responses/purchase_order_query_rs.xml +84 -0
- data/spec/support/responses/receive_payment_query_rs.xml +51 -0
- data/spec/support/responses/sales_receipt_add_rs.xml +89 -0
- data/test/unit/hash_to_xml_test.rb +78 -0
- data/test/unit/version_test.rb +24 -0
- data/test/unit/xml_to_hash_test.rb +281 -0
- metadata +242 -0
data/lib/qbxml/hash.rb
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# XML Conversion References
|
|
2
|
+
#
|
|
3
|
+
# https://github.com/rails/rails/blob/master/activesupport/lib/active_support/core_ext/hash/conversions.rb
|
|
4
|
+
# https://github.com/rails/rails/blob/master/activesupport/lib/active_support/xml_mini/nokogiri.rb
|
|
5
|
+
#
|
|
6
|
+
#
|
|
7
|
+
class Qbxml::Hash < ::Hash
|
|
8
|
+
include Qbxml::Types
|
|
9
|
+
|
|
10
|
+
CONTENT_ROOT = '__content__'.freeze
|
|
11
|
+
ATTR_ROOT = 'xml_attributes'.freeze
|
|
12
|
+
IGNORED_KEYS = [ATTR_ROOT]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def self.from_hash(hash, opts = {}, &block)
|
|
16
|
+
key_proc = \
|
|
17
|
+
if opts[:camelize]
|
|
18
|
+
lambda { |k|
|
|
19
|
+
# QB wants things like ListID, not ListId. Adding inflections then using camelize can accomplish
|
|
20
|
+
# the same thing, but then the inflections will apply to everything the user does everywhere.
|
|
21
|
+
k.camelize.gsub(Qbxml::Types::ACRONYM_REGEXP) { "#{$1}#{$2.upcase}#{$3}" }
|
|
22
|
+
}
|
|
23
|
+
elsif opts[:underscore]
|
|
24
|
+
lambda { |k| k.underscore }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
deep_convert(hash, opts, &key_proc)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_xml(opts = {})
|
|
31
|
+
self.class.to_xml(self, opts)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def self.to_xml(hash, opts = {})
|
|
35
|
+
opts[:root], hash = hash.first
|
|
36
|
+
opts[:attributes] = hash.delete(ATTR_ROOT)
|
|
37
|
+
hash_to_xml(hash, opts)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.from_xml(xml, opts = {})
|
|
41
|
+
from_hash(
|
|
42
|
+
xml_to_hash(Nokogiri::XML(xml).root, {}, opts), opts)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def self.hash_to_xml(hash, opts = {})
|
|
48
|
+
opts = opts.dup
|
|
49
|
+
opts[:indent] ||= 2
|
|
50
|
+
opts[:root] ||= :hash
|
|
51
|
+
opts[:attributes] ||= (hash.delete(ATTR_ROOT) || {})
|
|
52
|
+
opts[:builder] ||= Builder::XmlMarkup.new(indent: opts[:indent])
|
|
53
|
+
opts[:skip_types] = true unless opts.key?(:skip_types)
|
|
54
|
+
opts[:skip_instruct] = false unless opts.key?(:skip_instruct)
|
|
55
|
+
builder = opts[:builder]
|
|
56
|
+
|
|
57
|
+
unless opts.delete(:skip_instruct)
|
|
58
|
+
builder.instruct!(:xml, :encoding => "ISO-8859-1")
|
|
59
|
+
builder.instruct!(opts[:schema], version: opts[:version])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
builder.tag!(opts[:root], opts.delete(:attributes)) do
|
|
63
|
+
hash.each do |key, val|
|
|
64
|
+
case val
|
|
65
|
+
when Hash
|
|
66
|
+
self.hash_to_xml(val, opts.merge({root: key, skip_instruct: true}))
|
|
67
|
+
when Array
|
|
68
|
+
val.map { |i|
|
|
69
|
+
if i.is_a?(String)
|
|
70
|
+
next builder.tag!(key, i, {})
|
|
71
|
+
end
|
|
72
|
+
next self.hash_to_xml(i, opts.merge({root: key, skip_instruct: true}))
|
|
73
|
+
}
|
|
74
|
+
else
|
|
75
|
+
builder.tag!(key, val, {})
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
yield builder if block_given?
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.xml_to_hash(node, hash = {}, opts = {})
|
|
84
|
+
node_hash = {CONTENT_ROOT => '', ATTR_ROOT => {}}
|
|
85
|
+
name = node.name
|
|
86
|
+
schema = opts[:schema]
|
|
87
|
+
opts[:typecast_cache] ||= {}
|
|
88
|
+
opts[:is_repetitive_cache] ||= {}
|
|
89
|
+
|
|
90
|
+
# Insert node hash into parent hash correctly.
|
|
91
|
+
case hash[name]
|
|
92
|
+
when Array
|
|
93
|
+
hash[name] << node_hash
|
|
94
|
+
when Hash, String
|
|
95
|
+
# This parent has multiple nodes with the same name, but when we checked the first time,
|
|
96
|
+
# we found it is not defined as repetitive. I guess this means the schema is a liar.
|
|
97
|
+
hash[name] = [hash[name], node_hash]
|
|
98
|
+
else
|
|
99
|
+
# We didn't see this node name under this parent yet.
|
|
100
|
+
if is_repetitive?(schema, node.path, opts[:is_repetitive_cache])
|
|
101
|
+
hash[name] = [node_hash]
|
|
102
|
+
else
|
|
103
|
+
hash[name] = node_hash
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Handle child elements
|
|
108
|
+
node.children.each do |c|
|
|
109
|
+
if c.element?
|
|
110
|
+
xml_to_hash(c, node_hash, opts)
|
|
111
|
+
elsif c.text? || c.cdata?
|
|
112
|
+
node_hash[CONTENT_ROOT] << c.content
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Handle attributes
|
|
117
|
+
node.attribute_nodes.each { |a| node_hash[ATTR_ROOT][a.node_name] = a.value }
|
|
118
|
+
|
|
119
|
+
# TODO: Strip text
|
|
120
|
+
# node_hash[CONTENT_ROOT].strip!
|
|
121
|
+
|
|
122
|
+
# Format node
|
|
123
|
+
if node_hash.size > 2 || node_hash[ATTR_ROOT].present?
|
|
124
|
+
node_hash.delete(CONTENT_ROOT)
|
|
125
|
+
elsif node_hash[CONTENT_ROOT].present?
|
|
126
|
+
node_hash.delete(ATTR_ROOT)
|
|
127
|
+
v = schema ? typecast(schema, node.path, node_hash[CONTENT_ROOT], opts[:typecast_cache]) : node_hash[CONTENT_ROOT]
|
|
128
|
+
# We only updated the last element
|
|
129
|
+
if hash[name].is_a?(Array)
|
|
130
|
+
hash[name].pop
|
|
131
|
+
hash[name] << v
|
|
132
|
+
else
|
|
133
|
+
hash[name] = v
|
|
134
|
+
end
|
|
135
|
+
else
|
|
136
|
+
hash[name] = node_hash[CONTENT_ROOT]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
hash
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
def self.typecast(schema, xpath, value, typecast_cache)
|
|
146
|
+
type_path = xpath.gsub(/\[\d+\]/,'')
|
|
147
|
+
# This is fairly expensive. Cache it for better performance when parsing lots of records of the same type.
|
|
148
|
+
type_proc = typecast_cache[type_path] ||= Qbxml::TYPE_MAP[schema.xpath(type_path).first.try(:text)]
|
|
149
|
+
raise "#{xpath} is not a valid type" unless type_proc
|
|
150
|
+
type_proc[value]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Determines if the node is repetitive. Just because something is repetitive doesn't mean it always repeats.
|
|
154
|
+
# For example, a customer query could return 1 result or 100, but in both cases, we should be returning an
|
|
155
|
+
# Array.
|
|
156
|
+
def self.is_repetitive?(schema, xpath, is_repetitive_cache)
|
|
157
|
+
# Yes, we are parsing comments.
|
|
158
|
+
comment_path = xpath.gsub(/\[\d+\]/,'') + "/comment()"
|
|
159
|
+
return is_repetitive_cache[comment_path] || parse_repetitive_from_comment(schema, comment_path)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def self.parse_repetitive_from_comment(schema, comment_path)
|
|
163
|
+
comment = schema.xpath(comment_path).first
|
|
164
|
+
return false if comment.nil?
|
|
165
|
+
return comment.text.include?('may rep')
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def self.deep_convert(hash, opts = {}, &block)
|
|
169
|
+
hash.inject(self.new) do |h, (k,v)|
|
|
170
|
+
k = k.to_s
|
|
171
|
+
ignored = IGNORED_KEYS.include?(k)
|
|
172
|
+
if ignored
|
|
173
|
+
h[k] = v
|
|
174
|
+
else
|
|
175
|
+
key = block_given? ? yield(k) : k
|
|
176
|
+
h[key] = \
|
|
177
|
+
case v
|
|
178
|
+
when Hash
|
|
179
|
+
deep_convert(v, &block)
|
|
180
|
+
when Array
|
|
181
|
+
v.map { |i| i.is_a?(Hash) ? deep_convert(i, &block) : i }
|
|
182
|
+
else v
|
|
183
|
+
end
|
|
184
|
+
end; h
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
end
|
data/lib/qbxml/qbxml.rb
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
class Qbxml
|
|
2
|
+
include Types
|
|
3
|
+
|
|
4
|
+
SCHEMA_PATH = File.expand_path('../../../schema', __FILE__)
|
|
5
|
+
|
|
6
|
+
SCHEMAS = {
|
|
7
|
+
qb: {
|
|
8
|
+
"2.0" => "#{SCHEMA_PATH}/qbxmlops20.xml",
|
|
9
|
+
"CA3.0" => "#{SCHEMA_PATH}/qbxmlopsCA30.xml",
|
|
10
|
+
"3.0" => "#{SCHEMA_PATH}/qbxmlops30.xml",
|
|
11
|
+
"4.0" => "#{SCHEMA_PATH}/qbxmlops40.xml",
|
|
12
|
+
"4.1" => "#{SCHEMA_PATH}/qbxmlops41.xml",
|
|
13
|
+
"5.0" => "#{SCHEMA_PATH}/qbxmlops50.xml",
|
|
14
|
+
"6.0" => "#{SCHEMA_PATH}/qbxmlops60.xml",
|
|
15
|
+
"7.0" => "#{SCHEMA_PATH}/qbxmlops70.xml",
|
|
16
|
+
"8.0" => "#{SCHEMA_PATH}/qbxmlops80.xml",
|
|
17
|
+
"10.0" => "#{SCHEMA_PATH}/qbxmlops100.xml",
|
|
18
|
+
"11.0" => "#{SCHEMA_PATH}/qbxmlops110.xml",
|
|
19
|
+
"12.0" => "#{SCHEMA_PATH}/qbxmlops120.xml",
|
|
20
|
+
"13.0" => "#{SCHEMA_PATH}/qbxmlops130.xml",
|
|
21
|
+
"14.0" => "#{SCHEMA_PATH}/qbxmlops140.xml"
|
|
22
|
+
},
|
|
23
|
+
qbpos: {
|
|
24
|
+
"3.0" => "#{SCHEMA_PATH}/qbposxmlops30.xml"
|
|
25
|
+
}
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
HIDE_IVARS = [:@doc].freeze
|
|
29
|
+
|
|
30
|
+
def initialize(key = :qb, version = "7.0")
|
|
31
|
+
@schema = key
|
|
32
|
+
@version = version
|
|
33
|
+
@doc = parse_schema(key, version)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# returns all xml nodes matching a specified pattern
|
|
37
|
+
#
|
|
38
|
+
def types(pattern = nil)
|
|
39
|
+
@types ||= @doc.xpath("//*").map { |e| e.name }.uniq
|
|
40
|
+
|
|
41
|
+
pattern ?
|
|
42
|
+
@types.select { |t| t =~ Regexp.new(pattern) } :
|
|
43
|
+
@types
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# returns the xml node for the specified type
|
|
47
|
+
#
|
|
48
|
+
def describe(type)
|
|
49
|
+
@doc.xpath("//#{type}").first
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# converts a hash to qbxml with optional validation
|
|
53
|
+
#
|
|
54
|
+
def to_qbxml(hash, opts = {})
|
|
55
|
+
hash = Qbxml::Hash.from_hash(hash, camelize: true)
|
|
56
|
+
hash = namespace_qbxml_hash(hash) unless opts[:no_namespace]
|
|
57
|
+
validate_qbxml_hash(hash) if opts[:validate]
|
|
58
|
+
|
|
59
|
+
Qbxml::Hash.to_xml(hash, schema: XML_DIRECTIVES[@schema], version: @version)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# converts qbxml to a hash
|
|
63
|
+
#
|
|
64
|
+
def from_qbxml(xml, opts = {})
|
|
65
|
+
hash = Qbxml::Hash.from_xml(xml, underscore: true, schema: @doc)
|
|
66
|
+
|
|
67
|
+
opts[:no_namespace] ? hash : namespace_qbxml_hash(hash)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# making this more sane so that it doesn't dump the whole schema doc to stdout
|
|
71
|
+
# every time
|
|
72
|
+
#
|
|
73
|
+
def inspect
|
|
74
|
+
prefix = "#<#{self.class}:0x#{self.__id__.to_s(16)} "
|
|
75
|
+
|
|
76
|
+
(instance_variables - HIDE_IVARS).each do |var|
|
|
77
|
+
prefix << "#{var}=#{instance_variable_get(var).inspect}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
return "#{prefix}>"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# private
|
|
84
|
+
|
|
85
|
+
def parse_schema(key, version)
|
|
86
|
+
File.open(select_schema(key, version)) { |f| Nokogiri::XML(f) }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def select_schema(schema_key, version)
|
|
90
|
+
# Try to handle it if a user gave us a numeric version. Assume 1 decimal.
|
|
91
|
+
version = '%.1f' % version if version.is_a?(Numeric)
|
|
92
|
+
raise "invalid schema '#{schema_key}', must be one of #{SCHEMAS.keys.inspect}" if !SCHEMAS.has_key?(schema_key)
|
|
93
|
+
raise "invalid version '#{version}' for schema #{schema_key}, must be one of #{SCHEMAS[schema_key].keys.inspect}" if !SCHEMAS[schema_key].has_key?(version)
|
|
94
|
+
return SCHEMAS[schema_key][version]
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# hash to qbxml
|
|
98
|
+
|
|
99
|
+
def namespace_qbxml_hash(hash)
|
|
100
|
+
node = describe(hash.keys.first)
|
|
101
|
+
return hash unless node
|
|
102
|
+
|
|
103
|
+
path = node.path.split('/')[1...-1].reverse
|
|
104
|
+
path.inject(hash) { |h,p| Qbxml::Hash[ p => h ] }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def validate_qbxml_hash(hash, path = [])
|
|
108
|
+
hash.each do |k,v|
|
|
109
|
+
next if k == Qbxml::Hash::ATTR_ROOT
|
|
110
|
+
key_path = path.dup << k
|
|
111
|
+
if v.is_a?(Hash)
|
|
112
|
+
validate_qbxml_hash(v, key_path)
|
|
113
|
+
else
|
|
114
|
+
validate_xpath(key_path)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def validate_xpath(path)
|
|
120
|
+
xpath = "/#{path.join('/')}"
|
|
121
|
+
raise "#{xpath} is not a valid type" if @doc.xpath(xpath).empty?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
end
|
data/lib/qbxml/types.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Qbxml::Types
|
|
2
|
+
require 'bigdecimal'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
XML_DIRECTIVES = {
|
|
6
|
+
:qb => :qbxml,
|
|
7
|
+
:qbpos => :qbposxml
|
|
8
|
+
}.freeze
|
|
9
|
+
|
|
10
|
+
FLOAT_CAST = Proc.new {|d| d ? d.to_f : 0.0}
|
|
11
|
+
BOOL_CAST = Proc.new {|d| d ? (d.to_s.downcase == 'true' ? true : false) : false }
|
|
12
|
+
DATE_CAST = Proc.new {|d| d ? Date.parse(d).strftime("%Y-%m-%d") : Date.today.strftime("%Y-%m-%d") }
|
|
13
|
+
TIME_CAST = Proc.new {|d| d ? Time.parse(d).xmlschema : Time.now.xmlschema }
|
|
14
|
+
INT_CAST = Proc.new {|d| d ? Integer(d.to_i) : 0 }
|
|
15
|
+
STR_CAST = Proc.new {|d| d ? String(d) : ''}
|
|
16
|
+
BIGDECIMAL_CAST = Proc.new {|d| d ? BigDecimal(d) : 0.0}
|
|
17
|
+
|
|
18
|
+
TYPE_MAP= {
|
|
19
|
+
"AMTTYPE" => FLOAT_CAST,
|
|
20
|
+
"BOOLTYPE" => BOOL_CAST,
|
|
21
|
+
"DATETIMETYPE" => TIME_CAST,
|
|
22
|
+
"DATETYPE" => DATE_CAST,
|
|
23
|
+
"ENUMTYPE" => STR_CAST,
|
|
24
|
+
"FLOATTYPE" => FLOAT_CAST,
|
|
25
|
+
"GUIDTYPE" => STR_CAST,
|
|
26
|
+
"IDTYPE" => STR_CAST,
|
|
27
|
+
"INTTYPE" => INT_CAST,
|
|
28
|
+
"PERCENTTYPE" => FLOAT_CAST,
|
|
29
|
+
"PRICETYPE" => FLOAT_CAST,
|
|
30
|
+
"QUANTYPE" => BIGDECIMAL_CAST,
|
|
31
|
+
"STRTYPE" => STR_CAST,
|
|
32
|
+
"TIMEINTERVALTYPE" => STR_CAST
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Strings in tag names that should be capitalized in QB's XML
|
|
36
|
+
ACRONYMS = ['AP', 'AR', 'COGS', 'COM', 'UOM', 'QBXML', 'UI', 'AVS', 'ID',
|
|
37
|
+
'PIN', 'SSN', 'COM', 'CLSID', 'FOB', 'EIN', 'UOM', 'PO', 'PIN', 'QB']
|
|
38
|
+
|
|
39
|
+
# Based on the regexp in ActiveSupport::Inflector.camelize
|
|
40
|
+
# Substring 1: Start of string, lower case letter, or slash
|
|
41
|
+
# Substring 2: One of the acronyms above, In Capitalized Casing
|
|
42
|
+
# Substring 3: End of string or capital letter
|
|
43
|
+
ACRONYM_REGEXP = Regexp.new("(?:(^|[a-z]|\\/))(#{ACRONYMS.map{|a| a.capitalize}.join("|")})([A-Z]|$)")
|
|
44
|
+
|
|
45
|
+
end
|
data/lib/qbxml.rb
ADDED
data/qbxml.gemspec
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
|
+
require 'qbxml/version'
|
|
5
|
+
|
|
6
|
+
Gem::Specification.new do |gem|
|
|
7
|
+
gem.name = "mrglass-qbxml"
|
|
8
|
+
gem.version = Qbxml::VERSION
|
|
9
|
+
gem.authors = ["Alex Skryl", "Jason Barnabe"]
|
|
10
|
+
gem.email = ["rut216@gmail.com", "jason.barnabe@gmail.com"]
|
|
11
|
+
gem.description = %q{Quickbooks XML Parser}
|
|
12
|
+
gem.summary = %q{Quickbooks XML Parser and Validation Tool}
|
|
13
|
+
gem.homepage = ""
|
|
14
|
+
|
|
15
|
+
gem.files = `git ls-files`.split($/)
|
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
|
18
|
+
gem.require_paths = ["lib"]
|
|
19
|
+
|
|
20
|
+
gem.add_dependency('activesupport', '>= 5.0.0')
|
|
21
|
+
gem.add_dependency('nokogiri', '~> 1.5')
|
|
22
|
+
gem.add_dependency('builder', '~> 3.0')
|
|
23
|
+
|
|
24
|
+
gem.add_development_dependency('pry')
|
|
25
|
+
gem.add_development_dependency('pry-nav')
|
|
26
|
+
gem.add_development_dependency('rspec')
|
|
27
|
+
gem.add_development_dependency('rake')
|
|
28
|
+
gem.add_development_dependency('appraisal')
|
|
29
|
+
end
|