qbxml 0.1.3
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/.gitignore +17 -0
 - data/Gemfile +11 -0
 - data/LICENSE.txt +22 -0
 - data/README.md +95 -0
 - data/Rakefile +1 -0
 - data/TODO.md +3 -0
 - data/lib/qbxml.rb +12 -0
 - data/lib/qbxml/hash.rb +148 -0
 - data/lib/qbxml/qbxml.rb +102 -0
 - data/lib/qbxml/types.rb +39 -0
 - data/lib/qbxml/version.rb +3 -0
 - data/qbxml.gemspec +23 -0
 - data/schema/qbposxmlops30.xml +8343 -0
 - data/schema/qbxmlops70.xml +26714 -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
 - metadata +150 -0
 
    
        data/.gitignore
    ADDED
    
    
    
        data/Gemfile
    ADDED
    
    
    
        data/LICENSE.txt
    ADDED
    
    | 
         @@ -0,0 +1,22 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            Copyright (c) 2012 Alex Skryl
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            MIT License
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            Permission is hereby granted, free of charge, to any person obtaining
         
     | 
| 
      
 6 
     | 
    
         
            +
            a copy of this software and associated documentation files (the
         
     | 
| 
      
 7 
     | 
    
         
            +
            "Software"), to deal in the Software without restriction, including
         
     | 
| 
      
 8 
     | 
    
         
            +
            without limitation the rights to use, copy, modify, merge, publish,
         
     | 
| 
      
 9 
     | 
    
         
            +
            distribute, sublicense, and/or sell copies of the Software, and to
         
     | 
| 
      
 10 
     | 
    
         
            +
            permit persons to whom the Software is furnished to do so, subject to
         
     | 
| 
      
 11 
     | 
    
         
            +
            the following conditions:
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
            The above copyright notice and this permission notice shall be
         
     | 
| 
      
 14 
     | 
    
         
            +
            included in all copies or substantial portions of the Software.
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
         
     | 
| 
      
 17 
     | 
    
         
            +
            EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
         
     | 
| 
      
 18 
     | 
    
         
            +
            MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
         
     | 
| 
      
 19 
     | 
    
         
            +
            NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
         
     | 
| 
      
 20 
     | 
    
         
            +
            LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
         
     | 
| 
      
 21 
     | 
    
         
            +
            OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
         
     | 
| 
      
 22 
     | 
    
         
            +
            WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
         
     | 
    
        data/README.md
    ADDED
    
    | 
         @@ -0,0 +1,95 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # Qbxml
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            Qbxml is a QBXML parser and validation tool.
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            ## Installation
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            Add this line to your application's Gemfile:
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                gem 'qbxml'
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
            And then execute:
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                $ bundle
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
            Or install it yourself as:
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                $ gem install qbxml
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
            ## Usage
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
            ### Initialization
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
            The parser can be initialized to either Quickbooks (:qb) or Quickbooks Point of
         
     | 
| 
      
 24 
     | 
    
         
            +
            Sale (:qbpos)
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 27 
     | 
    
         
            +
            q = Qbxml.new(:qb)
         
     | 
| 
      
 28 
     | 
    
         
            +
            ```
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
            ### API Introspection
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
            Return all types defined in the schema
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 35 
     | 
    
         
            +
            q.types
         
     | 
| 
      
 36 
     | 
    
         
            +
            ```
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
            Return all types matching a certain pattern
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 41 
     | 
    
         
            +
            q.types('Customer')
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
            q.types(/Customer/)
         
     | 
| 
      
 44 
     | 
    
         
            +
            ```
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
            Print the xml template for a specific type
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 49 
     | 
    
         
            +
            puts q.describe('CustomerModRq')
         
     | 
| 
      
 50 
     | 
    
         
            +
            ```
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
            ### QBXML To Ruby
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
            Convert valid QBXML to a ruby hash
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 57 
     | 
    
         
            +
            q.from_qbxml(xml)
         
     | 
| 
      
 58 
     | 
    
         
            +
            ```
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
            ### Ruby To QBXML
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
            Convert a ruby hash to QBXML, skipping validation
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 65 
     | 
    
         
            +
            q.to_qbxml(hsh)
         
     | 
| 
      
 66 
     | 
    
         
            +
            ```
         
     | 
| 
      
 67 
     | 
    
         
            +
             
     | 
