dusty-noko_parser 0.0.9 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,51 @@
1
+ module Fruits; end
2
+ module Animals; end
3
+
4
+ class Fruits::Banana
5
+ include NokoParser::Properties
6
+ nodes :xpath => '//banana'
7
+
8
+ property :id, :xpath => 'id'
9
+ property :size, :xpath => 'size'
10
+
11
+ end
12
+
13
+
14
+ class Animals::Monkey
15
+ include NokoParser::Properties
16
+ nodes :xpath => '//monkey'
17
+
18
+ property :id, :xpath => '.', :attribute => 'id'
19
+
20
+ property :first_name, :xpath => 'contact/name[@type="first"]'
21
+
22
+ property :last_name, :xpath => 'contact/name[@type="last"]'
23
+
24
+ property :age, :xpath => 'yrsold'
25
+
26
+ property :funny, :xpath => 'personality', :attribute => :hilarious
27
+
28
+ property :loud, :xpath => 'personality'
29
+
30
+ property :street, :xpath => 'street'
31
+
32
+ property :city, :xpath => 'city'
33
+
34
+ property :state, :xpath => 'state'
35
+
36
+ property :zip, :xpath => 'zip'
37
+
38
+ embed :bananas, :xpath => 'bananas', :class => 'Fruits::Banana'
39
+
40
+ def address
41
+ [street, city, state].compact.join(", ") + " #{zip}"
42
+ end
43
+
44
+ def age
45
+ @age.to_i
46
+ end
47
+
48
+ def funny=(value)
49
+ @funny = (value == "true" ? true : false)
50
+ end
51
+ end
@@ -0,0 +1,52 @@
1
+ <xml>
2
+ <animals>
3
+ <elephant id="3">
4
+ <name>elephant1</name>
5
+ <trunk>large</trunk>
6
+ </elephant>
7
+ <monkey id="1">
8
+ <contact>
9
+ <name type='first'>monkey1</name>
10
+ <name type='last'>monkeylastname</name>
11
+ </contact>
12
+ <bananas>
13
+ <banana>
14
+ <id>1</id>
15
+ <size>small</size>
16
+ </banana>
17
+ <banana>
18
+ <id>2</id>
19
+ <size>medium</size>
20
+ </banana>
21
+ </bananas>
22
+ <yrsold>23</yrsold>
23
+ <personality hilarious="true">quiet</personality>
24
+ <street>300 Monkey St</street>
25
+ <city>Cincinnati</city>
26
+ <state>Oh</state>
27
+ <zip>45219</zip>
28
+ </monkey>
29
+ <monkey id="2">
30
+ <contact>
31
+ <name type='first'>monkey2</name>
32
+ <name type='last'>monkeylastname</name>
33
+ </contact>
34
+ <bananas>
35
+ <banana>
36
+ <id>3</id>
37
+ <size>large</size>
38
+ </banana>
39
+ <banana>
40
+ <id>4</id>
41
+ <size>huge</size>
42
+ </banana>
43
+ </bananas>
44
+ <yrsold>33</yrsold>
45
+ <personality hilarious="false">loud</personality>
46
+ <street>301 Monkey St</street>
47
+ <city>Cincinnati</city>
48
+ <state>Oh</state>
49
+ <zip>45219</zip>
50
+ </monkey>
51
+ </animals>
52
+ </xml>
@@ -0,0 +1,29 @@
1
+ require 'nokogiri'
2
+
3
+ module NokoParser
4
+
5
+ class NokoParserError < StandardError; end
6
+
7
+ def self.version
8
+ "0.1.0"
9
+ end
10
+
11
+ end
12
+
13
+ ## Stolen from merb-core
14
+ class Object
15
+ def full_const_get(name)
16
+ list = name.split("::")
17
+ list.shift if list.first.empty?
18
+ obj = self
19
+ list.each do |x|
20
+ # This is required because const_get tries to look for constants in the
21
+ # ancestor chain, but we only want constants that are HERE
22
+ obj = obj.const_defined?(x) ? obj.const_get(x) : obj.const_missing(x)
23
+ end
24
+ obj
25
+ end
26
+ end
27
+
28
+ require 'noko_parser/parser'
29
+ require 'noko_parser/properties'
@@ -0,0 +1,86 @@
1
+ module NokoParser
2
+
3
+ class Parser
4
+
5
+ ##
6
+ # Initialize the parser by passing in a class that has included
7
+ # NokoParser::Properties
8
+ #
9
+ # @raises [NokoParserError] unless class included NokoParser::Properties
10
+ def initialize(klass, xml)
11
+ unless klass.respond_to?('_is_noko_parser?')
12
+ raise NokoParserError, 'Class must include NokoParser::Properties'
13
+ end
14
+ @object = klass
15
+ @xml = Nokogiri::XML(xml)
16
+ @nodes = []
17
+ @text_properties = klass._text_properties
18
+ @attribute_properties = klass._attribute_properties
19
+ @embedded_properties = klass._embedded_properties
20
+ end
21
+
22
+ ##
23
+ # Runs the parser on the class. First finds the nodes that match
24
+ # and then parses the text and attribute properties on it
25
+ #
26
+ # return [Array<Object>] an array of objects
27
+ def run
28
+ @xml.xpath(@object._nodes_xpath).each do |node|
29
+ object = @object.new
30
+ parse_text_properties(node,object)
31
+ parse_attribute_properties(node,object)
32
+ parse_embedded_properties(node,object)
33
+ @nodes << object
34
+ end
35
+ @nodes
36
+ end
37
+
38
+ private
39
+
40
+ ##
41
+ # Loop through the text properties of the class and use the defined
42
+ # xpath query to find the match. If found add to the object using
43
+ # the setter.
44
+ def parse_text_properties(node,object)
45
+ @text_properties.each do |property|
46
+ if match = node.xpath(property.xpath).first
47
+ unless match.inner_text.empty?
48
+ object.send("#{property.name}=",match.inner_text)
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ ##
55
+ # Loop through the attribute properties of the class and use the defined
56
+ # xpath query to find the match. If found add to the object using
57
+ # the setter.
58
+ def parse_attribute_properties(node,object)
59
+ @attribute_properties.each do |property|
60
+ if match = node.xpath(property.xpath).first
61
+ object.send("#{property.name}=",match.attribute(property.attribute))
62
+ end
63
+ end
64
+ end
65
+
66
+ ##
67
+ # Loop through the embedded properties of the class and use the defined
68
+ # xpath query to find the match. If found add to the object using the
69
+ # setter.
70
+ def parse_embedded_properties(node,object)
71
+ @embedded_properties.each do |property|
72
+ unless parser = Object.full_const_get(property.class_name) rescue nil
73
+ raise NokoParserError,
74
+ "Class #{property.class_name} not found for embed :#{property.name}"
75
+ end
76
+ if match = node.xpath(property.xpath).first
77
+ result = parser.parse(match.to_xml)
78
+ result = result.first if property.single
79
+ object.send("#{property.name}=", result)
80
+ end
81
+ end
82
+ end
83
+
84
+ end
85
+
86
+ end
@@ -0,0 +1,199 @@
1
+ module NokoParser
2
+
3
+ module Properties
4
+
5
+ ##
6
+ # Text property is used to find the innerhtml of an element
7
+ class TextProperty
8
+ attr_reader :name, :xpath
9
+
10
+ def initialize(name,xpath)
11
+ @name = name.to_s
12
+ @xpath = xpath.to_s
13
+ end
14
+
15
+ end
16
+
17
+ ##
18
+ # Attribute property is used to find the value of an attribute of an
19
+ # element
20
+ class AttributeProperty
21
+ attr_reader :name, :xpath, :attribute
22
+
23
+ def initialize(name,xpath,attribute)
24
+ @name = name.to_s.downcase
25
+ @xpath = xpath.to_s
26
+ @attribute = attribute.to_s
27
+ end
28
+
29
+ end
30
+
31
+ ##
32
+ # Embedded property is used to find the value of sub-elements inside the
33
+ # element
34
+ #
35
+ # If single is present, only the first element will be returned
36
+ # otherwise an array is returned
37
+ class EmbeddedProperty
38
+ attr_reader :name, :xpath, :class_name, :single
39
+
40
+ def initialize(name,xpath,class_name,single)
41
+ @name = name.to_s.downcase
42
+ @xpath = xpath.to_s
43
+ @class_name = class_name.to_s
44
+ @single = single || false
45
+ end
46
+
47
+ end
48
+
49
+ def self.included(base)
50
+ base.instance_variable_set(:@_text_properties,[])
51
+ base.instance_variable_set(:@_attribute_properties,[])
52
+ base.instance_variable_set(:@_embedded_properties,[])
53
+ base.send(:extend, ClassMethods)
54
+ base.send(:include, InstanceMethods)
55
+ end
56
+
57
+ module ClassMethods
58
+
59
+ ##
60
+ # Parse an xml string
61
+ #
62
+ # @return [Array<Object>] an array of objects
63
+ # @see NokoParser::Parser
64
+ def parse(xml)
65
+ parser = NokoParser::Parser.new(self,xml)
66
+ parser.run
67
+ end
68
+
69
+ ##
70
+ # Parse an xml file
71
+ #
72
+ # @return [Array<Object>] an array of objects
73
+ # @see NokoParser::Parser
74
+ def parse_file(file)
75
+ parse(open(file))
76
+ end
77
+
78
+ ##
79
+ # Make sure this class is a noko_parser compatible class
80
+ def _is_noko_parser?
81
+ true
82
+ end
83
+
84
+ ##
85
+ # Returns the TextProperties defined on the class
86
+ #
87
+ # @return [Array<TextProperty>]
88
+ def _text_properties
89
+ @_text_properties
90
+ end
91
+
92
+ ##
93
+ # Returns the AttributeProperties defined in the class
94
+ #
95
+ # @return [Array<AttributeProperty]
96
+ def _attribute_properties
97
+ @_attribute_properties
98
+ end
99
+
100
+ ##
101
+ # Returns the EmbeddedProperties defined on the class
102
+ #
103
+ # @return [Array<EmbedProperty>]
104
+ def _embedded_properties
105
+ @_embedded_properties
106
+ end
107
+
108
+ ##
109
+ # Returns the xpath required to find the nodes
110
+ def _nodes_xpath
111
+ @_nodes_xpath || (raise NokoParserError, 'nodes :xpath => "" is required')
112
+ end
113
+
114
+ ##
115
+ # Set the xpath for finding the nodes.
116
+ #
117
+ # examples:
118
+ # nodes '//monkeys'
119
+ # nodes '//animals/monkeys/friendly'
120
+ def nodes(opts={})
121
+ unless opts[:xpath]
122
+ raise NokoParserError, ":xpath required for nodes definition"
123
+ end
124
+ @_nodes_xpath = opts[:xpath].to_s
125
+ end
126
+
127
+ ##
128
+ # Sets a property on the object. If :attribute is defined it will
129
+ # set an AttributeProperty. Otherwise, it will set a TextProperty
130
+ # :xpath => 'somexpathstring' is required. The xpath should be the
131
+ # query required to find this element inside the node.
132
+ #
133
+ # examples
134
+ # property :name, :xpath => 'MyName[@type="real_name"]'
135
+ # property :eye_color, :xpath => 'eyes', :attribute => 'color'
136
+ def property(name,opts={})
137
+ unless opts[:xpath]
138
+ raise NokoParserError, ":xpath required for #{name}"
139
+ end
140
+ if opts[:attribute]
141
+ _add_attribute_property(name,opts[:xpath],opts[:attribute])
142
+ else
143
+ _add_text_property(name,opts[:xpath])
144
+ end
145
+ end
146
+
147
+ ##
148
+ # Sets an embedded property on the option.
149
+ #
150
+ # examples:
151
+ # embed :names, :xpath => 'names', :class => Name
152
+ def embed(name,opts={})
153
+ unless opts[:class]
154
+ raise NokoParserError, ":xpath and :class required for #{name}"
155
+ end
156
+ _add_embedded_property(name,opts[:xpath],opts[:class],opts[:single])
157
+ end
158
+
159
+ ##
160
+ # Adds to the class variable @text_properties in addition to
161
+ # creating an accessor for this property
162
+ def _add_text_property(name,xpath)
163
+ @_text_properties << TextProperty.new(name,xpath)
164
+ send(:attr_accessor, name)
165
+ end
166
+
167
+ ##
168
+ # Adds to the class variable @attribute_properties in addition to
169
+ # creating an accessor for this property
170
+ def _add_attribute_property(name,xpath,attribute)
171
+ @_attribute_properties << AttributeProperty.new(
172
+ name,xpath,attribute
173
+ )
174
+ send(:attr_accessor, name)
175
+ end
176
+
177
+ ##
178
+ # Adds to the class variable @embedded_properties in addition to
179
+ # creating a reader for this property
180
+ def _add_embedded_property(name,xpath,class_name,single)
181
+ @_embedded_properties << EmbeddedProperty.new(
182
+ name,xpath,class_name,single
183
+ )
184
+ send(:attr_accessor, name)
185
+ end
186
+
187
+ end # ClassMethods
188
+
189
+ module InstanceMethods
190
+
191
+ # def _something
192
+ # true
193
+ # end
194
+
195
+ end
196
+
197
+ end
198
+
199
+ end # NokoParser
File without changes
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dusty-noko_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dusty Doris
@@ -30,10 +30,17 @@ extensions: []
30
30
 
31
31
  extra_rdoc_files:
32
32
  - README.txt
33
- files: []
34
-
33
+ files:
34
+ - README.txt
35
+ - examples/monkey.rb
36
+ - examples/monkeys.xml
37
+ - lib/noko_parser.rb
38
+ - lib/noko_parser/parser.rb
39
+ - lib/noko_parser/properties.rb
40
+ - test/test_noko_parser.rb
35
41
  has_rdoc: true
36
42
  homepage: http://code.dusty.name
43
+ licenses:
37
44
  post_install_message:
38
45
  rdoc_options: []
39
46
 
@@ -54,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
54
61
  requirements: []
55
62
 
56
63
  rubyforge_project: none
57
- rubygems_version: 1.2.0
64
+ rubygems_version: 1.3.5
58
65
  signing_key:
59
66
  specification_version: 2
60
67
  summary: Wrapper around Nokogiri to easily parse xml files with xpath