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 ADDED
@@ -0,0 +1,74 @@
1
+ INITIALIZATION
2
+ ------------------------------------------------------------------------------
3
+
4
+ # supported data types are:
5
+ # :qb - Quickbooks
6
+ # :qbpos - Quickbooks Point Of Sale
7
+
8
+ api = Quickbooks::API.instance(:qbpos)
9
+
10
+ # shorthand initialization
11
+ api = Quickbooks::API[:qbpos]
12
+
13
+
14
+ API INTROSPECTION
15
+ ------------------------------------------------------------------------------
16
+
17
+ # returns all available wrapper classes
18
+ api.qbxml_classes
19
+
20
+ # returns the top level wrapper class for the api
21
+ api.container
22
+
23
+ # find specific qbxml class by name
24
+ api.find('customer_mod_rq')
25
+
26
+ # find all qbxml classes that contain a pattern
27
+ api.grep(/_mod_rq/)
28
+
29
+ # returns a hash of all the data types for a wrapper class
30
+ wrapper_class.template
31
+
32
+ # returns the full teplate for a wrapper class (SLOOOW for top level classes)
33
+ wrapper_class.template(true)
34
+
35
+ # returns all the supported fields for the wrapper class
36
+ wrapper_class.attribute_names
37
+
38
+ # returns the qbxml template used to generate the wrapper class
39
+ wrapper_class.xml_template
40
+
41
+
42
+ QBXML TO RUBY
43
+ ------------------------------------------------------------------------------
44
+
45
+ # wrap qbxml data in a qbxml object
46
+ o = api.qbxml_to_obj(qbxml)
47
+
48
+ # convert qbxml object to hash
49
+ o.inner_attributes
50
+
51
+ # same as above but includes the top level containers
52
+ o.attributes
53
+
54
+ # retrieves attributes from nested objects
55
+ o.attributes(true)
56
+
57
+ # directly convert raw qbxml to a hash
58
+ h = api.qbxml_to_hash(qbxml)
59
+
60
+ # same as above but includes the top level containers
61
+ h = api.qbxml_to_hash(qbxml, true)
62
+
63
+
64
+ RUBY TO QBXML
65
+ ------------------------------------------------------------------------------
66
+
67
+ # convert a hash to a qbxml object (automagically creates the top level containers)
68
+ o = api.hash_to_obj(data_hash)
69
+
70
+ # convert a qbxml object to raw qbxml
71
+ o.to_qbxml.to_s
72
+
73
+ # convert a hash directly to raw qbxml
74
+ qbxml = api.hash_to_qbxml(data_hash)
data/TODO ADDED
@@ -0,0 +1,5 @@
1
+ - tests
2
+ - support for node type constraints and validation
3
+ - hash to obj doesn't work when converting from an inner level hash ex. 'customer_mod_rq'
4
+ - add processing instruction to node set <?qbposxml version="3.0"?>
5
+ - handle node attributes properly
data/lib/quickbooks.rb CHANGED
@@ -1,17 +1,19 @@
1
1
  require 'rubygems'
2
- require 'ruby2ruby' # must be version 1.2.1 of ruby2ruby
3
- require 'nokogiri'
4
- require 'active_support'
5
- require 'active_support/core_ext'
6
2
 
7
3
  module Quickbooks; end
4
+ module Quickbooks::QBXML; end
5
+ module Quickbooks::QBPOSXML; end
6
+ module Quickbooks::Support; end
7
+ module Quickbooks::Parser; end
8
8
 
9
- require 'quickbooks/support'
10
- require 'quickbooks/support/api'
11
- require 'quickbooks/support/logger'
12
- require 'quickbooks/support/qbxml'
13
- require 'quickbooks/support/class_builder'
14
- require 'quickbooks/qbxml_base'
9
+ require 'quickbooks/support/monkey_patches'
10
+ require 'quickbooks/support/inflection'
11
+ require 'quickbooks/logger'
12
+ require 'quickbooks/config'
13
+ require 'quickbooks/parser/xml_parsing'
14
+ require 'quickbooks/parser/xml_generation'
15
+ require 'quickbooks/parser/class_builder'
16
+ require 'quickbooks/parser/qbxml_base'
15
17
  require 'quickbooks/qbxml_parser'
