orcid_client 0.1

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