chemicals 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/CHANGELOG.md ADDED
@@ -0,0 +1,11 @@
1
+ ## 0.1.2
2
+
3
+ * Template option `:symbolize_keys` that allows hash keys to string or symbols. Default to `true`.
4
+
5
+ ## 0.1.1
6
+
7
+ * Performance improvement: Don't parse the source again if it is already a Nokogiri document (Parser).
8
+
9
+ ## 0.1.0
10
+
11
+ * Entirely removed Hashie dependency. All keys are now symbolized in the Parser (bonus: smaller memory footprint).
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in commute.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard 'minitest' do
2
+ watch(%r|^spec/(.*)_spec\.rb|)
3
+ watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
4
+ watch(%r|^spec/spec_helper\.rb|) { "spec" }
5
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Mattias Putman
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,36 @@
1
+ # Chemicals
2
+
3
+ ## TODO
4
+
5
+ * Add simple object conversion (ex. String to Fixnum)
6
+ * Add shorthand for parsing under same name of element/attribute:
7
+ `<person ch:as='#' />`
8
+ * Add support for constant (are not parsed/rendered).
9
+ * `as` with depth. ex: `ch:as='name/first_name'`
10
+ * More granular matching of nodes. ex Matching a simgle node within a collection
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ gem 'chemicals'
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install chemicals
25
+
26
+ ## Usage
27
+
28
+ TODO: Write usage instructions here
29
+
30
+ ## Contributing
31
+
32
+ 1. Fork it
33
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
34
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
35
+ 4. Push to the branch (`git push origin my-new-feature`)
36
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rake/testtask'
5
+
6
+ Rake::TestTask.new :test do |t|
7
+ t.test_files = FileList['spec/*/**_spec.rb']
8
+ end
9
+
10
+ task :default => :test
data/chemicals.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/chemicals/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.authors = ["Mattias Putman"]
6
+ s.email = ["mattias.putman@gmail.com"]
7
+ s.description = %q{Clever XML Parsing library}
8
+ s.summary = %q{Clever XML Parsing library}
9
+ s.homepage = ""
10
+
11
+ s.files = `git ls-files`.split($\)
12
+ s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
14
+ s.name = "chemicals"
15
+ s.require_paths = ["lib"]
16
+ s.version = Chemicals::VERSION
17
+
18
+ s.add_dependency 'nokogiri'
19
+
20
+ s.add_development_dependency 'rake'
21
+ s.add_development_dependency 'mocha'
22
+ s.add_development_dependency 'guard'
23
+ s.add_development_dependency 'guard-minitest'
24
+ end
data/lib/chemicals.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'nokogiri'
2
+
3
+ require 'chemicals/template'
4
+ require 'chemicals/parser'
5
+ require 'chemicals/renderer'
Binary file
@@ -0,0 +1,97 @@
1
+ module Chemicals
2
+ class Parser
3
+ def initialize template
4
+ @template = template
5
+ end
6
+
7
+ def parse source
8
+ @namespaces = {}
9
+ # get the document in nokogiri without blanks
10
+ # or if the source is already a nokogiri node, then assume it is without blanks.
11
+ root = if source.kind_of? Nokogiri::XML::Node
12
+ source.document.root
13
+ else
14
+ Nokogiri::XML(source.to_s) { |c| c.noblanks }.root
15
+ end
16
+ # delete all default namespaces and map the new ones in @namespaces
17
+ handle_namespace root
18
+ # map to a general namespace prefix => href hash
19
+ @namespaces = Hash[(@namespaces.values + root.namespace_definitions).map { |ns|
20
+ [ns.prefix, ns.href] if ns.prefix
21
+ }]
22
+ # begin parsing with the root node
23
+ parse_node root, @template.for(root.path, @namespaces)
24
+ end
25
+
26
+ private
27
+
28
+ def parse_node source, config
29
+ return nil unless config
30
+ # parse all child nodes and attribute nodes
31
+ parsed = (source.children.to_a + source.attribute_nodes).map do |node|
32
+ # parse node with the correspondent config (using node xpath)
33
+ parse_node(node, @template.for(node.path, @namespaces)) || {}
34
+ end
35
+ # reject nil values
36
+ parsed.reject! { |key, value| !value } if parsed.kind_of? Hash
37
+ # in arrays reject empty hashes
38
+ parsed.reject! { |value| value.empty? } if parsed.kind_of? Array
39
+ # we have a few cases here
40
+ parsed = case source
41
+ when Nokogiri::XML::Text
42
+ source.content
43
+ when Nokogiri::XML::Attr
44
+ source.content
45
+ when Nokogiri::XML::Element
46
+ # an array of arrays is flattened
47
+ if parsed.kind_of?(Array) && parsed.all? { |part| part.kind_of? Array }
48
+ parsed.flatten
49
+ # everything else is merged
50
+ else
51
+ parsed.inject { |result, part|
52
+ # internal arrays are added
53
+ result.merge!(part) { |key, old_value, new_value|
54
+ # This way mixed collections come together.
55
+ old_value + new_value
56
+ }
57
+ }
58
+ end
59
+ end
60
+ # If nothing got parsed but we expect a hash then make it a hash!
61
+ if parsed.empty? && config[:mode] == :merge
62
+ parsed = {}
63
+ end
64
+ # wrap in array if collecting
65
+ parsed = [parsed] if config[:mode] == :collect
66
+ # wrap in alias
67
+ parsed = {config[:as] => parsed} if config.has_key?(:as) && !config[:as].nil?
68
+ # return parsed version and reject all nil values!
69
+ parsed
70
+ end
71
+
72
+ def handle_namespace node
73
+ if node.namespace
74
+ # a namespace without a prefix is a default namespace
75
+ if !node.namespace.prefix
76
+ # not mapped this namespace?
77
+ unless @namespaces.has_key? node.namespace.href
78
+ # define the new namespace
79
+ node.document.root.add_namespace_definition "ns#{@namespaces.size}",
80
+ node.namespace.href
81
+ # add a mapping
82
+ @namespaces[node.namespace.href] = node.document.root.namespace_definitions.find { |ns|
83
+ ns.prefix && ns.href == node.namespace.href
84
+ }
85
+ end
86
+ # change the namespace
87
+ node.namespace = @namespaces[node.namespace.href]
88
+ # namespace with prefix
89
+ else
90
+ @namespaces[node.namespace.href] ||= node.namespace
91
+ end
92
+ end
93
+ # do the same for all child elements and attributes
94
+ (node.children.to_a + node.attribute_nodes).each { |child| handle_namespace child }
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,72 @@
1
+ module Chemicals
2
+ class Renderer
3
+
4
+ def initialize template
5
+ @template = template
6
+ end
7
+
8
+ def render source
9
+ # begin with a new document
10
+ document = Nokogiri::XML::Document.new
11
+ # add a temp node as a namespace referebce
12
+ document.root = Nokogiri::XML::Element.new 'temp', document
13
+ @template.add_namespaces document.root
14
+ # now render the entire document as the new root
15
+ document.root = render_node document, source, @template.raw.root
16
+ # again add all required namespaces to the real root
17
+ @template.add_namespaces document.root
18
+ # return the root
19
+ document.root
20
+ end
21
+
22
+ def render_node document, source, template, collect = true
23
+ # the template is already the template to apply this source on
24
+ # do nothing if there's no source
25
+ return nil unless source && config = @template.for(template.path)
26
+ # don't render the chemicals namespace
27
+ return nil if template.namespace && template.namespace.href == 'http://piesync.com/xml/chemicals'
28
+ # unwrap if necessary
29
+ source = source[config[:as].to_sym] if config.has_key?(:as) && !config[:as].nil? && collect
30
+ # is this a collect node? Rendering a collect node is the same as rendering the
31
+ # template for each part of the source (and render them as a non-collect node)
32
+ # The result are the rendered nodes.
33
+ if collect && config[:mode] == :collect
34
+ return source.map { |part| render_node document, part, template, false } if source
35
+ end
36
+ # render all children
37
+ rendered = (template.children.to_a + template.attribute_nodes).map do |child|
38
+ render_node document, source, child, true if source
39
+ end.flatten
40
+ # reject all nil values
41
+ rendered.reject! { |value| !value }
42
+ # we again have a few cases how to render
43
+ case template
44
+ when Nokogiri::XML::Attr
45
+ return unless source
46
+ node = Nokogiri::XML::Attr.new document, template.name
47
+ node.content = source
48
+ when Nokogiri::XML::Text
49
+ node = Nokogiri::XML::Text.new source, document if source
50
+ when Nokogiri::XML::Element
51
+ # don't render an element if it has no data in its children
52
+ return unless !rendered.empty?
53
+ node = Nokogiri::XML::Element.new template.name, document
54
+ rendered.each do |child|
55
+ # manually add attributes cause otherwise resulting in a segfault
56
+ if child.kind_of? Nokogiri::XML::Attr
57
+ node.set_attribute child.name, child.value
58
+ node.attribute(child.name).namespace = child.namespace
59
+ # add the non-attibutes
60
+ else
61
+ node.add_child child
62
+ end
63
+ end
64
+ end
65
+ # Add the correct namespaces!
66
+ node.namespace = document.root.namespace_definitions.find { |ns|
67
+ ns.href == template.namespace.href
68
+ } if node && !template.namespace.nil?
69
+ node
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,94 @@
1
+ module Chemicals
2
+ class Template
3
+
4
+ NS = 'http://piesync.com/xml/chemicals'
5
+
6
+ def initialize(template, options = {})
7
+ @template = Nokogiri::XML(template) { |c| c.noblanks }
8
+ @options = {
9
+ symbolize_keys: true
10
+ }.merge!(options)
11
+ @cache = {}
12
+ end
13
+
14
+ # returns the raw template
15
+ def raw
16
+ @template
17
+ end
18
+
19
+ # get the configuration for an xpath expression
20
+ def for path, namespaces = nil
21
+ @cache[path] ||= if namespaces
22
+ for_node @template.at(canonicalize(path), namespaces)
23
+ else
24
+ for_node @template.at(canonicalize(path))
25
+ end
26
+ end
27
+
28
+ # get the configuration for a config node
29
+ def for_node config_node
30
+ # nil if no config node
31
+ return nil unless config_node
32
+ return nil if @template.at(canonicalize(config_node.path)).nil?
33
+ # configuation is different in every case
34
+ config = case config_node
35
+ when Nokogiri::XML::Text
36
+ config_node.content == '@' ? {} : {
37
+ as: @options[:symbolize_keys] ? config_node.content.to_sym : config_node.content
38
+ }
39
+ when Nokogiri::XML::Attr
40
+ {
41
+ as: @options[:symbolize_keys] ? config_node.value.to_sym : config_node.value
42
+ }
43
+ when Nokogiri::XML::Element
44
+ as = attribute(config_node, :as)
45
+ {
46
+ as: as ? (@options[:symbolize_keys] ? as.to_sym : as) : nil,
47
+ mode: mode(config_node, as)
48
+ }
49
+ end
50
+ end
51
+
52
+ def mode config_node, as
53
+ mode = attribute(config_node, :mode)
54
+ if mode
55
+ mode.to_sym
56
+ elsif as # only merge if we parse this node.
57
+ :merge
58
+ else
59
+ nil
60
+ end
61
+ end
62
+
63
+ # add the required namespaces to a node
64
+ def add_namespaces node
65
+ raw.root.namespace_definitions.each do |ns|
66
+ node.add_namespace_definition ns.prefix, ns.href if ns.href != NS
67
+ end
68
+ end
69
+
70
+ # parse a document
71
+ def parse source
72
+ @parser ||= Parser.new self
73
+ @parser.parse source
74
+ end
75
+
76
+ # render a hash
77
+ def render source
78
+ @renderer ||= Renderer.new self
79
+ @renderer.render source
80
+ end
81
+
82
+ private
83
+
84
+ def attribute node, name
85
+ node.attribute_with_ns(name.to_s, NS).value if node.namespaced_key?(name.to_s, NS)
86
+ end
87
+
88
+ # canonicalize an xpath expression
89
+ def canonicalize xpath
90
+ # this means removing indexes
91
+ xpath.gsub(/\[[^\]]*\]/, '')
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,3 @@
1
+ module Chemicals
2
+ VERSION = "0.1.2"
3
+ end
@@ -0,0 +1,201 @@
1
+ # encoding: utf-8
2
+
3
+ require 'minitest/spec'
4
+ require 'minitest/autorun'
5
+
6
+ require 'chemicals'
7
+
8
+
9
+
10
+
11
+
12
+ describe Chemicals::Renderer do
13
+ describe 'rendering a text node' do
14
+ it 'should render the element value directly in the text node when the text node is aliased as @' do
15
+ template, raw = ChemicalsSpecHelper.test_example :simple_text
16
+ template.render('John Doe').to_xml.must_equal ChemicalsSpecHelper.format(raw)
17
+ end
18
+
19
+ it 'should render the content unwrapped from its alias' do
20
+ template, raw = ChemicalsSpecHelper.test_example :simple_text_alias
21
+ template.render(name: 'John Doe').to_xml.must_equal ChemicalsSpecHelper.format(raw)
22
+ end
23
+ end
24
+
25
+ describe 'rendering an attribute' do
26
+ it 'should render attributes with aliases' do
27
+ template, raw = ChemicalsSpecHelper.test_example :simple_attributes
28
+ template.render(full_name: 'John Doe').to_xml.must_equal ChemicalsSpecHelper.format(raw)
29
+ end
30
+ end
31
+
32
+ describe 'rendering an element' do
33
+ it 'should unwrap contents in aliased elements' do
34
+ template, raw = ChemicalsSpecHelper.test_example :simple_element_alias
35
+ template.render(individual: 'John Doe').to_xml.must_equal ChemicalsSpecHelper.format(raw)
36
+ end
37
+
38
+ it 'should skip an element when no alias is provided' do
39
+ template, raw = ChemicalsSpecHelper.test_example :skip_element
40
+ template.render(individual: 'John Doe').to_xml.must_equal ChemicalsSpecHelper.format(raw)
41
+ end
42
+ end
43
+
44
+ describe 'rendering a collection of elements' do
45
+ it 'should render each element' do
46
+ template, raw = ChemicalsSpecHelper.test_example :simple_collection
47
+ template.render(emails: [
48
+ 'john.doe@gmail.com',
49
+ 'john@acme.com'
50
+ ]).to_xml.must_equal ChemicalsSpecHelper.format(raw)
51
+ end
52
+
53
+ it 'should render multiple collections independently' do
54
+ template, raw = ChemicalsSpecHelper.test_example :multiple_collections
55
+ template.render(
56
+ contact: {
57
+ emails: [
58
+ 'john.doe@gmail.com',
59
+ 'john@acme.com'
60
+ ],
61
+ phone_numbers: ['1', '2']
62
+ }).to_xml.must_equal ChemicalsSpecHelper.format(raw)
63
+ end
64
+
65
+ it 'should be able to render collections in the presence of other elements' do
66
+ template, raw = ChemicalsSpecHelper.test_example :mixed_collection
67
+ template.render( \
68
+ contact: {
69
+ emails: [
70
+ 'john.doe@gmail.com',
71
+ 'john@acme.com'
72
+ ],
73
+ name: 'John Doe'
74
+ }).to_xml.must_equal ChemicalsSpecHelper.format(raw)
75
+ end
76
+
77
+ it 'should be able to render multiple collections in the presence of other elements' do
78
+ template, raw = ChemicalsSpecHelper.test_example :mixed_collections
79
+ template.render(
80
+ contact: {
81
+ emails: [
82
+ { address: 'john.doe@gmail.com', label: 'work' },
83
+ { address: 'john@acme.com' }
84
+ ],
85
+ phones: ['1'],
86
+ name: 'John Doe'
87
+ }).to_xml.must_equal ChemicalsSpecHelper.format(raw)
88
+ end
89
+
90
+ it 'should be able to extreme parse multiple collections in the presence of other elements' do
91
+ template, raw = ChemicalsSpecHelper.test_example :mixed_collections_extreme
92
+ template.render(
93
+ [{
94
+ emails: [
95
+ { address: 'john.doe@gmail.com', label: 'work' },
96
+ { address:'john@acme.com' }
97
+ ],
98
+ phones: ['1'],
99
+ name: 'John Doe'
100
+ },
101
+ {
102
+ phones: ['1', '2', '3'],
103
+ addresses: [{ country: 'Belgium', country_code: 'BE',
104
+ street: 'Désiré Van Monckhovenstraat', housenumber: '123' }]
105
+ }]).to_xml.must_equal ChemicalsSpecHelper.format(raw)
106
+ end
107
+ end
108
+
109
+ it 'should render mixed elements, collections and text nodes' do
110
+ template, raw, raw_render = ChemicalsSpecHelper.test_example :mixed_elements_text
111
+ template.render(
112
+ [{
113
+ name: { given: 'John', family: 'Doe' },
114
+ emails: ['john.doe@gmail.com', 'john@acme.com'],
115
+ phones: [
116
+ { country: 'Belgium', number: '1' },
117
+ { country: 'USA', number: '2' }
118
+ ]
119
+ },
120
+ {
121
+ name: { given: 'Jane' },
122
+ emails: [ 'jane.doe@gmail.com' ]
123
+ }
124
+ ]).to_xml.must_equal ChemicalsSpecHelper.format(raw_render)
125
+ end
126
+
127
+ it 'should render mixed elements, collections, text nodes, attributes and ignore useless nodes' do
128
+ template, raw, raw_render = ChemicalsSpecHelper.test_example :mixed_elements_text_attributes
129
+ template.render([
130
+ {
131
+ name: { given: 'John', family: 'Doe' },
132
+ emails: [
133
+ { label: 'home', address: 'john.doe@gmail.com' },
134
+ { address: 'john@acme.com' }
135
+ ],
136
+ phones: [
137
+ { country: 'Belgium', number: '1', system: 'sap' },
138
+ { country: 'USA', number: '2' }
139
+ ]
140
+ },
141
+ {
142
+ name: { given: 'Jane' },
143
+ emails: [{ label: 'work', address: 'jane.doe@gmail.com' }]
144
+ }
145
+ ]).to_xml.must_equal ChemicalsSpecHelper.format(raw_render)
146
+ end
147
+
148
+ describe 'rendering attributes' do
149
+ it 'should render attributes with aliases' do
150
+ template, raw = ChemicalsSpecHelper.test_example :simple_attributes
151
+ template.render(full_name: 'John Doe').to_xml.must_equal ChemicalsSpecHelper.format(raw)
152
+
153
+ end
154
+
155
+ it 'should not render attributes not mentioned in the template' do
156
+ template, raw = ChemicalsSpecHelper.test_example :ignore_attribute
157
+ template.render({}).must_equal nil
158
+ end
159
+
160
+ it 'should mix attributes and aliased text nodes' do
161
+ template, raw = ChemicalsSpecHelper.test_example :text_attributes
162
+ template.render(age: '24', name: 'John Doe').to_xml.must_equal ChemicalsSpecHelper.format(raw)
163
+
164
+ end
165
+ end
166
+
167
+ it 'should be able to render chinese characters' do
168
+ template, raw = ChemicalsSpecHelper.test_example :chinese
169
+ template.render('你好世界').to_xml.must_equal ChemicalsSpecHelper.format(raw)
170
+ end
171
+
172
+ it 'should be able to render namespaces' do
173
+ template, raw = ChemicalsSpecHelper.test_example :namespaces
174
+ template.render(
175
+ name: { age: '24', first_name: 'John', last_name: 'Doe' }).to_xml.must_equal ChemicalsSpecHelper.format(raw)
176
+
177
+ end
178
+
179
+ it 'should be reasonably fast' do
180
+ template, raw = ChemicalsSpecHelper.test_example :mixed_elements_text
181
+ 1000.times do
182
+ template.render(
183
+ [{
184
+ name: { given: 'John', family: 'Doe' },
185
+ emails: ['john.doe@gmail.com', 'john@acme.com'],
186
+ phones: [
187
+ { country: 'Belgium', number: '1' },
188
+ { country: 'USA', number: '2' }
189
+ ]
190
+ },
191
+ {
192
+ name: { given: 'Jane' },
193
+ emails: [ 'jane.doe@gmail.com' ]
194
+ }
195
+ ])
196
+ end
197
+ end
198
+ end
199
+
200
+ __END__
201
+