16
18
  require 'quickbooks/dtd_parser'
17
19
  require 'quickbooks/api'
@@ -1,143 +1,103 @@
1
1
  class Quickbooks::API
2
- include Quickbooks::Support
3
- include Quickbooks::Support::API
4
- include Quickbooks::Support::QBXML
2
+ include Quickbooks::Logger
3
+ include Quickbooks::Config
4
+ include Quickbooks::Support::Inflection
5
5
 
6
- attr_reader :dtd_parser, :qbxml_parser, :schema_type
7
- @@instances = {}
6
+ attr_reader :dtd_parser, :qbxml_parser, :schema_type
7
+ private_class_method :new
8
+ @@instances = {}
8
9
 
9
- def initialize(schema_type = nil, opts = {})
10
- @schema_type = schema_type
11
- use_disk_cache, log_level = opts.values_at(:use_disk_cache, :log_level)
10
+ def initialize(schema_type = nil, opts = {})
11
+ self.class.check_schema_type!(schema_type)
12
+ @schema_type = schema_type
12
13
 
13
- unless valid_schema_type?
14
- raise(ArgumentError, "schema type required: #{valid_schema_types.inspect}")
15
- end
14
+ @dtd_parser = Quickbooks::DtdParser.new(schema_type)
15
+ @qbxml_parser = Quickbooks::QbxmlParser.new(schema_type)
16
16
 
17
- @dtd_file = get_dtd_file
18
- @dtd_parser = DtdParser.new(schema_type)
19
- @qbxml_parser = QbxmlParser.new(schema_type)
17
+ load_qb_classes
18
+ @@instances[schema_type] = self
19
+ end
20
20
 
21
- load_qb_classes(use_disk_cache)
22
- @@instances[schema_type] = self
23
- end
21
+ # simple singleton constructor without caching support
22
+ #
23
+ def self.[](schema_type)
24
+ @@instances[schema_type] || new(schema_type)
25
+ end
24
26
 
25
- # returns the last created api instance
26
- def self.[](schema_type)
27
- @@instances[schema_type] || self.new(schema_type)
28
- end
27
+ # full singleton constructor
28
+ #
29
+ def self.instance(schema_type = nil, opts = {})
30
+ @@instances[schema_type] || new(schema_type, opts)
31
+ end
29
32
 
30
- def container
31
- get_container_class
32
- end
33
+ # user friendly api decorators. Not used anywhere else.
34
+ #
35
+ def container
36
+ container_class
37
+ end
33
38
 
34
- def qbxml_classes
35
- cached_classes
36
- end
39
+ def qbxml_classes
40
+ cached_classes
41
+ end
37
42
 
38
- def find(class_name)
39
- class_name = class_name.to_s
40
- cached_classes.find { |c| to_attribute_name(c) == class_name }
41
- end
43
+ # api introspection
44
+ #
45
+ def find(class_name)
46
+ cached_classes.find { |c| underscore(c) == class_name.to_s }
47
+ end
42
48
 
