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 +1 -1
- data/lib/om.rb +9 -0
- data/lib/om/xml.rb +23 -0
- data/lib/om/xml/accessors.rb +130 -0
- data/lib/om/xml/container.rb +32 -0
- data/lib/{opinionated-xml/ox.rb → om/xml/properties.rb} +10 -60
- data/lib/om/xml/property_value_operators.rb +111 -0
- data/lib/om/xml/validation.rb +63 -0
- data/opinionated-xml.gemspec +26 -14
- data/spec/fixtures/RUBRIC_mods_article_template.xml +89 -0
- data/spec/fixtures/mods_articles/hydrangea_article1.xml +90 -0
- data/spec/fixtures/test_dummy_mods.xml +17 -0
- data/spec/spec_helper.rb +1 -1
- data/spec/unit/accessors_spec.rb +156 -0
- data/spec/unit/container_spec.rb +60 -0
- data/spec/unit/{opinionated-xml_spec.rb → properties_spec.rb} +13 -66
- data/spec/unit/property_value_operators_spec.rb +245 -0
- data/spec/unit/validation_spec.rb +78 -0
- data/spec/unit/xml_spec.rb +21 -0
- metadata +40 -13
- data/lib/opinionated-xml.rb +0 -9
- data/lib/opinionated-xml/ox_property_values_helper.rb +0 -62
- data/spec/helpers/ox_property_values_helper_spec.rb +0 -55
- data/spec/unit/ox_integration_spec.rb +0 -124
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0
|
1
|
+
0.1.0
|
data/lib/om.rb
ADDED
data/lib/om/xml.rb
ADDED
@@ -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
|
-
|
2
|
-
|
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
|
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 :
|
386
|
-
|
387
|
-
|
388
|
-
|
343
|
+
private :applicable_attributes
|
389
344
|
end
|
390
345
|
|
391
|
-
# Instance Methods -- These methods will be available on instances of
|
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
|