orcid_client 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/.env.example +10 -0
  3. data/.gitignore +42 -0
  4. data/.travis.yml +17 -0
  5. data/CHANGELOG.md +0 -0
  6. data/Gemfile +4 -0
  7. data/Gemfile.lock +115 -0
  8. data/LICENSE.md +21 -0
  9. data/README.md +45 -0
  10. data/Rakefile +5 -0
  11. data/lib/orcid_client.rb +3 -0
  12. data/lib/orcid_client/api.rb +75 -0
  13. data/lib/orcid_client/author.rb +56 -0
  14. data/lib/orcid_client/base.rb +21 -0
  15. data/lib/orcid_client/date.rb +61 -0
  16. data/lib/orcid_client/metadata.rb +128 -0
  17. data/lib/orcid_client/notification.rb +125 -0
  18. data/lib/orcid_client/version.rb +3 -0
  19. data/lib/orcid_client/work.rb +200 -0
  20. data/lib/orcid_client/work_type.rb +68 -0
  21. data/orcid_client.gemspec +37 -0
  22. data/resources/common_2.0_rc3/common-2.0_rc3.xsd +1624 -0
  23. data/resources/common_2.0_rc3/samples/common-2.0_rc3.xml +8 -0
  24. data/resources/notification_2.0_rc3/notification-custom-2.0_rc3.xsd +32 -0
  25. data/resources/notification_2.0_rc3/notification-permission-2.0_rc3.xsd +128 -0
  26. data/resources/notification_2.0_rc3/samples/notification-custom-2.0_rc3.xml +45 -0
  27. data/resources/notification_2.0_rc3/samples/notification-permission-2.0_rc3.xml +58 -0
  28. data/resources/record_2.0_rc3/activities-2.0_rc3.xsd +187 -0
  29. data/resources/record_2.0_rc3/address-2.0_rc3.xsd +73 -0
  30. data/resources/record_2.0_rc3/bulk-2.0_rc3.xsd +57 -0
  31. data/resources/record_2.0_rc3/deprecated-2.0_rc3.xsd +80 -0
  32. data/resources/record_2.0_rc3/education-2.0_rc3.xsd +96 -0
  33. data/resources/record_2.0_rc3/email-2.0_rc3.xsd +74 -0
  34. data/resources/record_2.0_rc3/employment-2.0_rc3.xsd +96 -0
  35. data/resources/record_2.0_rc3/error-2.0_rc3.xsd +84 -0
  36. data/resources/record_2.0_rc3/funding-2.0_rc3.xsd +257 -0
  37. data/resources/record_2.0_rc3/history-2.0_rc3.xsd +203 -0
  38. data/resources/record_2.0_rc3/internal-2.0_rc3.xsd +199 -0
  39. data/resources/record_2.0_rc3/keyword-2.0_rc3.xsd +72 -0
  40. data/resources/record_2.0_rc3/other-name-2.0_rc3.xsd +88 -0
  41. data/resources/record_2.0_rc3/peer-review-2.0_rc3.xsd +246 -0
  42. data/resources/record_2.0_rc3/person-2.0_rc3.xsd +98 -0
  43. data/resources/record_2.0_rc3/person-external-identifier-2.0_rc3.xsd +63 -0
  44. data/resources/record_2.0_rc3/personal-details-2.0_rc3.xsd +186 -0
  45. data/resources/record_2.0_rc3/preferences-2.0_rc3.xsd +50 -0
  46. data/resources/record_2.0_rc3/record-2.0_rc3.xsd +105 -0
  47. data/resources/record_2.0_rc3/researcher-url-2.0_rc3.xsd +90 -0
  48. data/resources/record_2.0_rc3/samples/activities-2.0_rc3.xml +236 -0
  49. data/resources/record_2.0_rc3/samples/address-2.0_rc3.xml +16 -0
  50. data/resources/record_2.0_rc3/samples/addresses-2.0_rc3.xml +32 -0
  51. data/resources/record_2.0_rc3/samples/biography-2.0_rc3.xml +10 -0
  52. data/resources/record_2.0_rc3/samples/bulk-work-2.0_rc3.json +56 -0
  53. data/resources/record_2.0_rc3/samples/bulk-work-2.0_rc3.xml +62 -0
  54. data/resources/record_2.0_rc3/samples/credit-name-2.0_rc3.xml +5 -0
  55. data/resources/record_2.0_rc3/samples/deprecated-2.0_rc3.xml +13 -0
  56. data/resources/record_2.0_rc3/samples/education-2.0_rc3.xml +30 -0
  57. data/resources/record_2.0_rc3/samples/education-full-2.0_rc3.xml +40 -0
  58. data/resources/record_2.0_rc3/samples/email-2.0_rc3.xml +16 -0
  59. data/resources/record_2.0_rc3/samples/emails-2.0_rc3.xml +31 -0
  60. data/resources/record_2.0_rc3/samples/employment-2.0_rc3.xml +30 -0
  61. data/resources/record_2.0_rc3/samples/employment-full-2.0_rc3.xml +40 -0
  62. data/resources/record_2.0_rc3/samples/error-2.0_rc3.xml +8 -0
  63. data/resources/record_2.0_rc3/samples/external-identifier-2.0_rc3.xml +20 -0
  64. data/resources/record_2.0_rc3/samples/external-identifiers-2.0_rc3.xml +36 -0
  65. data/resources/record_2.0_rc3/samples/funding-2.0_rc3.xml +65 -0
  66. data/resources/record_2.0_rc3/samples/funding-full-2.0_rc3.xml +75 -0
  67. data/resources/record_2.0_rc3/samples/history-2.0_rc3.xml +22 -0
  68. data/resources/record_2.0_rc3/samples/keyword-2.0_rc3.xml +18 -0
  69. data/resources/record_2.0_rc3/samples/keywords-2.0_rc3.xml +34 -0
  70. data/resources/record_2.0_rc3/samples/name-2.0_rc3.xml +12 -0
  71. data/resources/record_2.0_rc3/samples/other-name-2.0_rc3.xml +16 -0
  72. data/resources/record_2.0_rc3/samples/other-names-2.0_rc3.xml +31 -0
  73. data/resources/record_2.0_rc3/samples/peer-review-2.0_rc3.xml +49 -0
  74. data/resources/record_2.0_rc3/samples/peer-review-full-2.0_rc3.xml +59 -0
  75. data/resources/record_2.0_rc3/samples/person-2.0_rc3.xml +122 -0
  76. data/resources/record_2.0_rc3/samples/personal-details-2.0_rc3.xml +47 -0
  77. data/resources/record_2.0_rc3/samples/preferences-2.0_rc3.xml +4 -0
  78. data/resources/record_2.0_rc3/samples/record-2.0_rc3.xml +420 -0
  79. data/resources/record_2.0_rc3/samples/researcher-url-2.0_rc3.xml +19 -0
  80. data/resources/record_2.0_rc3/samples/researcher-urls-2.0_rc3.xml +21 -0
  81. data/resources/record_2.0_rc3/samples/search-2.0_rc3.xml +368 -0
  82. data/resources/record_2.0_rc3/samples/work-2.0_rc3.xml +49 -0
  83. data/resources/record_2.0_rc3/samples/work-full-2.0_rc3.xml +59 -0
  84. data/resources/record_2.0_rc3/search-2.0_rc3.xsd +82 -0
  85. data/resources/record_2.0_rc3/work-2.0_rc3.xsd +445 -0
  86. data/spec/api_spec.rb +59 -0
  87. data/spec/fixtures/vcr_cassettes/OrcidClient/get/should_get_works.yml +646 -0
  88. data/spec/fixtures/vcr_cassettes/OrcidClient/notifications/post/should_create_notification.yml +104 -0
  89. data/spec/fixtures/vcr_cassettes/OrcidClient/works/delete/should_delete_work.yml +38 -0
  90. data/spec/fixtures/vcr_cassettes/OrcidClient/works/get/should_get_works.yml +646 -0
  91. data/spec/fixtures/vcr_cassettes/OrcidClient/works/post/should_create_work.yml +116 -0
  92. data/spec/fixtures/vcr_cassettes/OrcidClient/works/put/should_update_work.yml +155 -0
  93. data/spec/fixtures/vcr_cassettes/OrcidClient_Notification/data.yml +44 -0
  94. data/spec/fixtures/vcr_cassettes/OrcidClient_Notification/schema/validates_data.yml +44 -0
  95. data/spec/fixtures/vcr_cassettes/OrcidClient_Notification/schema/validates_item_type_work.yml +41 -0
  96. data/spec/fixtures/vcr_cassettes/OrcidClient_Work/citation.yml +50 -0
  97. data/spec/fixtures/vcr_cassettes/OrcidClient_Work/contributors/literal.yml +47 -0
  98. data/spec/fixtures/vcr_cassettes/OrcidClient_Work/contributors/multiple_titles.yml +47 -0
  99. data/spec/fixtures/vcr_cassettes/OrcidClient_Work/contributors/valid.yml +50 -0
  100. data/spec/fixtures/vcr_cassettes/OrcidClient_Work/contributors/with_ORCID_IDs.yml +157 -0
  101. data/spec/fixtures/vcr_cassettes/OrcidClient_Work/data.yml +50 -0
  102. data/spec/fixtures/vcr_cassettes/OrcidClient_Work/publication_date.yml +50 -0
  103. data/spec/fixtures/vcr_cassettes/OrcidClient_Work/schema/validates_ORCID_IDs_for_contributors.yml +157 -0
  104. data/spec/fixtures/vcr_cassettes/OrcidClient_Work/schema/validates_data.yml +50 -0
  105. data/spec/fixtures/vcr_cassettes/OrcidClient_Work/schema/validates_work_type_data-set.yml +47 -0
  106. data/spec/fixtures/work.xml +33 -0
  107. data/spec/notification_spec.rb +42 -0
  108. data/spec/spec_helper.rb +32 -0
  109. data/spec/work_spec.rb +100 -0
  110. metadata +392 -0