| 
      
 68 
     | 
    
         
            +
            Convert a ruby hash to QBXML and validate all types
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 71 
     | 
    
         
            +
            q.to_qbxml(hsh, validate: true)
         
     | 
| 
      
 72 
     | 
    
         
            +
            ```
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
            ## Caveats
         
     | 
| 
      
 75 
     | 
    
         
            +
             
     | 
| 
      
 76 
     | 
    
         
            +
            Correct case conversion depends on the following ActiveSupport inflection
         
     | 
| 
      
 77 
     | 
    
         
            +
            settings. Correct behaviour cannot be guaranteed if any of the following
         
     | 
| 
      
 78 
     | 
    
         
            +
            inflections are modified.
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 81 
     | 
    
         
            +
            ACRONYMS = ['AP', 'AR', 'COGS', 'COM', 'UOM', 'QBXML', 'UI', 'AVS', 'ID',
         
     | 
| 
      
 82 
     | 
    
         
            +
                        'PIN', 'SSN', 'COM', 'CLSID', 'FOB', 'EIN', 'UOM', 'PO', 'PIN', 'QB']
         
     | 
| 
      
 83 
     | 
    
         
            +
             
     | 
| 
      
 84 
     | 
    
         
            +
            ActiveSupport::Inflector.inflections do |inflect|
         
     | 
| 
      
 85 
     | 
    
         
            +
              ACRONYMS.each { |a| inflect.acronym a }
         
     | 
| 
      
 86 
     | 
    
         
            +
            end
         
     | 
| 
      
 87 
     | 
    
         
            +
            ```
         
     | 
| 
      
 88 
     | 
    
         
            +
             
     | 
| 
      
 89 
     | 
    
         
            +
            ## Contributing
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
      
 91 
     | 
    
         
            +
            1. Fork it
         
     | 
| 
      
 92 
     | 
    
         
            +
            2. Create your feature branch (`git checkout -b my-new-feature`)
         
     | 
| 
      
 93 
     | 
    
         
            +
            3. Commit your changes (`git commit -am 'Add some feature'`)
         
     | 
| 
      
 94 
     | 
    
         
            +
            4. Push to the branch (`git push origin my-new-feature`)
         
     | 
| 
      
 95 
     | 
    
         
            +
            5. Create new Pull Request
         
     | 
    
        data/Rakefile
    ADDED
    
    | 
         @@ -0,0 +1 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "bundler/gem_tasks"
         
     | 
    
        data/TODO.md
    ADDED
    
    
    
        data/lib/qbxml.rb
    ADDED
    
    | 
         @@ -0,0 +1,12 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require "qbxml/version"
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'nokogiri'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'active_support/builder'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'active_support/inflections'
         
     | 
| 
      
 6 
     | 
    
         
            +
            require 'active_support/core_ext/string'
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
            class Qbxml; end
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            require_relative 'qbxml/types.rb'
         
     | 
| 
      
 11 
     | 
    
         
            +
            require_relative 'qbxml/qbxml.rb'
         
     | 
| 
      
 12 
     | 
    
         
            +
            require_relative 'qbxml/hash.rb'
         
     | 
    
        data/lib/qbxml/hash.rb
    ADDED
    
    | 
         @@ -0,0 +1,148 @@ 
     | 
|
| 
      
 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| k.camelize } 
         
     | 
| 
      
 19 
     | 
    
         
            +
                  elsif opts[:underscore]
         
     | 
| 
      
 20 
     | 
    
         
            +
                    lambda { |k| k.underscore } 
         
     | 
| 
      
 21 
     | 
    
         
            +
                  end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                deep_convert(hash, opts, &key_proc)
         
     | 
| 
      
 24 
     | 
    
         
            +
              end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
              def to_xml(opts = {})
         
     | 
| 
      
 27 
     | 
    
         
            +
                hash = self.class.to_xml(self, opts)
         
     | 
| 
      
 28 
     | 
    
         
            +
              end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
              def self.to_xml(hash, opts = {})
         
     | 
| 
      
 31 
     | 
    
         
            +
                opts[:root], hash = hash.first
         
     | 
| 
      
 32 
     | 
    
         
            +
                opts[:attributes] = hash.delete(ATTR_ROOT)
         
     | 
| 
      
 33 
     | 
    
         
            +
                hash_to_xml(hash, opts)
         
     | 