43
- def grep(pattern)
44
- case pattern
45
- when Regexp
46
- cached_classes.select { |c| to_attribute_name(c).match(pattern) }
47
- when String
48
- cached_classes.select { |c| to_attribute_name(c).include?(pattern) }
49
+ def grep(pattern)
50
+ cached_classes.select { |c| underscore(c).match(/#{pattern}/) }
49
51
  end
50
- end
51
52
 
52
- # QBXML 2 RUBY
53
+ # QBXML 2 RUBY
53
54
 
54
- def qbxml_to_obj(qbxml)
55
- case qbxml
56
- when IO
57
- qbxml_parser.parse_file(qbxml)
58
- else
55
+ def qbxml_to_obj(qbxml)
59
56
  qbxml_parser.parse(qbxml)
60
57
  end
61
- end
62
58
 
63
- def qbxml_to_hash(qbxml, include_container = false)
64
- qb_obj = qbxml_to_obj(qbxml)
65
- unless include_container
66
- qb_obj.inner_attributes
67
- else
68
- qb_obj.attributes
59
+ def qbxml_to_hash(qbxml, include_container = false)
60
+ if include_container
61
+ qbxml_to_obj(qbxml).attributes
62
+ else
63
+ qbxml_to_obj(qbxml).inner_attributes
64
+ end
69
65
  end
70
- end
71
-
72
66
 
73
- # RUBY 2 QBXML
67
+ # RUBY 2 QBXML
74
68
 
75
- def hash_to_obj(data)
76
- key = data.keys.first
77
- value = data[key]
69
+ def hash_to_obj(data)
70
+ key, value = data.detect { |name, value| name != :xml_attributes }
71
+ key_path = container_class.template(true).path_to_nested_key(key.to_s)
72
+ raise(RuntimeError, "#{key} class not found in api template") unless key_path
78
73
 
79
- key_path = find_nested_key(container.template(true), key.to_s)
80
- raise(RuntimeError, "#{key} class not found in api template") unless key_path
81
-
82
- wrapped_data = build_hash_wrapper(key_path, value)
83
- container.new(wrapped_data)
84
- end
85
-
86
- def hash_to_qbxml(data)
87
- hash_to_obj(data).to_qbxml
88
- end
89
-
90
- # Disk Cache
91
-
92
- def clear_disk_cache(rebuild = false)
93
- qbxml_cache = Dir["#{get_disk_cache_path}/*.rb"]
94
- template_cache = Dir["#{get_template_cache_path}/*.yml"]
95
- File.delete(*(qbxml_cache + template_cache))
96
- load_qb_classes(rebuild)
97
- end
74
+ wrapped_data = Hash.nest(key_path, value)
75
+ container_class.new(wrapped_data)
76
+ end
98
77
 
78
+ def hash_to_qbxml(data)
79
+ hash_to_obj(data).to_qbxml
80
+ end
99
81
 
100
82
  private
101
83
 
102
-
103
- def load_qb_classes(use_disk_cache = false)
104
- if use_disk_cache
105
- disk_cache = Dir["#{get_disk_cache_path}/*.rb"]
106
- if disk_cache.empty?
107
- log.info "Warning: on disk schema cache is empty, rebuilding..."
108
- rebuild_schema_cache(false, true)
109
- else
110
- disk_cache.each {|file| require file }
111
- end
112
- else
113
- rebuild_schema_cache(false, false)
84
+ def load_qb_classes
85
+ rebuild_schema_cache(false)
86
+ load_full_container_template
87
+ container_class
114
88
  end
115
89
 
116
- # load the container class template into memory (significantly speeds up wrapping of partial data hashes)
117
- get_container_class.template(true, use_disk_cache, use_disk_cache)
118
- true
119
- end
120
-
121
- # rebuilds schema cache in memory and writes to disk if desired
122
- #
123
- def rebuild_schema_cache(force = false, write_to_disk = false)
124
- dtd_parser.parse_file(@dtd_file) if (cached_classes.empty? || force)
125
- dump_cached_classes if write_to_disk
126
- end
127
-
128
- # writes dynamically generated api classes to disk
129
- #
130
- def dump_cached_classes
131
- cached_classes.each do |c|
132
- File.open("#{get_disk_cache_path}/#{to_attribute_name(c)}.rb", 'w') do |f|
133
- f << Ruby2Ruby.translate(c)
134
- end
90
+ # rebuilds schema cache in memory
91
+ #
92
+ def rebuild_schema_cache(force = false)
93
+ dtd_parser.parse_file(dtd_file) if (cached_classes.empty? || force)
135
94
  end
136
- end
137
-
138
- def self.log
139
- @@log ||= Logger.new(STDOUT, DEFAULT_LOG_LEVEL)
140
- end
141
95
 
96
+ # load the recursive container class template into memory (significantly
97
+ # speeds up wrapping of partial data hashes)
98
+ #
99
+ def load_full_container_template(use_disk_cache = false)
100
+ container_class.template(true)
101
+ end
142
102
 
143
103
  end
@@ -0,0 +1,59 @@
1
+ module Quickbooks::Config
2
+
3
+ API_ROOT = File.join(File.dirname(__FILE__), '..', '..').freeze
4
+ XML_SCHEMA_PATH = File.join(API_ROOT, 'xml_schema').freeze
5
+ RUBY_SCHEMA_PATH = File.join(API_ROOT, 'ruby_schema').freeze
6
+
7
+ SCHEMA_MAP = {
8
+ :qb => {:dtd_file => "qbxmlops70.xml",
9
+ :namespace => Quickbooks::QBXML,
10
+ :container_class => 'QBXML'
11
+ }.freeze,
12
+ :qbpos => {:dtd_file => "qbposxmlops30.xml",
13
+ :namespace => Quickbooks::QBPOSXML,
14
+ :container_class => 'QBPOSXML'
15
+ }.freeze,
16
+ }.freeze
17
+
18
+ def self.included(klass)
19
+ klass.extend ClassMethods
20
+ end
21
+
22
+ private
23
+
24
+ def container_class
25
+ schema_namespace.const_get(SCHEMA_MAP[schema_type][:container_class])
26
+ end
27
+
28
+ def dtd_file
29
+ "#{XML_SCHEMA_PATH}/#{SCHEMA_MAP[schema_type][:dtd_file]}"
30
+ end
31
+
32
+ def schema_namespace
33
+ SCHEMA_MAP[schema_type][:namespace]
34
+ end
35
+
36
+ # introspection
37
+
38
+ def cached_classes
39
+ schema_namespace.constants.map { |const| schema_namespace.const_get(const) }
40
+ end
41
+
42
+ module ClassMethods
43
+
44
+ def check_schema_type!(schema_type)
45
+ unless SCHEMA_MAP.include?(schema_type)
46
+ raise(ArgumentError, "valid schema type required: #{valid_schema_types.inspect}")
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def valid_schema_types
53
+ SCHEMA_MAP.keys
54
+ end
55
+
56
+ end
57
+
58
+
59
+ end
@@ -1,47 +1,42 @@
1
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
2
+ include Quickbooks::Parser::ClassBuilder
9
3
 
10
4
  private
11
5
 
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)
6
+ def process_leaf_node(xml_obj, parent_class)
7
+ attr_name, qb_type = parse_leaf_node_data(xml_obj)
8
+ if parent_class
9
+ add_casting_attribute(parent_class, attr_name, qb_type)
10
+ end
16
11
  end
17
- end
18
12
 
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)
13
+ def process_non_leaf_node(xml_obj, parent_class)
14
+ klass = build_qbxml_class(xml_obj)
15
+ attr_name = underscore(xml_obj)
16
+ if parent_class
17
+ add_strict_attribute(parent_class, attr_name, klass)
18
+ end
19
+ klass
24
20
  end
25
- klass
26
- end
27
21
 
28
- def process_comment_node(xml_obj, parent_class)
29
- parent_class
30
- end
31
-
32
- # helpers
22
+ #TODO: stub
23
+ def process_comment_node(xml_obj, parent_class)
24
+ parent_class
25
+ end
33
26
 
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
- xml_obj = set_required_attributes(xml_obj)
40
- add_xml_template(klass, xml_obj.to_xml)
41
- else
42
- klass = get_schema_namespace.const_get(obj_name)
27
+ # helpers
28
+
29
+ def build_qbxml_class(xml_obj)
30
+ obj_name = xml_obj.name
31
+ unless schema_namespace.const_defined?(obj_name)
32
+ klass = Class.new(Quickbooks::Parser::QbxmlBase)
33
+ schema_namespace.const_set(obj_name, klass)
34
+ klass.xml_attributes = parse_xml_attributes(xml_obj)
35
+ add_xml_template(klass, xml_obj.to_xml)
36
+ else
37
+ klass = schema_namespace.const_get(obj_name)
38
+ end
39
+ klass
43
40
  end
44
- klass
45
- end
46
41
 
47
42
  end