@@ -0,0 +1,61 @@
1
+ module OrcidClient
2
+ module Date
3
+ def get_date_parts(iso8601_time)
4
+ return { "date_parts" => [[]] } if iso8601_time.nil?
5
+
6
+ year = iso8601_time[0..3].to_i
7
+ month = iso8601_time[5..6].to_i
8
+ day = iso8601_time[8..9].to_i
9
+ { 'date-parts' => [[year, month, day].reject { |part| part == 0 }] }
10
+ end
11
+
12
+ def get_year_month(iso8601_time)
13
+ return [nil, nil] if iso8601_time.nil?
14
+
15
+ year = iso8601_time[0..3].to_i
16
+ month = iso8601_time[5..6].to_i
17
+
18
+ [year, month]
19
+ end
20
+
21
+ def get_date_parts_from_parts(year = nil, month = nil, day = nil)
22
+ { 'date-parts' => [[year.to_i, month.to_i, day.to_i].reject { |part| part == 0 }] }
23
+ end
24
+
25
+ def get_parts_from_date_parts(date_parts)
26
+ parts = date_parts.fetch('date-parts', []).first
27
+ return { "date_parts" => [[]] } unless parts.present?
28
+
29
+ { 'year' => parts[0],
30
+ 'month' => parts[1],
31
+ 'day' => parts[2] }.compact
32
+ end
33
+
34
+ def get_year_month_day(iso8601_time)
35
+ return [] if iso8601_time.nil?
36
+
37
+ year = iso8601_time[0..3]
38
+ month = iso8601_time[5..6]
39
+ day = iso8601_time[8..9]
40
+
41
+ { 'year' => year.to_i,
42
+ 'month' => month.to_i,
43
+ 'day' => day.to_i }.delete_if { |key, value| value == 0 }
44
+ end
45
+
46
+ def get_iso8601_from_time(time)
47
+ return nil if time.blank?
48
+
49
+ Time.zone.parse(time.to_s).utc.iso8601
50
+ end
51
+
52
+ def get_iso8601_from_epoch(epoch)
53
+ return nil if epoch.blank?
54
+
55
+ # handle milliseconds
56
+ epoch = epoch.to_i
57
+ epoch = epoch / 1000 if epoch > 9999999999
58
+ Time.at(epoch).utc.iso8601
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,128 @@
1
+ module OrcidClient
2
+ module Metadata
3
+ def get_metadata(id, service, options = {})
4
+ case service
5
+ when "crossref" then get_crossref_metadata(id, options = {})
6
+ when "datacite" then get_datacite_metadata(id, options = {})
7
+ when "orcid" then get_orcid_metadata(id, options = {})
8
+ else
9
+ { "errors" => [{ "title" => 'Not found.', "status" => 404 }] }
10
+ end
11
+ end
12
+
13
+ def get_crossref_metadata(doi, options = {})
14
+ return {} if doi.blank?
15
+
16
+ url = "http://api.crossref.org/works/" + PostRank::URI.escape(doi)
17
+ response = Maremma.get(url, options.merge(host: true))
18
+ return response if response["errors"]
19
+
20
+ metadata = response.fetch("data", {}).fetch("message", {})
21
+ return { "errors" => [{ "title" => "Not found.", "status" => 404 }] } if metadata.blank?
22
+
23
+ date_parts = metadata.fetch("issued", {}).fetch("date-parts", []).first
24
+ year, month, day = date_parts[0], date_parts[1], date_parts[2]
25
+
26
+ # use date indexed if date issued is in the future
27
+ if year.nil? || Date.new(*date_parts) > Time.zone.now.to_date
28
+ date_parts = metadata.fetch("indexed", {}).fetch("date-parts", []).first
29
+ year, month, day = date_parts[0], date_parts[1], date_parts[2]
30
+ end
31
+ metadata["issued"] = { "date-parts" => [date_parts] }
32
+
33
+ metadata["title"] = case metadata["title"].length
34
+ when 0 then nil
35
+ when 1 then metadata["title"][0]
36
+ else metadata["title"][0].presence || metadata["title"][1]
37
+ end
38
+
39
+ if metadata["title"].blank? && !TYPES_WITH_TITLE.include?(metadata["type"])
40
+ metadata["title"] = metadata["container-title"][0].presence || "No title"
41
+ end
42
+
43
+ metadata["container-title"] = metadata.fetch("container-title", [])[0]
44
+ metadata["type"] = CROSSREF_TYPE_TRANSLATIONS[metadata["type"]] if metadata["type"]
45
+ metadata["author"] = metadata["author"].map { |author| author.except("affiliation") }
46
+
47
+ metadata
48
+ end
49
+
50
+ def get_datacite_metadata(doi, options = {})
51
+ return {} if doi.blank?
52
+
53
+ params = { q: "doi:" + doi,
54
+ rows: 1,
55
+ fl: "doi,creator,title,publisher,publicationYear,resourceTypeGeneral,description,datacentre,datacentre_symbol,prefix,relatedIdentifier,xml,minted,updated",
56
+ wt: "json" }
57
+ url = "http://search.datacite.org/api?" + URI.encode_www_form(params)
58
+ response = Maremma.get(url, options)
59
+ return response if response["errors"]
60
+
61
+ metadata = response.fetch("data", {}).fetch("response", {}).fetch("docs", []).first
62
+ return { "errors" => [{ "title" => "Not found.", "status" => 404 }] } if metadata.blank?
63
+
64
+ doi = metadata.fetch("doi", nil)
65
+ doi = doi.upcase if doi.present?
66
+ title = metadata.fetch("title", []).first
67
+ title = title.chomp(".") if title.present?
68
+
69
+ xml = Base64.decode64(metadata.fetch('xml', "PGhzaD48L2hzaD4=\n"))
70
+ xml = Hash.from_xml(xml).fetch("resource", {})
71
+ authors = xml.fetch("creators", {}).fetch("creator", [])
72
+ authors = [authors] if authors.is_a?(Hash)
73
+
74
+ { "author" => get_hashed_authors(authors),
75
+ "title" => title,
76
+ "container-title" => metadata.fetch("publisher", nil),
77
+ "description" => metadata.fetch("description", nil),
78
+ "published" => metadata.fetch("publicationYear", nil),
79
+ "issued" => metadata.fetch("minted", nil),
80
+ "DOI" => doi,
81
+ "type" => metadata.fetch("resourceTypeGeneral", nil),
82
+ "subtype" => metadata.fetch("resourceType", nil),
83
+ "publisher_id" => metadata.fetch("datacentre_symbol", nil) }
84
+ end
85
+
86
+ def get_orcid_metadata(orcid, options = {})
87
+ return {} if orcid.blank?
88
+
89
+ url = "http://pub.orcid.org/v1.2/#{orcid}/orcid-bio"
90
+ response = Maremma.get(url, options)
91
+ return response if response["errors"]
92
+
93
+ metadata = response.fetch("data", {}).fetch("orcid_message", {}).fetch("orcid_profile", nil)
94
+ metadata.extend Hashie::Extensions::DeepFetch
95
+
96
+ personal_details = metadata.deep_fetch("orcid_bio", "personal_details") { {} }
97
+ personal_details.extend Hashie::Extensions::DeepFetch
98
+
99
+ author = { "family" => personal_details.fetch("family_name", nil),
100
+ "given" => personal_details.fetch("given_names", nil) }
101
+ url = metadata.deep_fetch("orcid_identifier", "uri") { nil }
102
+ timestamp = Time.zone.now.utc.iso8601
103
+
104
+ { "author" => [author],
105
+ "title" => "ORCID record for #{author.fetch('given', '')} #{author.fetch('family', '')}",
106
+ "container-title" => "ORCID Registry",
107
+ "issued" => get_date_parts(timestamp),
108
+ "timestamp" => timestamp,
109
+ "URL" => url,
110
+ "type" => 'entry' }
111
+ end
112
+
113
+ def get_doi_ra(doi, options = {})
114
+ return {} if doi.blank?
115
+
116
+ url = "http://doi.crossref.org/doiRA/" + CGI.unescape(doi)
117
+ response = Maremma.get(url, options.merge(host: true))
118
+ return response if response["errors"]
119
+
120
+ ra = response.fetch("data", {}).first.fetch("RA", nil)
121
+ if ra.present?
122
+ ra.delete(' ').downcase
123
+ else
124
+ { "errors" => [{ "title" => "An error occured", "status" => 400 }] }
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,125 @@
1
+ require 'active_support/all'
2
+ require 'nokogiri'
3
+
4
+ require_relative 'api'
5
+ require_relative 'author'
6
+ require_relative 'base'
7
+ require_relative 'date'
8
+ require_relative 'metadata'
9
+
10
+ module OrcidClient
11
+ class Notification
12
+ include OrcidClient::Base
13
+ include OrcidClient::Metadata
14
+ include OrcidClient::Author
15
+ include OrcidClient::Date
16
+ include OrcidClient::Api
17
+
18
+ attr_reader :doi, :orcid, :schema, :notification_access_token, :put_code, :subject, :intro, :notification_host, :validation_errors
19
+
20
+ def initialize(doi:, orcid:, notification_access_token:, **options)
21
+ @doi = doi
22
+ @orcid = orcid
23
+ @notification_access_token = notification_access_token
24
+ @put_code = options.fetch(:put_code, nil)
25
+ @subject = options.fetch(:subject, nil)
26
+ @intro = options.fetch(:intro, nil)
27
+ @notification_host = options[:sandbox] ? 'sandbox.orcid.org' : 'orcid.org'
28
+ end
29
+
30
+ SCHEMA = File.expand_path("../../../resources/notification_#{API_VERSION}/notification-permission-#{API_VERSION}.xsd", __FILE__)
31
+
32
+ def metadata
33
+ @metadata ||= get_metadata(doi, 'datacite')
34
+ end
35
+
36
+ def item_name
37
+ metadata.fetch('title', nil)
38
+ end
39
+
40
+ def item_type
41
+ "work"
42
+ end
43
+
44
+ def has_required_elements?
45
+ doi && item_name
46
+ end
47
+
48
+ def data
49
+ return nil unless has_required_elements?
50
+
51
+ Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
52
+ xml.send(:'notification:notification', root_attributes) do
53
+ insert_notification(xml)
54
+ end
55
+ end.to_xml
56
+ end
57
+
58
+ def insert_notification(xml)
59
+ insert_notification_type(xml)
60
+ insert_authorization_url(xml)
61
+ insert_notification_subject(xml)
62
+ insert_notification_intro(xml)
63
+ insert_items(xml)
64
+ end
65
+
66
+ def insert_notification_type(xml)
67
+ xml.send(:'notification:notification-type', "permission")
68
+ end
69
+
70
+ def insert_authorization_url(xml)
71
+ xml.send(:'notification:authorization-url') do
72
+ xml.send(:'notification:path', work_notification_path)
73
+ xml.send(:'notification:host', notification_host)
74
+ end
75
+ end
76
+
77
+ def work_notification_path
78
+ "/oauth/authorize?client_id=#{ENV['ORCID_CLIENT_ID']}&response_type=code&scope=/read-limited%20/activities/update%20/person/update&redirect_uri=#{ENV['REDIRECT_URI']}"
79
+ end
80
+
81
+ def insert_notification_subject(xml)
82
+ xml.send(:'notification:notification-subject', subject)
83
+ end
84
+
85
+ def insert_notification_intro(xml)
86
+ xml.send(:'notification:notification-intro', intro)
87
+ end
88
+
89
+ def insert_items(xml)
90
+ return nil unless has_required_elements?
91
+
92
+ xml.send(:'notification:items') do
93
+ xml.send(:'notification:item') do
94
+ xml.send(:'notification:item-type', item_type)
95
+ xml.send(:'notification:item-name', item_name)
96
+
97
+ insert_id(xml, "doi", doi, "self")
98
+ end
99
+ end
100
+ end
101
+
102
+ def insert_id(xml, id_type, value, relationship)
103
+ xml.send(:'common:external-id') do
104
+ xml.send(:'common:external-id-type', id_type)
105
+ xml.send(:'common:external-id-value', value)
106
+ xml.send(:'common:external-id-relationship', relationship)
107
+ end
108
+ end
109
+
110
+ def root_attributes
111
+ { :'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
112
+ :'xsi:schemaLocation' => 'http://www.orcid.org/ns/notification ../notification-permission-2.0_rc3.xsd',
113
+ :'xmlns:common' => 'http://www.orcid.org/ns/common',
114
+ :'xmlns:notification' => 'http://www.orcid.org/ns/notification' }
115
+ end
116
+
117
+ def schema
118
+ Nokogiri::XML::Schema(open(SCHEMA))
119
+ end
120
+
121
+ def validation_errors
122
+ @validation_errors ||= schema.validate(Nokogiri::XML(data)).map { |error| error.to_s }
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,3 @@
1
+ module OrcidClient
2
+ VERSION = "0.1"
3
+ end
@@ -0,0 +1,200 @@
1
+ require 'active_support/all'
2
+ require 'nokogiri'
3
+ require 'sanitize'
4
+
5
+ require_relative 'api'
6
+ require_relative 'author'
7
+ require_relative 'base'
8
+ require_relative 'date'
9
+ require_relative 'metadata'
10
+ require_relative 'work_type'
11
+
12
+ module OrcidClient
13
+ class Work
14
+ include OrcidClient::Base
15
+ include OrcidClient::Metadata
16
+ include OrcidClient::Author
17
+ include OrcidClient::Date
18
+ include OrcidClient::WorkType
19
+ include OrcidClient::Api
20
+
21
+ attr_reader :doi, :orcid, :schema, :access_token, :put_code, :validation_errors
22
+
23
+ def initialize(doi:, orcid:, access_token:, **options)
24
+ @doi = doi
25
+ @orcid = orcid
26
+ @access_token = access_token
27
+ @put_code = options.fetch(:put_code, nil)
28
+ end
29
+
30
+ SCHEMA = File.expand_path("../../../resources/record_#{API_VERSION}/work-#{API_VERSION}.xsd", __FILE__)
31
+
32
+ def metadata
33
+ @metadata ||= get_metadata(doi, 'datacite')
34
+ end
35
+
36
+ def contributors
37
+ Array(metadata.fetch('author', nil)).map do |contributor|
38
+ { orcid: contributor.fetch('ORCID', nil),
39
+ credit_name: get_credit_name(contributor),
40
+ role: nil }.compact
41
+ end
42
+ end
43
+
44
+ def author_string
45
+ Array(metadata.fetch('author', nil)).map do |contributor|
46
+ get_full_name(contributor)
47
+ end.join(" and ")
48
+ end
49
+
50
+ def sanitize(string)
51
+ Sanitize.fragment(string).squish
52
+ end
53
+
54
+ def title
55
+ metadata.fetch('title', nil)
56
+ end
57
+
58
+ def container_title
59
+ metadata.fetch('container-title', nil)
60
+ end
61
+
62
+ def publisher_id
63
+ metadata.fetch('publisher_id', nil)
64
+ end
65
+
66
+ def publication_date
67
+ get_year_month_day(metadata.fetch('published', nil))
68
+ end
69
+
70
+ def description
71
+ sanitize(Array(metadata.fetch('description', nil)).first)
72
+ end
73
+
74
+ def type
75
+ orcid_work_type(metadata.fetch('type', nil), metadata.fetch('subtype', nil))
76
+ end
77
+
78
+ def has_required_elements?
79
+ doi && contributors && title && container_title && publication_date
80
+ end
81
+
82
+ def data
83
+ return nil unless has_required_elements?
84
+
85
+ Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
86
+ xml.send(:'work:work', root_attributes) do
87
+ insert_work(xml)
88
+ end
89
+ end.to_xml
90
+ end
91
+
92
+ def insert_work(xml)
93
+ insert_titles(xml)
94
+ insert_description(xml)
95
+ insert_type(xml)
96
+ insert_pub_date(xml)
97
+ insert_ids(xml)
98
+ insert_contributors(xml)
99
+ end
100
+
101
+ def insert_titles(xml)
102
+ if title
103
+ xml.send(:'work:title') do
104
+ xml.send(:'common:title', title)
105
+ end
106
+ end
107
+
108
+ xml.send(:'work:journal-title', container_title) if container_title
109
+ end
110
+
111
+ def insert_description(xml)
112
+ return nil unless description.present?
113
+
114
+ xml.send(:'work:short-description', description.truncate(2500, separator: ' '))
115
+ end
116
+
117
+ def insert_type(xml)
118
+ xml.send(:'work:type', type)
119
+ end
120
+
121
+ def insert_pub_date(xml)
122
+ if publication_date['year']
123
+ xml.send(:'common:publication-date') do
124
+ xml.year(publication_date.fetch('year'))
125
+ xml.month(publication_date.fetch('month', nil)) if publication_date['month']
126
+ xml.day(publication_date.fetch('day', nil)) if publication_date['month'] && publication_date['day']
127
+ end
128
+ end
129
+ end
130
+
131
+ def insert_ids(xml)
132
+ xml.send(:'common:external-ids') do
133
+ insert_id(xml, 'doi', doi, 'self')
134
+ end
135
+ end
136
+
137
+ def insert_id(xml, id_type, value, relationship)
138
+ xml.send(:'common:external-id') do
139
+ xml.send(:'common:external-id-type', id_type)
140
+ xml.send(:'common:external-id-value', value)
141
+ xml.send(:'common:external-id-relationship', relationship)
142
+ end
143
+ end
144
+
145
+ def insert_contributors(xml)
146
+ return nil unless contributors.present?
147
+
148
+ xml.send(:'work:contributors') do
149
+ contributors.each do |contributor|
150
+ xml.contributor do
151
+ insert_contributor(xml, contributor)
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ def insert_contributor(xml, contributor)
158
+ if contributor[:orcid].present?
159
+ xml.send(:'common:contributor-orcid') do
160
+ xml.send(:'common:uri', contributor[:orcid])
161
+ xml.send(:'common:path', contributor[:orcid][17..-1])
162
+ xml.send(:'common:host', 'orcid.org')
163
+ end
164
+ end
165
+ xml.send(:'credit-name', contributor[:credit_name])
166
+ if contributor[:role]
167
+ xml.send(:'contributor-attributes') do
168
+ xml.send(:'contributor-role', contributor[:role])
169
+ end
170
+ end
171
+ end
172
+
173
+ def without_control(s)
174
+ r = ''
175
+ s.each_codepoint do |c|
176
+ if c >= 32
177
+ r << c
178
+ end
179
+ end
180
+ r
181
+ end
182
+
183
+ def root_attributes
184
+ { :'put-code' => put_code,
185
+ :'visibility' => 'public',
186
+ :'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance',
187
+ :'xsi:schemaLocation' => 'http://www.orcid.org/ns/work ../work-2.0_rc3.xsd',
188
+ :'xmlns:common' => 'http://www.orcid.org/ns/common',
189
+ :'xmlns:work' => 'http://www.orcid.org/ns/work' }.compact
190
+ end
191
+
192
+ def schema
193
+ Nokogiri::XML::Schema(open(SCHEMA))
194
+ end
195
+
196
+ def validation_errors
197
+ @validation_errors ||= schema.validate(Nokogiri::XML(data)).map { |error| error.to_s }
198
+ end
199
+ end
200
+ end