| 
      
 34 
     | 
    
         
            +
              end
         
     | 
| 
      
 35 
     | 
    
         
            +
             
     | 
| 
      
 36 
     | 
    
         
            +
              def self.from_xml(xml, opts = {})
         
     | 
| 
      
 37 
     | 
    
         
            +
                from_hash(
         
     | 
| 
      
 38 
     | 
    
         
            +
                  xml_to_hash(Nokogiri::XML(xml).root, {}, opts), opts)
         
     | 
| 
      
 39 
     | 
    
         
            +
              end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
            private
         
     | 
| 
      
 42 
     | 
    
         
            +
             
     | 
| 
      
 43 
     | 
    
         
            +
              def self.hash_to_xml(hash, opts = {})
         
     | 
| 
      
 44 
     | 
    
         
            +
                opts = opts.dup
         
     | 
| 
      
 45 
     | 
    
         
            +
                opts[:indent]          ||= 2
         
     | 
| 
      
 46 
     | 
    
         
            +
                opts[:root]            ||= :hash
         
     | 
| 
      
 47 
     | 
    
         
            +
                opts[:attributes]      ||= (hash.delete(ATTR_ROOT) || {})
         
     | 
| 
      
 48 
     | 
    
         
            +
                opts[:xml_directive]   ||= [:xml, {}]
         
     | 
| 
      
 49 
     | 
    
         
            +
                opts[:builder]         ||= Builder::XmlMarkup.new(indent: opts[:indent])
         
     | 
| 
      
 50 
     | 
    
         
            +
                opts[:skip_types]      = true unless opts.key?(:skip_types) 
         
     | 
| 
      
 51 
     | 
    
         
            +
                opts[:skip_instruct]   = false unless opts.key?(:skip_instruct)
         
     | 
| 
      
 52 
     | 
    
         
            +
                builder = opts[:builder]
         
     | 
| 
      
 53 
     | 
    
         
            +
                
         
     | 
| 
      
 54 
     | 
    
         
            +
                unless opts.delete(:skip_instruct)
         
     | 
| 
      
 55 
     | 
    
         
            +
                  builder.instruct!(opts[:xml_directive].first, opts[:xml_directive].last)
         
     | 
| 
      
 56 
     | 
    
         
            +
                end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                builder.tag!(opts[:root], opts.delete(:attributes)) do
         
     | 
| 
      
 59 
     | 
    
         
            +
                  hash.each do |key, val| 
         
     | 
| 
      
 60 
     | 
    
         
            +
                    case val
         
     | 
| 
      
 61 
     | 
    
         
            +
                    when Hash
         
     | 
| 
      
 62 
     | 
    
         
            +
                      self.hash_to_xml(val, opts.merge({root: key, skip_instruct: true}))
         
     | 
| 
      
 63 
     | 
    
         
            +
                    when Array
         
     | 
| 
      
 64 
     | 
    
         
            +
                      val.map { |i| self.hash_to_xml(i, opts.merge({root: key, skip_instruct: true})) }
         
     | 
| 
      
 65 
     | 
    
         
            +
                    else
         
     | 
| 
      
 66 
     | 
    
         
            +
                      builder.tag!(key, val, {})
         
     | 
| 
      
 67 
     | 
    
         
            +
                    end
         
     | 
| 
      
 68 
     | 
    
         
            +
                  end
         
     | 
| 
      
 69 
     | 
    
         
            +
             
     | 
| 
      
 70 
     | 
    
         
            +
                  yield builder if block_given?
         
     | 
| 
      
 71 
     | 
    
         
            +
                end
         
     | 
| 
      
 72 
     | 
    
         
            +
              end
         
     | 
| 
      
 73 
     | 
    
         
            +
              
         
     | 
| 
      
 74 
     | 
    
         
            +
              def self.xml_to_hash(node, hash = {}, opts = {})
         
     | 
| 
      
 75 
     | 
    
         
            +
                node_hash = {CONTENT_ROOT => '', ATTR_ROOT => {}}
         
     | 
| 
      
 76 
     | 
    
         
            +
                name = node.name
         
     | 
| 
      
 77 
     | 
    
         
            +
                schema = opts[:schema]
         
     | 
| 
      
 78 
     | 
    
         
            +
             
     | 
| 
      
 79 
     | 
    
         
            +
                # Insert node hash into parent hash correctly.
         
     | 
