opinionated-xml 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.1.0
@@ -0,0 +1,9 @@
1
+ require 'rubygems'
2
+
3
+ require 'nokogiri'
4
+ require "facets"
5
+
6
+ module OM; end
7
+ module OM::XML; end
8
+
9
+ require "om/xml"
@@ -0,0 +1,23 @@
1
+ require "om/xml/container"
2
+ require "om/xml/accessors"
3
+ require "om/xml/validation"
4
+ require "om/xml/properties"
5
+ require "om/xml/property_value_operators"
6
+
7
+ module OM::XML
8
+
9
+ attr_accessor :ng_xml
10
+
11
+ # Instance Methods -- These methods will be available on instances of classes that include this module
12
+
13
+ def self.included(klass)
14
+ klass.send(:include, OM::XML::Container)
15
+ klass.send(:include, OM::XML::Accessors)
16
+ klass.send(:include, OM::XML::Validation)
17
+ klass.send(:include, OM::XML::Properties)
18
+ klass.send(:include, OM::XML::PropertyValueOperators)
19
+
20
+ # klass.send(:include, OM::XML::Schema)
21
+ end
22
+
23
+ end
@@ -0,0 +1,130 @@
1
+ module OM::XML::Accessors
2
+
3
+ module ClassMethods
4
+ attr_accessor :accessors
5
+
6
+ def accessor(accessor_name, opts={})
7
+ @accessors ||= {}
8
+ insert_accessor(accessor_name, opts)
9
+ end
10
+
11
+ def insert_accessor(accessor_name, accessor_opts, parent_names=[])
12
+ unless accessor_opts.has_key?(:relative_xpath)
13
+ accessor_opts[:relative_xpath] = "oxns:#{accessor_name}"
14
+ end
15
+
16
+ destination = @accessors
17
+ parent_names.each do |parent_name|
18
+ destination = destination[parent_name][:children]
19
+ end
20
+
21
+ destination[accessor_name] = accessor_opts
22
+
23
+ # Recursively call insert_accessor for any children
24
+ if accessor_opts.has_key?(:children)
25
+ children_array = accessor_opts[:children].dup
26
+ accessor_opts[:children] = {}
27
+ children_array.each do |child|
28
+ if child.kind_of?(Hash)
29
+ child_name = child.keys.first
30
+ child_opts = child.values.first
31
+ else
32
+ child_name = child
33
+ child_opts = {}
34
+ end
35
+ insert_accessor(child_name, child_opts, parent_names+[accessor_name] )
36
+ end
37
+ end
38
+
39
+ end
40
+
41
+ # Returns the configuration info for the selected accessor.
42
+ # Ingores any integers in the array (ie. nodeset indices intended for use in other accessor convenience methods)
43
+ def accessor_info(*args)
44
+ # Ignore any nodeset indexes in the args array
45
+ keys = args.select {|x| !x.kind_of?(Integer) }
46
+ info = @accessors
47
+ keys.each do |k|
48
+ unless keys.index(k) == 0
49
+ info = info.fetch(:children, nil)
50
+ if info.nil?
51
+ debugger
52
+ return nil
53
+ end
54
+ end
55
+
56
+ info = info.fetch(k, nil)
57
+ if info.nil?
58
+ return nil
59
+ end
60
+ end
61
+ return info
62
+ end
63
+
64
+
65
+ def accessor_xpath(*args)
66
+ keys = even_values(args)
67
+ indices = odd_values(args)
68
+ xpath = "//"
69
+ keys.each do |k|
70
+ key_index = keys.index(k)
71
+ accessor_info = accessor_info(*keys[0..key_index])
72
+ relative_path = accessor_info[:relative_xpath]
73
+
74
+ if relative_path.kind_of?(Hash)
75
+ if relative_path.has_key?(:attribute)
76
+ relative_path = "@"+relative_path[:attribute]
77
+ end
78
+ else
79
+
80
+ if indices[key_index]
81
+ add_position_predicate!(relative_path, indices[key_index])
82
+ end
83
+
84
+ if accessor_info.has_key?(:default_content_path)
85
+ relative_path << "/"+accessor_info[:default_content_path]
86
+ end
87
+
88
+ end
89
+ if key_index > 0
90
+ relative_path.insert(0, "/")
91
+ end
92
+ xpath << relative_path
93
+ end
94
+
95
+ return xpath
96
+ end
97
+
98
+ def add_position_predicate!(xpath_query, array_index_value)
99
+ position_function = "position()=#{array_index_value + 1}"
100
+
101
+ if xpath_query.include?("]")
102
+ xpath_query.insert(xpath_query.rindex("]"), " and #{position_function}")
103
+ else
104
+ xpath_query << "[#{position_function}]"
105
+ end
106
+ end
107
+
108
+ def odd_values(array)
109
+ array.values_at(* array.each_index.select {|i| i.odd?})
110
+ end
111
+ def even_values(array)
112
+ array.values_at(* array.each_index.select {|i| i.even?})
113
+ end
114
+ end
115
+
116
+ # Instance Methods -- These methods will be available on instances of OM classes (ie. the actual xml documents)
117
+
118
+ def self.included(klass)
119
+ klass.extend(ClassMethods)
120
+ end
121
+
122
+ # *args Variable length array of values in format [:accessor_name, index, :accessor_name ...]
123
+ # example: [:person, 1, :first_name]
124
+ # Currently, indexes must be integers.
125
+ def retrieve(*args)
126
+ xpath = self.class.accessor_xpath(*args)
127
+ ng_xml.xpath(xpath, "oxns"=>"http://www.loc.gov/mods/v3")
128
+ end
129
+
130
+ end
@@ -0,0 +1,32 @@
1
+ module OM::XML::Container
2
+
3
+ attr_accessor :ng_xml
4
+
5
+ # Class Methods -- These methods will be available on classes that include this Module
6
+
7
+ module ClassMethods
8
+
9
+ # @xml Sting, File or Nokogiri::XML::Node
10
+ # @tmpl ActiveFedora::MetadataDatastream
11
+ def from_xml(xml, tmpl=self.new) # :nodoc:
12
+ if xml.kind_of? Nokogiri::XML::Node
13
+ tmpl.ng_xml = xml
14
+ else
15
+ tmpl.ng_xml = Nokogiri::XML::Document.parse(xml)
16
+ end
17
+ return tmpl
18
+ end
19
+
20
+ end
21
+
22
+ # Instance Methods -- These methods will be available on instances of classes that include this module
23
+
24
+ def self.included(klass)
25
+ klass.extend(ClassMethods)
26
+ end
27
+
28
+ def to_xml
29
+ ng_xml.to_xml
30
+ end
31
+
32
+ end
@@ -1,14 +1,11 @@
1
- require "open-uri"
2
- require "logger"
3
-
4
- module OX
1
+ module OM::XML::Properties
2
+
3
+ attr_accessor :ng_xml
5
4
 
