datacite-mapping 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.
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