| 
      
 80 
     | 
    
         
            +
                case hash[name]
         
     | 
| 
      
 81 
     | 
    
         
            +
                when Array then hash[name] << node_hash
         
     | 
| 
      
 82 
     | 
    
         
            +
                when Hash  then hash[name] = [hash[name], node_hash]
         
     | 
| 
      
 83 
     | 
    
         
            +
                else hash[name] = node_hash
         
     | 
| 
      
 84 
     | 
    
         
            +
                end
         
     | 
| 
      
 85 
     | 
    
         
            +
             
     | 
| 
      
 86 
     | 
    
         
            +
                # Handle child elements
         
     | 
| 
      
 87 
     | 
    
         
            +
                node.children.each do |c|
         
     | 
| 
      
 88 
     | 
    
         
            +
                  if c.element?
         
     | 
| 
      
 89 
     | 
    
         
            +
                    xml_to_hash(c, node_hash, opts)
         
     | 
| 
      
 90 
     | 
    
         
            +
                  elsif c.text? || c.cdata?
         
     | 
| 
      
 91 
     | 
    
         
            +
                    node_hash[CONTENT_ROOT] << c.content
         
     | 
| 
      
 92 
     | 
    
         
            +
                  end
         
     | 
| 
      
 93 
     | 
    
         
            +
                end
         
     | 
| 
      
 94 
     | 
    
         
            +
             
     | 
| 
      
 95 
     | 
    
         
            +
                # Handle attributes
         
     | 
| 
      
 96 
     | 
    
         
            +
                node.attribute_nodes.each { |a| node_hash[ATTR_ROOT][a.node_name] = a.value }
         
     | 
| 
      
 97 
     | 
    
         
            +
             
     | 
| 
      
 98 
     | 
    
         
            +
                # TODO: Strip text
         
     | 
| 
      
 99 
     | 
    
         
            +
                # node_hash[CONTENT_ROOT].strip!
         
     | 
| 
      
 100 
     | 
    
         
            +
             
     | 
| 
      
 101 
     | 
    
         
            +
                # Format node
         
     | 
| 
      
 102 
     | 
    
         
            +
                if node_hash.size > 2 || node_hash[ATTR_ROOT].present?
         
     | 
| 
      
 103 
     | 
    
         
            +
                  node_hash.delete(CONTENT_ROOT)
         
     | 
| 
      
 104 
     | 
    
         
            +
                elsif node_hash[CONTENT_ROOT].present?
         
     | 
| 
      
 105 
     | 
    
         
            +
                  node_hash.delete(ATTR_ROOT)
         
     | 
| 
      
 106 
     | 
    
         
            +
                  hash[name] = \
         
     | 
| 
      
 107 
     | 
    
         
            +
                    if schema
         
     | 
| 
      
 108 
     | 
    
         
            +
                      typecast(schema, node.path, node_hash[CONTENT_ROOT])
         
     | 
| 
      
 109 
     | 
    
         
            +
                    else
         
     | 
| 
      
 110 
     | 
    
         
            +
                      node_hash[CONTENT_ROOT]
         
     | 
| 
      
 111 
     | 
    
         
            +
                    end
         
     | 
| 
      
 112 
     | 
    
         
            +
                else
         
     | 
| 
      
 113 
     | 
    
         
            +
                  hash[name] = node_hash[CONTENT_ROOT]
         
     | 
| 
      
 114 
     | 
    
         
            +
                end
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
                hash
         
     | 
| 
      
 117 
     | 
    
         
            +
              end
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
            private
         
     | 
| 
      
 121 
     | 
    
         
            +
             
     | 
| 
      
 122 
     | 
    
         
            +
              def self.typecast(schema, xpath, value)
         
     | 
| 
      
 123 
     | 
    
         
            +
                type_path = xpath.gsub(/\[\d+\]/,'')
         
     | 
| 
      
 124 
     | 
    
         
            +
                type_proc = Qbxml::TYPE_MAP[schema.xpath(type_path).first.try(:text)]
         
     | 
| 
      
 125 
     | 
    
         
            +
                raise "#{xpath} is not a valid type" unless type_proc
         
     | 
| 
      
 126 
     | 
    
         
            +
                type_proc[value]
         
     | 
| 
      
 127 
     | 
    
         
            +
              end
         
     | 