6
- # Class Methods -- These methods will be available on classes that include OX (ie. the OX MODS class)
5
+ # Class Methods -- These methods will be available on classes that include this Module
7
6
 
8
7
  module ClassMethods
9
-
10
- attr_accessor :root_property_ref, :root_config, :ox_namespaces, :schema_url
11
- attr_writer :schema_file
8
+ attr_accessor :root_property_ref, :root_config, :ox_namespaces
12
9
  attr_reader :properties
13
10
 
14
11
  def root_property( property_ref, path, namespace, opts={})
@@ -269,45 +266,6 @@ module OX
269
266
  return property_info
270
267
  end
271
268
 
272
- ##
273
- # Validation Support
274
- ##
275
-
276
- # Validate the given document against the Schema provided by the root_property for this class
277
- def validate(doc)
278
- schema.validate(doc).each do |error|
279
- puts error.message
280
- end
281
- end
282
-
283
- # Retrieve the Nokogiri Schema for this class
284
- def schema
285
- @schema ||= Nokogiri::XML::Schema(schema_file.read)
286
- end
287
-
288
- # Retrieve the schema file for this class
289
- # If the schema file is not already set, it will be loaded from the schema url provided in the root_property configuration for the class
290
- def schema_file
291
- @schema_file ||= file_from_url(schema_url)
292
- end
293
-
294
- # Retrieve file from a url (used by schema_file method to retrieve schema file from the schema url)
295
- def file_from_url( url )
296
- # parsed_url = URI.parse( url )
297
- #
298
- # if parsed_url.class != URI::HTTP
299
- # raise "Invalid URL. Could not parse #{url} as a HTTP url."
300
- # end
301
-
302
- begin
303
- file = open( url )
304
- return file
305
- rescue OpenURI::HTTPError => e
306
- raise "Could not retrieve file from #{url}. Error: #{e}"
307
- rescue Exception => e
308
- raise "Could not retrieve file from #{url}. Error: #{e}"
309
- end
310
- end
311
269
 
