opinionated-xml 0.0.1 → 0.1.0

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/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