| 
      
 128 
     | 
    
         
            +
             
     | 
| 
      
 129 
     | 
    
         
            +
              def self.deep_convert(hash, opts = {}, &block)
         
     | 
| 
      
 130 
     | 
    
         
            +
                hash.inject(self.new) do |h, (k,v)|
         
     | 
| 
      
 131 
     | 
    
         
            +
                  ignored = IGNORED_KEYS.include?(k) 
         
     | 
| 
      
 132 
     | 
    
         
            +
                  if ignored
         
     | 
| 
      
 133 
     | 
    
         
            +
                    h[k] = v
         
     | 
| 
      
 134 
     | 
    
         
            +
                  else
         
     | 
| 
      
 135 
     | 
    
         
            +
                    key = block_given? ? yield(k.to_s) : k
         
     | 
| 
      
 136 
     | 
    
         
            +
                    h[key] = \
         
     | 
| 
      
 137 
     | 
    
         
            +
                      case v
         
     | 
| 
      
 138 
     | 
    
         
            +
                      when Hash
         
     | 
| 
      
 139 
     | 
    
         
            +
                        deep_convert(v, &block)
         
     | 
| 
      
 140 
     | 
    
         
            +
                      when Array
         
     | 
| 
      
 141 
     | 
    
         
            +
                        v.map { |i| i.is_a?(Hash) ? deep_convert(i, &block) : i }
         
     | 
| 
      
 142 
     | 
    
         
            +
                      else v
         
     | 
| 
      
 143 
     | 
    
         
            +
                      end
         
     | 
| 
      
 144 
     | 
    
         
            +
                  end; h
         
     | 
| 
      
 145 
     | 
    
         
            +
                end
         
     | 
| 
      
 146 
     | 
    
         
            +
              end
         
     | 
| 
      
 147 
     | 
    
         
            +
             
     | 
| 
      
 148 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/qbxml/qbxml.rb
    ADDED
    
    | 
         @@ -0,0 +1,102 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            class Qbxml
         
     | 
| 
      
 2 
     | 
    
         
            +
              include Types
         
     | 
| 
      
 3 
     | 
    
         
            +
             
     | 
| 
      
 4 
     | 
    
         
            +
              SCHEMA_PATH = File.expand_path('../../../schema', __FILE__)
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
              SCHEMAS = {
         
     | 
| 
      
 7 
     | 
    
         
            +
                qb:    "#{SCHEMA_PATH}/qbxmlops70.xml",
         
     | 
| 
      
 8 
     | 
    
         
            +
                qbpos: "#{SCHEMA_PATH}/qbposxmlops30.xml" 
         
     | 
| 
      
 9 
     | 
    
         
            +
              }.freeze
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
              HIDE_IVARS = [:@doc].freeze
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
              def initialize(key = :qb)
         
     | 
| 
      
 14 
     | 
    
         
            +
                @schema = key
         
     | 
| 
      
 15 
     | 
    
         
            +
                @doc    = parse_schema(key)
         
     | 
| 
      
 16 
     | 
    
         
            +
              end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
              # returns all xml nodes matching a specified pattern
         
     | 
| 
      
 19 
     | 
    
         
            +
              #
         
     | 
| 
      
 20 
     | 
    
         
            +
              def types(pattern = nil)
         
     | 
| 
      
 21 
     | 
    
         
            +
                @types ||= @doc.xpath("//*").map { |e| e.name }.uniq
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                pattern ?
         
     | 
| 
      
 24 
     | 
    
         
            +
                  @types.select { |t| t =~ Regexp.new(pattern) } :
         
     | 
| 
      
 25 
     | 
    
         
            +
                  @types
         
     | 
| 
      
 26 
     | 
    
         
            +
              end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
              # returns the xml node for the specified type
         
     | 
| 
      
 29 
     | 
    
         
            +
              #
         
     | 
| 
      
 30 
     | 
    
         
            +
              def describe(type)
         
     | 
| 
      
 31 
     | 
    
         
            +
                @doc.xpath("//#{type}").first
         
     | 
| 
      
 32 
     | 
    
         
            +
              end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
              # converts a hash to qbxml with optional validation
         
     | 
| 
      
 35 
     | 
    
         
            +
              #
         
     | 
| 
      
 36 
     | 
    
         
            +
              def to_qbxml(hash, opts = {})
         
     | 
