ox-mapper 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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