312
270
  def delimited_list( values_array, delimiter=", ")
313
271
  result = values_array.collect{|a| a + delimiter}.to_s.chomp(delimiter)
@@ -382,17 +340,13 @@ module OX
382
340
  @logger ||= defined?(RAILS_DEFAULT_LOGGER) ? RAILS_DEFAULT_LOGGER : Logger.new(STDOUT)
383
341
  end
384
342
 
385
- private :file_from_url, :applicable_attributes
386
-
387
-
388
-
343
+ private :applicable_attributes
389
344
  end
390
345
 
391
- # Instance Methods -- These methods will be available on instances of OX classes (ie. the actual xml documents)
346
+ # Instance Methods -- These methods will be available on instances of classes that include this module
392
347
 
393
348
  def self.included(klass)
394
349
  klass.extend(ClassMethods)
395
- klass.send(:include, OX::PropertyValuesHelper)
396
350
  end
397
351
 
398
352
  # Applies the property's corresponding xpath query, returning the result Nokogiri::XML::NodeSet
@@ -402,7 +356,7 @@ module OX
402
356
  if xpath_query.nil?
403
357
  result = []
404
358
  else
405
- result = xpath(xpath_query, ox_namespaces)
359
+ result = ng_xml.xpath(xpath_query, ox_namespaces)
406
360
  end
407
361
 
408
362
  return result
@@ -412,11 +366,7 @@ module OX
412
366
  # Returns a hash combining the current documents namespaces (provided by nokogiri) and any namespaces that have been set up by your class definiton.
413
367
  # Most importantly, this matches the 'oxns' namespace to the namespace you provided in your root property config
414
368
  def ox_namespaces
415
- @ox_namespaces ||= namespaces.merge(self.class.ox_namespaces)
416
- end
417
-
418
- def validate
419
- self.class.validate(self)
369
+ @ox_namespaces ||= ng_xml.namespaces.merge(self.class.ox_namespaces)
420
370
  end
421
371
 
422
372
  def xpath_query_for( property_ref, query_opts={}, opts={} )
@@ -426,5 +376,5 @@ module OX
426
376
  def property_info_for(property_ref)
427
377
  self.class.property_info_for(property_ref)
428
378
  end
429
-
379
+
430
380
  end
