ox-mapper 0.2.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/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.rbc
3
+ *.so
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ .idea
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ox-mapper.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Alexei Mikhailov
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,53 @@
1
+ # Ox::Mapper
2
+
3
+ Ox::Mapper's intention is to simplify creation of parsers based on [`ox`](https://github.com/ohler55/ox)
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'ox-mapper'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install ox-mapper
18
+
19
+ ## Usage
20
+
21
+ All you need to do is to setup callbacks for elements and attributes in Ruby style
22
+ ```ruby
23
+ mapper = Ox::Mapper.new
24
+
25
+ mapper.on(:book) { |book| puts book.attributes.inspect }
26
+ mapper.on(:title) { |title| title.parent[:title] = title.text }
27
+
28
+ # collected attributes should be set up explicitely
29
+ mapper.on(:author, :attributes => :name) { |e| e.parent[:author] = e[:name] }
30
+
31
+ # setup transformation for attribute "value" of "price" element
32
+ mapper.on(:price, :attributes => :value) { |e| e.parent[:price] = Float(e[:value]) }
33
+
34
+ mapper.parse(StringIO.new <<-XML) # => {:title => "Serenity", :author => "John Dow", :price => 1123.0}
35
+ <xml>
36
+ <book>
37
+ <title>Serenity</title>
38
+ <author name="John Dow" age="99" />
39
+ <price value="1123" />
40
+ </book>
41
+ </xml>
42
+ XML
43
+ ```
44
+
45
+ This API is unstable and a subject to change.
46
+
47
+ ## Contributing
48
+
49
+ 1. Fork it
50
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
51
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
52
+ 4. Push to the branch (`git push origin my-new-feature`)
53
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ # Setup spec task
5
+ RSpec::Core::RakeTask.new
data/lib/ox-mapper.rb ADDED
@@ -0,0 +1,2 @@
1
+ # coding: utf-8
2
+ require "ox/mapper"
data/lib/ox/mapper.rb ADDED
@@ -0,0 +1,36 @@
1
+ # coding: utf-8
2
+ require "ox/mapper/version"
3
+
4
+ module Ox
5
+ # Ox::Mapper's intention is to simplify usage of Ox::Sax parsers
6
+ # All you need to do is to setup callbacks for elements and attributes in Ruby style
7
+ #
8
+ # Example:
9
+ # mapper = Ox::Mapper.new
10
+ # mapper.on_element(:book) { |e| puts book.attributes.inspect }
11
+ # mapper.on_element(:title) { |e| e.parent[:title] = e.text }
12
+ #
13
+ # mapper.collect_attribute(:author => :name)
14
+ # mapper.on_element(:author) { |e| e.parent[:author] = e[:name] }
15
+ #
16
+ # # setup transformation for attribute "value" of "price" element
17
+ # mapper.on_attribute(:price => :value) { |v| Float(v) }
18
+ # mapper.on_element(:price) { |e| e.parent[:price] = e[:value] }
19
+ #
20
+ # mapper.parse(StringIO.new <<-XML) # => {:title => "Serenity", :author => "John Dow", :price => 1123.0}
21
+ # <xml>
22
+ # <book>
23
+ # <title>Serenity</title>
24
+ # <author name="John Dow" />
25
+ # <price value="1123" />
26
+ # </book>
27
+ # </xml>
28
+ # XML
29
+ module Mapper
30
+ autoload :Parser, "ox/mapper/parser"
31
+
32
+ def self.new
33
+ Parser.new
34
+ end
35
+ end # module Mapper
36
+ end
@@ -0,0 +1,45 @@
1
+ # coding: utf-8
2
+
3
+ module Ox
4
+ module Mapper
5
+ # An element representing XML-node
6
+ #
7
+ # @api private
8
+ class Element
9
+ attr_accessor :parent, :name, :text, :line, :column
10
+ attr_writer :attributes
11
+
12
+ # Initialize element with +name+
13
+ #
14
+ # @param [Symbol] name
15
+ # @param [Integer] line
16
+ # @param [Integer] column
17
+ def initialize(name, line = nil, column = nil)
18
+ @name, @line, @column = name, line, column
19
+ end
20
+
21
+ # Set element attribute
22
+ #
23
+ # @param [Symbol, String] name attribute name
24
+ # @param [Object] value attribute value
25
+ def []=(name, value)
26
+ attributes[name] = value
27
+ end
28
+
29
+ # Get attribute value
30
+ #
31
+ # @param [Symbol, String] name attribute name
32
+ # @return [Object] attribute value
33
+ def [](name)
34
+ @attributes && @attributes[name.to_sym]
35
+ end
36
+
37
+ # Get attributes hash
38
+ #
39
+ # @return [Hash] attributes
40
+ def attributes
41
+ @attributes ||= {}
42
+ end
43
+ end # class Element
44
+ end # module Mapper
45
+ end # module Ox
@@ -0,0 +1,109 @@
1
+ require 'set'
2
+ require 'ox/mapper/element'
3
+
4
+ module Ox
5
+ module Mapper
6
+ # Sax handler
7
+ #
8
+ # @api private
9
+ class Handler
10
+ OUTPUT_ENCODING = Encoding::UTF_8
11
+
12
+ def initialize
13
+ # ox supports lines and columns starting from 1.9.0
14
+ # we just need to set these ivars
15
+ @line, @column = nil, nil
16
+ @stack = []
17
+ # collected elements
18
+ @elements_callbacks = Hash.new
19
+ # collected attributes
20
+ @attributes = Hash.new
21
+ end
22
+
23
+ # Assigns +callback+ to elements with given tag +name+
24
+ #
25
+ # @param [String, Symbol] name
26
+ # @param [Proc] callback
27
+ def setup_element_callback(name, callback)
28
+ (@elements_callbacks[name.to_sym] ||= []) << callback.to_proc
29
+ end
30
+
31
+ # Collect values of attributes with given +attribute_name+
32
+ # at elements with tag of given +tag_name+
33
+ #
34
+ # @param [String, Symbol] tag_name
35
+ # @param [String, Symbol] attribute_name
36
+ def collect_attribute(tag_name, attribute_name)
37
+ (@attributes[tag_name.to_sym] ||= Set.new) << attribute_name.to_sym
38
+ end
39
+
40
+ # "start_element" handler just pushes an element to stack and assigns a pointer to parent element
41
+ #
42
+ # @api private
43
+ def start_element(name)
44
+ element = Element.new(name, @line, @column)
45
+ element.parent = top
46
+
47
+ @stack.push(element)
48
+ end
49
+
50
+ # "end_element" handler pushes an element if it is attached to callbacks
51
+ #
52
+ # @api private
53
+ def end_element(*)
54
+ element = @stack.pop
55
+
56
+ if collect_element?(element.name)
57
+ encode(element.text)
58
+
59
+ @elements_callbacks[element.name].each { |cb| cb.call(element) }
60
+ end
61
+ end
62
+
63
+ # attributes handler assigns attribute value to current element
64
+ #
65
+ # @api private
66
+ def attr(name, value)
67
+ top[name] = encode(value) if collect_attribute?(name)
68
+ end
69
+
70
+ # text handler appends given +value+ to current element +text+ attribute
71
+ #
72
+ # @api private
73
+ def text(value)
74
+ if value && top
75
+ value.strip!
76
+
77
+ if top.text
78
+ top.text << encode(value)
79
+ else
80
+ top.text = encode(value)
81
+ end
82
+ end
83
+ end
84
+ alias cdata text
85
+
86
+ private
87
+ def collect_element?(element)
88
+ element && @elements_callbacks.has_key?(element)
89
+ end
90
+
91
+ def collect_attribute?(name)
92
+ top &&
93
+ collect_element?(top.name) &&
94
+ @attributes.has_key?(top.name) &&
95
+ @attributes[top.name].include?(name)
96
+ end
97
+
98
+ # encode +str+ to utf-8
99
+ def encode(str)
100
+ str && !str.ascii_only? ? str.encode!(OUTPUT_ENCODING) : str
101
+ end # def encode
102
+
103
+ # returns top element
104
+ def top
105
+ @stack.last
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,66 @@
1
+ # coding: utf-8
2
+ require 'ox'
3
+ require 'ox/mapper/handler'
4
+
5
+ module Ox
6
+ module Mapper
7
+ # Configurable SAX-parser
8
+ #
9
+ # Usage:
10
+ # parser = Mapper.new
11
+ #
12
+ # parser.on(:offer) { |offer| puts offer }
13
+ # parser.on(:offer => [:id]) { |v| v.to_i }
14
+ class Parser
15
+ def initialize
16
+ @handler = Handler.new
17
+ end
18
+
19
+ # Parse given +io+ object
20
+ def parse(io, options = {})
21
+ Ox.sax_parse(@handler, io, options)
22
+ end
23
+
24
+ # Define a callbacks to be called when +elements+ processed
25
+ #
26
+ # @example
27
+ # parser.on(:offer, :price) { |elem| p elem }
28
+ # parser.on(:book, :attributes => [:author, :isbn]) { |book| p book[:author], book[:isbn] }
29
+ #
30
+ # @param [Array<String, Symbol>] elements elements names
31
+ # @yield [element]
32
+ # @yieldparam [Ox::Mapper::Element] element
33
+ # @option options [Array<String, Symbol>] :attributes list of collected attributes
34
+ def on(*elements, &block)
35
+ options = (Hash === elements.last) ? elements.pop : {}
36
+
37
+ attributes = Array(options[:attributes]).flatten
38
+
39
+ elements.each do |e|
40
+ @handler.setup_element_callback(e, block)
41
+
42
+ attributes.each { |attr| @handler.collect_attribute(e, attr) }
43
+ end
44
+ end
45
+ alias on_element on
46
+
47
+ # Set attribute callback
48
+ #
49
+ # @example
50
+ # parser.collect_attribute(:offer => :id, 'ns:offer' => 'ns:id')
51
+ #
52
+ # @param [Hash{String, Symbol => String, Symbol}] map hash with element names as keys
53
+ # and attributes names as value
54
+ # @deprecated
55
+ def collect_attribute(map)
56
+ warn 'Ox::Mapper::Parser#on_attribute method is deprecated and shall be removed in future versions. '\
57
+ 'Please use #on(element_name, :attributes => [...]) notation.'
58
+
59
+ map.each_pair do |k, attributes|
60
+ Array(attributes).flatten.each { |attr| @handler.collect_attribute(k, attr) }
61
+ end
62
+ end
63
+ alias on_attribute collect_attribute
64
+ end # class Parser
65
+ end # module Mapper
66
+ end # module Ox
@@ -0,0 +1,6 @@
1
+ # coding: utf-8
2
+ module Ox
3
+ module Mapper
4
+ VERSION = '0.2.0'
5
+ end
6
+ end
data/ox-mapper.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ox/mapper/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'ox-mapper'
8
+ gem.version = Ox::Mapper::VERSION
9
+ gem.authors = ['Alexei Mikhailov']
10
+ gem.email = %w(amikhailov83@gmail.com)
11
+ gem.summary = %q{Create SAX parsers based on `ox` with simple DSL}
12
+ gem.homepage = 'https://github.com/take-five/ox-mapper'
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = %w(lib)
18
+
19
+ gem.add_dependency 'ox'
20
+
21
+ gem.add_development_dependency 'rake'
22
+ gem.add_development_dependency 'rspec'
23
+ gem.add_development_dependency 'bundler'
24
+ gem.add_development_dependency 'simplecov'
25
+ end
@@ -0,0 +1,89 @@
1
+ # coding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'ox/mapper/parser'
5
+ require 'stringio'
6
+
7
+ describe Ox::Mapper::Parser do
8
+ let(:parser) { Ox::Mapper::Parser.new }
9
+ let :xml do
10
+ StringIO.new <<-XML
11
+ <xml>
12
+ <offer id="1" id2="0">
13
+ <price value="1"/>
14
+ </offer>
15
+ <offer id="2" id2="0">
16
+ <price value="2"/>
17
+ </offer>
18
+ <text>text</text>
19
+ <text>
20
+ <![CDATA[
21
+ text
22
+ ]]>
23
+ </text>
24
+ <ns:offer ns:id="3" />
25
+ </xml>
26
+ XML
27
+ end
28
+
29
+ describe '#on' do
30
+ let(:elements) { [] }
31
+
32
+ context 'when one element given' do
33
+ before { parser.on(:offer) { |e| elements << e } }
34
+ before { parser.parse(xml) }
35
+
36
+ it 'should execute given block on each given element' do
37
+ elements.should have(2).items
38
+ end
39
+ end
40
+
41
+ context 'when multiple elements given' do
42
+ before do
43
+ parser.on :offer,
44
+ :price,
45
+ 'ns:offer',
46
+ :attributes => [:id, :value, 'ns:id'] do |e|
47
+ elements << e
48
+ end
49
+ end
50
+ before { parser.parse(xml) }
51
+
52
+ subject { elements }
53
+
54
+ it { should have(5).items }
55
+
56
+ it 'should collect elements in ascending order (starting from leafs to root)' do
57
+ elements[0].name.should eq :price
58
+ elements[0][:value].should eq '1'
59
+
60
+ elements[1].name.should eq :offer
61
+ elements[1][:id].should eq '1'
62
+
63
+ elements[2].name.should eq :price
64
+ elements[2][:value].should eq '2'
65
+
66
+ elements[3].name.should eq :offer
67
+ elements[3][:id].should eq '2'
68
+
69
+ elements[4]['ns:id'].should eq '3'
70
+ end
71
+
72
+ it 'should collect parent element' do
73
+ elements[0].parent.should be elements[1]
74
+ elements[1].parent.name.should eq :xml
75
+ end
76
+ end
77
+ end
78
+
79
+ describe 'parsing element contents' do
80
+ let(:elements) { [] }
81
+ before { parser.on_element(:text) { |e| elements << e.text } }
82
+ before { parser.parse(xml) }
83
+
84
+ subject { elements }
85
+
86
+ it { should have(2).items }
87
+ it { should eq %w(text text) }
88
+ end
89
+ end
@@ -0,0 +1,13 @@
1
+ # coding: utf-8
2
+
3
+ require 'rubygems'
4
+ require 'bundler/setup'
5
+ require 'rspec'
6
+ require 'simplecov'
7
+
8
+ #RSpec.configure do |config|
9
+ #end
10
+
11
+ SimpleCov.start do
12
+ add_filter '/spec/'
13
+ end
metadata ADDED
@@ -0,0 +1,148 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ox-mapper
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.2.0
6
+ platform: ruby
7
+ authors:
8
+ - Alexei Mikhailov
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-15 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ version_requirements: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ none: false
21
+ prerelease: false
22
+ name: ox
23
+ type: :runtime
24
+ requirement: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ! '>='
27
+ - !ruby/object:Gem::Version
28
+ version: '0'
29
+ none: false
30
+ - !ruby/object:Gem::Dependency
31
+ version_requirements: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ! '>='
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ none: false
37
+ prerelease: false
38
+ name: rake
39
+ type: :development
40
+ requirement: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ! '>='
43
+ - !ruby/object:Gem::Version
44
+ version: '0'
45
+ none: false
46
+ - !ruby/object:Gem::Dependency
47
+ version_requirements: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ! '>='
50
+ - !ruby/object:Gem::Version
51
+ version: '0'
52
+ none: false
53
+ prerelease: false
54
+ name: rspec
55
+ type: :development
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ none: false
62
+ - !ruby/object:Gem::Dependency
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ! '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ none: false
69
+ prerelease: false
70
+ name: bundler
71
+ type: :development
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ none: false
78
+ - !ruby/object:Gem::Dependency
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ! '>='
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ none: false
85
+ prerelease: false
86
+ name: simplecov
87
+ type: :development
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ! '>='
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ none: false
94
+ description:
95
+ email:
96
+ - amikhailov83@gmail.com
97
+ executables: []
98
+ extensions: []
99
+ extra_rdoc_files: []
100
+ files:
101
+ - .gitignore
102
+ - .rspec
103
+ - Gemfile
104
+ - LICENSE.txt
105
+ - README.md
106
+ - Rakefile
107
+ - lib/ox-mapper.rb
108
+ - lib/ox/mapper.rb
109
+ - lib/ox/mapper/element.rb
110
+ - lib/ox/mapper/handler.rb
111
+ - lib/ox/mapper/parser.rb
112
+ - lib/ox/mapper/version.rb
113
+ - ox-mapper.gemspec
114
+ - spec/ox-mapper/parser_spec.rb
115
+ - spec/spec_helper.rb
116
+ homepage: https://github.com/take-five/ox-mapper
117
+ licenses: []
118
+ post_install_message:
119
+ rdoc_options: []
120
+ require_paths:
121
+ - lib
122
+ required_ruby_version: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ! '>='
125
+ - !ruby/object:Gem::Version
126
+ segments:
127
+ - 0
128
+ hash: 639433533
129
+ version: '0'
130
+ none: false
131
+ required_rubygems_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ! '>='
134
+ - !ruby/object:Gem::Version
135
+ segments:
136
+ - 0
137
+ hash: 639433533
138
+ version: '0'
139
+ none: false
140
+ requirements: []
141
+ rubyforge_project:
142
+ rubygems_version: 1.8.25
143
+ signing_key:
144
+ specification_version: 3
145
+ summary: Create SAX parsers based on `ox` with simple DSL
146
+ test_files:
147
+ - spec/ox-mapper/parser_spec.rb
148
+ - spec/spec_helper.rb