| 
      
 37 
     | 
    
         
            +
                hash = Qbxml::Hash.from_hash(hash, camelize: true)
         
     | 
| 
      
 38 
     | 
    
         
            +
                hash = namespace_qbxml_hash(hash) unless opts[:no_namespace] 
         
     | 
| 
      
 39 
     | 
    
         
            +
                validate_qbxml_hash(hash) if opts[:validate]
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                Qbxml::Hash.to_xml(hash, xml_directive: XML_DIRECTIVES[@schema])
         
     | 
| 
      
 42 
     | 
    
         
            +
              end
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
              # converts qbxml to a hash
         
     | 
| 
      
 45 
     | 
    
         
            +
              #
         
     | 
| 
      
 46 
     | 
    
         
            +
              def from_qbxml(xml, opts = {})
         
     | 
| 
      
 47 
     | 
    
         
            +
                hash = Qbxml::Hash.from_xml(xml, underscore: true, schema: @doc)
         
     | 
| 
      
 48 
     | 
    
         
            +
             
     | 
| 
      
 49 
     | 
    
         
            +
                opts[:no_namespace] ? hash : namespace_qbxml_hash(hash)
         
     | 
| 
      
 50 
     | 
    
         
            +
              end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
              # making this more sane so that it doesn't dump the whole schema doc to stdout
         
     | 
| 
      
 53 
     | 
    
         
            +
              # every time
         
     | 
| 
      
 54 
     | 
    
         
            +
              #
         
     | 
| 
      
 55 
     | 
    
         
            +
              def inspect
         
     | 
| 
      
 56 
     | 
    
         
            +
                prefix = "#<#{self.class}:0x#{self.__id__.to_s(16)} "
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                (instance_variables - HIDE_IVARS).each do |var|
         
     | 
| 
      
 59 
     | 
    
         
            +
                  prefix << "#{var}=#{instance_variable_get(var).inspect}"
         
     | 
| 
      
 60 
     | 
    
         
            +
                end
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                return "#{prefix}>"
         
     | 
| 
      
 63 
     | 
    
         
            +
              end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
            # private
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
              def parse_schema(key)
         
     | 
| 
      
 68 
     | 
    
         
            +
                File.open(select_schema(key)) { |f| Nokogiri::XML(f) }
         
     | 
| 
      
 69 
     | 
    
         
            +
              end
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
              def select_schema(schema_key)
         
     | 
| 
      
 72 
     | 
    
         
            +
                SCHEMAS[schema_key] || raise("invalid schema, must be one of #{SCHEMA.keys.inspect}")
         
     | 
| 
      
 73 
     | 
    
         
            +
              end
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
            # hash to qbxml
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
              def namespace_qbxml_hash(hash)
         
     | 
| 
      
 78 
     | 
    
         
            +
                node = describe(hash.keys.first)
         
     | 
| 
      
 79 
     | 
    
         
            +
                return hash unless node
         
     | 
| 
      
 80 
     | 
    
         
            +
             
     | 
| 
      
 81 
     | 
    
         
            +
                path = node.path.split('/')[1...-1].reverse
         
     | 
| 
      
 82 
     | 
    
         
            +
                path.inject(hash) { |h,p| Qbxml::Hash[ p => h ] }
         
     | 
| 
      
 83 
     | 
    
         
            +
              end
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
              def validate_qbxml_hash(hash, path = [])
         
     | 
| 
      
 86 
     | 
    
         
            +
                hash.each do |k,v|
         
     | 
| 
      
 87 
     | 
    
         
            +
                  next if k == Qbxml::HASH::ATTR_ROOT
         
     | 
| 
      
 88 
     | 
    
         
            +
                  key_path = path.dup << k
         
     | 
| 
      
 89 
     | 
    
         
            +
                  if v.is_a?(Hash)
         
     | 
| 
      
 90 
     | 
    
         
            +
                    validate_qbxml_hash(v, key_path)
         
     | 
| 
      
 91 
     | 
    
         
            +
                  else
         
     | 
| 
      
 92 
     | 
    
         
            +
                    validate_xpath(key_path)
         
     | 
| 
      
 93 
     | 
    
         
            +
                  end
         
     | 
| 
      
 94 
     | 
    
         
            +
                end
         
     | 
| 
      
 95 
     | 
    
         
            +
              end
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
              def validate_xpath(path)
         
     | 