@@ -0,0 +1,111 @@
1
+ require "open-uri"
2
+ require "logger"
3
+
4
+ class OM::XML::ParentNodeNotFoundError < RuntimeError; end
5
+ module OM::XML::PropertyValueOperators
6
+
7
+ def property_values(lookup_args)
8
+ result = []
9
+ lookup(lookup_args).each {|node| result << node.text }
10
+ return result
11
+ end
12
+
13
+ def property_values_append(opts={})
14
+ parent_select = Array( opts[:parent_select] )
15
+ child_index = opts[:child_index]
16
+ template = opts[:template]
17
+ new_values = Array( opts[:values] )
18
+
19
+ # If template is a string, use it as the template, otherwise use it as arguments to builder_template
20
+ unless template.instance_of?(String)
21
+ template_args = Array(template)
22
+ if template_args.last.kind_of?(Hash)
23
+ template_opts = template_args.delete_at(template_args.length - 1)
24
+ else
25
+ template_opts = {}
26
+ end
27
+ template = self.class.builder_template( template_args, template_opts )
28
+ end
29
+
30
+ parent_nodeset = lookup(parent_select[0], parent_select[1])
31
+ parent_node = node_from_set(parent_nodeset, child_index)
32
+
33
+ if parent_node.nil?
34
+ raise OX::ParentNodeNotFoundError, "Failed to find a parent node to insert values into based on :parent_select #{parent_select.inspect} with :child_index #{child_index.inspect}"
35
+ end
36
+
37
+ builder = Nokogiri::XML::Builder.with(parent_node) do |xml|
38
+ new_values.each do |builder_new_value|
39
+ builder_arg = eval('"'+ template + '"') # this inserts builder_new_value into the builder template
40
+ eval(builder_arg)
41
+ end
42
+ end
43
+
44
+ # Nokogiri::XML::Node.new(builder.to_xml, foo)
45
+
46
+ return parent_node
47
+
48
+ end
49
+
50
+ def property_value_update(opts={})
51
+ parent_select = Array( opts[:parent_select] )
52
+ child_index = opts[:child_index]
53
+ template = opts[:template]
54
+ new_value = opts[:value]
55
+ xpath_select = opts[:select]
56
+
57
+ if !xpath_select.nil?
58
+ node = lookup(xpath_select, nil).first
59
+ else
60
+ parent_nodeset = lookup(parent_select[0], parent_select[1])
61
+ node = node_from_set(parent_nodeset, child_index)
62
+ end
63
+
64
+ node.content = new_value
65
+
66
+ end
67
+
68
+ # def property_value_set(property_ref, query_opts, node_index, new_value)
69
+ # end
70
+
71
+ def property_value_delete(opts={})
72
+ parent_select = Array( opts[:parent_select] )
73
+ parent_index = opts[:parent_index]
74
+ child_index = opts[:child_index]
75
+ xpath_select = opts[:select]
76
+
77
+ if !xpath_select.nil?
78
+ node = lookup(xpath_select, nil).first
79
+ else
80
+ parent_nodeset = lookup(parent_select, parent_select)
81
+ # parent_nodeset = lookup(parent_select[0])
82
+
83
+ if parent_index.nil?
84
+ node = node_from_set(parent_nodeset, child_index)
85
+ else
86
+ parent = node_from_set(parent_nodeset, parent_index)
87
+ # this next line is a hack around the fact that element_children() sometimes doesn't work.
88
+ node = node_from_set(parent.xpath("*"), child_index)
89
+ end
90
+ end
91
+
92
+ node.remove
93
+ end
94
+
95
+
96
+ # Allows you to provide an array index _or_ a symbol representing the function to call on the nodeset in order to retrieve the node.
97
+ def node_from_set(nodeset, index)
98
+ if index.kind_of?(Integer)
99
+ node = nodeset[index]
100
+ elsif index.kind_of?(Symbol) && nodeset.respond_to?(index)
101
+ node = nodeset.send(index)
102
+ else
103
+ raise "Could not retrieve node using index #{index}."
104
+ end
105
+
106
+ return node
107
+ end
108
+
109
+ private :node_from_set
110
+
111
+ end
@@ -0,0 +1,63 @@
1
+ module OM::XML::Validation
2
+
3
+ # Class Methods -- These methods will be available on classes that include this Module
4
+
5
+ module ClassMethods
6
+ attr_accessor :schema_url
7
+ attr_writer :schema_file
8
+
9
+ ##
10
+ # Validation Support
11
+ ##
12
+
13
+ # Validate the given document against the Schema provided by the root_property for this class
14
+ def validate(doc)
15
+ schema.validate(doc).each do |error|
16
+ puts error.message
17
+ end
18
+ end
19
+
20
+ # Retrieve the Nokogiri Schema for this class
21
+ def schema
22
+ @schema ||= Nokogiri::XML::Schema(schema_file.read)
23
+ end
24
+
25
+ # Retrieve the schema file for this class
26
+ # If the schema file is not already set, it will be loaded from the schema url provided in the root_property configuration for the class
27
+ def schema_file
28
+ @schema_file ||= file_from_url(schema_url)
29
+ end
30
+
31
+ # Retrieve file from a url (used by schema_file method to retrieve schema file from the schema url)
32
+ def file_from_url( url )
33
+ # parsed_url = URI.parse( url )
34
+ #
35
+ # if parsed_url.class != URI::HTTP
36
+ # raise "Invalid URL. Could not parse #{url} as a HTTP url."
37
+ # end
38
+
39
+ begin
40
+ file = open( url )
41
+ return file
42
+ rescue OpenURI::HTTPError => e
43
+ raise "Could not retrieve file from #{url}. Error: #{e}"
44
+ rescue Exception => e
45
+ raise "Could not retrieve file from #{url}. Error: #{e}"
46
+ end
47
+ end
48
+
49
+ private :file_from_url
50
+
51
+ end
52
+
53
+ # Instance Methods -- These methods will be available on instances of classes that include this module
54
+
55
+ def self.included(klass)
56
+ klass.extend(ClassMethods)
57
+ end
58
+
59
+ def validate
60
+ self.class.validate(self)
61
+ end
62
+
63
+ end