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