| 
      
 98 
     | 
    
         
            +
                xpath = "/#{path.join('/')}"
         
     | 
| 
      
 99 
     | 
    
         
            +
                raise "#{xpath} is not a valid type" if @doc.xpath(xpath).empty?
         
     | 
| 
      
 100 
     | 
    
         
            +
              end
         
     | 
| 
      
 101 
     | 
    
         
            +
             
     | 
| 
      
 102 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/qbxml/types.rb
    ADDED
    
    | 
         @@ -0,0 +1,39 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module Qbxml::Types
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
              XML_DIRECTIVES = { 
         
     | 
| 
      
 4 
     | 
    
         
            +
                :qb => [:qbxml, { version: '7.0' }],
         
     | 
| 
      
 5 
     | 
    
         
            +
                :qbpos => [:qbposxml, { version: '3.0' }]
         
     | 
| 
      
 6 
     | 
    
         
            +
              }.freeze
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
              FLOAT_CAST = Proc.new {|d| d ? Float(d) : 0.0}
         
     | 
| 
      
 9 
     | 
    
         
            +
              BOOL_CAST  = Proc.new {|d| d ? (d == 'True' ? true : false) : false }
         
     | 
| 
      
 10 
     | 
    
         
            +
              DATE_CAST  = Proc.new {|d| d ? Date.parse(d).strftime("%Y-%m-%d") : Date.today.strftime("%Y-%m-%d") }
         
     | 
| 
      
 11 
     | 
    
         
            +
              TIME_CAST  = Proc.new {|d| d ? Time.parse(d).xmlschema : Time.now.xmlschema }
         
     | 
| 
      
 12 
     | 
    
         
            +
              INT_CAST   = Proc.new {|d| d ? Integer(d.to_i) : 0 }
         
     | 
| 
      
 13 
     | 
    
         
            +
              STR_CAST   = Proc.new {|d| d ? String(d) : ''}
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
              TYPE_MAP= {
         
     | 
| 
      
 16 
     | 
    
         
            +
                "AMTTYPE"          => FLOAT_CAST,
         
     | 
| 
      
 17 
     | 
    
         
            +
                "BOOLTYPE"         => BOOL_CAST,
         
     | 
| 
      
 18 
     | 
    
         
            +
                "DATETIMETYPE"     => TIME_CAST,
         
     | 
| 
      
 19 
     | 
    
         
            +
                "DATETYPE"         => DATE_CAST,
         
     | 
| 
      
 20 
     | 
    
         
            +
                "ENUMTYPE"         => STR_CAST,
         
     | 
| 
      
 21 
     | 
    
         
            +
                "FLOATTYPE"        => FLOAT_CAST,
         
     | 
| 
      
 22 
     | 
    
         
            +
                "GUIDTYPE"         => STR_CAST,
         
     | 
| 
      
 23 
     | 
    
         
            +
                "IDTYPE"           => STR_CAST,
         
     | 
| 
      
 24 
     | 
    
         
            +
                "INTTYPE"          => INT_CAST,
         
     | 
| 
      
 25 
     | 
    
         
            +
                "PERCENTTYPE"      => FLOAT_CAST,
         
     | 
| 
      
 26 
     | 
    
         
            +
                "PRICETYPE"        => FLOAT_CAST,
         
     | 
| 
      
 27 
     | 
    
         
            +
                "QUANTYPE"         => INT_CAST,
         
     | 
| 
      
 28 
     | 
    
         
            +
                "STRTYPE"          => STR_CAST,
         
     | 
| 
      
 29 
     | 
    
         
            +
                "TIMEINTERVALTYPE" => STR_CAST
         
     | 
| 
      
 30 
     | 
    
         
            +
              }
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
              ACRONYMS = ['AP', 'AR', 'COGS', 'COM', 'UOM', 'QBXML', 'UI', 'AVS', 'ID',
         
     | 
| 
      
 33 
     | 
    
         
            +
                          'PIN', 'SSN', 'COM', 'CLSID', 'FOB', 'EIN', 'UOM', 'PO', 'PIN', 'QB']
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
              ActiveSupport::Inflector.inflections do |inflect|
         
     | 
| 
      
 36 
     | 
    
         
            +
                ACRONYMS.each { |a| inflect.acronym a }
         
     | 
| 
      
 37 
     | 
    
         
            +
              end
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
            end
         
     |