datacite-mapping 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +42 -0
  3. data/.rubocop.yml +28 -0
  4. data/.ruby-version +1 -0
  5. data/.travis.yml +2 -0
  6. data/.yardopts +2 -0
  7. data/CHANGES.md +3 -0
  8. data/Gemfile +3 -0
  9. data/LICENSE.md +22 -0
  10. data/README.md +168 -0
  11. data/Rakefile +49 -0
  12. data/datacite-mapping.gemspec +37 -0
  13. data/examples/reading.rb +75 -0
  14. data/examples/writing.rb +49 -0
  15. data/lib/datacite/mapping.rb +36 -0
  16. data/lib/datacite/mapping/alternate_identifier.rb +45 -0
  17. data/lib/datacite/mapping/contributor.rb +125 -0
  18. data/lib/datacite/mapping/creator.rb +48 -0
  19. data/lib/datacite/mapping/date.rb +153 -0
  20. data/lib/datacite/mapping/description.rb +121 -0
  21. data/lib/datacite/mapping/geo_location.rb +49 -0
  22. data/lib/datacite/mapping/geo_location_box.rb +137 -0
  23. data/lib/datacite/mapping/geo_location_point.rb +102 -0
  24. data/lib/datacite/mapping/identifier.rb +45 -0
  25. data/lib/datacite/mapping/module_info.rb +12 -0
  26. data/lib/datacite/mapping/name_identifier.rb +48 -0
  27. data/lib/datacite/mapping/related_identifier.rb +209 -0
  28. data/lib/datacite/mapping/resource.rb +201 -0
  29. data/lib/datacite/mapping/resource_type.rb +83 -0
  30. data/lib/datacite/mapping/rights.rb +36 -0
  31. data/lib/datacite/mapping/subject.rb +55 -0
  32. data/lib/datacite/mapping/title.rb +69 -0
  33. data/spec/.rubocop.yml +7 -0
  34. data/spec/data/resource.xml +61 -0
  35. data/spec/rspec_custom_matchers.rb +69 -0
  36. data/spec/spec_helper.rb +31 -0
  37. data/spec/unit/datacite/mapping/alternate_identifier_spec.rb +60 -0
  38. data/spec/unit/datacite/mapping/contributor_spec.rb +129 -0
  39. data/spec/unit/datacite/mapping/creator_spec.rb +125 -0
  40. data/spec/unit/datacite/mapping/date_spec.rb +246 -0
  41. data/spec/unit/datacite/mapping/description_spec.rb +89 -0
  42. data/spec/unit/datacite/mapping/geo_location_box_spec.rb +241 -0
  43. data/spec/unit/datacite/mapping/geo_location_point_spec.rb +148 -0
  44. data/spec/unit/datacite/mapping/geo_location_spec.rb +116 -0
  45. data/spec/unit/datacite/mapping/identifier_spec.rb +75 -0
  46. data/spec/unit/datacite/mapping/name_identifier_spec.rb +89 -0
  47. data/spec/unit/datacite/mapping/related_identifier_spec.rb +157 -0
  48. data/spec/unit/datacite/mapping/resource_spec.rb +727 -0
  49. data/spec/unit/datacite/mapping/resource_type_spec.rb +69 -0
  50. data/spec/unit/datacite/mapping/rights_spec.rb +78 -0
  51. data/spec/unit/datacite/mapping/subject_spec.rb +108 -0
  52. data/spec/unit/datacite/mapping/title_spec.rb +113 -0
  53. metadata +262 -0
