chemicals 0.1.2

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,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
+