peachy 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/README.rdoc +54 -0
- data/lib/invalid_proxy_parameters.rb +10 -0
- data/lib/method_not_in_ruby_convention.rb +12 -0
- data/lib/no_matching_xml_part.rb +5 -0
- data/lib/peachy/childless_proxy_with_attributes.rb +20 -0
- data/lib/peachy/convention_checks.rb +11 -0
- data/lib/peachy/method_mask.rb +11 -0
- data/lib/peachy/method_name.rb +31 -0
- data/lib/peachy/proxy.rb +150 -0
- data/lib/peachy/string_styler.rb +28 -0
- data/lib/peachy/version.rb +3 -0
- data/lib/peachy.rb +12 -0
- metadata +91 -0
data/README.rdoc
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
= Peachy
|
2
|
+
|
3
|
+
Peachy dynamically slurps XML from an underlying XML DOM, creating ruby methods
|
4
|
+
on the fly to match the elements and attributes of the XML.
|
5
|
+
|
6
|
+
Source available at http://github.com/njpearman/Peachy
|
7
|
+
|
8
|
+
== Install
|
9
|
+
There is no gem available for Peachy yet. This is the next bit of work to
|
10
|
+
complete. In the meantime, if you wish to mess around with Peachy, download and
|
11
|
+
include the source files in /lib within your project.
|
12
|
+
|
13
|
+
== Usage
|
14
|
+
The Peachy::Proxy is the key class in Peachy. Create a new instance of a
|
15
|
+
Peachy::Proxy passing in either a raw XML string, or a Nokogiri::XML instance
|
16
|
+
|
17
|
+
proxy = Peachy::Proxy.new :xml => '<xml><node>Peachy</node></xml>'
|
18
|
+
|
19
|
+
or
|
20
|
+
|
21
|
+
proxy = Peachy::Proxy.new :nokogiri => Nokogiri::XML('<xml><node>Peachy</node></xml>')
|
22
|
+
|
23
|
+
Once you have a Proxy, it's straightforward to drill down through the XML by
|
24
|
+
node name:
|
25
|
+
|
26
|
+
puts 'Contents: ' + proxy.xml.node
|
27
|
+
-> Contents: Peachy
|
28
|
+
|
29
|
+
Peachy expects method names to be called in the Ruby convention of lowercase with
|
30
|
+
underscores. It will do it's best to match method names to elements and attributes
|
31
|
+
following different conventions (currently, this is camelCaseNames, PascalCaseNames
|
32
|
+
or hyphen-separated-names)
|
33
|
+
|
34
|
+
More detailed usage examples can be found in the .rb files in the /test directory.
|
35
|
+
|
36
|
+
=== XML parsing
|
37
|
+
|
38
|
+
Currently, Peachy is hard wired to use Nokogiri for XML parsing, but this will
|
39
|
+
be abstracted out at some point.
|
40
|
+
|
41
|
+
=== Elements and Attributes
|
42
|
+
|
43
|
+
Currently, elements and attributes are accessed in exactly the same way; that is,
|
44
|
+
call a method on your current node matching the attribute or element name that
|
45
|
+
is required next.
|
46
|
+
As Peachy is just for slurping XML, it makes it easy to know how to access
|
47
|
+
the property that you're after if the conventions are the same for noth elements
|
48
|
+
and attributes.
|
49
|
+
|
50
|
+
=== No method name match
|
51
|
+
|
52
|
+
Peachy is currently short-tempered, in that if no element or attribute match is
|
53
|
+
found when drilling down through proxies, a NoMatchingXmlPart error will be
|
54
|
+
raised.
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class InvalidProxyParameters < Exception
|
2
|
+
def initialize parameters_hash={}
|
3
|
+
parameters_string = []
|
4
|
+
parameters_hash.each {|pair| parameters_string << ":#{pair.first} = #{pair.last.nil?? 'nil' : pair.last}" }
|
5
|
+
super <<MESSAGE
|
6
|
+
The parameters that you passed to the Proxy were invalid.
|
7
|
+
#{parameters_string.sort * "\n"}
|
8
|
+
MESSAGE
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class MethodNotInRubyConvention < Exception
|
2
|
+
def initialize method_name
|
3
|
+
super(MessageTemplate.gsub /method_name/, method_name.to_s)
|
4
|
+
end
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
MessageTemplate = <<EOF
|
9
|
+
You've tried to infer method_name using Peachy, but Peachy doesn't currently like defining methods that break Ruby convention.
|
10
|
+
Please use methods matching ^[a-z]+(?:_[a-z]+)?{0,}$ and Peachy will try to do the rest with your XML.*/
|
11
|
+
EOF
|
12
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Peachy
|
2
|
+
class ChildlessProxyWithAttributes < Proxy
|
3
|
+
def value
|
4
|
+
@nokogiri_node.content
|
5
|
+
end
|
6
|
+
|
7
|
+
private
|
8
|
+
def generate_method_for_xml method_name
|
9
|
+
check_for_convention(method_name)
|
10
|
+
match = find_match_by_attributes method_name, nokogiri_node
|
11
|
+
raise NoMatchingXmlPart.new method_name if match.nil?
|
12
|
+
return create_content_child(match) {|child| define_child method_name, child }
|
13
|
+
end
|
14
|
+
|
15
|
+
def find_match_by_attributes method_name, node
|
16
|
+
mapped = method_name.variations.map {|variation| node.attribute variation }
|
17
|
+
mapped.find {|match| match != nil }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Peachy
|
2
|
+
module ConventionChecks
|
3
|
+
def check_for_convention method_name
|
4
|
+
raise MethodNotInRubyConvention.new(method_name) unless matches_convention(method_name)
|
5
|
+
end
|
6
|
+
|
7
|
+
def matches_convention method_name
|
8
|
+
method_name.to_s =~ /^[a-z]+(?:_[a-z]+){0,}$/
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Peachy
|
2
|
+
class MethodName
|
3
|
+
include StringStyler
|
4
|
+
|
5
|
+
def initialize method_name
|
6
|
+
@method_name = method_name.to_s
|
7
|
+
end
|
8
|
+
|
9
|
+
# Returns an array of distinct valid variations in the method name.
|
10
|
+
# The valid varations are the underlying method name, plus all variations
|
11
|
+
# provided by methods defined in the StringStyler module.
|
12
|
+
def variations
|
13
|
+
(variation_methods.inject([@method_name]) do |array, method|
|
14
|
+
array << send(method)
|
15
|
+
end).uniq
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
return @method_name
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_sym
|
23
|
+
return @method_name.to_sym
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def variation_methods
|
28
|
+
Peachy::StringStyler.private_instance_methods
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/peachy/proxy.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
module Peachy
|
5
|
+
class Proxy
|
6
|
+
alias_method :original_method_missing, :method_missing
|
7
|
+
extend MethodMask
|
8
|
+
include ConventionChecks
|
9
|
+
|
10
|
+
# This hides all public methods on the class except for 'methods' and
|
11
|
+
# 'respond_to?' and 'inspect', which I've found are too useful to hide for
|
12
|
+
# the time being.
|
13
|
+
hide_public_methods ['methods', 'respond_to?', 'inspect']
|
14
|
+
|
15
|
+
# Takes a hash as an argument. Valid keys are:
|
16
|
+
# :xml -
|
17
|
+
# used to pass raw XML into the Proxy, and the XML parser will be
|
18
|
+
# created on the fly.
|
19
|
+
# :nokogiri -
|
20
|
+
# can be used to pass in a Nokogiri::XML instance, if one has
|
21
|
+
# already been created.
|
22
|
+
def initialize arguments
|
23
|
+
@nokogiri_node = arguments[:nokogiri]
|
24
|
+
@xml = arguments[:xml]
|
25
|
+
end
|
26
|
+
|
27
|
+
# Overloaded so that calls to methods representative of an XML element or
|
28
|
+
# attribute can be generated dynamically.
|
29
|
+
#
|
30
|
+
# For example, if a proxy is referenced in code as proxy.first_node, and
|
31
|
+
# first_node does match a child of the DOM encapsulated by proxy, then
|
32
|
+
# first_node will be generated on proxy.
|
33
|
+
#
|
34
|
+
# However, if first_node does not represent a child in the underlying DOM
|
35
|
+
# then proxy will raise a NoMatchingXmlPart error.
|
36
|
+
#
|
37
|
+
# The method name used to access an XML node has to follow the standard Ruby
|
38
|
+
# convention for method names, i.e. ^[a-z]+(?:_[a-z]+)?{0,}$. If an attempt
|
39
|
+
# is made to call a method on a Peachy::Proxy that breaks this convention,
|
40
|
+
# Peachy will simply raise a MethodNotInRubyConvention error.
|
41
|
+
#
|
42
|
+
# Any calls to undefined methods that include arguments or a block will be
|
43
|
+
# deferred to the default implementation of method_missing.
|
44
|
+
def method_missing method_name_symbol, *args, &block
|
45
|
+
original_method_missing method_name_symbol, args, &block if args.any? or block_given?
|
46
|
+
generate_method_for_xml MethodName.new(method_name_symbol)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def generate_method_for_xml method_name
|
51
|
+
check_for_convention(method_name)
|
52
|
+
attribute_content = create_from_parent_with_attribute method_name, nokogiri_node
|
53
|
+
return attribute_content unless attribute_content.nil?
|
54
|
+
create_method_for_child_or_content method_name, nokogiri_node
|
55
|
+
end
|
56
|
+
|
57
|
+
def nokogiri_node
|
58
|
+
raise InvalidProxyParameters.new(:xml => nil, :nokogiri => nil) if variables_are_nil?
|
59
|
+
@nokogiri_node ||= Nokogiri::XML(@xml)
|
60
|
+
end
|
61
|
+
|
62
|
+
def variables_are_nil?
|
63
|
+
@xml.nil? and @nokogiri_node.nil?
|
64
|
+
end
|
65
|
+
|
66
|
+
def create_method_for_child_or_content method_name, node
|
67
|
+
matches = find_matches(method_name, node)
|
68
|
+
return create_from_element_list method_name, matches if matches.size > 1
|
69
|
+
return create_from_element(matches[0]) {|child| define_child method_name, child }
|
70
|
+
end
|
71
|
+
|
72
|
+
# Runs the xpath for the method name against the underlying XML DOM, raising
|
73
|
+
# a NoMatchingXmlPart if no element or attribute matching the method name is
|
74
|
+
# found in the children of the current location in the DOM.
|
75
|
+
def find_matches method_name, node #=nokogiri_node
|
76
|
+
matches = node.xpath(xpath_for(method_name))
|
77
|
+
raise NoMatchingXmlPart.new(method_name) if matches.length < 1
|
78
|
+
return matches
|
79
|
+
end
|
80
|
+
|
81
|
+
def xpath_for method_name
|
82
|
+
method_name.variations.map {|variation| "./#{variation}" } * '|'
|
83
|
+
end
|
84
|
+
|
85
|
+
def create_from_parent_with_attribute method_name, node
|
86
|
+
if there_are_child_nodes?(node) and node_has_attributes?(node)
|
87
|
+
match = node.attribute(method_name.to_s)
|
88
|
+
create_content_child(match) {|child| define_child method_name, child } unless match.nil?
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def create_from_element_list method_name, matches
|
93
|
+
define_method(method_name) { return matches_to_array matches }
|
94
|
+
end
|
95
|
+
|
96
|
+
def matches_to_array matches
|
97
|
+
matches.inject([]) {|array, child| array << create_from_element(child) }
|
98
|
+
end
|
99
|
+
|
100
|
+
def create_from_element match, &block
|
101
|
+
return create_proxy match, &block if there_are_child_nodes?(match)
|
102
|
+
return create_proxy_with_attributes match, &block if node_has_attributes?(match)
|
103
|
+
return create_content_child match, &block
|
104
|
+
end
|
105
|
+
|
106
|
+
def node_has_attributes? match
|
107
|
+
match.attribute_nodes.size > 0
|
108
|
+
end
|
109
|
+
|
110
|
+
# Determines whether the given element contains any child elements or not.
|
111
|
+
# The choice of implementation is based on performance tests between using
|
112
|
+
# XPath and a Ruby iterator.
|
113
|
+
def there_are_child_nodes? match
|
114
|
+
match.children.any? {|child| child.kind_of? Nokogiri::XML::Element }
|
115
|
+
end
|
116
|
+
|
117
|
+
def create_content_child match, &block
|
118
|
+
create_child match.content, &block
|
119
|
+
end
|
120
|
+
|
121
|
+
def create_proxy match, &block
|
122
|
+
create_child Proxy.new(:nokogiri => match), &block
|
123
|
+
end
|
124
|
+
|
125
|
+
def create_proxy_with_attributes match, &block
|
126
|
+
create_child ChildlessProxyWithAttributes.new(:nokogiri => match), &block
|
127
|
+
end
|
128
|
+
|
129
|
+
def create_child child
|
130
|
+
yield child if block_given?
|
131
|
+
return child
|
132
|
+
end
|
133
|
+
|
134
|
+
def define_child method_name, child
|
135
|
+
define_method(method_name) { return child }
|
136
|
+
end
|
137
|
+
|
138
|
+
# I don't like this hacky way of getting hold of the singleton class to define
|
139
|
+
# a method, but it's better than instance_eval'ing a dynamic string to define
|
140
|
+
# a method.
|
141
|
+
def define_method method_name, &block
|
142
|
+
get_my_singleton_class.class_eval { define_method method_name.to_sym, &block }
|
143
|
+
yield
|
144
|
+
end
|
145
|
+
|
146
|
+
def get_my_singleton_class
|
147
|
+
(class << self; self; end)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Peachy
|
2
|
+
# Included in Peachy::MethodName. All methods in this module expect an instance
|
3
|
+
# variable named @method_name to be defined.
|
4
|
+
module StringStyler
|
5
|
+
private
|
6
|
+
module Stripper
|
7
|
+
# This is a bit rubbish. Need to include the method somewhere other than
|
8
|
+
# the private instance block in StringStyler.
|
9
|
+
def strip_underscores_and_upcase string
|
10
|
+
string.gsub(/_([a-z])/){|s| s.upcase}.delete('_')
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
include Stripper
|
15
|
+
|
16
|
+
def as_camel_case
|
17
|
+
strip_underscores_and_upcase(@method_name)
|
18
|
+
end
|
19
|
+
|
20
|
+
def as_pascal_case
|
21
|
+
strip_underscores_and_upcase(@method_name.capitalize)
|
22
|
+
end
|
23
|
+
|
24
|
+
def as_hyphen_separated
|
25
|
+
@method_name.gsub(/_/, '-')
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/peachy.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'invalid_proxy_parameters')
|
2
|
+
require File.join(File.dirname(__FILE__), 'method_not_in_ruby_convention')
|
3
|
+
require File.join(File.dirname(__FILE__), 'no_matching_xml_part')
|
4
|
+
require File.join(File.dirname(__FILE__), 'peachy/convention_checks')
|
5
|
+
require File.join(File.dirname(__FILE__), 'peachy/string_styler')
|
6
|
+
require File.join(File.dirname(__FILE__), 'peachy/method_name')
|
7
|
+
require File.join(File.dirname(__FILE__), 'peachy/method_mask')
|
8
|
+
require File.join(File.dirname(__FILE__), 'peachy/proxy')
|
9
|
+
require File.join(File.dirname(__FILE__), 'peachy/childless_proxy_with_attributes')
|
10
|
+
|
11
|
+
module Peachy
|
12
|
+
end
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: peachy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 2
|
9
|
+
version: 0.1.2
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- NJ Pearman
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-05-05 00:00:00 +01:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: nokogiri
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 1
|
29
|
+
- 3
|
30
|
+
- 3
|
31
|
+
version: 1.3.3
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
description: |
|
35
|
+
Peachy is an XML slurper that sits on to of existing XML parsers. It dynamically
|
36
|
+
creates object-style hierachies for simple ingration of XML data sources.
|
37
|
+
|
38
|
+
email:
|
39
|
+
- n.pearman@gmail.com
|
40
|
+
executables: []
|
41
|
+
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files:
|
45
|
+
- README.rdoc
|
46
|
+
files:
|
47
|
+
- lib/peachy/convention_checks.rb
|
48
|
+
- lib/peachy/childless_proxy_with_attributes.rb
|
49
|
+
- lib/peachy/method_name.rb
|
50
|
+
- lib/peachy/method_mask.rb
|
51
|
+
- lib/peachy/version.rb
|
52
|
+
- lib/peachy/proxy.rb
|
53
|
+
- lib/peachy/string_styler.rb
|
54
|
+
- lib/no_matching_xml_part.rb
|
55
|
+
- lib/invalid_proxy_parameters.rb
|
56
|
+
- lib/method_not_in_ruby_convention.rb
|
57
|
+
- lib/peachy.rb
|
58
|
+
- README.rdoc
|
59
|
+
has_rdoc: true
|
60
|
+
homepage: http://github.com/njpearman/peachy
|
61
|
+
licenses: []
|
62
|
+
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options:
|
65
|
+
- --main
|
66
|
+
- README.rdoc
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
version: "0"
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
segments:
|
81
|
+
- 0
|
82
|
+
version: "0"
|
83
|
+
requirements: []
|
84
|
+
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 1.3.6
|
87
|
+
signing_key:
|
88
|
+
specification_version: 3
|
89
|
+
summary: Peachy gives a very simple object-style interface on top of an XML DOM.
|
90
|
+
test_files: []
|
91
|
+
|