@@ -0,0 +1,121 @@
1
+ require 'xml/mapping_extensions'
2
+
3
+ module Datacite
4
+ module Mapping
5
+
6
+ # Controlled vocabulary of description types.
7
+ class DescriptionType < TypesafeEnum::Base
8
+ # @!parse ABSTRACT = Abstract
9
+ new :ABSTRACT, 'Abstract'
10
+
11
+ # @!parse METHODS = Methods
12
+ new :METHODS, 'Methods'
13
+
14
+ # @!parse SERIES_INFORMATION = SeriesInformation
15
+ new :SERIES_INFORMATION, 'SeriesInformation'
16
+
17
+ # @!parse TABLE_OF_CONTENTS = TableOfContents
18
+ new :TABLE_OF_CONTENTS, 'TableOfContents'
19
+
20
+ # @!parse OTHER = Other
21
+ new :OTHER, 'Other'
22
+
23
+ end
24
+
25
+ # XML mapping class preserving `<br/>` tags in description values
26
+ class BreakPreservingValueNode < XML::Mapping::SingleAttributeNode
27
+ # Collapses a sequence of text nodes and `<br/>` tags into a single string value.
28
+ # Implements `SingleAttributeNode#xml_to_obj`.
29
+ # @param obj [Description] the object being created
30
+ # @param xml [REXML::Element] the XML being read
31
+ def xml_to_obj(obj, xml)
32
+ value_str = xml.children.map { |c| c.respond_to?(:value) ? c.value : c.to_s }.join
33
+ obj.value = value_str.strip
34
+ end
35
+
36
+ # Converts a string value to a sequence of text nodes and `<br/>` tags.
37
+ # Implements `SingleAttributeNode#obj_to_xml`.
38
+ # @param obj [Description] the object being serialized
39
+ # @param xml [REXML::Element] the XML being written
40
+ def obj_to_xml(obj, xml)
41
+ value_str = obj.value || ''
42
+ values = value_str.split(%r{<br[^/]?/>|<br>[^<]*</br>})
43
+ values.each_with_index do |v, i|
44
+ xml.add_text(v)
45
+ xml.add_element('br') unless i + 1 >= values.size
46
+ end
47
+ end
48
+ end
49
+ XML::Mapping.add_node_class BreakPreservingValueNode
50
+
51
+ # A additional information that does not fit in the other more specific {Resource}
52
+ # attributes.
53
+ #
54
+ # Note: In accordance with the DataCite spec, description text can be separated by
55
+ # HTML `<br/>` tags. The {Description} class will preserve these, but at the expense
56
+ # of converting escaped `<br/>` in text values to actual `<br/>` tags. For example,
57
+ # when reading the following tag:
58
+ #
59
+ # <description xml:lang="en-us" descriptionType="Abstract">
60
+ # Line 1<br/>Line 2 containing escaped &lt;br/&gt; tag<br/>Line 3
61
+ # </description>
62
+ #
63
+ # the value will be returned as the string
64
+ #
65
+ # "Line 1<br/>Line 2 containing escaped <br/> tag<br/>Line 3"
66
+ #
67
+ # in which it is impossible to distinguish the escaped an un-escaped `<br/>`s. The
68
+ # value would thus be written back to XML as:
69
+ #
70
+ # <description xml:lang="en-us" descriptionType="Abstract">
71
+ # Line 1<br/>Line 2 containing escaped <br/> tag<br/>Line 3
72
+ # </description>
73
+ #
74
+ # Other escaped HTML or XML tags will still be escaped when written back, and other
75
+ # un-escaped HTML and XML tags are of course not allowed.
76
+ class Description
77
+ include XML::Mapping
78
+
79
+ # @!attribute [rw] language
80
+ # @return [String] an IETF BCP 47, ISO 639-1 language code identifying the language.
81
+ # It's unclear from the spec whether language is required; to play it safe, if it's missing, we default to 'en'.
82
+ text_node :language, '@xml:lang', default_value: nil
83
+
84
+ # @!attribute [rw] type
85
+ # @return [DescriptionType] the description type.
86
+ typesafe_enum_node :type, '@descriptionType', class: DescriptionType
87
+
88
+ # @!attribute [rw] value
89
+ # @return [String] the description itself. See {Description} for notes on special
90
+ # handling of `<br/>` tags.
91
+ break_preserving_value_node :value, 'node()'
92
+
93
+ alias_method :_language, :language
94
+ private :_language
95
+
96
+ alias_method :_language=, :language=
97
+ private :_language=
98
+
99
+ # Initializes a new {Description}
100
+ # @param language [String] an IETF BCP 47, ISO 639-1 language code identifying the language.
101
+ # It's unclear from the spec whether language is required; to play it safe, if it's missing, we default to 'en'.
102
+ # @param type [DescriptionType] the description type.
103
+ # @param value [String] the description itself. See {Description} for notes on special
104
+ # handling of `<br/>` tags.
105
+ def initialize(language: 'en', type:, value:)
106
+ self.language = language
107
+ self.type = type
108
+ self.value = value
109
+ end
110
+
111
+ def language
112
+ _language || 'en'
113
+ end
114
+
115
+ def language=(value)
116
+ self._language = value.strip if value
117
+ end
118
+
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,49 @@
1
+ require 'xml/mapping_extensions'
2
+ require_relative 'geo_location_point'
3
+ require_relative 'geo_location_box'
4
+
5
+ module Datacite
6
+ module Mapping
7
+
8
+ # A location at which the data was gathered or about which the data is focused, in the
9
+ # form of a latitude-longitude point, a latitude-longitude quadrangle, and/or a place name.
10
+ #
11
+ # *Note:* Due to a quirk of the DataCite spec, it is possible for a {GeoLocation} to be empty, with
12
+ # none of these present.
13
+ class GeoLocation
14
+ include XML::Mapping
15
+
16
+ root_element_name 'geoLocation'
17
+
18
+ # @!attribute [rw] point
19
+ # @return [GeoLocationPoint, nil] the latitude and longitude at which the data was gathered or about which the data is focused.
20
+ geo_location_point_node :point, 'geoLocationPoint', default_value: nil
21
+
22
+ # @!attribute [rw] box
23
+ # @return [GeoLocationBox, nil] the latitude-longitude quadrangle containing the area where the data was gathered or about which the data is focused.
24
+ geo_location_box_node :box, 'geoLocationBox', default_value: nil
25
+
26
+ # @!attribute [rw] place
27
+ # @return [String, nil] the spatial region or named place where the data was gathered or about which the data is focused.
28
+ text_node :place, 'geoLocationPlace', default_value: nil
29
+
30
+ # Initializes a new {GeoLocation}
31
+ # @param point [GeoLocationPoint, nil] the latitude and longitude at which the data was gathered or about which the data is focused.
32
+ # @param box [GeoLocationBox, nil] the latitude-longitude quadrangle containing the area where the data was gathered or about which the data is focused.
33
+ # @param place [String, nil] the spatial region or named place where the data was gathered or about which the data is focused.
34
+ def initialize(point: nil, box: nil, place: nil)
35
+ self.point = point
36
+ self.box = box
37
+ self.place = place
38
+ end
39
+
40
+ alias_method :_place=, :place=
41
+ private :_place=
42
+
43
+ def place=(value)
44
+ self._place = value.respond_to?(:strip) ? value.strip : value
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,137 @@
1
+ require 'xml/mapping'
2
+
3
+ module Datacite
4
+ module Mapping
5
+ # A latitude-longitude quadrangle containing the area where the data was gathered or about
6
+ # which the data is focused.
7
+ #
8
+ # @!attribute [rw] south_latitude
9
+ # @return [Numeric] the latitude of the south edge of the box
10
+ # @!attribute [rw] west_longitude
11
+ # @return [Numeric] the longitude of the west edge of the box
12
+ # @!attribute [rw] north_latitude
13
+ # @return [Numeric] the latitude of the north edge of the box
14
+ # @!attribute [rw] east_longitude
15
+ # @return [Numeric] the longitude of the east edge of the box
16
+ class GeoLocationBox
17
+ include Comparable
18
+
19
+ attr_reader :south_latitude
20
+ attr_reader :west_longitude
21
+ attr_reader :north_latitude
22
+ attr_reader :east_longitude
23
+
24
+ # Initializes a new {GeoLocationBox}. The arguments can be provided
25
+ # either as a named-parameter hash, or as a list of four coordinates
26
+ # in the form `lat, long, lat, long` (typically
27
+ # `south_latitude, west_longitude, north_latitude, east_longitude`
28
+ # but not necessarily; north/south and east/west will be flipped if
29
+ # need be). That is, the following forms are equivalent:
30
+ #
31
+ # GeoLocationBox.new(
32
+ # south_latitude: -33.45,
33
+ # west_longitude: -122.33,
34
+ # north_latitude: 47.61,
35
+ # east_longitude: -70.67
36
+ # )
37
+ #
38
+ # GeoLocationBox.new(-33.45, -122.33, 47.61, -70.67)
39
+ #
40
+ # @param south_latitude [Numeric]
41
+ # the latitude of the south edge of the box
42
+ # @param west_longitude [Numeric]
43
+ # the longitude of the west edge of the box
44
+ # @param north_latitude [Numeric]
45
+ # the latitude of the north edge of the box
46
+ # @param east_longitude [Numeric]
47
+ # the longitude of the east edge of the box
48
+ def initialize(*args)
49
+ case args.length
50
+ when 1
51
+ init_from_hash(args[0])
52
+ when 4
53
+ init_from_array(args)
54
+ else
55
+ fail ArgumentError, "Can't construct GeoLocationBox from arguments: #{args}"
56
+ end
57
+ end
58
+
59
+ def south_latitude=(value)
60
+ fail ArgumentError, 'South latitude cannot be nil' unless value
61
+ fail ArgumentError, "#{value} is not a valid south latitude" unless value >= -90 && value <= 90
62
+ @south_latitude = value
63
+ end
64
+
65
+ def west_longitude=(value)
66
+ fail ArgumentError, 'West longitude cannot be nil' unless value
67
+ fail ArgumentError, "#{value} is not a valid west longitude" unless value >= -180 && value <= 180
68
+ @west_longitude = value
69
+ end
70
+
71
+ def north_latitude=(value)
72
+ fail ArgumentError, 'North latitude cannot be nil' unless value
73
+ fail ArgumentError, "#{value} is not a valid north latitude" unless value >= -90 && value <= 90
74
+ @north_latitude = value
75
+ end
76
+
77
+ def east_longitude=(value)
78
+ fail ArgumentError, 'East longitude cannot be nil' unless value
79
+ fail ArgumentError, "#{value} is not a valid east longitude" unless value >= -180 && value <= 180
80
+ @east_longitude = value
81
+ end
82
+
83
+ # Gets the box coordinates as a string.
84
+ # @return [String] the coordinates of the box as a sequence of four numbers, in the order S W N E.
85
+ def to_s
86
+ "#{south_latitude} #{west_longitude} #{north_latitude} #{east_longitude}"
87
+ end
88
+
89
+ # Sorts boxes from north to south and east to west, first by south edge, then west
90
+ # edge, then north edge, then east edge, and compares them for equality.
91
+ # @param other [GeoLocationBox] the box to compare
92
+ # @return [Fixnum, nil] the sort order (-1, 0, or 1), or nil if `other` is not a
93
+ # {GeoLocationBox}
94
+ def <=>(other)
95
+ return nil unless other.class == self.class
96
+ [:south_latitude, :west_longitude, :north_latitude, :east_longitude].each do |c|
97
+ order = send(c) <=> other.send(c)
98
+ return order if order != 0
99
+ end
100
+ 0
101
+ end
102
+
103
+ # Returns a hash code consistent with {GeoLocationBox#&lt;=&gt;}
104
+ # @return [Integer] the hash code
105
+ def hash
106
+ [south_latitude, west_longitude, north_latitude, east_longitude].hash
107
+ end
108
+
109
+ private
110
+
111
+ def init_from_hash(south_latitude:, west_longitude:, north_latitude:, east_longitude:)
112
+ self.south_latitude = south_latitude
113
+ self.west_longitude = west_longitude
114
+ self.north_latitude = north_latitude
115
+ self.east_longitude = east_longitude
116
+ end
117
+
118
+ def init_from_array(coordinates)
119
+ self.south_latitude, self.north_latitude = [coordinates[0], coordinates[2]].sort
120
+ self.west_longitude, self.east_longitude = [coordinates[1], coordinates[3]].sort
121
+ end
122
+ end
123
+
124
+ # XML mapping node for `<geoLocationBox/>`
125
+ class GeoLocationBoxNode < XML::MappingExtensions::NodeBase
126
+ # Converts a whitespace-separated list of coordinates to a {GeoLocationBox}.
127
+ # @param xml_text [String] the coordinates, in the order `lat long lat long`.
128
+ def to_value(xml_text)
129
+ stripped = xml_text.strip
130
+ coords = stripped.split(/\s+/).map(&:to_f)
131
+ GeoLocationBox.new(*coords)
132
+ end
133
+ end
134
+ XML::Mapping.add_node_class GeoLocationBoxNode
135
+
136
+ end
137
+ end
@@ -0,0 +1,102 @@
1
+ require 'xml/mapping'
2
+
3
+ module Datacite
4
+ module Mapping
5
+ # A latitude-longitude point at which the data was gathered or about
6
+ # which the data is focused.
7
+ #
8
+ # @!attribute [rw] latitude
9
+ # @return [Numeric] the latitude
10
+ # @!attribute [rw] longitude
11
+ # @return [Numeric] the longitude
12
+ class GeoLocationPoint
13
+ include Comparable
14
+
15
+ attr_reader :latitude
16
+ attr_reader :longitude
17
+
18
+ # Initializes a new {GeoLocationPoint}. The arguments can be provided
19
+ # either as a named-parameter hash, or as a pair of coordinates in the
20
+ # form `lat, long`. That is, the following forms are equivalent:
21
+ #
22
+ # GeoLocationPoint.new(latitude: 47.61, longitude: -122.33)
23
+ #
24
+ # GeoLocationPoint.new(47.61, -122.33)
25
+ #
26
+ # @param latitude [Numeric] the latitude
27
+ # @param longitude [Numeric] the longitude
28
+ def initialize(*args)
29
+ case args.length
30
+ when 1
31
+ init_from_hash(args[0])
32
+ when 2
33
+ init_from_array(args)
34
+ else
35
+ fail ArgumentError, "Can't construct GeoLocationPoint from arguments: #{args}"
36
+ end
37
+ end
38
+
39
+ def latitude=(value)
40
+ fail ArgumentError, 'Latitude cannot be nil' unless value
41
+ fail ArgumentError, "#{value} is not a valid latitude" unless value >= -90 && value <= 90
42
+ @latitude = value
43
+ end
44
+
45
+ def longitude=(value)
46
+ fail ArgumentError, 'Longitude cannot be nil' unless value
47
+ fail ArgumentError, "#{value} is not a valid longitude" unless value >= -180 && value <= 180
48
+ @longitude = value
49
+ end
50
+
51
+ # Gets the coordinates as a string.
52
+ # @return [String] the coordinates as a pair of numbers separated by a space, in the
53
+ # order `lat` `long`.
54
+ def to_s
55
+ "#{latitude} #{longitude}"
56
+ end
57
+
58
+ # Sorts points from north to south and from east to west, and compares them for equality.
59
+ # @param other [GeoLocationPoint] the point to compare
60
+ # @return [Fixnum, nil] the sort order (-1, 0, or 1), or nil if `other` is not a
61
+ # {GeoLocationPoint}
62
+ def <=>(other)
63
+ return nil unless other.class == self.class
64
+ [:latitude, :longitude].each do |c|
65
+ order = send(c) <=> other.send(c)
66
+ return order if order != 0
67
+ end
68
+ 0
69
+ end
70
+
71
+ # Returns a hash code consistent with {GeoLocationPoint#&lt;=&gt;}
72
+ # @return [Integer] the hash code
73
+ def hash
74
+ [latitude, longitude].hash
75
+ end
76
+
77
+ private
78
+
79
+ def init_from_hash(latitude:, longitude:)
80
+ self.latitude = latitude
81
+ self.longitude = longitude
82
+ end
83
+
84
+ def init_from_array(args)
85
+ self.latitude, self.longitude = args
86
+ end
87
+ end
88
+
89
+ # XML mapping node for `<geoLocationPoint/>`
90
+ class GeoLocationPointNode < XML::MappingExtensions::NodeBase
91
+ # Converts a whitespace-separated pair of coordinates to a {GeoLocationPoint}.
92
+ # @param xml_text [String] the coordinates, in the order `lat` `long`.
93
+ def to_value(xml_text)
94
+ stripped = xml_text.strip
95
+ coords = stripped.split(/\s+/).map(&:to_f)
96
+ GeoLocationPoint.new(*coords)
97
+ end
98
+ end
99
+ XML::Mapping.add_node_class GeoLocationPointNode
100
+
101
+ end
102
+ end
@@ -0,0 +1,45 @@
1
+ require 'xml/mapping'
2
+
3
+ module Datacite
4
+ module Mapping
5
+ # The persistent identifier that identifies the resource.
6
+ #
7
+ # @!attribute [r] identifier_type
8
+ # @return [String] the identifier type (always 'DOI')
9
+ # @!attribute [rw] value
10
+ # @return [String] the identifier value. Must be a valid DOI value (`10.`_registrant code_`/`_suffix_)
11
+ class Identifier
12
+ include XML::Mapping
13
+
14
+ text_node :identifier_type, '@identifierType'
15
+ text_node :value, 'text()'
16
+
17
+ # Initializes a new {Identifier}
18
+ # @param value [String]
19
+ # the identifier value. Must be a valid DOI value (`10.`_registrant code_`/`_suffix_)
20
+ def initialize(value:)
21
+ self.identifier_type = 'DOI'
22
+ self.value = value
23
+ end
24
+
25
+ alias_method :_value=, :value=
26
+ private :_value=
27
+
28
+ alias_method :_identifier_type=, :identifier_type=
29
+ private :_identifier_type=
30
+
31
+ def value=(v)
32
+ fail ArgumentError, "Identifier value '#{v}' is not a valid DOI" unless v.match(%r{10\..+/.+})
33
+ self._value = v
34
+ end
35
+
36
+ # Sets the identifier type. Should only be called by the XML mapping engine.
37
+ # @param v [String]
38
+ # the identifier type (always 'DOI')
39
+ def identifier_type=(v)
40
+ fail ArgumentError, "Identifier type '#{v}' must be 'DOI'" unless 'DOI' == v
41
+ self._identifier_type = v
42
+ end
43
+ end
44
+ end
45
+ end