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 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,5 @@
1
+ class NoMatchingXmlPart < Exception
2
+ def initialize method_name
3
+ super "#{method_name} is not contained as a child of this node."
4
+ end
5
+ 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,11 @@
1
+ module Peachy
2
+ module MethodMask
3
+ def hide_public_methods exceptions
4
+ methods_to_hide = public_instance_methods.clone
5
+ exceptions.each do |to_stay_public|
6
+ methods_to_hide.delete(to_stay_public)
7
+ end
8
+ private *methods_to_hide
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Peachy
2
+ VERSION = '0.1.2'
3
+ 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
+