From cc29888985e430fcf61fd5675950ef6834b4d9e8 Mon Sep 17 00:00:00 2001 From: briri Date: Tue, 15 Aug 2023 15:13:22 -0700 Subject: [PATCH 01/13] updates to fix issues with awards apis, added citation service, updated dynamo to use on-demand instead of provisioned --- config/dev/regional/dynamo.yaml | 6 +- .../components/contributors/contributors.js | 2 +- .../src/components/outputs/outputs.js | 7 +- .../src/components/works/works.js | 8 +- src/landing_page/src/index.scss | 4 + src/landing_page/src/pages/landing/landing.js | 8 +- src/sam/functions/citer/app.rb | 208 ++++++++++++++++++ src/sam/functions/ezid_publisher/app.rb | 4 +- src/sam/functions/get_awards_crossref/app.rb | 24 +- src/sam/functions/get_awards_nih/app.rb | 4 +- src/sam/functions/get_awards_nsf/app.rb | 3 +- .../lib/uc3-dmp-dynamo/client.rb | 3 +- .../lib/uc3-dmp-event-bridge/publisher.rb | 5 +- .../lib/uc3-dmp-event-bridge/version.rb | 2 +- .../uc3-dmp-event-bridge-0.0.3.gem | Bin 6144 -> 0 bytes .../uc3-dmp-event-bridge-0.0.6.gem | Bin 0 -> 6144 bytes .../lib/uc3-dmp-external-api/client.rb | 4 +- .../lib/uc3-dmp-external-api/version.rb | 2 +- .../uc3-dmp-external-api-0.0.11.gem | Bin 6144 -> 0 bytes .../uc3-dmp-external-api-0.0.15.gem | Bin 0 -> 6144 bytes .../gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb | 13 ++ .../gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb | 15 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb | 16 ++ .../gems/uc3-dmp-id/lib/uc3-dmp-id/version.rb | 2 +- .../gems/uc3-dmp-id/uc3-dmp-id-0.0.127.gem | Bin 28672 -> 0 bytes .../gems/uc3-dmp-id/uc3-dmp-id-0.0.137.gem | Bin 0 -> 29184 bytes src/sam/layers/Gemfile.lock | 10 +- src/sam/template.yaml | 69 +++++- templates/dynamo.yaml | 51 +++-- 29 files changed, 418 insertions(+), 52 deletions(-) create mode 100644 src/sam/functions/citer/app.rb delete mode 100644 src/sam/gems/uc3-dmp-event-bridge/uc3-dmp-event-bridge-0.0.3.gem create mode 100644 src/sam/gems/uc3-dmp-event-bridge/uc3-dmp-event-bridge-0.0.6.gem delete mode 100644 src/sam/gems/uc3-dmp-external-api/uc3-dmp-external-api-0.0.11.gem create mode 100644 src/sam/gems/uc3-dmp-external-api/uc3-dmp-external-api-0.0.15.gem delete mode 100644 src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.0.127.gem create mode 100644 src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.0.137.gem diff --git a/config/dev/regional/dynamo.yaml b/config/dev/regional/dynamo.yaml index 2e5853cf..b4597e67 100644 --- a/config/dev/regional/dynamo.yaml +++ b/config/dev/regional/dynamo.yaml @@ -14,9 +14,9 @@ parameters: DynamoTableClass: 'STANDARD' DynamoEnableContributorInsights: 'true' DynamoEnablePointInTimeRecovery: 'false' - DynamoBillingMode: 'PROVISIONED' - DynamoReadCapacityUnits: '16' - DynamoWriteCapacityUnits: '60' + DynamoBillingMode: 'PAY_PER_REQUEST' + DynamoReadCapacityUnits: '8' + DynamoWriteCapacityUnits: '30' hooks: after_create: diff --git a/src/landing_page/src/components/contributors/contributors.js b/src/landing_page/src/components/contributors/contributors.js index 0430def7..867b6605 100644 --- a/src/landing_page/src/components/contributors/contributors.js +++ b/src/landing_page/src/components/contributors/contributors.js @@ -34,7 +34,7 @@ function OrgLink(props) { if (org !== undefined) { if (org.affiliation_id !== '') { return ( - + ); } else { return org.name; diff --git a/src/landing_page/src/components/outputs/outputs.js b/src/landing_page/src/components/outputs/outputs.js index fe8c6708..3bfc0f4d 100644 --- a/src/landing_page/src/components/outputs/outputs.js +++ b/src/landing_page/src/components/outputs/outputs.js @@ -57,12 +57,11 @@ function LicenseLink(props) { let licenses = props?.licenses || []; let idx = props?.index; + // DMPTool only allows for one license, so just display the first one return ( - {licenses.map((license, index) => ( - - ))} + ); } diff --git a/src/landing_page/src/components/works/works.js b/src/landing_page/src/components/works/works.js index c92f5fe7..49e4a25c 100644 --- a/src/landing_page/src/components/works/works.js +++ b/src/landing_page/src/components/works/works.js @@ -1,3 +1,4 @@ +import { SanitizeHTML } from '../../utils'; import { Link } from '../../components/link/link'; function groupByType(works) { @@ -17,7 +18,12 @@ function Work(props) { return (
  • - No citation available. + {work.citation !== undefined && +

    + } + {work.citation === undefined && +

    No citation available.

    + }
  • ); } diff --git a/src/landing_page/src/index.scss b/src/landing_page/src/index.scss index 54b717f8..1a56e953 100644 --- a/src/landing_page/src/index.scss +++ b/src/landing_page/src/index.scss @@ -312,6 +312,10 @@ code { margin-right: 10px; } + main .landing-list p { + line-height: var(--dmptool-brand-line-height1); + } + // ***** List item separators ***** // li.comma-separated a:not(:last-child) { &::after { diff --git a/src/landing_page/src/pages/landing/landing.js b/src/landing_page/src/pages/landing/landing.js index a26ba335..35ed19eb 100644 --- a/src/landing_page/src/pages/landing/landing.js +++ b/src/landing_page/src/pages/landing/landing.js @@ -117,7 +117,11 @@ function Landing() { } } function filterWorks(works) { - return works.filter((work) => work?.work_type !== 'output_management_plan' ); + if (works !== undefined) { + return works.filter((work) => work?.work_type !== 'output_management_plan' ); + } else { + return []; + } } return ( @@ -137,7 +141,7 @@ function Landing() {

    This page describes a data management plan written for the using the . - You can access this infomation as

    + You can access this infomation as here.

    {formData.title === '' ? formData.project_title : formData.title}

    {isPublic() && narrativeUrl() && ( diff --git a/src/sam/functions/citer/app.rb b/src/sam/functions/citer/app.rb new file mode 100644 index 00000000..2489052c --- /dev/null +++ b/src/sam/functions/citer/app.rb @@ -0,0 +1,208 @@ +# frozen_string_literal: true + +# Docs say that the LambdaLayer gems are found mounted as /opt/ruby/gems but an inspection +# of the $LOAD_PATH shows that only /opt/ruby/lib is available. So we add what we want here +# and indicate exactly which folders contain the *.rb files +my_gem_path = Dir['/opt/ruby/gems/**/lib/'] +$LOAD_PATH.unshift(*my_gem_path) + +require 'bibtex' +require 'citeproc' +require 'csl/styles' + +require 'uc3-dmp-api-core' +require 'uc3-dmp-cloudwatch' +require 'uc3-dmp-dynamo' +require 'uc3-dmp-event-bridge' +require 'uc3-dmp-external-api' +require 'uc3-dmp-id' + +module Functions + # Lambda function that is invoked by SNS and communicates with EZID to register/update DMP IDs + class Citer + SOURCE = 'Citer' + + APPLICATION_NAME = 'DMPTool' + DEFAULT_CITATION_STYLE = 'chicago-author-date' + DEFAULT_DOI_URL = 'http://doi.org' + DEFAULT_WORK_TYPE = 'Dataset' + + MSG_BIBTEX_FAILURE = 'Unable to fetch Bibtex for the specified DOI.' + MSG_UNABLE_TO_UPDATE = 'Unable to update the citations on the DMP ID.' + + # Parameters + # ---------- + # event: Hash, required + # EventBridge Event input: + # { + # "version": "0", + # "id": "5c9a3747-293c-59d7-dcee-a2210ac034fc", + # "detail-type": "DMP change", + # "source": "dmphub.uc3dev.cdlib.net:lambda:event_publisher", + # "account": "1234567890", + # "time": "2023-02-14T16:42:06Z", + # "region": "us-west-2", + # "resources": [], + # "detail": { + # "PK": "DMP#doi.org/10.12345/ABC123", + # "SK": "VERSION#latest", + # "dmproadmap_related_identifier": { + # "work_type": "article", + # "descriptor": "references", + # "type": "doi", + # "identifier": "https://dx.doi.org/10.12345/ABCD1234" + # } + # } + # } + # + # context: object, required + # Lambda Context runtime methods and attributes + # Context doc: https://docs.aws.amazon.com/lambda/latest/dg/ruby-context.html + class << self + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def process(event:, context:) + # Setup the Logger + log_level = ENV.fetch('LOG_LEVEL', 'error') + req_id = context.aws_request_id if context.is_a?(LambdaContext) + logger = Uc3DmpCloudwatch::Logger.new(source: SOURCE, request_id: req_id, event: event, level: log_level) + + # No need to validate the source and detail-type because that is done by the EventRule + detail = event.fetch('detail', {}) + json = detail.is_a?(Hash) ? detail : JSON.parse(detail) + dmp_pk = json['PK'] + dmp_sk = json.fetch('SK', Uc3DmpId::Helper::DMP_LATEST_VERSION) + _respond(status: 400, errors: [Uc3DmpApiCore::MSG_INVALID_ARGS], event: event) if dmp_pk.nil? || dmp_sk.nil? + + # Load the DMP metadata + dmp = Uc3DmpId::Finder.by_pk(p_key: dmp_pk, s_key: dmp_sk, cleanse: false, logger: logger) + _respond(status: 404, errors: [Uc3DmpId::MSG_DMP_NOT_FOUND], event: event) if dmp.nil? + + # Get all of the related identifiers that are DOIs and are un-cited + identifiers = dmp.fetch('dmp', {}).fetch('dmproadmap_related_identifiers', []) + uncited = Uc3DmpId::Helper.citable_related_identifiers(dmp: dmp['dmp']) + _respond(status: 200, items: [], event: event) if identifiers.empty? || uncited.empty? + + existing_citations = identifiers.reject { |id| uncited.include?(id) } + headers = { Accept: 'application/x-bibtex' } + + processed = [] + uncited.each do |identifier| + uri = _doi_to_uri(doi: identifier['identifier']&.strip) + if !uri.nil? && !uri.blank? + logger.debug(message: "Fetching BibTeX from: #{uri}") + resp = Uc3DmpExternalApi::Client.call(url: uri, method: :get, additional_headers: headers, logger: logger) + + unless resp.nil? || resp.to_s.strip.empty? + bibtex = BibTeX.parse(_cleanse_bibtex(text: resp)) + work_type = identifier['work_type'].nil? ? determine_work_type(bibtex: bibtex) : identifier['work_type'] + identifier['citation'] = _bibtex_to_citation(uri: uri, work_type: work_type, bibtex: bibtex) + end + end + + processed << identifier + end + + logger.debug(message: 'Results of citation retrieval', details: processed) + dmp['dmp']['dmproadmap_related_identifiers'] = existing_citations + processed + + # Remove the version info because we don't want to save it on the record + dmp['dmp'].delete('dmphub_versions') + + client = Uc3DmpDynamo::Client.new + resp = client.put_item(json: dmp['dmp'], logger: logger) + _respond(status: 500, errros: [MSG_UNABLE_TO_UPDATE], event: event) if resp.nil? + + _respond(status: 200, items: [], event: event) + rescue Uc3DmpId::FinderError => e + logger.error(message: e.message, details: e.backtrace) + _respond(status: 500, errors: [e.message], event: event) + rescue Uc3DmpExternalApi::ExternalApiError => e + _respond(status: 500, errors: [e.message], event: event) + rescue StandardError => e + logger.error(message: e.message, details: e.backtrace) + { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + + private + + # Send the output to the Responder + def _respond(status:, items: [], errors: [], event: {}, params: {}) + Uc3DmpApiCore::Responder.respond( + status: status, items: items, errors: errors, event: event, + page: params['page'], per_page: params['per_page'] + ) + end + + # Will convert 'doi:10.1234/abcdefg' to 'http://doi.org/10.1234/abcdefg' + def _doi_to_uri(doi:) + +puts "Matched? #{doi.match(Uc3DmpId::Helper::DOI_REGEX).to_s}" + + val = doi.match(Uc3DmpId::Helper::DOI_REGEX).to_s + return nil if val.nil? || val.strip == '' + + doi.start_with?('http') ? doi : "#{DEFAULT_DOI_URL}/#{doi.gsub('doi:', '')}" + end + + # If no :work_type was specified we can try to derive it from the BibTeX metadata + def _determine_work_type(bibtex:) + return '' if bibtex.nil? || bibtex.data.nil? || bibtex.data.first.nil? + + return 'article' unless bibtex.data.first.journal.nil? + + '' + end + + def _cleanse_bibtex(text:) + return nil if text.nil? || text.to_s.strip == '' + + # Make sure we're working with UTF8 + utf8 = text.force_encoding('UTF-8') + + # Remove any encoded HTML (e.g. "Regular text $\\lt$strong$\\gt$Bold text$\\lt$/strong$\\gt$") + utf8 = utf8.gsub(%r{\$?\\\$?(less|lt|Lt)\$/?[a-zA-Z]+\$?\\\$?(greater|gt|Gt)\$}, '') + # Replace any special dash and quote characters with a minus sign or single/double quote + utf8.gsub(%r{\$?\\(T|t)ext[a-zA-Z]+dash\$?}, '-').gsub(%r{\{(T|t)ext[a-zA-Z]+dash\}}, '-') + .gsub(%r{\$?\\(M|m)athsemicolon\$?}, ':').gsub(%r{\{(M|m)semicolon\}}, ':') + .gsub(%r{\$?\\(T|t)extquotesingle\$?}, "'").gsub(%r{\{(T|t)extquotesingle\}}, "'") + .gsub(%r{\$?\\(T|t)extquotedouble\$?}, '"').gsub(%r{\{(T|t)extquotedouble\}}, '"') + end + + # Convert the BibTeX item to a citation + def _bibtex_to_citation(uri:, work_type: DEFAULT_WORK_TYPE, bibtex:, style: DEFAULT_CITATION_STYLE) + return nil unless uri.is_a?(String) && uri.strip != '' + return nil if bibtex.nil? || bibtex.data.nil? || bibtex.data.first.nil? + + cp = CiteProc::Processor.new(style: style, format: 'html') + cp.import(bibtex.to_citeproc) + citation = cp.render(:bibliography, id: bibtex.data.first.id) + return nil unless citation.is_a?(Array) && citation.any? + + # The CiteProc renderer has trouble with some things so fix them here + # - For some reason words in all caps in the title get wrapped in curl brackets + citation = citation.first.gsub('{', '').gsub('}', '') + + unless work_type.nil? || work_type.strip == '' + # This supports the :apa and :chicago-author-date styles + citation = citation.gsub(/\.”\s+/, "\.” [#{work_type.gsub('_', ' ').capitalize}]. ") + .gsub(/<\/i>\.\s+/, "<\/i>\. [#{work_type.gsub('_', ' ').capitalize}]. ") + end + + # Convert the URL into a link. Ensure that the trailing period is not a part of + # the link! + citation.gsub(URI.regexp) do |url| + if url.start_with?('http') + '%{url}.' % { + url: url.end_with?('.') ? uri : "#{uri}." + } + else + url + end + end + end + end + end +end diff --git a/src/sam/functions/ezid_publisher/app.rb b/src/sam/functions/ezid_publisher/app.rb index 21f94700..defb6d14 100644 --- a/src/sam/functions/ezid_publisher/app.rb +++ b/src/sam/functions/ezid_publisher/app.rb @@ -46,7 +46,7 @@ class EzidPublisher # "version": "0", # "id": "5c9a3747-293c-59d7-dcee-a2210ac034fc", # "detail-type": "DMP change", - # "source": "dmphub-dev.cdlib.org:lambda:event_publisher", + # "source": "dmphub.uc3dev.cdlib.net:lambda:event_publisher", # "account": "1234567890", # "time": "2023-02-14T16:42:06Z", # "region": "us-west-2", @@ -101,7 +101,7 @@ def process(event:, context:) # If submissions are paused, toss the event into the EventBridge archive where it can be # replayed at a later time if paused - logger.info("SUBMISSIONS PAUSED: Placing event #{event['id']} in the EventBridge archive.", details: payload) + logger.info(message: "SUBMISSIONS PAUSED: Placing event #{event['id']} in the EventBridge archive.", details: json) Uc3DmpEventBridge.publish(source: SOURCE, dmp: json, event_type: 'paused', logger: logger) _respond(status: 200, items: [], event: event) end diff --git a/src/sam/functions/get_awards_crossref/app.rb b/src/sam/functions/get_awards_crossref/app.rb index cc0598e9..a696f04e 100644 --- a/src/sam/functions/get_awards_crossref/app.rb +++ b/src/sam/functions/get_awards_crossref/app.rb @@ -36,12 +36,12 @@ def self.process(event:, context:) logger = Uc3DmpCloudwatch::Logger.new(source: SOURCE, request_id: req_id, event: event, level: log_level) funder = event.fetch('pathParameters', {})['funder_id'] - return Responder.respond(status: 400, errors: MSG_NO_FUNDER) if funder.nil? || funder.empty? + return _respond(status: 400, errors: [MSG_NO_FUNDER]) if funder.nil? || funder.empty? params = event.fetch('queryStringParameters', {}) pi_names = params.fetch('pi_names', '') project_num = params.fetch('project', '') - title = params.fetch('keyword', '') + title = params.fetch('keywords', '') years = params.fetch('years', (Date.today.year..Date.today.year - 3).to_a.join(',')) years = years.split(',').map(&:to_i) return _respond(status: 400, errors: [MSG_BAD_ARGS], event: event) if (project_num.nil? || project_num.empty?) && @@ -50,7 +50,7 @@ def self.process(event:, context:) (years.nil? || years.empty?) url = "/#{project_num}" unless project_num.nil? || project_num.empty? - url = "?#{_prepare_query_string(funder: funder, pi_names: pi_names, title: title, years: years)}" if url.nil? + url = "?#{_prepare_query_string2(funder: funder, pi_names: pi_names, title: title, years: years)}" if url.nil? url = "#{API_BASE_URL}#{url}" logger.debug(message: "Calling Crossref Award API: #{url}") if logger.respond_to?(:debug) @@ -88,6 +88,23 @@ def _sanitize_params(str:, params: {}) str end + def _prepare_query_string2(funder:, pi_names: [], title: '', years: []) + return '' if funder.nil? + + args = [] + args << title unless title.nil? || title.to_s.strip == '' + args << pi_names.split(',')&.map { |pi| pi.strip } unless pi_names.nil? || pi_names.to_s.strip == '' + args << years if years.is_a?(Array) && years.any? + return '' unless args.is_a?(Array) && args.any? + + qs = ['sort=score'] + qs << "filter=type:grant,funder:#{funder}" + args = args.flatten.join(', ') + qs << _sanitize_params(str: 'query.bibliographic=%22:args%22', params: { args: args }) + qs << "mailto=#{ENV.fetch('ADMIN_EMAIL', 'dmptool@ucop.edu')}" + qs.join('&') + end + # Prepare the query string for the API call def _prepare_query_string(funder:, pi_names: [], title: '', years: []) return '' if funder.nil? @@ -107,6 +124,7 @@ def _prepare_query_string(funder:, pi_names: [], title: '', years: []) end: "#{years.last}-12-31" } qs += _sanitize_params(str: ",from-awarded-date::start,until-awarded-date::end", params: filter_params) + qs += "&mailto:#{ENV.fetch('ADMIN_EMAIL', 'dmptool@ucop.edu')}" end # Convert the PI info from the response into "Last, First" diff --git a/src/sam/functions/get_awards_nih/app.rb b/src/sam/functions/get_awards_nih/app.rb index 9c1e6cdf..68cdf6ff 100644 --- a/src/sam/functions/get_awards_nih/app.rb +++ b/src/sam/functions/get_awards_nih/app.rb @@ -43,7 +43,7 @@ def self.process(event:, context:) years: params[:fiscal_years], pi_names: _prepare_pi_names_for_search(pi_names: params[:pi_names]), opportunity_nbrs: [params[:opportunity_nbr]], - project_nums: [params[:project_num]] + project_nums: [params[:project_num]], title: params[:title] ) @@ -79,7 +79,7 @@ def _parse_params(event:) project_num: params.fetch('project', ''), opportunity_nbr: params.fetch('opportunity', ''), pi_names: params.fetch('pi_names', ''), - title: params.fetch('keyword', ''), + title: params.fetch('keywords', ''), fiscal_years: fiscal_years.split(',').map(&:to_i) } end diff --git a/src/sam/functions/get_awards_nsf/app.rb b/src/sam/functions/get_awards_nsf/app.rb index b5457901..dad19715 100644 --- a/src/sam/functions/get_awards_nsf/app.rb +++ b/src/sam/functions/get_awards_nsf/app.rb @@ -126,10 +126,11 @@ def _transform_response(response_body:) response_body.fetch('response', {}).fetch('award', []).map do |award| next if award['title'].nil? || award['id'].nil? || award['piLastName'].nil? + date_parts = award.fetch('date', '').split('/') { project: { title: award['title'], - start: award.fetch('date', '').split('/').reverse.join('-'), + start: date_parts.length == 3 ? [date_parts[2], date_parts[0], date_parts[1]].join('-') : '', funding: [ dmproadmap_award_amount: award['fundsObligatedAmt'], dmproadmap_project_number: award['id'], diff --git a/src/sam/gems/uc3-dmp-dynamo/lib/uc3-dmp-dynamo/client.rb b/src/sam/gems/uc3-dmp-dynamo/lib/uc3-dmp-dynamo/client.rb index 1afc792f..1ca3e2d0 100644 --- a/src/sam/gems/uc3-dmp-dynamo/lib/uc3-dmp-dynamo/client.rb +++ b/src/sam/gems/uc3-dmp-dynamo/lib/uc3-dmp-dynamo/client.rb @@ -86,7 +86,8 @@ def put_item(json:, logger: nil) raise ClientError, MSG_INVALID_KEY unless json.is_a?(Hash) && !json['PK'].nil? && !json['SK'].nil? resp = @connection.put_item( - { table_name: @table, + { + table_name: @table, item: json, return_consumed_capacity: logger&.level == 'debug' ? 'TOTAL' : 'NONE' } diff --git a/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/publisher.rb b/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/publisher.rb index 2be24f0c..3273a6f6 100644 --- a/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/publisher.rb +++ b/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/publisher.rb @@ -27,15 +27,16 @@ def initialize(**_args) # Publish an event to the EventBus so that other Lambdas can do their thing # rubocop:disable Metrics/AbcSize - def publish(source:, dmp:, event_type: DEFAULT_EVENT_TYPE, logger: nil) + def publish(source:, dmp:, event_type: DEFAULT_EVENT_TYPE, detail: nil, logger: nil) source = "#{source} -> #{SOURCE}.publish" + detail = detail.nil? ? _generate_detail(dmp: dmp).to_json : (detail.is_a?(Hash) ? detail.to_json : detail.to_s) message = { entries: [{ time: Time.now, source: "#{ENV.fetch('DOMAIN', nil)}:lambda:event_publisher", detail_type: event_type.to_s, - detail: _generate_detail(dmp: dmp).to_json, + detail: detail, event_bus_name: ENV.fetch('EVENT_BUS_NAME', nil) }] } diff --git a/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/version.rb b/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/version.rb index d8c4f347..0187fc74 100644 --- a/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/version.rb +++ b/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Uc3DmpEventBridge - VERSION = '0.0.3' + VERSION = '0.0.6' end diff --git a/src/sam/gems/uc3-dmp-event-bridge/uc3-dmp-event-bridge-0.0.3.gem b/src/sam/gems/uc3-dmp-event-bridge/uc3-dmp-event-bridge-0.0.3.gem deleted file mode 100644 index 7359b03f7acc6e9df2850e4f5db19a80b315035a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6144 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 G2>cEj^iQ(@ diff --git a/src/sam/gems/uc3-dmp-event-bridge/uc3-dmp-event-bridge-0.0.6.gem b/src/sam/gems/uc3-dmp-event-bridge/uc3-dmp-event-bridge-0.0.6.gem new file mode 100644 index 0000000000000000000000000000000000000000..dc7700774507d41906948e26b281f3d0cf6ccbdb GIT binary patch literal 6144 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 diff --git a/src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.0.137.gem b/src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.0.137.gem new file mode 100644 index 0000000000000000000000000000000000000000..af8c56c4fbff7d8970c6ea6965fe4495bcafb359 GIT binary patch literal 29184 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 literal 0 HcmV?d00001 diff --git a/src/sam/layers/Gemfile.lock b/src/sam/layers/Gemfile.lock index 3cda6529..28a6e4c2 100644 --- a/src/sam/layers/Gemfile.lock +++ b/src/sam/layers/Gemfile.lock @@ -5,11 +5,11 @@ GEM public_suffix (>= 2.0.2, < 6.0) ast (2.4.2) aws-eventstream (1.2.0) - aws-partitions (1.799.0) + aws-partitions (1.804.0) aws-sdk-cognitoidentityprovider (1.79.0) aws-sdk-core (~> 3, >= 3.177.0) aws-sigv4 (~> 1.1) - aws-sdk-core (3.180.2) + aws-sdk-core (3.180.3) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.651.0) aws-sigv4 (~> 1.5) @@ -120,16 +120,16 @@ GEM aws-sdk-dynamodb (~> 1.83) json (~> 2.6) logger (~> 1.4) - uc3-dmp-event-bridge (0.0.3) + uc3-dmp-event-bridge (0.0.6) aws-sdk-eventbridge (~> 1.44) json (~> 2.6) logger (~> 1.4) - uc3-dmp-external-api (0.0.11) + uc3-dmp-external-api (0.0.15) aws-sdk-sns (~> 1.60) aws-sdk-ssm (~> 1.150) httparty (~> 0.21.0) json (~> 2.6) - uc3-dmp-id (0.0.127) + uc3-dmp-id (0.0.137) json (~> 2.6) json-schema (~> 3.0) uc3-dmp-dynamo (~> 0.0) diff --git a/src/sam/template.yaml b/src/sam/template.yaml index 4c938233..6e221564 100644 --- a/src/sam/template.yaml +++ b/src/sam/template.yaml @@ -359,7 +359,7 @@ Resources: Environment: Variables: LAMBDA_ENV: !Ref Env - LOG_LEVEL: !Ref DebugLevel # 'debug' + LOG_LEVEL: 'debug' # !Ref DebugLevel # 'debug' DOMAIN: !Ref DomainName SNS_FATAL_ERROR_TOPIC: !Ref SnsEmailTopicArn CORS_ORIGIN: !If @@ -797,6 +797,73 @@ Resources: # EventBridge Lambdas # See: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-sns.html # ---------------------------------------------------- + # Lambda responsible for communications with EZID + CiterFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: 'functions/citer/' + Handler: 'app.Functions::Citer.process' + Runtime: 'ruby2.7' + # Depending on the DMP ID, there may be a lot of citations so we timeout after 5 minutes + Timeout: 300 + Architectures: + - 'x86_64' + Layers: + - !Ref LambdaLayer + Policies: + - 'arn:aws:iam::aws:policy/AmazonSSMFullAccess' + - 'arn:aws:iam::aws:policy/AmazonSNSFullAccess' + - 'arn:aws:iam::aws:policy/AmazonSQSFullAccess' + - 'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess' + - !Ref DmpHubDynamoTableWritePolicy + - !Ref EventBusPolicy + Environment: + Variables: + LAMBDA_ENV: !Ref Env + LOG_LEVEL: 'debug' # !Ref DebugLevel # 'debug' + DOMAIN: !Ref DomainName + DMP_ID_BASE_URL: 'doi.org' + API_BASE_URL: !Sub 'https://api.${DomainName}' + SNS_FATAL_ERROR_TOPIC: !Ref SnsEmailTopicArn + EVENT_BUS_NAME: !Ref EventBusArn + DYNAMO_TABLE: !Ref DynamoTableName + Events: + CiterEvent: + Type: 'EventBridgeRule' + Properties: + DeadLetterConfig: + Arn: !Ref DeadLetterQueueArn + EventBusName: !Ref EventBusArn + Pattern: !Sub > + { + "source": ["${DomainName}:lambda:event_publisher"], + "detail-type": ["Citation Fetch"], + "detail": { + "SK": ["VERSION#latest"] + } + } + RetryPolicy: + MaximumEventAgeInSeconds: 180 + MaximumRetryAttempts: 3 + State: 'ENABLED' # 'DISABLED' + + CiterPermission: + Type: 'AWS::Lambda::Permission' + Properties: + FunctionName: !Ref CiterFunction + Action: 'lambda:InvokeFunction' + Principal: 'events.amazonaws.com' + # SourceArn: !GetAtt CiterEventRule.Arn + + # Annoyingly, SAM does not auto-generate LogGroup for a non-API tiggered lambda + CiterFunctionLogGroup: + Type: 'AWS::Logs::LogGroup' + DependsOn: + - CiterFunction + Properties: + LogGroupName: !Sub "/aws/lambda/${CiterFunction}" + RetentionInDays: !Ref LogRetentionDays + # Lambda responsible for communications with EZID EzidPublisherFunction: Type: 'AWS::Serverless::Function' diff --git a/templates/dynamo.yaml b/templates/dynamo.yaml index e95dd17f..c86b6714 100644 --- a/templates/dynamo.yaml +++ b/templates/dynamo.yaml @@ -52,6 +52,9 @@ Conditions: PreventDelete: !Equals [!Ref Env, 'prd'] + ProvisionedCapacity: + !Not [!Equals [!Ref DynamoBillingMode, 'PAY_PER_REQUEST']] + Resources: # ---------------------------------------------- # DynamoDB polymorphic table @@ -116,9 +119,11 @@ Resources: NonKeyAttributes: ['SK', 'title', 'description', 'created', 'modified', 'contact', 'contributor', 'project', 'dmproadmap_assertion_needed'] ContributorInsightsSpecification: Enabled: !Ref DynamoEnableContributorInsights - ProvisionedThroughput: - ReadCapacityUnits: !Ref DynamoReadCapacityUnits - WriteCapacityUnits: !Ref DynamoWriteCapacityUnits + ProvisionedThroughput: !If + - ProvisionedCapacity + - ReadCapacityUnits: !Ref DynamoReadCapacityUnits + WriteCapacityUnits: !Ref DynamoWriteCapacityUnits + - !Ref AWS::NoValue # An index to help us quickly fetch all of the DMPs for a specific organization - IndexName: 'dmphub_owner_id_gsi' @@ -132,9 +137,11 @@ Resources: NonKeyAttributes: ['SK', 'title', 'description', 'created', 'modified', 'contact', 'contributor', 'project', 'dmproadmap_assertion_needed'] ContributorInsightsSpecification: Enabled: !Ref DynamoEnableContributorInsights - ProvisionedThroughput: - ReadCapacityUnits: !Ref DynamoReadCapacityUnits - WriteCapacityUnits: !Ref DynamoWriteCapacityUnits + ProvisionedThroughput: !If + - ProvisionedCapacity + - ReadCapacityUnits: !Ref DynamoReadCapacityUnits + WriteCapacityUnits: !Ref DynamoWriteCapacityUnits + - !Ref AWS::NoValue # An index to help us quickly fetch all of the DMPs for a specific organization - IndexName: 'dmphub_owner_org_gsi' @@ -148,9 +155,11 @@ Resources: NonKeyAttributes: ['SK', 'title', 'description', 'created', 'modified', 'contact', 'contributor', 'project', 'dmproadmap_assertion_needed'] ContributorInsightsSpecification: Enabled: !Ref DynamoEnableContributorInsights - ProvisionedThroughput: - ReadCapacityUnits: !Ref DynamoReadCapacityUnits - WriteCapacityUnits: !Ref DynamoWriteCapacityUnits + ProvisionedThroughput: !If + - ProvisionedCapacity + - ReadCapacityUnits: !Ref DynamoReadCapacityUnits + WriteCapacityUnits: !Ref DynamoWriteCapacityUnits + - !Ref AWS::NoValue # An index to help us quickly fetch a DMP by its identifier for the provenance system - IndexName: 'dmphub_provenance_identifier_gsi' @@ -164,9 +173,11 @@ Resources: NonKeyAttributes: ['PK', 'SK', 'title'] ContributorInsightsSpecification: Enabled: !Ref DynamoEnableContributorInsights - ProvisionedThroughput: - ReadCapacityUnits: !Ref DynamoReadCapacityUnits - WriteCapacityUnits: !Ref DynamoWriteCapacityUnits + ProvisionedThroughput: !If + - ProvisionedCapacity + - ReadCapacityUnits: !Ref DynamoReadCapacityUnits + WriteCapacityUnits: !Ref DynamoWriteCapacityUnits + - !Ref AWS::NoValue # An index to help us quickly fetch the DMP IDs that have been 'featured' - IndexName: 'dmproadmap_featured_gsi' @@ -180,15 +191,19 @@ Resources: NonKeyAttributes: ['PK', 'SK', 'title', 'description', 'created', 'modified', 'contact', 'contributor', 'project', 'dmproadmap_assertion_needed'] ContributorInsightsSpecification: Enabled: !Ref DynamoEnableContributorInsights - ProvisionedThroughput: - ReadCapacityUnits: !Ref DynamoReadCapacityUnits - WriteCapacityUnits: !Ref DynamoWriteCapacityUnits + ProvisionedThroughput: !If + - ProvisionedCapacity + - ReadCapacityUnits: !Ref DynamoReadCapacityUnits + WriteCapacityUnits: !Ref DynamoWriteCapacityUnits + - !Ref AWS::NoValue BillingMode: !Ref DynamoBillingMode TableClass: !Ref DynamoTableClass - ProvisionedThroughput: - ReadCapacityUnits: !Ref DynamoReadCapacityUnits - WriteCapacityUnits: !Ref DynamoWriteCapacityUnits + ProvisionedThroughput: !If + - ProvisionedCapacity + - ReadCapacityUnits: !Ref DynamoReadCapacityUnits + WriteCapacityUnits: !Ref DynamoWriteCapacityUnits + - !Ref AWS::NoValue ContributorInsightsSpecification: Enabled: !Ref DynamoEnableContributorInsights # Determine cost before enabling From cbc431957d763123fc9dfc3cb3df091ff3ad52e4 Mon Sep 17 00:00:00 2001 From: briri Date: Thu, 17 Aug 2023 10:56:46 -0700 Subject: [PATCH 02/13] added code that will notify admin via email if any of the lambdas have a fatal error --- config/stg/regional/eventbridge.yaml | 2 +- src/sam/functions/citer/app.rb | 98 +++--- src/sam/functions/delete_dmp/app.rb | 1 + src/sam/functions/ezid_publisher/app.rb | 101 +++--- src/sam/functions/get_awards_crossref/app.rb | 6 + src/sam/functions/get_awards_nih/app.rb | 6 + src/sam/functions/get_awards_nsf/app.rb | 6 + src/sam/functions/get_citation/app.rb | 146 --------- src/sam/functions/get_dmp/app.rb | 2 + src/sam/functions/get_dmps/app.rb | 2 + src/sam/functions/post_dmps/app.rb | 2 + src/sam/functions/post_narratives/app.rb | 4 + src/sam/functions/put_dmp/app.rb | 2 + src/sam/functions/tmp_asserter/app.rb | 142 ++++++++ src/sam/functions/validate_dmp/app.rb | 2 + .../lib/uc3-dmp-external-api/client.rb | 2 +- .../lib/uc3-dmp-external-api/version.rb | 2 +- .../uc3-dmp-external-api-0.0.15.gem | Bin 6144 -> 0 bytes .../uc3-dmp-external-api-0.0.16.gem | Bin 0 -> 6144 bytes .../uc3-dmp-id/lib/uc3-dmp-id/asserter.rb | 36 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb | 2 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb | 2 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb | 2 +- .../lib/uc3-dmp-id/schemas/author.rb | 307 ++++++++++++++++-- .../gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb | 2 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/version.rb | 2 +- .../gems/uc3-dmp-id/uc3-dmp-id-0.0.137.gem | Bin 29184 -> 0 bytes .../gems/uc3-dmp-id/uc3-dmp-id-0.0.140.gem | Bin 0 -> 31232 bytes src/sam/layers/Gemfile.lock | 6 +- src/sam/template.yaml | 61 +++- templates/eventbridge.yaml | 3 +- 31 files changed, 655 insertions(+), 294 deletions(-) delete mode 100644 src/sam/functions/get_citation/app.rb create mode 100644 src/sam/functions/tmp_asserter/app.rb delete mode 100644 src/sam/gems/uc3-dmp-external-api/uc3-dmp-external-api-0.0.15.gem create mode 100644 src/sam/gems/uc3-dmp-external-api/uc3-dmp-external-api-0.0.16.gem delete mode 100644 src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.0.137.gem create mode 100644 src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.0.140.gem diff --git a/config/stg/regional/eventbridge.yaml b/config/stg/regional/eventbridge.yaml index f201c11e..9d7dc240 100644 --- a/config/stg/regional/eventbridge.yaml +++ b/config/stg/regional/eventbridge.yaml @@ -11,4 +11,4 @@ parameters: # Log and Archive retention LogRetentionDays: '7' - ArchiveRetentionDays: '14' + ArchiveRetentionDays: '7' diff --git a/src/sam/functions/citer/app.rb b/src/sam/functions/citer/app.rb index 2489052c..f26a36d3 100644 --- a/src/sam/functions/citer/app.rb +++ b/src/sam/functions/citer/app.rb @@ -72,56 +72,55 @@ def process(event:, context:) json = detail.is_a?(Hash) ? detail : JSON.parse(detail) dmp_pk = json['PK'] dmp_sk = json.fetch('SK', Uc3DmpId::Helper::DMP_LATEST_VERSION) - _respond(status: 400, errors: [Uc3DmpApiCore::MSG_INVALID_ARGS], event: event) if dmp_pk.nil? || dmp_sk.nil? - - # Load the DMP metadata - dmp = Uc3DmpId::Finder.by_pk(p_key: dmp_pk, s_key: dmp_sk, cleanse: false, logger: logger) - _respond(status: 404, errors: [Uc3DmpId::MSG_DMP_NOT_FOUND], event: event) if dmp.nil? - - # Get all of the related identifiers that are DOIs and are un-cited - identifiers = dmp.fetch('dmp', {}).fetch('dmproadmap_related_identifiers', []) - uncited = Uc3DmpId::Helper.citable_related_identifiers(dmp: dmp['dmp']) - _respond(status: 200, items: [], event: event) if identifiers.empty? || uncited.empty? - - existing_citations = identifiers.reject { |id| uncited.include?(id) } - headers = { Accept: 'application/x-bibtex' } - - processed = [] - uncited.each do |identifier| - uri = _doi_to_uri(doi: identifier['identifier']&.strip) - if !uri.nil? && !uri.blank? - logger.debug(message: "Fetching BibTeX from: #{uri}") - resp = Uc3DmpExternalApi::Client.call(url: uri, method: :get, additional_headers: headers, logger: logger) - - unless resp.nil? || resp.to_s.strip.empty? - bibtex = BibTeX.parse(_cleanse_bibtex(text: resp)) - work_type = identifier['work_type'].nil? ? determine_work_type(bibtex: bibtex) : identifier['work_type'] - identifier['citation'] = _bibtex_to_citation(uri: uri, work_type: work_type, bibtex: bibtex) + + if !dmp_pk.nil? && !dmp_sk.nil? + # Load the DMP metadata + dmp = Uc3DmpId::Finder.by_pk(p_key: dmp_pk, s_key: dmp_sk, cleanse: false, logger: logger) + if !dmp.nil? + # Get all of the related identifiers that are DOIs and are un-cited + identifiers = dmp.fetch('dmp', {}).fetch('dmproadmap_related_identifiers', []) + uncited = Uc3DmpId::Helper.citable_related_identifiers(dmp: dmp['dmp']) + + if identifiers.any? && uncited.any? + existing_citations = identifiers.reject { |id| uncited.include?(id) } + headers = { Accept: 'application/x-bibtex' } + + processed = [] + uncited.each do |identifier| + uri = _doi_to_uri(doi: identifier['identifier']&.strip) + if !uri.nil? && !uri.blank? + logger.debug(message: "Fetching BibTeX from: #{uri}") + resp = Uc3DmpExternalApi::Client.call(url: uri, method: :get, additional_headers: headers, logger: logger) + + unless resp.nil? || resp.to_s.strip.empty? + bibtex = BibTeX.parse(_cleanse_bibtex(text: resp)) + work_type = identifier['work_type'].nil? ? determine_work_type(bibtex: bibtex) : identifier['work_type'] + identifier['citation'] = _bibtex_to_citation(uri: uri, work_type: work_type, bibtex: bibtex) + end + end + + processed << identifier + end + + logger.debug(message: 'Results of citation retrieval', details: processed) + dmp['dmp']['dmproadmap_related_identifiers'] = existing_citations + processed + + # Remove the version info because we don't want to save it on the record + dmp['dmp'].delete('dmphub_versions') + + client = Uc3DmpDynamo::Client.new + resp = client.put_item(json: dmp['dmp'], logger: logger) end end - - processed << identifier end - - logger.debug(message: 'Results of citation retrieval', details: processed) - dmp['dmp']['dmproadmap_related_identifiers'] = existing_citations + processed - - # Remove the version info because we don't want to save it on the record - dmp['dmp'].delete('dmphub_versions') - - client = Uc3DmpDynamo::Client.new - resp = client.put_item(json: dmp['dmp'], logger: logger) - _respond(status: 500, errros: [MSG_UNABLE_TO_UPDATE], event: event) if resp.nil? - - _respond(status: 200, items: [], event: event) rescue Uc3DmpId::FinderError => e logger.error(message: e.message, details: e.backtrace) - _respond(status: 500, errors: [e.message], event: event) rescue Uc3DmpExternalApi::ExternalApiError => e - _respond(status: 500, errors: [e.message], event: event) + logger.error(message: e.message, details: e.backtrace) rescue StandardError => e logger.error(message: e.message, details: e.backtrace) - { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } + deets = { message: "Fatal error - #{e.message}", event_details: json} + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity @@ -138,9 +137,6 @@ def _respond(status:, items: [], errors: [], event: {}, params: {}) # Will convert 'doi:10.1234/abcdefg' to 'http://doi.org/10.1234/abcdefg' def _doi_to_uri(doi:) - -puts "Matched? #{doi.match(Uc3DmpId::Helper::DOI_REGEX).to_s}" - val = doi.match(Uc3DmpId::Helper::DOI_REGEX).to_s return nil if val.nil? || val.strip == '' @@ -164,11 +160,13 @@ def _cleanse_bibtex(text:) # Remove any encoded HTML (e.g. "Regular text $\\lt$strong$\\gt$Bold text$\\lt$/strong$\\gt$") utf8 = utf8.gsub(%r{\$?\\\$?(less|lt|Lt)\$/?[a-zA-Z]+\$?\\\$?(greater|gt|Gt)\$}, '') - # Replace any special dash and quote characters with a minus sign or single/double quote - utf8.gsub(%r{\$?\\(T|t)ext[a-zA-Z]+dash\$?}, '-').gsub(%r{\{(T|t)ext[a-zA-Z]+dash\}}, '-') - .gsub(%r{\$?\\(M|m)athsemicolon\$?}, ':').gsub(%r{\{(M|m)semicolon\}}, ':') - .gsub(%r{\$?\\(T|t)extquotesingle\$?}, "'").gsub(%r{\{(T|t)extquotesingle\}}, "'") - .gsub(%r{\$?\\(T|t)extquotedouble\$?}, '"').gsub(%r{\{(T|t)extquotedouble\}}, '"') + # Replace any special dash, semicolon and quote characters with a minus sign or single/double quote + utf8 = utf8.gsub(%r{\$?\\(T|t)ext[a-zA-Z]+dash\$?}, '-').gsub(%r{\{(T|t)ext[a-zA-Z]+dash\}}, '-') + .gsub(%r{\$?\\(M|m)athsemicolon\$?}, ':').gsub(%r{\{(M|m)semicolon\}}, ':') + .gsub(%r{\$?\\(T|t)extquotesingle\$?}, "'").gsub(%r{\{(T|t)extquotesingle\}}, "'") + .gsub(%r{\$?\\(T|t)extquotedouble\$?}, '"').gsub(%r{\{(T|t)extquotedouble\}}, '"') + # Remove any remaining `\v` entries which attempt to construct an accented character + utf8.gsub(%r{\\v}, '') end # Convert the BibTeX item to a citation diff --git a/src/sam/functions/delete_dmp/app.rb b/src/sam/functions/delete_dmp/app.rb index cd33cac7..b10a56cd 100644 --- a/src/sam/functions/delete_dmp/app.rb +++ b/src/sam/functions/delete_dmp/app.rb @@ -48,6 +48,7 @@ def self.process(event:, context:) _respond(status: 400, errors: [Uc3DmpId::MSG_DMP_NO_DMP_ID, e.message], event: event) rescue StandardError => e logger.error(message: e.message, details: e.backtrace) + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: { dmp_id: p_key }, event: event) { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength diff --git a/src/sam/functions/ezid_publisher/app.rb b/src/sam/functions/ezid_publisher/app.rb index defb6d14..bc0f9075 100644 --- a/src/sam/functions/ezid_publisher/app.rb +++ b/src/sam/functions/ezid_publisher/app.rb @@ -45,7 +45,7 @@ class EzidPublisher # { # "version": "0", # "id": "5c9a3747-293c-59d7-dcee-a2210ac034fc", - # "detail-type": "DMP change", + # "detail-type": "EZID update", # "source": "dmphub.uc3dev.cdlib.net:lambda:event_publisher", # "account": "1234567890", # "time": "2023-02-14T16:42:06Z", @@ -101,60 +101,61 @@ def process(event:, context:) # If submissions are paused, toss the event into the EventBridge archive where it can be # replayed at a later time if paused - logger.info(message: "SUBMISSIONS PAUSED: Placing event #{event['id']} in the EventBridge archive.", details: json) - Uc3DmpEventBridge.publish(source: SOURCE, dmp: json, event_type: 'paused', logger: logger) - _respond(status: 200, items: [], event: event) - end - - # Load the DMP metadata - dmp = Uc3DmpId::Finder.by_pk(p_key: dmp_pk, logger: logger) - _respond(status: 404, errors: [Uc3DmpId::MSG_DMP_NOT_FOUND], event: event) if dmp.nil? - - dmp_id = dmp.fetch('dmp', {}).fetch('dmp_id', {})['identifier'].gsub(/https?:\/\//, '').gsub(ENV['DMP_ID_BASE_URL'], '') - dmp_id = dmp_id.start_with?('/') ? dmp_id[1..dmp_id.length] : dmp_id - ezid_url = Uc3DmpApiCore::SsmReader.get_ssm_value(key: :dmp_id_api_url, logger: logger) - ezid_url = ezid_url.end_with?('/') ? ezid_url : "#{ezid_url}/" - base_url = Uc3DmpApiCore::SsmReader.get_ssm_value(key: :base_url, logger: logger) - - url = "#{ezid_url}id/doi:#{dmp_id}?update_if_exists=yes" - landing_page_url = "#{base_url}/dmps/#{dmp_id}" - datacite_xml = dmp_to_datacite_xml(dmp_id: dmp_id, dmp: dmp['dmp'])&.gsub(/[\r\n]/, ' ') - logger.error(message: "Failed to build DatCite XML for #{dmp_id}", details: dmp) if datacite_xml.nil? - - payload = <<~TEXT - _target: #{landing_page_url} - datacite: #{datacite_xml} - TEXT - logger.debug(message: 'Prepared DMP ID metadata for EZID.', details: payload) - - if skip_ezid - logger.info(message: 'EZID is currently in Debug mode.', details: payload) - _respond(status: 200, items: [], event: event) + logger.info(message: "EZID submissions paused: You can replay events from the archive when ready.", + details: json) + deets = { message: "EZID Paused", event_details: json } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) + else + # Load the DMP metadata + dmp = Uc3DmpId::Finder.by_pk(p_key: dmp_pk, logger: logger) + # _respond(status: 404, errors: [Uc3DmpId::MSG_DMP_NOT_FOUND], event: event) if dmp.nil? + + dmp_id = dmp.fetch('dmp', {}).fetch('dmp_id', {})['identifier'].gsub(/https?:\/\//, '').gsub(ENV['DMP_ID_BASE_URL'], '') + dmp_id = dmp_id.start_with?('/') ? dmp_id[1..dmp_id.length] : dmp_id + ezid_url = Uc3DmpApiCore::SsmReader.get_ssm_value(key: :dmp_id_api_url, logger: logger) + ezid_url = ezid_url.end_with?('/') ? ezid_url : "#{ezid_url}/" + base_url = Uc3DmpApiCore::SsmReader.get_ssm_value(key: :base_url, logger: logger) + + url = "#{ezid_url}id/doi:#{dmp_id}?update_if_exists=yes" + landing_page_url = "#{base_url}/dmps/#{dmp_id}" + datacite_xml = dmp_to_datacite_xml(dmp_id: dmp_id, dmp: dmp['dmp'])&.gsub(/[\r\n]/, ' ') + logger.error(message: "Failed to build DatCite XML for #{dmp_id}", details: dmp) if datacite_xml.nil? + + payload = <<~TEXT + _target: #{landing_page_url} + datacite: #{datacite_xml} + TEXT + logger.debug(message: 'Prepared DMP ID metadata for EZID.', details: payload) + + if skip_ezid + logger.info(message: 'EZID is currently in Debug mode. Skipping EZID submission', details: payload) + else + headers = { + 'Content-Type': 'text/plain', + 'Accept': 'text/plain' + } + auth = { + username: Uc3DmpApiCore::SsmReader.get_ssm_value(key: :dmp_id_client_id, logger: logger), + password: Uc3DmpApiCore::SsmReader.get_ssm_value(key: :dmp_id_client_secret, logger: logger) + } + logger.info(message: "Sending updated DMP ID metadata to EZID for #{dmp_id}") + logger.debug(message: "Sending DMP ID metadata to EZID for #{dmp_id}", + details: { url: url, headers: headers, payload: payload.to_s }) + resp = Uc3DmpExternalApi::Client.call(url: url, method: :put, body: payload.to_s, basic_auth: auth, + additional_headers: headers, logger: logger) + end end - - headers = { - 'Content-Type': 'text/plain', - 'Accept': 'text/plain' - } - auth = { - username: Uc3DmpApiCore::SsmReader.get_ssm_value(key: :dmp_id_client_id, logger: logger), - password: Uc3DmpApiCore::SsmReader.get_ssm_value(key: :dmp_id_client_secret, logger: logger) - } - logger.debug(message: "Sending DMP ID metadata to EZID for #{dmp_id}", - details: { url: url, headers: headers, payload: payload.to_s }) - resp = Uc3DmpExternalApi::Client.call(url: url, method: :put, body: payload.to_s, basic_auth: auth, - additional_headers: headers, logger: logger) - _respond(status: 500, errors: MSG_EZID_FAILURE, event: event) if resp.nil? - - _respond(status: 200, items: [], event: event) rescue Uc3DmpId::FinderError => e logger.error(message: e.message, details: e.backtrace) - _respond(status: 500, errors: [e.message], event: event) rescue Uc3DmpExternalApi::ExternalApiError => e - _respond(status: 500, errors: [e.message], event: event) + # EZID returned an error, so notify the admin. They can replay once the issue is resolved + logger.error(message: e.message, details: json) + deets = { message: "EZID returned an error #{e.message}", event_details: json} + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) rescue StandardError => e logger.error(message: e.message, details: e.backtrace) - { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } + deets = { message: "Fatal error - #{e.message}", event_details: json} + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity @@ -195,7 +196,7 @@ def dmp_to_datacite_xml(dmp_id:, dmp:) { name: fund['name'], type: id['type'], - identifier: %w[fundref ror].include?(id['type'].downcase) ? id['identifier'] : nil, + identifier: %w[fundref ror].include?(id['type']&.downcase) ? id['identifier'] : nil, grant: id['grant_id'], title: dmp['title'] } diff --git a/src/sam/functions/get_awards_crossref/app.rb b/src/sam/functions/get_awards_crossref/app.rb index a696f04e..9a3e2d19 100644 --- a/src/sam/functions/get_awards_crossref/app.rb +++ b/src/sam/functions/get_awards_crossref/app.rb @@ -65,12 +65,18 @@ def self.process(event:, context:) _respond(status: 200, items: results.compact.uniq, event: event, params: params) rescue Uc3DmpExternalApi::ExternalApiError => e logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, query_string: params } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) _respond(status: 500, errors: [Uc3DmpApiCore::MSG_SERVER_ERROR], event: event) rescue Aws::Errors::ServiceError => e logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, query_string: params } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) _respond(status: 500, errors: [Uc3DmpApiCore::MSG_SERVER_ERROR], event: event) rescue StandardError => e logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, query_string: params } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } end diff --git a/src/sam/functions/get_awards_nih/app.rb b/src/sam/functions/get_awards_nih/app.rb index 68cdf6ff..a733912c 100644 --- a/src/sam/functions/get_awards_nih/app.rb +++ b/src/sam/functions/get_awards_nih/app.rb @@ -59,12 +59,18 @@ def self.process(event:, context:) _respond(status: 200, items: results.compact.uniq, event: event, params: params) rescue Uc3DmpExternalApi::ExternalApiError => e logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, query_string: params } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) _respond(status: 500, errors: [Uc3DmpApiCore::MSG_SERVER_ERROR], event: event) rescue Aws::Errors::ServiceError => e logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, query_string: params } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) _respond(status: 500, errors: [Uc3DmpApiCore::MSG_SERVER_ERROR], event: event) rescue StandardError => e logger.error(message: e.message, details: e.backtrace) unless logger.nil? + deets = { message: e.message, query_string: params } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } end diff --git a/src/sam/functions/get_awards_nsf/app.rb b/src/sam/functions/get_awards_nsf/app.rb index dad19715..22b791be 100644 --- a/src/sam/functions/get_awards_nsf/app.rb +++ b/src/sam/functions/get_awards_nsf/app.rb @@ -61,12 +61,18 @@ def self.process(event:, context:) _respond(status: 200, items: results.compact.uniq, event: event, params: params) rescue Uc3DmpExternalApi::ExternalApiError => e logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, query_string: params } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) _respond(status: 500, errors: [Uc3DmpApiCore::MSG_SERVER_ERROR], event: event) rescue Aws::Errors::ServiceError => e logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, query_string: params } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) _respond(status: 500, errors: [Uc3DmpApiCore::MSG_SERVER_ERROR], event: event) rescue StandardError => e logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, query_string: params } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } end diff --git a/src/sam/functions/get_citation/app.rb b/src/sam/functions/get_citation/app.rb deleted file mode 100644 index f2f70041..00000000 --- a/src/sam/functions/get_citation/app.rb +++ /dev/null @@ -1,146 +0,0 @@ -# frozen_string_literal: true - -# Docs say that the LambdaLayer gems are found mounted as /opt/ruby/gems but an inspection -# of the $LOAD_PATH shows that only /opt/ruby/lib is available. So we add what we want here -# and indicate exactly which folders contain the *.rb files -my_gem_path = Dir['/opt/ruby/gems/**/lib/'] -$LOAD_PATH.unshift(*my_gem_path) - -require 'bibtex' -require 'citeproc' -require 'csl/styles' - -require 'uc3-dmp-api-core' -require 'uc3-dmp-cloudwatch' -require 'uc3-dmp-external-api' -require 'uc3-dmp-id' - -module Functions - # A Proxy service that queries the NSF Awards API and transforms the results into a common format - class CitationFetcher - SOURCE = 'CitationFetcher' - - DEFAULT_CITATION_STYLE 'chicago-author-date' - DEFAULT_DOI_URL = 'https://doi.org' - - def self.process(event:, context:) - log_level = ENV.fetch('LOG_LEVEL', 'error') - req_id = context.aws_request_id if context.is_a?(LambdaContext) - logger = Uc3DmpCloudwatch::Logger.new(source: SOURCE, request_id: req_id, event: event, level: log_level) - - detail = event.fetch('detail', {}) - json = detail.is_a?(Hash) ? detail : JSON.parse(detail) - provenance_pk = json['dmphub_provenance_id'] - dmp_pk = json['PK'] - _respond(status: 400, errors: [Uc3DmpApiCore::MSG_INVALID_ARGS], event: event) if provenance_pk.nil? || dmp_pk.nil? - - # Load the DMP metadata - dmp = Uc3DmpId::Finder.by_pk(p_key: dmp_pk, logger: logger) - _respond(status: 404, errors: [Uc3DmpId::MSG_DMP_NOT_FOUND], event: event) if dmp.nil? - - works = Helper.deep_copy_dmp(obj: dmp.fetch('dmproadmap_related_identifiers', [])) - updated = false - - works.each do |work| - # Ignore the link to the PDF narrative for the DMP - next unless work['citation'].nil? || - (work['descriptor'] == 'is_metadata_for' && work['work_type'] == 'output_management_plan') - - updated = true - uri = _doi_to_uri(doi: work['identifier'].strip) - logger.info(message: "Fetching BibTeX from: #{uri}") if logger.respond_to?(:debug) - resp = Uc3DmpExternalApi::Client.call(url: uri, method: :get, logger: logger) - - if resp.nil? || resp.to_s.strip.empty? - work['citation'] = "#{uri} No citation available." - else - bibtex = BibTeX.parse(resp.body) - logger.debug(message: "Received BibTeX from: #{uri}", details: resp.body) if logger.respond_to?(:debug) - - work['citation'] = _bibtex_to_citation( - uri: uri, - work_type: bibtex?.data?.first?.journal?.nil? ? work['work_type'].humanize : 'article', - bibtex: bibtex, - style: DEFAULT_CITATION_STYLE - ) - logger.debug(message: 'Generated citation:', details: citation) if logger.respond_to?(:debug) - end - end - # Just return if nothing was updated - _respond(status: 200, items: [], event: event) unless updated - - dmp['dmproadmap_related_identifiers'] = works - # Save the changes - client = Uc3DmpDynamo::Client.new - resp = client.put_item(json: version, logger: logger) - raise UpdaterError, MSG_DMP_UNABLE_TO_VERSION if resp.nil? - - # Return the new version record - logger.info(message: "Updated DMP ID: #{dmp['dmp_id']}") if logger.respond_to?(:debug) - _respond(status: 200, items: [], event: event) - rescue Uc3DmpId::FinderError => e - logger.error(message: e.message, details: e.backtrace) - _respond(status: 500, errors: [e.message], event: event) - rescue Uc3DmpExternalApi::ExternalApiError => e - _respond(status: 500, errors: [e.message], event: event) - rescue StandardError => e - logger.error(message: e.message, details: e.backtrace) - { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } - end - - private - - # Will convert 'doi:10.1234/abcdefg' to 'http://doi.org/10.1234/abcdefg' - def _doi_to_uri(doi:) - return nil unless doi.is_a?(String) && doi.strip != '' - - doi.start_with?('http') ? doi : "#{DEFAULT_DOI_URL}/#{doi.gsub('doi:', '')}" - end - - # Convert the BibTeX item to a citation - def _bibtex_to_citation(uri:, work_type:, bibtex:, style:) - return nil unless uri.is_a?(String) && uri.strip != '' - return nil if bibtex.nil? || bibtex.data.nil? || bibtex.data.first.nil? - - cp = CiteProc::Processor.new(style: style, format: 'html') - cp.import(bibtex.to_citeproc) - citation = cp.render(:bibliography, id: bibtex.data.first.id) - return nil unless citation.is_a?(Array) && citation.any? - - # The CiteProc renderer has trouble with some things so fix them here - # - # - It has a '{textendash}' sometimes because it cannot render the correct char - # - For some reason words in all caps in the title get wrapped in curl brackets - # - We want to add the work type after the title. e.g. `[Dataset].` - # - citation = citation.first.gsub(/{\\Textendash}/i, '-') - .gsub('{', '').gsub('}', '') - - unless work_type.nil? || work_type.strip == '' - # This supports the :apa and :chicago-author-date styles - citation = citation.gsub(/\.”\s+/, "\.” [#{work_type.gsub('_', ' ').capitalize}]. ") - .gsub(/<\/i>\.\s+/, "<\/i>\. [#{work_type.gsub('_', ' ').capitalize}]. ") - end - - # Convert the URL into a link. Ensure that the trailing period is not a part of - # the link! - citation.gsub(URI.regexp) do |url| - if url.start_with?('http') - '%{url}.' % { - url: url.end_with?('.') ? uri : "#{uri}." - } - else - url - end - end - end - - # Send the output to the Responder - def _respond(status:, items: [], errors: [], event: {}, params: {}) - Uc3DmpApiCore::Responder.respond( - status: status, items: items, errors: errors, event: event, - page: params['page'], per_page: params['per_page'] - ) - end - end -end diff --git a/src/sam/functions/get_dmp/app.rb b/src/sam/functions/get_dmp/app.rb index 5ca45452..81dd1cc1 100644 --- a/src/sam/functions/get_dmp/app.rb +++ b/src/sam/functions/get_dmp/app.rb @@ -53,6 +53,8 @@ def self.process(event:, context:) _respond(status: 500, errors: [Uc3DmpApiCore::MSG_SERVER_ERROR], event: event) rescue StandardError => e logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, pk: p_key, sk: s_key } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength diff --git a/src/sam/functions/get_dmps/app.rb b/src/sam/functions/get_dmps/app.rb index f150f03b..b4e4c4c1 100644 --- a/src/sam/functions/get_dmps/app.rb +++ b/src/sam/functions/get_dmps/app.rb @@ -56,6 +56,8 @@ def self.process(event:, context:) _respond(status: 400, errors: [Uc3DmpId::MSG_DMP_NO_DMP_ID, e.message], event: event) rescue StandardError => e logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, params: params } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength diff --git a/src/sam/functions/post_dmps/app.rb b/src/sam/functions/post_dmps/app.rb index acfa9700..0b0132f5 100644 --- a/src/sam/functions/post_dmps/app.rb +++ b/src/sam/functions/post_dmps/app.rb @@ -44,6 +44,8 @@ def self.process(event:, context:) _respond(status: 400, errors: [Uc3DmpId::MSG_DMP_NO_DMP_ID, e.message], event: event) rescue StandardError => e logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, body: body } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength diff --git a/src/sam/functions/post_narratives/app.rb b/src/sam/functions/post_narratives/app.rb index 14a64c74..0d1cf469 100644 --- a/src/sam/functions/post_narratives/app.rb +++ b/src/sam/functions/post_narratives/app.rb @@ -55,12 +55,16 @@ def self.process(event:, context:) dmp = Uc3DmpId::Finder.by_pk(p_key: params[:dmp_id], logger: logger) _respond(status: 201, items: [dmp], event: event) rescue Aws::Errors::ServiceError => e + deets = { message: e.message, dmp_id: params[:dmp_id], params: params } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) _respond(status: 500, errors: [Uc3DmpApiCore::MSG_SERVER_ERROR], event: event) rescue Uc3DmpId::UpdaterError => e _respond(status: 400, errors: [e.message], event: event) rescue StandardError => e # Just do a print here (ends up in CloudWatch) in case it was the Uc3DmpApiCore::Responder.rb that failed logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, dmp_id: params[:dmp_id], params: params } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } end diff --git a/src/sam/functions/put_dmp/app.rb b/src/sam/functions/put_dmp/app.rb index 0b6f8c30..799da850 100644 --- a/src/sam/functions/put_dmp/app.rb +++ b/src/sam/functions/put_dmp/app.rb @@ -54,6 +54,8 @@ def self.process(event:, context:) _respond(status: 400, errors: [Uc3DmpId::MSG_DMP_NO_DMP_ID, e.message], event: event) rescue StandardError => e logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, dmp_id: p_key, body: body } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength diff --git a/src/sam/functions/tmp_asserter/app.rb b/src/sam/functions/tmp_asserter/app.rb new file mode 100644 index 00000000..e41c785e --- /dev/null +++ b/src/sam/functions/tmp_asserter/app.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +# Docs say that the LambdaLayer gems are found mounted as /opt/ruby/gems but an inspection +# of the $LOAD_PATH shows that only /opt/ruby/lib is available. So we add what we want here +# and indicate exactly which folders contain the *.rb files +my_gem_path = Dir['/opt/ruby/gems/**/lib/'] +$LOAD_PATH.unshift(*my_gem_path) + +require 'uc3-dmp-api-core' +require 'uc3-dmp-cloudwatch' +require 'uc3-dmp-dynamo' +require 'uc3-dmp-id' +require 'uc3-dmp-provenance' + +module Functions + # The handler for PUT /tmp/{dmp_id+} + class TmpAsserter + SOURCE = 'PUT /tmp/{dmp_id+}' + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def self.process(event:, context:) + # Setup the Logger + log_level = ENV.fetch('LOG_LEVEL', 'error') + req_id = context.aws_request_id if context.is_a?(LambdaContext) + logger = Uc3DmpCloudwatch::Logger.new(source: SOURCE, request_id: req_id, event: event, level: log_level) + + # Get the params + params = event.fetch('pathParameters', {}) + dmp_id = params['dmp_id'] + json = JSON.parse(event.fetch('body', '{"works":2}')) + + # Fail if the DMP ID is not a valid DMP ID + p_key = Uc3DmpId::Helper.path_parameter_to_pk(param: dmp_id) + p_key = Uc3DmpId::Helper.append_pk_prefix(p_key: p_key) unless p_key.nil? + return _respond(status: 400, errors: Uc3DmpId::MSG_DMP_INVALID_DMP_ID, event: event) if p_key.nil? + + _set_env(logger: logger) + + # Fail if the Provenance could not be loaded + claim = event.fetch('requestContext', {}).fetch('authorizer', {})['claims'] + provenance = Uc3DmpProvenance::Finder.from_lambda_cotext(identity: claim, logger: logger) + return _respond(status: 403, errors: Uc3DmpId::MSG_DMP_FORBIDDEN, event: event) if provenance.nil? + + # Fetch the DMP ID + logger.debug(message: "Searching for PK: #{p_key}, SK: #{s_key}") if logger.respond_to?(:debug) + result = Uc3DmpId::Finder.by_pk(p_key: p_key, s_key: s_key, logger: logger) + + work_count = json.fetch('works', '2').to_s.strip.to_i + grant_ror = json.fetch('grant', 'https://ror.org/01bj3aw27').to_s.downcase.strip + + mods = [] + work_count.times do + prov = %w[crossref datacite openaire].sample + + mods << { + id: SecureRandom.hex(8), + provenance: prov, + timstamp: Time.now.iso8601, + note: "data received from #{prov} API", + status: "pending", + dmproadmap_related_identifier: _add_work + } + end + unless grant_ror.nil? + funders = [ + { name: "National Institutes of Health", ror: "https://ror.org/01cwqze88", acronym: 'NIH' }, + { name: "National Science Foundation", ror: "https://ror.org/021nxhr62", acronym: 'NSF' }, + { name: "United States Department of Energy", ror: "https://ror.org/01bj3aw27", acronym: 'Crossref' }, + ] + funder = funders.select { |funder| funder[:ror] == ror }.first + + mods << { + id: SecureRandom.hex(8), + provenance: funder[:acronym], + timstamp: Time.now.iso8601, + note: "data received from #{funder[:acronym]} API", + status: "pending", + funding: [_add_grant(funder: funder)] + } + end + mods = { dmphub_modifications: mods } + logger.debug(message: "Tmp Asserter update to PK: #{p_key}", details: { requested: json, mods: mods }) + + # Update the DMP ID + # resp = Uc3DmpId::Updater.update(logger: logger, provenance: provenance, p_key: p_key, json: json) + # return _respond(status: 400, errors: Uc3DmpId::MSG_DMP_NO_DMP_ID) if resp.nil? + + _respond(status: 200, items: [resp], event: event) + rescue Uc3DmpId::UpdaterError => e + _respond(status: 400, errors: [Uc3DmpId::MSG_DMP_NO_DMP_ID, e.message], event: event) + rescue StandardError => e + logger.error(message: e.message, details: e.backtrace) + { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + private + + class << self + # Set the Cognito User Pool Id and DyanmoDB Table name for the downstream Uc3DmpCognito and Uc3DmpDynamo + def _set_env(logger:) + ENV['COGNITO_USER_POOL_ID'] = ENV['COGNITO_USER_POOL_ID']&.split('/')&.last + ENV['DMP_ID_SHOULDER'] = Uc3DmpApiCore::SsmReader.get_ssm_value(key: :dmp_id_shoulder, logger: logger) + ENV['DMP_ID_BASE_URL'] = Uc3DmpApiCore::SsmReader.get_ssm_value(key: :dmp_id_base_url, logger: logger) + end + + # Send the output to the Responder + def _respond(status:, items: [], errors: [], event: {}, params: {}) + Uc3DmpApiCore::Responder.respond( + status: status, items: items, errors: errors, event: event, + page: params['page'], per_page: params['per_page'] + ) + end + + def _add_grant(funder:) + return nil if funder.nil? + + { + "name": funder[:name], + "funder_id": { + "type": "ror", + "identifier": funder[:ror] + }, + "funding_status": "granted", + "grant_id": { + "identifier": "https://doi.org/11.1111/#{SecureRandom.hex(6)}", + "type": "doi" + } + } + end + + def _add_work + { + "work_type": %w[dataset article data_paper software].sample, + "descriptor": %w[references cites is_part_of].sample, + "type": "doi", + "identifier": "https://dx.doi.org/77.6666/#{SecureRandom.hex(4)}" + } + end + end + end +end diff --git a/src/sam/functions/validate_dmp/app.rb b/src/sam/functions/validate_dmp/app.rb index 326933be..35209e33 100644 --- a/src/sam/functions/validate_dmp/app.rb +++ b/src/sam/functions/validate_dmp/app.rb @@ -31,6 +31,8 @@ def self.process(event:, context:) _respond(status: 400, errors: errors, event: event) rescue StandardError => e logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, body: body } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } end # rubocop:enable Metrics/AbcSize diff --git a/src/sam/gems/uc3-dmp-external-api/lib/uc3-dmp-external-api/client.rb b/src/sam/gems/uc3-dmp-external-api/lib/uc3-dmp-external-api/client.rb index ae97550d..ae44c163 100644 --- a/src/sam/gems/uc3-dmp-external-api/lib/uc3-dmp-external-api/client.rb +++ b/src/sam/gems/uc3-dmp-external-api/lib/uc3-dmp-external-api/client.rb @@ -25,7 +25,7 @@ def call(url:, method: :get, body: '', basic_auth: {}, additional_headers: {}, l opts = _options(body: body, basic_auth: basic_auth, additional_headers: additional_headers, logger: logger) resp = HTTParty.send(method.to_sym, uri, opts) - if resp.code != 200 + if ![200, 201].include?(resp.code) msg = "status: #{resp&.code}, body: #{resp&.body}" raise ExternalApiError, "#{format(MSG_ERROR_FROM_EXTERNAL_API, url: url)} - #{msg}" end diff --git a/src/sam/gems/uc3-dmp-external-api/lib/uc3-dmp-external-api/version.rb b/src/sam/gems/uc3-dmp-external-api/lib/uc3-dmp-external-api/version.rb index e794418c..7bed7958 100644 --- a/src/sam/gems/uc3-dmp-external-api/lib/uc3-dmp-external-api/version.rb +++ b/src/sam/gems/uc3-dmp-external-api/lib/uc3-dmp-external-api/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Uc3DmpExternalApi - VERSION = '0.0.15' + VERSION = '0.0.16' end diff --git a/src/sam/gems/uc3-dmp-external-api/uc3-dmp-external-api-0.0.15.gem b/src/sam/gems/uc3-dmp-external-api/uc3-dmp-external-api-0.0.15.gem deleted file mode 100644 index 279b93d9c81c91866f780f51af1d62d5b227b973..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6144 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 D;B%*p literal 0 HcmV?d00001 diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/asserter.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/asserter.rb index 46579607..54193db3 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/asserter.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/asserter.rb @@ -22,42 +22,42 @@ def add(updater:, dmp:, mods:, note: nil, logger: nil) # Return the DMP ID as-is if there are no assertable changes return dmp if contact.nil? && contributor.empty? && project.empty? - # Clone any existing assertions on the current DMP ID so we can manipulate them - assertions = Helper.deep_copy_dmp(obj: dmp.fetch('dmphub_assertions', [])) + # Clone any existing pending_assertions on the current DMP ID so we can manipulate them + assertions = Helper.deep_copy_dmp(obj: dmp.fetch('dmphub_modifications', [])) # Return the DMP ID as-is if the assertion is already on the record - return dmp if assertions.select { |entry| entry['provenance'] == updater && entry['assertions'] == mods } + return dmp if assertions.select { |entry| entry['provenance'] == updater && entry['pending_assertions'] == mods } assertions << _generate_assertion(updater: updater, mods: mods, note: note) - dmp['dmphub_assertions'] = assertions.flatten + dmp['dmphub_modifications'] = assertions.flatten dmp end # Splice together assertions made while the user was updating the DMP ID def splice(latest_version:, modified_version:, logger: nil) -puts "LATEST_VERSION ASSERTIONS: #{latest_version['dmphub_assertions']}" -puts "MODIFIED_VERSION ASSERTIONS: #{modified_version['dmphub_assertions']}" +puts "LATEST_VERSION ASSERTIONS: #{latest_version['dmphub_modifications']}" +puts "MODIFIED_VERSION ASSERTIONS: #{modified_version['dmphub_modifications']}" # Return the modified_version if the timestamps are the same (meaning no new assertions were made while the # user was working on the DMP ID) OR neither version has assertions return modified_version if latest_version['modified'] == modified_version['modified'] || - (latest_version.fetch('dmphub_assertions', []).empty? && - modified_version.fetch('dmphub_assertions', []).empty?) + (latest_version.fetch('dmphub_modifications', []).empty? && + modified_version.fetch('dmphub_modifications', []).empty?) # Clone any existing assertions on the current DMP ID so we can manipulate them - existing_assertions = Helper.deep_copy_dmp(obj: latest_version.fetch('dmphub_assertions', [])) - incoming_assertions = Helper.deep_copy_dmp(obj: modified_version.fetch('dmphub_assertions', [])) + existing_assertions = Helper.deep_copy_dmp(obj: latest_version.fetch('dmphub_modifications', [])) + incoming_assertions = Helper.deep_copy_dmp(obj: modified_version.fetch('dmphub_modifications', [])) logger.debug(message: "Existing assertions", details: existing_assertions) if logger.respond_to?(:debug) logger.debug(message: "Incoming modifications", details: incoming_assertions) if logger.respond_to?(:debug) # Keep any assetions that were made after the modified on the incoming changes - modified_version['dmphub_assertions'] = existing_assertions.select do |entry| + modified_version['dmphub_modifications'] = existing_assertions.select do |entry| !entry['timestamp'].nil? && Time.parse(entry['timestamp']) > Time.parse(modified_version['modified']) end return modified_version unless incoming_assertions.any? # Add any of the assertions still on the incoming record back to the latest record - incoming_assertions.each { |entry| modified_version['dmphub_assertions'] << entry } + incoming_assertions.each { |entry| modified_version['dmphub_modifications'] << entry } modified_version end @@ -70,7 +70,7 @@ def splice(latest_version:, modified_version:, logger: nil) # "provenance": "dmphub", # "timestamp": "2023-07-07T14:50:23+00:00", # "note": "data received from the NIH API", - # "assertions": { + # "pending_assertions": { # "contact": { # "name": "Wrong Person" # }, @@ -80,6 +80,14 @@ def splice(latest_version:, modified_version:, logger: nil) # "role": ["Investigation"] # } # ], + # "dmproadmap_related_identifiers": [ + # { + # "work_type": "article", + # "descriptor": "is_cited_by", + # "type": "doi", + # "identifier": "https://dx.doi.org/99.9876/ZYX987.V6" + # } + # ], # "project": [ # { # "start": "2024-01-01T00:00:00+07:00", @@ -110,7 +118,7 @@ def _generate_assertion(updater:, mods:, note: '') timestamp: Time.now.utc.iso8601, status: 'new', note: note, - assertions: mods + pending_assertions: mods }.to_json) end end diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb index a61cf53e..a13b63d4 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb @@ -91,7 +91,7 @@ def _post_process(json:, logger: nil) json['dmphub_updater_is_provenance'] = true # Publish the change to the EventBridge publisher = Uc3DmpEventBridge::Publisher.new - publisher.publish(source: 'DmpCreator', dmp: json, logger: logger) + publisher.publish(source: 'DmpCreator', event_type: 'EZID update', dmp: json, logger: logger) # Determine if there are any related identifiers that we should try to fetch a citation for citable_identifiers = Helper.citable_related_identifiers(dmp: json) diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb index 20e9d461..bcdb42c4 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb @@ -67,7 +67,7 @@ def _post_process(json:, logger: nil) json['dmphub_updater_is_provenance'] = true # Publish the change to the EventBridge publisher = Uc3DmpEventBridge::Publisher.new - publisher.publish(source: 'DmpDeleter', dmp: json, logger: logger) + publisher.publish(source: 'DmpDeleter', event_type: 'EZID update', dmp: json, logger: logger) true end end diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb index 63555282..f18e3bec 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb @@ -220,7 +220,7 @@ def cleanse_dmp_json(json:) return json.map { |obj| cleanse_dmp_json(json: obj) }.compact if json.is_a?(Array) cleansed = {} - allowable = %w[dmphub_versions] + allowable = %w[dmphub_modifications dmphub_versions] json.each_key do |key| next if (key.to_s.start_with?('dmphub') && !allowable.include?(key)) || %w[PK SK].include?(key.to_s) diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/schemas/author.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/schemas/author.rb index 294425fd..4d0b4f57 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/schemas/author.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/schemas/author.rb @@ -640,7 +640,7 @@ def load "start_date": { "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/license/items/properties/start_date", "type": "string", - "format": "date", + "format": "date-time", "title": "The Dataset Distribution License Start Date Schema", "description": "If date is set in the future, it indicates embargo period. Encoded using the relevant ISO 8601 Date and Time compliant string.", "examples": ["2019-06-30"] @@ -669,7 +669,7 @@ def load "issued": { "$id": "#/properties/dmp/properties/dataset/items/properties/issued", "type": "string", - "format": "date", + "format": "date-time", "title": "The Dataset Date of Issue Schema", "description": "Issued. Encoded using the relevant ISO 8601 Date and Time compliant string.", "examples": ["2019-06-30"] @@ -953,6 +953,289 @@ def load "type" ] }, + "dmphub_modifications": { + "$id": "#/properties/dmp/properties/dmphub_modifications", + "type": "array", + "title": "External modifications", + "description": "Modifications made by an external system that does not own the DMP ID", + "items": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items", + "type": "object", + "title": "An external modification", + "properties": { + "id": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/id", + "type": "string", + "title": "Modification identifier", + "examples": ["12345ABCD"] + }, + "provenance": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/provenance", + "type": "string", + "title": "Modifier", + "examples": ["datacite"] + }, + "timestamp": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/timestamp", + "type": "string", + "format": "date-time", + "title": "The modification date and time", + "examples": ["2023-07-27T15:08:32Z"] + }, + "note": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/note", + "type": "string", + "title": "Descriptive note", + "examples": ["data received from event data"] + }, + "status": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/status", + "type": "string", + "title": "Modification status", + "enum": [ + "accepted", + "pending", + "rejected" + ] + }, + "dmproadmap_related_identifier": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier", + "type": "object", + "title": "A related identifier", + "properties": { + "descriptor": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier/properties/descriptor", + "type": "string", + "enum": [ + "is_cited_by", + "cites", + "is_supplement_to", + "is_supplemented_by", + "is_described_by", + "describes", + "has_metadata", + "is_metadata_for", + "is_part_of", + "has_part", + "is_referenced_by", + "references", + "is_documented_by", + "documents", + "is_new_version_of", + "is_previous_version_of" + ] + }, + "identifier": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier/properties/identifier", + "type": "string", + "title": "A unique identifier for the item", + "description": "Identifier for a DMP", + "examples": ["https://doi.org/10.1371/journal.pcbi.1006750"] + }, + "type": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier/properties/type", + "type": "string", + "enum": [ + "handle", + "doi", + "ark", + "url", + "other" + ] + }, + "work_type": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier/properties/work_type", + "type": "string" + } + }, + "required": [ + "descriptor", + "identifier", + "type", + "work_type" + ] + }, + "funding": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding", + "type": "object", + "title": "A modification to Funding", + "properties": { + "dmproadmap_project_number": { + "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/dmproadmap_project_number", + "type": "string", + "title": "The funder's identifier for the research project", + "description": "The funder's identifier used to identify the research project", + "examples": ["prj-XYZ987-UCB"] + }, + "funder_id": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/funder_id", + "type": "object", + "title": "The Funder ID Schema", + "description": "Funder ID of the associated project", + "properties": { + "identifier": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/funder_id/properties/identifier", + "type": "string", + "title": "The Funder ID Value Schema", + "description": "Funder ID, recommended to use CrossRef Funder Registry. See: https://www.crossref.org/services/funder-registry/", + "examples": ["501100002428"] + }, + "type": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/funder_id/properties/type", + "type": "string", + "enum": [ + "fundref", + "ror", + "url", + "other" + ], + "title": "The Funder ID Type Schema", + "description": "Identifier type. Allowed values: fundref, url, other", + "examples": ["fundref"] + } + }, + "required": [ + "identifier", + "type" + ] + }, + "funding_status": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/funding_status", + "type": "string", + "enum": [ + "planned", + "applied", + "granted", + "rejected" + ], + "title": "The Funding Status Schema", + "description": "To express different phases of project lifecycle. Allowed values: planned, applied, granted, rejected", + "examples": ["granted"] + }, + "grant_id": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/grant_id", + "type": "object", + "title": "The Funding Grant ID Schema", + "description": "Grant ID of the associated project", + "properties": { + "identifier": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/grant_id/properties/identifier", + "type": "string", + "title": "The Funding Grant ID Value Schema", + "description": "Grant ID", + "examples": ["776242"] + }, + "type": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/grant_id/properties/type", + "type": "string", + "title": "The Funding Grant ID Type Schema", + "enum": [ + "doi", + "url", + "other" + ], + "description": "Identifier type. Allowed values: url, other", + "examples": ["other"] + } + }, + "required": [ + "identifier", + "type" + ] + }, + "name": { + "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/name", + "type": "string", + "title": "The name of the funding instituion / organization", + "description": "Name", + "examples": ["National Science Foundation"] + } + }, + "required": [ + "funding_status", + "name" + ] + }, + "project": { + "$id": "#/properties/dmp/properties/dmphub_modifications/project", + "type": "object", + "title": "The DMP Project Items Schema", + "properties": { + "description": { + "$id": "#/properties/dmp/properties/dmphub_modifications/project/properties/description", + "type": "string", + "title": "The DMP Project Description Schema", + "description": "Project description", + "examples": ["Project develops novel..."] + }, + "end": { + "$id": "#/properties/dmp/properties/dmphub_modifications/project/properties/end", + "type": "string", + "format": "date-time", + "title": "The DMP Project End Date Schema", + "description": "Project end date. Encoded using the relevant ISO 8601 Date and Time compliant string.", + "examples": ["2020-03-31T00:00:00Z"] + }, + "start": { + "$id": "#/properties/dmp/properties/dmphub_modifications/project/properties/start", + "type": "string", + "format": "date-time", + "title": "The DMP Project Start Date Schema", + "description": "Project start date. Encoded using the relevant ISO 8601 Date and Time compliant string.", + "examples": ["2019-04-01T00:00:00Z"] + }, + "title": { + "$id": "#/properties/dmp/properties/dmphub_modifications/project/properties/title", + "type": "string", + "title": "The DMP Project Title Schema", + "description": "Project title", + "examples": ["Our New Project"] + } + }, + "required": [ + "title" + ] + } + } + }, + "required": [ + "id", + "provenance", + "status", + "timestamp" + ] + }, + "dmphub_versions": { + "$id": "#/properties/dmp/properties/dmphub_versions", + "type": "array", + "title": "DMP ID versions", + "description": "Links to all of the DMPs versions", + "items": { + "$id": "#/properties/dmp/properties/dmphub_versions/items", + "type": "object", + "title": "DMP version", + "properties": { + "timestamp": { + "$id": "#/properties/dmp/properties/dmphub_versions/items/properties/timestamp", + "type": "string", + "format": "date-time", + "title": "The version date and time", + "examples": ["2023-08-17T16:14:39Z"] + }, + "url": { + "$id": "#/properties/dmp/properties/dmphub_versions/items/properties/url", + "type": "string", + "format": "uri", + "title": "The URL to retrieve the specified version", + "examples": ["https://somesite.org/dmps/doi.org/10.1234/A1B2C3D4?version=2023-08-17T16:14:39Z"] + } + } + }, + "required": [ + "timestamp", + "url" + ] + }, "dmproadmap_related_identifiers": { "$id": "#/properties/dmp/properties/dmproadmap_related_identifiers", "type": "array", @@ -1005,21 +1288,7 @@ def load }, "work_type": { "$id": "#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/work_type", - "type": "string", - "enum": [ - "article", - "book", - "dataset", - "metadata_template", - "other", - "output_management_plan", - "paper", - "preprint", - "preregistration", - "protocol", - "software", - "supplemental_information" - ] + "type": "string" } }, "required": [ @@ -1169,7 +1438,7 @@ def load "end": { "$id": "#/properties/dmp/properties/project/items/properties/end", "type": "string", - "format": "date", + "format": "date-time", "title": "The DMP Project End Date Schema", "description": "Project end date. Encoded using the relevant ISO 8601 Date and Time compliant string.", "examples": ["2020-03-31"] @@ -1342,7 +1611,7 @@ def load "start": { "$id": "#/properties/dmp/properties/project/items/properties/start", "type": "string", - "format": "date", + "format": "date-time", "title": "The DMP Project Start Date Schema", "description": "Project start date. Encoded using the relevant ISO 8601 Date and Time compliant string.", "examples": ["2019-04-01"] diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb index 7f52f1a4..0f1a3e81 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb @@ -143,7 +143,7 @@ def _post_process(provenance:, json:, logger: nil) json['dmphub_updater_is_provenance'] = provenance['PK'] == json['dmphub_provenance_id'] # Publish the change to the EventBridge publisher = Uc3DmpEventBridge::Publisher.new - publisher.publish(source: 'DmpUpdater', dmp: json, logger: logger) + publisher.publish(source: 'DmpUpdater', event_type: 'EZID update', dmp: json, logger: logger) # Determine if there are any related identifiers that we should try to fetch a citation for citable_identifiers = Helper.citable_related_identifiers(dmp: json) diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/version.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/version.rb index 1dc3c83e..d75f02d4 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/version.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Uc3DmpId - VERSION = '0.0.137' + VERSION = '0.0.140' end diff --git a/src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.0.137.gem b/src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.0.137.gem deleted file mode 100644 index af8c56c4fbff7d8970c6ea6965fe4495bcafb359..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29184 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 diff --git a/src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.0.140.gem b/src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.0.140.gem new file mode 100644 index 0000000000000000000000000000000000000000..60bc7532b992bb11d5a6f2e89af478fa8c79542e GIT binary patch literal 31232 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 2.0.2, < 6.0) ast (2.4.2) aws-eventstream (1.2.0) - aws-partitions (1.804.0) + aws-partitions (1.805.0) aws-sdk-cognitoidentityprovider (1.79.0) aws-sdk-core (~> 3, >= 3.177.0) aws-sigv4 (~> 1.1) @@ -124,12 +124,12 @@ GEM aws-sdk-eventbridge (~> 1.44) json (~> 2.6) logger (~> 1.4) - uc3-dmp-external-api (0.0.15) + uc3-dmp-external-api (0.0.16) aws-sdk-sns (~> 1.60) aws-sdk-ssm (~> 1.150) httparty (~> 0.21.0) json (~> 2.6) - uc3-dmp-id (0.0.137) + uc3-dmp-id (0.0.140) json (~> 2.6) json-schema (~> 3.0) uc3-dmp-dynamo (~> 0.0) diff --git a/src/sam/template.yaml b/src/sam/template.yaml index 6e221564..04d7b84c 100644 --- a/src/sam/template.yaml +++ b/src/sam/template.yaml @@ -281,6 +281,63 @@ Resources: # API Lambdas # See: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-api.html # ---------------------------------------------------- + TmpAsserterFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: 'functions/tmp_asserter/' + Handler: 'app.Functions::TmpAsserter.process' + Runtime: 'ruby2.7' + Timeout: 5 + Architectures: + - 'x86_64' + Layers: + - !Ref LambdaLayer + Policies: + - 'arn:aws:iam::aws:policy/AmazonSSMFullAccess' + - 'arn:aws:iam::aws:policy/AmazonSNSFullAccess' + - 'arn:aws:iam::aws:policy/AmazonSQSFullAccess' + - !Ref DmpHubDynamoTableWritePolicy + - !Ref EventBusPolicy + - !Ref CognitoPolicy + DeadLetterQueue: + Type: 'SQS' + TargetArn: !Ref DeadLetterQueueArn + Environment: + Variables: + LAMBDA_ENV: !Ref Env + LOG_LEVEL: 'debug' + DOMAIN: !Ref DomainName + DMP_ID_BASE_URL: 'doi.org' + SNS_FATAL_ERROR_TOPIC: !Ref SnsEmailTopicArn + EVENT_BUS_NAME: !Ref EventBusArn + COGNITO_USER_POOL_ID: !Ref CognitoUserPoolArn + DYNAMO_TABLE: !Ref DynamoTableName + CORS_ORIGIN: !If + - IsDev + - "*" + - !Sub "https://${DomainName}" + Events: + DeleteDmps: + # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Type: 'Api' + Properties: + RestApiId: !Ref DmpHubRestApi + Path: '/tmp/{dmp_id+}/' + Method: 'put' + RequestParameters: + - 'method.request.header.Authorization' + Auth: + Authorizer: 'DmpHubCognitoAuthorizer' + AuthorizationScopes: + - !Sub 'https://auth.${DomainName}/${Env}.update' + + TmpAsserterLogGroup: + Type: 'AWS::Logs::LogGroup' + Properties: + LogGroupName: !Sub "/aws/lambda/${TmpAsserterFunction}" + # Explicit retention time + RetentionInDays: !Ref LogRetentionDays + DeleteDmpFunction: Type: 'AWS::Serverless::Function' Properties: @@ -820,7 +877,7 @@ Resources: Environment: Variables: LAMBDA_ENV: !Ref Env - LOG_LEVEL: 'debug' # !Ref DebugLevel # 'debug' + LOG_LEVEL: !Ref DebugLevel # 'debug' DOMAIN: !Ref DomainName DMP_ID_BASE_URL: 'doi.org' API_BASE_URL: !Sub 'https://api.${DomainName}' @@ -903,7 +960,7 @@ Resources: Pattern: !Sub > { "source": ["${DomainName}:lambda:event_publisher"], - "detail-type": ["DMP change", "EZID Replay"], + "detail-type": ["EZID update"], "detail": { "SK": ["VERSION#latest"] } diff --git a/templates/eventbridge.yaml b/templates/eventbridge.yaml index 11242822..9130050a 100644 --- a/templates/eventbridge.yaml +++ b/templates/eventbridge.yaml @@ -77,8 +77,7 @@ Resources: EventPattern: source: - !Sub "${DomainName}:lambda:event_publisher" - detail-type: - - 'paused' + detail-type: ["EZID update"] RetentionDays: !Ref ArchiveRetentionDays SourceArn: !GetAtt EventBus.Arn From f20e7677830d75d3222fcdc0f542eec0d48e4c57 Mon Sep 17 00:00:00 2001 From: briri Date: Sat, 19 Aug 2023 10:00:51 -0700 Subject: [PATCH 03/13] added tests for uc3-dmp-id gem's Helper class --- .../gems/uc3-dmp-id/spec/support/shared.rb | 33 +- .../uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb | 473 +++++++++++++++++- 2 files changed, 503 insertions(+), 3 deletions(-) diff --git a/src/sam/gems/uc3-dmp-id/spec/support/shared.rb b/src/sam/gems/uc3-dmp-id/spec/support/shared.rb index 77781835..c31a1367 100644 --- a/src/sam/gems/uc3-dmp-id/spec/support/shared.rb +++ b/src/sam/gems/uc3-dmp-id/spec/support/shared.rb @@ -23,4 +23,35 @@ def mock_dmp } }.to_json) -end \ No newline at end of file +end + +# Helper to compare 2 hashes +def assert_dmps_match(obj_a: {}, obj_b: {}) + pass = obj_a.class.name == obj_b.class.name + + # puts "::::: #{obj_a.class.name}" + # pp obj_a + # p '-----------' + # pp obj_b + + if pass + case obj_a.class.name + when 'Array' + pass = false unless obj_a.length == obj_b.length + obj_a.each { |entry| pass = false unless obj_b.include?(entry) } + when 'Hash' + obj_a.each_pair do |key, value| + if %w[Array, Hash].include?(value.class.name) + pass = assert_dmps_match(obj_a: value, obj_b: obj_b.nil? ? nil : obj_b[key.to_s]) + else + #puts "Hash item #{key} not a sub Hash/Array #{value} == #{obj_b.nil? ? nil : obj_b[key.to_s]}" + pass = false unless value == obj_b.nil? ? nil : obj_b[key.to_s] + end + end + else + # puts "#{obj_a} == #{obj_b}" + pass = false unless obj_a == obj_b + end + end + pass +end diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb index 5e24156c..47e683b7 100644 --- a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb @@ -6,8 +6,8 @@ let!(:described_class) { Uc3DmpId::Helper } before do - mock_uc3_dmp_dynamo - allow(described_class).to receive(:puts).and_return(true) + # mock_uc3_dmp_dynamo + # allow(described_class).to receive(:puts).and_return(true) end describe 'append_pk_prefix(p_key:)' do @@ -33,4 +33,473 @@ expect(described_class.remove_pk_prefix(p_key: "#{described_class::PK_DMP_PREFIX}#{key}")).to eql(key) end end + + describe 'append_sk_prefix(s_key:)' do + it 'appends the prefix' do + key = 'foo/bar' + expect(described_class.append_sk_prefix(s_key: key)).to eql("#{described_class::SK_DMP_PREFIX}#{key}") + end + + it 'returns the :s_key as is if it already starts with the prefix' do + key = "#{described_class::SK_DMP_PREFIX}foo/bar" + expect(described_class.append_sk_prefix(s_key: key)).to eql(key) + end + end + + describe 'remove_sk_prefix(s_key:)' do + it 'returns the :s_key as is if it does not start with the prefix' do + key = 'foo/bar' + expect(described_class.remove_sk_prefix(s_key: key)).to eql(key) + end + + it 'removes the prefix' do + key = "foo/bar" + expect(described_class.remove_sk_prefix(s_key: "#{described_class::SK_DMP_PREFIX}#{key}")).to eql(key) + end + end + + describe 'dmp_id_base_url' do + it 'returns the default if no ENV[\'DMP_ID_BASE_URL\'] is defined' do + ENV.delete('DMP_ID_BASE_URL') + expect(described_class.dmp_id_base_url).to eql(described_class::DEFAULT_LANDING_PAGE_URL) + end + it 'returns the ENV[\'DMP_ID_BASE_URL\']' do + ENV['DMP_ID_BASE_URL'] = 'http://foo.bar/' + expect(described_class.dmp_id_base_url).to eql(ENV['DMP_ID_BASE_URL']) + end + it 'appends a trailing \'/\' if necessary' do + ENV['DMP_ID_BASE_URL'] = 'http://foo.bar' + expect(described_class.dmp_id_base_url).to eql("#{ENV['DMP_ID_BASE_URL']}/") + end + end + + describe 'landing_page_url' do + it 'returns the default if no ENV[\'DMP_ID_LANDING_URL\'] is defined' do + ENV.delete('DMP_ID_LANDING_URL') + expect(described_class.landing_page_url).to eql(described_class::DEFAULT_LANDING_PAGE_URL) + end + it 'returns the ENV[\'DMP_ID_LANDING_URL\']' do + ENV['DMP_ID_LANDING_URL'] = 'http://foo.bar/' + expect(described_class.landing_page_url).to eql(ENV['DMP_ID_LANDING_URL']) + end + it 'appends a trailing \'/\' if necessary' do + ENV['DMP_ID_LANDING_URL'] = 'http://foo.bar' + expect(described_class.landing_page_url).to eql("#{ENV['DMP_ID_LANDING_URL']}/") + end + end + + describe 'format_dmp_id(value:, with_protocol: false)' do + let!(:dmp_id) { 'doi.org/11.2222/3333.444'} + + before do + ENV['DMP_ID_BASE_URL'] = 'https://doi.org' + end + + it 'returns nil if the value is not a DOI' do + expect(described_class.format_dmp_id(value: 'https://dmptool.org')).to eql(nil) + end + it 'removes the protocol from the value by default' do + val = "#{ENV['DMP_ID_BASE_URL']}/#{dmp_id}" + expect(described_class.format_dmp_id(value: val)).to eql("doi.org/#{dmp_id}") + end + it 'returns the value as is if it starts with a protocol' do + val = "#{ENV['DMP_ID_BASE_URL']}/#{dmp_id}" + expect(described_class.format_dmp_id(value: val, with_protocol: true)).to eql(val) + end + it 'removes the `doi:` prefix' do + val = "doi:#{dmp_id}" + expect(described_class.format_dmp_id(value: val)).to eql(dmp_id) + end + it 'removes preceding `/` character' do + val = "/#{dmp_id}" + expect(described_class.format_dmp_id(value: val)).to eql(dmp_id) + end + it 'does not include the protocol by default' do + expect(described_class.format_dmp_id(value: dmp_id)).to eql(dmp_id) + end + it 'includes the protocol if we specify :with_protocol' do + expected = "https://#{dmp_id}" + expect(described_class.format_dmp_id(value: dmp_id, with_protocol: true)).to eql(expected) + end + end + + describe 'path_parameter_to_pk(param:)' do + before do + ENV['DMP_ID_BASE_URL'] = 'https://doi.org' + end + + it 'returns nil if param is not a string' do + expect(described_class.path_parameter_to_pk(param: 123)).to eql(nil) + end + it 'handles URI escaped characters' do + expect(described_class.path_parameter_to_pk(param: 'doi%3A11%2E2222%2F33333')).to eql('DMP#doi.org/11.2222/33333') + end + it 'replaces a domain with our base domain' do + expect(described_class.path_parameter_to_pk(param: 'doi:foo.bar/11.2222/33333')).to eql('DMP#doi.org/11.2222/33333') + end + it 'returns the DMP ID as a PK' do + expect(described_class.path_parameter_to_pk(param: '11.2222/33333')).to eql('DMP#doi.org/11.2222/33333') + end + end + + describe 'dmp_id_to_pk(json:)' do + it 'returns nil if :json is not a Hash' do + expect(described_class.dmp_id_to_pk(json: 123)).to eql(nil) + end + it 'returns nil if :json does not contain :identifier' do + expect(described_class.dmp_id_to_pk(json: JSON.parse({ foo: 'bar' }.to_json))).to eql(nil) + end + it 'returns nil if :format_dmp_id returns nil' do + allow(described_class).to receive(:format_dmp_id).and_return(nil) + expect(described_class.dmp_id_to_pk(json: JSON.parse({ identifier: '11.2222/12345' }.to_json))).to eql(nil) + end + it 'formats the PK as expected' do + val = 'doi.org/11.2222/12345' + allow(described_class).to receive(:format_dmp_id).and_return(val) + expect(described_class.dmp_id_to_pk(json: JSON.parse({ identifier: '11.2222/12345' }.to_json))).to eql("DMP##{val}") + end + end + + describe 'pk_to_dmp_id(p_key:)' do + it 'returns nil if :p_key is nil' do + expect(described_class.pk_to_dmp_id(p_key: nil)).to eql(nil) + end + it 'returns the expected Hash' do + ENV['DMP_ID_BASE_URL'] = 'https://doi.org' + expected = { type: 'doi', identifier: 'https://doi.org/11.2222.12345'} + allow(described_class).to receive(:remove_pk_prefix).and_return('doi.org/11.2222.12345') + allow(described_class).to receive(:format_dmp_id).and_return(expected[:identifier]) + expect(described_class.pk_to_dmp_id(p_key: 'DMP#doi.org/11.2222.12345')).to eql(expected) + end + end + + describe 'parse_json(json:)' do + it 'returns the :json as-is if it is already a Hash' do + expect(described_class.parse_json(json: { foo: 'bar' })).to eql({ foo: 'bar' }) + end + it 'returns nil if :json is not a String or Hash' do + expect(described_class.parse_json(json: 123)).to eql(nil) + end + it 'raises a JSON::ParserError if the :json String is invalid' do + expect { described_class.parse_json(json: 'foo: bar') }.to raise_error(JSON::ParserError) + end + it 'parses the String into a Hash' do + expect(described_class.parse_json(json: '{"foo":"bar"}')).to eql(JSON.parse({ foo: 'bar' }.to_json)) + end + end + + describe 'eql?(dmp_a:, dmp_b:)' do + let!(:dmp) do + now = Time.now.utc.iso8601 + + JSON.parse({ + dmp: { + PK: "#{described_class::PK_DMP_PREFIX}foo", + SK: described_class::DMP_LATEST_VERSION, + title: 'Foo bar', + created: now, + modified: now, + dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), + dmphub_versions: [ + { timestamp: now, url: 'http://foo.bar/foo' }, + { timestamp: now, url: 'http://foo.bar/foo?version=2000-01-01T00:00:00+00:00' } + ] + } + }.to_json) + end + + it 'just compares the 2 values as-is if :dmp_a or :dmp_b is not a Hash' do + expect(described_class.eql?(dmp_a: 'foo', dmp_b: { dmp: { bar: 'foo' } })).to eql(false) + end + it 'just compares the 2 values as-is if :dmp_a or dmp_b does not have a top level :dmp' do + expect(described_class.eql?(dmp_a: { foo: 'bar' }, dmp_b: { dmp: { bar: 'foo' } })).to eql(false) + end + it 'returns true if :dmp_a and :dmp_b are identical' do + expect(described_class.eql?(dmp_a: dmp, dmp_b: dmp)).to eql(true) + end + it 'returns false if :dmp_a PK does not start with the DMP ID :PK prefix' do + dmp_a = dmp.clone[:PK] = 'FOO' + expect(described_class.eql?(dmp_a: dmp_a, dmp_b: dmp)).to eql(false) + end + it 'returns false if :dmp_b PK does not start with the DMP ID :PK prefix' do + dmp_b = dmp.clone[:PK] = 'FOO' + expect(described_class.eql?(dmp_a: dmp, dmp_b: dmp_b)).to eql(false) + end + it 'returns false if :dmp_a and :dmp_b :PKs do not match' do + dmp_a = dmp.clone[:PK] = "#{described_class::PK_DMP_PREFIX}FOO" + expect(described_class.eql?(dmp_a: dmp_a, dmp_b: dmp)).to eql(false) + end + it 'ignores :SK, :created, :modified, :dmphub_modification_day and :dmphub_versions' do + dmp_a = JSON.parse({ + dmp: { + PK: "#{described_class::PK_DMP_PREFIX}foo", + SK: "#{described_class::SK_DMP_PREFIX}2000-01-01T00:00:00+00:00", + title: 'Foo bar', + created: '2000-01-01T00:00:00+00:00', + modified: '2000-01-01T00:00:00+00:00', + dmphub_modification_day: '2000-01-01', + dmphub_versions: [] + } + }.to_json) + expect(described_class.eql?(dmp_a: dmp_a, dmp_b: dmp)).to eql(true) + end + end + + describe 'extract_owner_id(json: {})' do + let!(:dmp) do + JSON.parse({ + dmp: { + PK: "#{described_class::PK_DMP_PREFIX}foo", + SK: described_class::DMP_LATEST_VERSION, + contact: { + contact_id: { type: 'orcid', identifier: 'contact' } + }, + contributor: [ + { contributor_id: { type: 'orcid', identifier: 'first' } }, + { contributor_id: { type: 'orcid', identifier: 'last' } } + ] + } + }.to_json) + end + + it 'returns nil if :json is not a Hash' do + expect(described_class.extract_owner_id(json: 123)).to eql(nil) + end + it 'returns the :contact_id if available' do + expect(described_class.extract_owner_id(json: dmp)).to eql('contact') + end + it 'returns the first :contributor_id if :contact_id is not available' do + dmp['dmp'].delete('contact') + expect(described_class.extract_owner_id(json: dmp)).to eql('first') + end + end + + describe 'extract_owner_org(json: {})' do + let!(:dmp) do + JSON.parse({ + dmp: { + PK: "#{described_class::PK_DMP_PREFIX}foo", + SK: described_class::DMP_LATEST_VERSION, + contact: { + dmproadmap_affiliation: { affiliation_id: { type: 'ror', identifier: 'contact' } } + }, + contributor: [ + { dmproadmap_affiliation: { affiliation_id: { type: 'ror', identifier: 'first' } } }, + { dmproadmap_affiliation: { affiliation_id: { type: 'ror', identifier: 'last' } } }, + { dmproadmap_affiliation: {affiliation_id: { type: 'ror', identifier: 'last' } } } + ] + } + }.to_json) + end + + it 'returns nil if :json is not a Hash' do + expect(described_class.extract_owner_org(json: 123)).to eql(nil) + end + it 'returns the :contact affiliation if available' do + expect(described_class.extract_owner_org(json: dmp)).to eql('contact') + end + it 'returns the most common :contributor affiliation if :contact is not available' do + dmp['dmp'].delete('contact') + expect(described_class.extract_owner_org(json: dmp)).to eql('last') + end + end + + describe 'annotate_dmp_json(provenance:, p_key:, json:)' do + let!(:p_key) { 'DMP#doi.org/11.2222/12345' } + let!(:provenance) { JSON.parse({ PK: 'PROVENANCE#foo', seedingWithLiveDmpIds: false }.to_json) } + let!(:dmp) do + JSON.parse({ + title: 'Foo bar' + }.to_json) + end + + before do + ENV['DMP_ID_BASE_URL'] = 'https://doi.org' + allow(described_class).to receive(:extract_owner_id).and_return('orcid123') + allow(described_class).to receive(:extract_owner_org).and_return('ror123') + end + + it 'returns the :json as-is if :provenance is nil' do + expect(described_class.annotate_dmp_json(provenance: nil, p_key: p_key, json: dmp)).to eql(dmp) + end + it 'returns the :json as-is if :p_key is nil' do + expect(described_class.annotate_dmp_json(provenance: provenance, p_key: nil, json: dmp)).to eql(dmp) + end + it 'returns nil if :json is not parseable' do + allow(described_class).to receive(:parse_json).and_return(nil) + expect(described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: 123)).to eql(nil) + end + it 'returns the :json as-is if :p_key does not match the :dmp_id' do + dmp['PK'] = 'DMP#doi.org/99.9999/99999' + expect(described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp)).to eql(dmp) + end + it 'returns the expected JSON if :dmphub_provenance_id if not defined in the :json' do + expected = JSON.parse({ + title: 'Foo bar', + PK: p_key, + SK: described_class::DMP_LATEST_VERSION, + dmp_id: { type: 'doi', identifier: "https://#{p_key.gsub(described_class::PK_DMP_PREFIX, '')}" }, + dmproadmap_featured: '0', + dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), + dmphub_owner_id: 'orcid123', + dmphub_owner_org: 'ror123', + dmphub_provenance_id: provenance['PK'] + }.to_json) + result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) + expect(assert_dmps_match(obj_a: result, obj_b: expected)).to be(true) + end + it 'properly translates :dmproadmap_featured' do + dmp['dmproadmap_featured'] = 'yes' + + expected = JSON.parse({ + title: 'Foo bar', + PK: p_key, + SK: described_class::DMP_LATEST_VERSION, + dmp_id: { type: 'doi', identifier: "https://#{p_key.gsub(described_class::PK_DMP_PREFIX, '')}" }, + dmproadmap_featured: '1', + dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), + dmphub_owner_id: 'orcid123', + dmphub_owner_org: 'ror123', + dmphub_provenance_id: provenance['PK'] + }.to_json) + result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) + expect(assert_dmps_match(obj_a: result, obj_b: expected)).to be(true) + end + it 'adds the expected JSON if :dmphub_provenance_identifier if not defined in the :json' do + dmp['dmproadmap_featured'] = 1 + dmp['dmphub_provenance_identifier'] = 'http://foo.bar/dmp/123' + dmp.delete('dmp_id') + + expected = JSON.parse({ + title: 'Foo bar', + PK: p_key, + SK: described_class::DMP_LATEST_VERSION, + dmp_id: { type: "doi", identifier: "https://#{p_key.gsub(described_class::PK_DMP_PREFIX, '')}" }, + dmproadmap_featured: '1', + dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), + dmphub_owner_id: 'orcid123', + dmphub_owner_org: 'ror123', + dmphub_provenance_id: provenance['PK'], + dmphub_provenance_identifier: 'http://foo.bar/dmp/123' + }.to_json) + result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) + expect(assert_dmps_match(obj_a: expected, obj_b: result)).to be(true) + end + it 'retains the DMP ID specified if the provenance is :seedingWithLiveDmpIds' do + provenance[:seedingWithLiveDmpIds] = true + dmp['dmp_id'] = 'http://foo.bar/dmp/123' + dmp['dmproadmap_featured'] = '1' + + expected = JSON.parse({ + title: 'Foo bar', + PK: "#{described_class::PK_DMP_PREFIX}foo.bar/dmp/123", + SK: described_class::DMP_LATEST_VERSION, + dmp_id: { type: "doi", identifier: 'http://foo.bar/dmp/123' }, + dmproadmap_featured: '1', + dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), + dmphub_owner_id: 'orcid123', + dmphub_owner_org: 'ror123', + dmphub_provenance_id: provenance['PK'], + dmphub_provenance_identifier: 'http://foo.bar/dmp/123' + }.to_json) + result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) + expect(assert_dmps_match(obj_a: result, obj_b: expected)).to be(true) + end + it 'does NOT retain the specified DMP ID if the provenance is not :seedingWithLiveDmpIds' do + dmp['dmp_id'] = JSON.parse({ type: 'url', identifier: 'http://foo.bar/dmp/123' }.to_json) + dmp['dmproadmap_featured'] = '1' + + expected = JSON.parse({ + title: 'Foo bar', + PK: p_key, + SK: described_class::DMP_LATEST_VERSION, + dmp_id: { type: "doi", identifier: "https://#{p_key.gsub(described_class::PK_DMP_PREFIX, '')}" }, + dmproadmap_featured: '1', + dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), + dmphub_owner_id: 'orcid123', + dmphub_owner_org: 'ror123', + dmphub_provenance_id: provenance['PK'], + dmphub_provenance_identifier: 'http://foo.bar/dmp/123' + }.to_json) + result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) + expect(assert_dmps_match(obj_a: expected, obj_b: result)).to be(true) + end + end + + describe 'cleanse_dmp_json(json:)' do + let!(:dmp) do + JSON.parse({ + title: 'Foo bar', + PK: 'FOO', + SK: described_class::DMP_LATEST_VERSION, + dmp_id: { type: "doi", identifier: "https://FOO" }, + dmproadmap_featured: '1', + dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), + dmphub_owner_id: 'orcid123', + dmphub_owner_org: 'ror123', + dmphub_provenance_id: 'fooo', + dmphub_provenance_identifier: 'http://foo.bar/dmp/123', + dmphub_foo: 'bar', + dmphub_modifications: 'still here', + dmphub_versions: { result: 'still here'} + }.to_json) + end + + it 'returns :json as-is if it is not a Hash or Array' do + expect(described_class.cleanse_dmp_json(json: 123)).to eql(123) + end + it 'calls itself for each item if :json is an array' do + allow(described_class).to receive(:cleanse_dmp_json).twice + described_class.cleanse_dmp_json(json: [dmp, dmp]) + end + it 'returns the cleansed :json' do + expected = JSON.parse({ + title: 'Foo bar', + dmp_id: { type: "doi", identifier: "https://FOO" }, + dmproadmap_featured: '1', + dmphub_modifications: 'still here', + dmphub_versions: { result: 'still here'} + }.to_json) + + result = described_class.cleanse_dmp_json(json: dmp) + expect(assert_dmps_match(obj_a: expected, obj_b: result)).to be(true) + end + end + + describe 'citable_related_identifiers(dmp:)' do + it 'returns an empty array if :dmp is not a Hash' do + expect(described_class.citable_related_identifiers(dmp: 123)).to eql([]) + end + it 'returns the expected :dmproadmap_related_identifiers' do + dmp = JSON.parse({ + title: 'Foo bar', + dmp_id: { type: "doi", identifier: "https://FOO" }, + dmproadmap_related_identifiers: [ + { work_type: 'output_management_plan', descriptor: 'is_metadata_for', identifier: 'http://skip.me', type: 'url' }, + { work_type: 'article', descriptor: 'cites', identifier: 'http://skip.me', type: 'doi', citation: 'fooooo' }, + { work_type: 'dataset', descriptor: 'references', identifier: 'http://keep.me', type: 'url', citation: 'baaaarr' }, + { work_type: 'dataset', descriptor: 'references', identifier: 'http://keep.me', type: 'url' }, + { work_type: 'software', descriptor: 'cites', identifier: 'http://keep.me', type: 'url' }, + ] + }.to_json) + expected = dmp['dmproadmap_related_identifiers'].reject { |id| id['identifier'] == 'http://skip.me' } + expect(assert_dmps_match(obj_a: expected, obj_b: described_class.citable_related_identifiers(dmp: dmp))).to be(true) + end + end + + describe 'deep_copy_dmp(obj:)' do + it 'makes a copy of the object' do + obj = JSON.parse({ + foo: { + array_one: ['a', 'b', 'c'], + key_one: 'value one', + nested: { + array_two: ['Z', 'Y', 'X'], + key_two: 'value two', + } + } + }.to_json) + expect(assert_dmps_match(obj_a: described_class.deep_copy_dmp(obj: obj), obj_b: obj)).to be(true) + end + end end From 66d8bd54da269e1a059f8e50ce4d0f8cc2952b11 Mon Sep 17 00:00:00 2001 From: briri Date: Sat, 19 Aug 2023 10:01:11 -0700 Subject: [PATCH 04/13] added citation gem and functions --- src/sam/functions/citer/app.rb | 99 +--------------- src/sam/functions/post_citations/app.rb | 65 +++++++++++ src/sam/functions/tmp_asserter/app.rb | 9 +- src/sam/gems/uc3-dmp-citation/Gemfile | 6 + src/sam/gems/uc3-dmp-citation/README.md | 3 + .../uc3-dmp-citation/lib/uc3-dmp-citation.rb | 110 ++++++++++++++++++ .../lib/uc3-dmp-citation/version.rb | 5 + .../uc3-dmp-citation-0.0.5.gem | Bin 0 -> 6656 bytes .../uc3-dmp-citation/uc3-dmp-citation.gemspec | 29 +++++ src/sam/gems/uc3-dmp-id/.rspec_status | 71 ++++++++++- src/sam/gems/uc3-dmp-id/Gemfile.lock | 2 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb | 27 ++--- src/sam/layers/Gemfile | 1 + src/sam/layers/Gemfile.lock | 11 +- src/sam/template.yaml | 58 ++++++++- 15 files changed, 368 insertions(+), 128 deletions(-) create mode 100644 src/sam/functions/post_citations/app.rb create mode 100644 src/sam/gems/uc3-dmp-citation/Gemfile create mode 100644 src/sam/gems/uc3-dmp-citation/README.md create mode 100644 src/sam/gems/uc3-dmp-citation/lib/uc3-dmp-citation.rb create mode 100644 src/sam/gems/uc3-dmp-citation/lib/uc3-dmp-citation/version.rb create mode 100644 src/sam/gems/uc3-dmp-citation/uc3-dmp-citation-0.0.5.gem create mode 100644 src/sam/gems/uc3-dmp-citation/uc3-dmp-citation.gemspec diff --git a/src/sam/functions/citer/app.rb b/src/sam/functions/citer/app.rb index f26a36d3..271ace05 100644 --- a/src/sam/functions/citer/app.rb +++ b/src/sam/functions/citer/app.rb @@ -6,15 +6,11 @@ my_gem_path = Dir['/opt/ruby/gems/**/lib/'] $LOAD_PATH.unshift(*my_gem_path) -require 'bibtex' -require 'citeproc' -require 'csl/styles' - require 'uc3-dmp-api-core' +require 'uc3-dmp-citation' require 'uc3-dmp-cloudwatch' require 'uc3-dmp-dynamo' require 'uc3-dmp-event-bridge' -require 'uc3-dmp-external-api' require 'uc3-dmp-id' module Functions @@ -22,14 +18,6 @@ module Functions class Citer SOURCE = 'Citer' - APPLICATION_NAME = 'DMPTool' - DEFAULT_CITATION_STYLE = 'chicago-author-date' - DEFAULT_DOI_URL = 'http://doi.org' - DEFAULT_WORK_TYPE = 'Dataset' - - MSG_BIBTEX_FAILURE = 'Unable to fetch Bibtex for the specified DOI.' - MSG_UNABLE_TO_UPDATE = 'Unable to update the citations on the DMP ID.' - # Parameters # ---------- # event: Hash, required @@ -83,22 +71,10 @@ def process(event:, context:) if identifiers.any? && uncited.any? existing_citations = identifiers.reject { |id| uncited.include?(id) } - headers = { Accept: 'application/x-bibtex' } - processed = [] uncited.each do |identifier| - uri = _doi_to_uri(doi: identifier['identifier']&.strip) - if !uri.nil? && !uri.blank? - logger.debug(message: "Fetching BibTeX from: #{uri}") - resp = Uc3DmpExternalApi::Client.call(url: uri, method: :get, additional_headers: headers, logger: logger) - - unless resp.nil? || resp.to_s.strip.empty? - bibtex = BibTeX.parse(_cleanse_bibtex(text: resp)) - work_type = identifier['work_type'].nil? ? determine_work_type(bibtex: bibtex) : identifier['work_type'] - identifier['citation'] = _bibtex_to_citation(uri: uri, work_type: work_type, bibtex: bibtex) - end - end - + citation = Uc3DmpCitation::Citer.fetch_citation(doi: identifier['identifier']&.strip, logger: logger) + identifier['citation'] = citation unless citation.nil? processed << identifier end @@ -115,6 +91,8 @@ def process(event:, context:) end rescue Uc3DmpId::FinderError => e logger.error(message: e.message, details: e.backtrace) + rescue Uc3DmpCitation::CiterError => e + logger.error(message: e.message, details: e.backtrace) rescue Uc3DmpExternalApi::ExternalApiError => e logger.error(message: e.message, details: e.backtrace) rescue StandardError => e @@ -134,73 +112,6 @@ def _respond(status:, items: [], errors: [], event: {}, params: {}) page: params['page'], per_page: params['per_page'] ) end - - # Will convert 'doi:10.1234/abcdefg' to 'http://doi.org/10.1234/abcdefg' - def _doi_to_uri(doi:) - val = doi.match(Uc3DmpId::Helper::DOI_REGEX).to_s - return nil if val.nil? || val.strip == '' - - doi.start_with?('http') ? doi : "#{DEFAULT_DOI_URL}/#{doi.gsub('doi:', '')}" - end - - # If no :work_type was specified we can try to derive it from the BibTeX metadata - def _determine_work_type(bibtex:) - return '' if bibtex.nil? || bibtex.data.nil? || bibtex.data.first.nil? - - return 'article' unless bibtex.data.first.journal.nil? - - '' - end - - def _cleanse_bibtex(text:) - return nil if text.nil? || text.to_s.strip == '' - - # Make sure we're working with UTF8 - utf8 = text.force_encoding('UTF-8') - - # Remove any encoded HTML (e.g. "Regular text $\\lt$strong$\\gt$Bold text$\\lt$/strong$\\gt$") - utf8 = utf8.gsub(%r{\$?\\\$?(less|lt|Lt)\$/?[a-zA-Z]+\$?\\\$?(greater|gt|Gt)\$}, '') - # Replace any special dash, semicolon and quote characters with a minus sign or single/double quote - utf8 = utf8.gsub(%r{\$?\\(T|t)ext[a-zA-Z]+dash\$?}, '-').gsub(%r{\{(T|t)ext[a-zA-Z]+dash\}}, '-') - .gsub(%r{\$?\\(M|m)athsemicolon\$?}, ':').gsub(%r{\{(M|m)semicolon\}}, ':') - .gsub(%r{\$?\\(T|t)extquotesingle\$?}, "'").gsub(%r{\{(T|t)extquotesingle\}}, "'") - .gsub(%r{\$?\\(T|t)extquotedouble\$?}, '"').gsub(%r{\{(T|t)extquotedouble\}}, '"') - # Remove any remaining `\v` entries which attempt to construct an accented character - utf8.gsub(%r{\\v}, '') - end - - # Convert the BibTeX item to a citation - def _bibtex_to_citation(uri:, work_type: DEFAULT_WORK_TYPE, bibtex:, style: DEFAULT_CITATION_STYLE) - return nil unless uri.is_a?(String) && uri.strip != '' - return nil if bibtex.nil? || bibtex.data.nil? || bibtex.data.first.nil? - - cp = CiteProc::Processor.new(style: style, format: 'html') - cp.import(bibtex.to_citeproc) - citation = cp.render(:bibliography, id: bibtex.data.first.id) - return nil unless citation.is_a?(Array) && citation.any? - - # The CiteProc renderer has trouble with some things so fix them here - # - For some reason words in all caps in the title get wrapped in curl brackets - citation = citation.first.gsub('{', '').gsub('}', '') - - unless work_type.nil? || work_type.strip == '' - # This supports the :apa and :chicago-author-date styles - citation = citation.gsub(/\.”\s+/, "\.” [#{work_type.gsub('_', ' ').capitalize}]. ") - .gsub(/<\/i>\.\s+/, "<\/i>\. [#{work_type.gsub('_', ' ').capitalize}]. ") - end - - # Convert the URL into a link. Ensure that the trailing period is not a part of - # the link! - citation.gsub(URI.regexp) do |url| - if url.start_with?('http') - '%{url}.' % { - url: url.end_with?('.') ? uri : "#{uri}." - } - else - url - end - end - end end end end diff --git a/src/sam/functions/post_citations/app.rb b/src/sam/functions/post_citations/app.rb new file mode 100644 index 00000000..67e2354e --- /dev/null +++ b/src/sam/functions/post_citations/app.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +# Docs say that the LambdaLayer gems are found mounted as /opt/ruby/gems but an inspection +# of the $LOAD_PATH shows that only /opt/ruby/lib is available. So we add what we want here +# and indicate exactly which folders contain the *.rb files +my_gem_path = Dir['/opt/ruby/gems/**/lib/'] +$LOAD_PATH.unshift(*my_gem_path) + +require 'uc3-dmp-api-core' +require 'uc3-dmp-citation' +require 'uc3-dmp-cloudwatch' + +module Functions + # A service that fetches the BibTex for DOIs and converts them into citations + class PostCitations + SOURCE = 'POST /citations' + + MSG_INVALID_BODY = 'Invalid body! Expecting JSON like: `{"dois":[{"work_type":"dataset","value":"https://doi.org/11.1234/ab12"}]}`' + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def self.process(event:, context:) + # Setup the Logger + log_level = ENV.fetch('LOG_LEVEL', 'error') + req_id = context.aws_request_id if context.is_a?(LambdaContext) + logger = Uc3DmpCloudwatch::Logger.new(source: SOURCE, request_id: req_id, event: event, level: log_level) + + body = event.fetch('body', '') + json = JSON.parse(body) + return _respond(status: 400, errors: MSG_INVALID_BODY, event: event) unless json['dois'].is_a?(Array) && + json['dois'].any? + + style = json['style'].nil? ? Uc3DmpCitation::Citer::DEFAULT_CITATION_STYLE : json['style'].to_s.downcase + citations = [] + json['dois'].each do |entry| + resp = Uc3DmpCitation::Citer.fetch_citation(doi: entry['value']&.strip, work_type: entry['work_type']&.strip, + style: style, logger: logger) + citations << { doi: entry['value'], citation: resp } + end + _respond(status: 200, items: citations, event: event) + rescue JSON::ParserError + logger.debug(message: MSG_INVALID_BODY, details: body.to_s) + _respond(status: 400, errors: MSG_INVALID_BODY, event: event) + rescue Uc3DmpCitation::CiterError => e + logger.debug(message: e.message, details: body.to_s) + _respond(status: 500, errors: e.message, event: event) + rescue StandardError => e + logger.error(message: e.message, details: e.backtrace) + deets = { message: e.message, body: body } + Uc3DmpApiCore::Notifier.notify_administrator(source: SOURCE, details: deets, event: event) + { statusCode: 500, body: { errors: [Uc3DmpApiCore::MSG_SERVER_ERROR] }.to_json } + end + + private + + class << self + # Send the output to the Responder + def _respond(status:, items: [], errors: [], event: {}, params: {}) + Uc3DmpApiCore::Responder.respond( + status: status, items: items, errors: errors, event: event, + page: params['page'], per_page: params['per_page'] + ) + end + end + end +end diff --git a/src/sam/functions/tmp_asserter/app.rb b/src/sam/functions/tmp_asserter/app.rb index e41c785e..ef561533 100644 --- a/src/sam/functions/tmp_asserter/app.rb +++ b/src/sam/functions/tmp_asserter/app.rb @@ -43,7 +43,7 @@ def self.process(event:, context:) # Fetch the DMP ID logger.debug(message: "Searching for PK: #{p_key}, SK: #{s_key}") if logger.respond_to?(:debug) - result = Uc3DmpId::Finder.by_pk(p_key: p_key, s_key: s_key, logger: logger) + dmp = Uc3DmpId::Finder.by_pk(p_key: p_key, s_key: s_key, logger: logger) work_count = json.fetch('works', '2').to_s.strip.to_i grant_ror = json.fetch('grant', 'https://ror.org/01bj3aw27').to_s.downcase.strip @@ -78,12 +78,13 @@ def self.process(event:, context:) funding: [_add_grant(funder: funder)] } end - mods = { dmphub_modifications: mods } + dmp['dmp']['dmphub_modifications'] = mods logger.debug(message: "Tmp Asserter update to PK: #{p_key}", details: { requested: json, mods: mods }) # Update the DMP ID - # resp = Uc3DmpId::Updater.update(logger: logger, provenance: provenance, p_key: p_key, json: json) - # return _respond(status: 400, errors: Uc3DmpId::MSG_DMP_NO_DMP_ID) if resp.nil? + client = Uc3DmpDynamo::Client.new + resp = client.put_item(json: dmp['dmp'], logger: logger) + return _respond(status: 500, errors: ["Unable to add dmphub_modifications!"], event: event) if resp.nil? _respond(status: 200, items: [resp], event: event) rescue Uc3DmpId::UpdaterError => e diff --git a/src/sam/gems/uc3-dmp-citation/Gemfile b/src/sam/gems/uc3-dmp-citation/Gemfile new file mode 100644 index 00000000..40d1ee9f --- /dev/null +++ b/src/sam/gems/uc3-dmp-citation/Gemfile @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +# Specify your gem's dependencies in the gemspec +gemspec diff --git a/src/sam/gems/uc3-dmp-citation/README.md b/src/sam/gems/uc3-dmp-citation/README.md new file mode 100644 index 00000000..5d002e48 --- /dev/null +++ b/src/sam/gems/uc3-dmp-citation/README.md @@ -0,0 +1,3 @@ +# uc3-dmp-logger + +Logger service to write CloudWatch logs in a specific format diff --git a/src/sam/gems/uc3-dmp-citation/lib/uc3-dmp-citation.rb b/src/sam/gems/uc3-dmp-citation/lib/uc3-dmp-citation.rb new file mode 100644 index 00000000..f50f7c43 --- /dev/null +++ b/src/sam/gems/uc3-dmp-citation/lib/uc3-dmp-citation.rb @@ -0,0 +1,110 @@ +# rubocop:disable Naming/FileName +# frozen_string_literal: true + +require 'bibtex' +require 'citeproc' +require 'csl/styles' + +require 'uc3-dmp-external-api' + +module Uc3DmpCitation + class CiterError < StandardError; end + + class Citer + DEFAULT_CITATION_STYLE = 'chicago-author-date' + DEFAULT_DOI_URL = 'http://doi.org' + DEFAULT_WORK_TYPE = 'Dataset' + + DOI_REGEX = %r{[0-9]{2}\.[0-9]{4,}/[a-zA-Z0-9/_.-]+} + + MSG_BIBTEX_FAILURE = 'Unable to fetch Bibtex for the specified DOI.' + MSG_UNABLE_TO_UPDATE = 'Unable to update the citations on the DMP ID.' + + class << self + def fetch_citation(doi:, work_type: DEFAULT_WORK_TYPE, style: DEFAULT_CITATION_STYLE, logger: nil) + uri = _doi_to_uri(doi: doi) + return nil if uri.nil? || uri.blank? + + headers = { Accept: 'application/x-bibtex' } + logger.debug(message: "Fetching BibTeX from: #{uri}") if logger.respond_to?(:debug) + resp = Uc3DmpExternalApi::Client.call(url: uri, method: :get, additional_headers: headers, logger: logger) + return nil if resp.nil? || resp.to_s.strip.empty? + + bibtex = BibTeX.parse(_cleanse_bibtex(text: resp)) + work_type = work_type.nil? ? determine_work_type(bibtex: bibtex) : work_type + style = style.nil? ? DEFAULT_CITATION_STYLE : style + _bibtex_to_citation(uri: uri, work_type: work_type, style: style, bibtex: bibtex) + end + + private + + # Will convert 'doi:10.1234/abcdefg' to 'http://doi.org/10.1234/abcdefg' + def _doi_to_uri(doi:) + val = doi.match(DOI_REGEX).to_s + return nil if val.nil? || val.strip == '' + + doi.start_with?('http') ? doi : "#{DEFAULT_DOI_URL}/#{doi.gsub('doi:', '')}" + end + + # If no :work_type was specified we can try to derive it from the BibTeX metadata + def _determine_work_type(bibtex:) + return '' if bibtex.nil? || bibtex.data.nil? || bibtex.data.first.nil? + + return 'article' unless bibtex.data.first.journal.nil? + + '' + end + + def _cleanse_bibtex(text:) + return nil if text.nil? || text.to_s.strip == '' + + # Make sure we're working with UTF8 + utf8 = text.force_encoding('UTF-8') + + # Remove any encoded HTML (e.g. "Regular text $\\lt$strong$\\gt$Bold text$\\lt$/strong$\\gt$") + utf8 = utf8.gsub(%r{\$?\\\$?(less|lt|Lt)\$/?[a-zA-Z]+\$?\\\$?(greater|gt|Gt)\$}, '') + # Replace any special dash, semicolon and quote characters with a minus sign or single/double quote + utf8 = utf8.gsub(%r{\$?\\(T|t)ext[a-zA-Z]+dash\$?}, '-').gsub(%r{\{(T|t)ext[a-zA-Z]+dash\}}, '-') + .gsub(%r{\$?\\(M|m)athsemicolon\$?}, ':').gsub(%r{\{(M|m)semicolon\}}, ':') + .gsub(%r{\$?\\(T|t)extquotesingle\$?}, "'").gsub(%r{\{(T|t)extquotesingle\}}, "'") + .gsub(%r{\$?\\(T|t)extquotedouble\$?}, '"').gsub(%r{\{(T|t)extquotedouble\}}, '"') + # Remove any remaining `\v` entries which attempt to construct an accented character + utf8.gsub(%r{\\v}, '') + end + + # Convert the BibTeX item to a citation + def _bibtex_to_citation(uri:, work_type: DEFAULT_WORK_TYPE, bibtex:, style: DEFAULT_CITATION_STYLE) + return nil unless uri.is_a?(String) && uri.strip != '' + return nil if bibtex.nil? || bibtex.data.nil? || bibtex.data.first.nil? + + cp = CiteProc::Processor.new(style: style, format: 'html') + cp.import(bibtex.to_citeproc) + citation = cp.render(:bibliography, id: bibtex.data.first.id) + return nil unless citation.is_a?(Array) && citation.any? + + # The CiteProc renderer has trouble with some things so fix them here + # - For some reason words in all caps in the title get wrapped in curl brackets + citation = citation.first.gsub('{', '').gsub('}', '') + + unless work_type.nil? || work_type.strip == '' + # This supports the :apa and :chicago-author-date styles + citation = citation.gsub(/\.”\s+/, "\.” [#{work_type.gsub('_', ' ').capitalize}]. ") + .gsub(/<\/i>\.\s+/, "<\/i>\. [#{work_type.gsub('_', ' ').capitalize}]. ") + end + + # Convert the URL into a link. Ensure that the trailing period is not a part of + # the link! + citation.gsub(URI.regexp) do |url| + if url.start_with?('http') + '%{url}.' % { + url: url.end_with?('.') ? uri : "#{uri}." + } + else + url + end + end + end + end + end +end +# rubocop:enable Naming/FileName diff --git a/src/sam/gems/uc3-dmp-citation/lib/uc3-dmp-citation/version.rb b/src/sam/gems/uc3-dmp-citation/lib/uc3-dmp-citation/version.rb new file mode 100644 index 00000000..62af4ed1 --- /dev/null +++ b/src/sam/gems/uc3-dmp-citation/lib/uc3-dmp-citation/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Uc3DmpCitation + VERSION = '0.0.5' +end diff --git a/src/sam/gems/uc3-dmp-citation/uc3-dmp-citation-0.0.5.gem b/src/sam/gems/uc3-dmp-citation/uc3-dmp-citation-0.0.5.gem new file mode 100644 index 0000000000000000000000000000000000000000..21b23481dab060a85fe9ba502458a031e923df19 GIT binary patch literal 6656 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 literal 0 HcmV?d00001 diff --git a/src/sam/gems/uc3-dmp-citation/uc3-dmp-citation.gemspec b/src/sam/gems/uc3-dmp-citation/uc3-dmp-citation.gemspec new file mode 100644 index 00000000..690bdbff --- /dev/null +++ b/src/sam/gems/uc3-dmp-citation/uc3-dmp-citation.gemspec @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +$LOAD_PATH.push File.expand_path('lib', __dir__) +require 'uc3-dmp-citation/version' + +Gem::Specification.new do |spec| + spec.name = 'uc3-dmp-citation' + spec.version = Uc3DmpCitation::VERSION + spec.platform = Gem::Platform::RUBY + spec.authors = ['Brian Riley'] + spec.email = ['brian.riley@ucop.edu'] + + spec.summary = 'DMPTool gem that provides support for fetching BibTex for a DOI and converting it into a citation' + spec.description = 'Helper for working with DOI citations' + spec.homepage = 'https://github.com/CDLUC3/dmp-hub-cfn/blob/main/src/sam/gems/uc3-dmp-citation' + spec.license = 'MIT' + + spec.files = Dir['lib/**/*'] + %w[README.md] + spec.require_paths = ['lib'] + spec.required_ruby_version = '>= 2.7' + + # Requirements for running RSpec + spec.add_development_dependency('byebug', '11.1.3') + spec.add_development_dependency('rspec', '3.9.0') + spec.add_development_dependency('rubocop', '1.50.2') + spec.add_development_dependency('rubocop-rspec', '2.20.0') + + spec.metadata['rubygems_mfa_required'] = 'false' +end diff --git a/src/sam/gems/uc3-dmp-id/.rspec_status b/src/sam/gems/uc3-dmp-id/.rspec_status index 1b756a77..da221d5b 100644 --- a/src/sam/gems/uc3-dmp-id/.rspec_status +++ b/src/sam/gems/uc3-dmp-id/.rspec_status @@ -1,6 +1,65 @@ -example_id | status | run_time | ---------------------------------------- | ------ | --------------- | -./spec/uc3-dmp-id/helper_spec.rb[1:1:1] | passed | 0.00386 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:1:2] | passed | 0.00024 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:2:1] | passed | 0.00018 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:2:2] | passed | 0.00017 seconds | +example_id | status | run_time | +---------------------------------------- | ------ | --------------- | +./spec/uc3-dmp-id/helper_spec.rb[1:1:1] | passed | 0.00043 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:1:2] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:2:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:2:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:3:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:3:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:4:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:4:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:5:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:5:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:5:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:6:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:6:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:6:3] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:4] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:5] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:6] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:7] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:3] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:4] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:3] | passed | 0.00484 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:4] | passed | 0.00008 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:10:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:10:2] | passed | 0.00008 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:3] | passed | 0.00064 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:4] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:1] | passed | 0.00005 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:2] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:4] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:5] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:6] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:7] | passed | 0.00005 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:13:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:13:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:13:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:14:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:14:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:14:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:1] | passed | 0.00008 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:2] | passed | 0.00007 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:3] | passed | 0.0001 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:4] | passed | 0.00006 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:5] | passed | 0.00055 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:6] | passed | 0.00012 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:7] | passed | 0.00011 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:8] | passed | 0.00012 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:9] | passed | 0.00012 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:16:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:16:2] | passed | 0.00007 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:16:3] | passed | 0.00005 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:17:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:17:2] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:18:1] | passed | 0.00003 seconds | diff --git a/src/sam/gems/uc3-dmp-id/Gemfile.lock b/src/sam/gems/uc3-dmp-id/Gemfile.lock index ef6dfe23..2b05295e 100644 --- a/src/sam/gems/uc3-dmp-id/Gemfile.lock +++ b/src/sam/gems/uc3-dmp-id/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - uc3-dmp-id (0.0.123) + uc3-dmp-id (0.0.140) json (~> 2.6) json-schema (~> 3.0) uc3-dmp-dynamo (~> 0.0) diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb index f18e3bec..c3d7bb92 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb @@ -21,6 +21,9 @@ class Helper DMP_LATEST_VERSION = "#{SK_DMP_PREFIX}latest" DMP_TOMBSTONE_VERSION = "#{SK_DMP_PREFIX}tombstone" + DEFAULT_API_URL = 'https://api.dmphub.uc3dev.cdlib.net/dmps/' + DEFAULT_LANDING_PAGE_URL = 'https://dmphub.uc3dev.cdlib.net/dmps/' + class << self # Append the PK prefix for the object # ------------------------------------------------------------------------------------- @@ -48,19 +51,13 @@ def remove_sk_prefix(s_key:) # Return the base URL for a DMP ID def dmp_id_base_url - url = ENV.fetch('DMP_ID_BASE_URL', 'https://dmphub.uc3dev.cdlib.net/dmps/') + url = ENV.fetch('DMP_ID_BASE_URL', DEFAULT_LANDING_PAGE_URL) url&.end_with?('/') ? url : "#{url}/" end # The landing page URL (NOT the DOI URL) def landing_page_url - url = ENV.fetch('DMP_ID_LANDING_URL', 'https://dmphub.uc3dev.cdlib.net/dmps/') - url&.end_with?('/') ? url : "#{url}/" - end - - # Return the base URL for the API - def api_base_url - url = ENV.fetch('DMP_ID_BASE_URL', 'https://api.dmphub.uc3dev.cdlib.net/dmps/') + url = ENV.fetch('DMP_ID_LANDING_URL', DEFAULT_LANDING_PAGE_URL) url&.end_with?('/') ? url : "#{url}/" end @@ -69,7 +66,7 @@ def format_dmp_id(value:, with_protocol: false) dmp_id = value.match(DOI_REGEX).to_s return nil if dmp_id.nil? || dmp_id == '' # If it's already a URL, return it as is - return value if value.start_with?('http') + return with_protocol ? value : value.gsub(%r{https?://}, '') if value.start_with?('http') dmp_id = dmp_id.gsub('doi:', '') dmp_id = dmp_id.start_with?('/') ? dmp_id[1..dmp_id.length] : dmp_id @@ -90,7 +87,7 @@ def path_parameter_to_pk(param:) # Append the :PK prefix to the :dmp_id def dmp_id_to_pk(json:) - return nil if json.nil? || json['identifier'].nil? + return nil if !json.is_a?(Hash) || json['identifier'].nil? # If it's a DOI format it correctly dmp_id = format_dmp_id(value: json['identifier'].to_s) @@ -132,9 +129,9 @@ def eql?(dmp_a:, dmp_b:) b = deep_copy_dmp(obj: dmp_b) # ignore some of the attributes before comparing - %w[SK dmphub_modification_day modified created dmphub_assertions].each do |key| - a['dmp'].delete(key) unless a['dmp'][key].nil? - b['dmp'].delete(key) unless b['dmp'][key].nil? + %w[SK dmphub_modification_day modified created dmphub_versions].each do |key| + a['dmp'].delete(key) unless a['dmp'][key].nil? + b['dmp'].delete(key) unless b['dmp'][key].nil? end a == b end @@ -147,7 +144,7 @@ def extract_owner_id(json: {}) id = dmp.fetch('contact', {}).fetch('contact_id', {})['identifier'] return id unless id.nil? - dmp.fetch('contributor', []).map { |contributor| contributor.fetch('contact_id', {})['identifier'] }.first + dmp.fetch('contributor', []).map { |contributor| contributor.fetch('contributor_id', {})['identifier'] }.first end # Extract the Contact's affiliaiton ROR ID @@ -179,7 +176,7 @@ def annotate_dmp_json(provenance:, p_key:, json:) annotated['SK'] = DMP_LATEST_VERSION # Ensure that the :dmp_id matches the :PK - annotated['dmp_id'] = pk_to_dmp_id(p_key: remove_pk_prefix(p_key: annotated['PK'])) + annotated['dmp_id'] = JSON.parse(pk_to_dmp_id(p_key: remove_pk_prefix(p_key: annotated['PK'])).to_json) owner_id = extract_owner_id(json: json) owner_org = extract_owner_org(json: json) diff --git a/src/sam/layers/Gemfile b/src/sam/layers/Gemfile index 49336c83..569b0fb8 100644 --- a/src/sam/layers/Gemfile +++ b/src/sam/layers/Gemfile @@ -25,6 +25,7 @@ gem 'citeproc-ruby' # Unfortunately you cannot include gems by path here because AWS doesn't have access when it builds the layer # so the gems must exist in RubyGems gem 'uc3-dmp-api-core' +gem 'uc3-dmp-citation' gem 'uc3-dmp-cloudwatch' gem 'uc3-dmp-external-api' gem 'uc3-dmp-id' diff --git a/src/sam/layers/Gemfile.lock b/src/sam/layers/Gemfile.lock index 50c3b106..96913d79 100644 --- a/src/sam/layers/Gemfile.lock +++ b/src/sam/layers/Gemfile.lock @@ -6,7 +6,7 @@ GEM ast (2.4.2) aws-eventstream (1.2.0) aws-partitions (1.805.0) - aws-sdk-cognitoidentityprovider (1.79.0) + aws-sdk-cognitoidentityprovider (1.80.0) aws-sdk-core (~> 3, >= 3.177.0) aws-sigv4 (~> 1.1) aws-sdk-core (3.180.3) @@ -23,7 +23,7 @@ GEM aws-sdk-kms (1.71.0) aws-sdk-core (~> 3, >= 3.177.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.132.0) + aws-sdk-s3 (1.132.1) aws-sdk-core (~> 3, >= 3.179.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.6) @@ -85,7 +85,8 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.12.0) rspec-support (3.12.1) - rubocop (1.55.1) + rubocop (1.56.0) + base64 (~> 0.1.1) json (~> 2.3) language_server-protocol (>= 3.17.0) parallel (~> 1.10) @@ -102,7 +103,7 @@ GEM rubocop (~> 1.41) rubocop-factory_bot (2.23.1) rubocop (~> 1.33) - rubocop-rspec (2.23.0) + rubocop-rspec (2.23.2) rubocop (~> 1.33) rubocop-capybara (~> 2.17) rubocop-factory_bot (~> 2.22) @@ -112,6 +113,7 @@ GEM aws-sdk-ssm (~> 1.150) json (~> 2.6) logger (~> 1.4) + uc3-dmp-citation (0.0.5) uc3-dmp-cloudwatch (0.0.3) uc3-dmp-cognito (0.0.6) aws-sdk-cognitoidentityprovider (~> 1.73) @@ -163,6 +165,7 @@ DEPENDENCIES rubocop rubocop-rspec uc3-dmp-api-core + uc3-dmp-citation uc3-dmp-cloudwatch uc3-dmp-external-api uc3-dmp-id diff --git a/src/sam/template.yaml b/src/sam/template.yaml index 04d7b84c..c24c9867 100644 --- a/src/sam/template.yaml +++ b/src/sam/template.yaml @@ -344,7 +344,7 @@ Resources: CodeUri: 'functions/delete_dmp/' Handler: 'app.Functions::DeleteDmp.process' Runtime: 'ruby2.7' - Timeout: 5 + Timeout: 15 Architectures: - 'x86_64' Layers: @@ -638,6 +638,56 @@ Resources: # Explicit retention time RetentionInDays: !Ref LogRetentionDays + # Get Citations for the DOIs + PostCitationsFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: 'functions/post_citations/' + Handler: 'app.Functions::PostCitations.process' + Runtime: 'ruby2.7' + Timeout: 60 + Architectures: + - 'x86_64' + Layers: + - !Ref LambdaLayer + Policies: + - 'arn:aws:iam::aws:policy/AmazonSNSFullAccess' + - 'arn:aws:iam::aws:policy/AmazonSQSFullAccess' + DeadLetterQueue: + Type: 'SQS' + TargetArn: !Ref DeadLetterQueueArn + Environment: + Variables: + LAMBDA_ENV: !Ref Env + LOG_LEVEL: 'debug' # !Ref DebugLevel # 'debug' + DOMAIN: !Ref DomainName + SNS_FATAL_ERROR_TOPIC: !Ref SnsEmailTopicArn + CORS_ORIGIN: !If + - IsDev + - "*" + - !Sub "https://${DomainName}" + Events: + PostCitations: + # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api + Type: 'Api' + Properties: + RestApiId: !Ref DmpHubRestApi + Path: '/citations' + Method: 'post' + RequestParameters: + - 'method.request.header.Authorization' + Auth: + Authorizer: 'DmpHubCognitoAuthorizer' + AuthorizationScopes: + - !Sub 'https://auth.${DomainName}/${Env}.read' + + PostCitationsLogGroup: + Type: 'AWS::Logs::LogGroup' + Properties: + LogGroupName: !Sub "/aws/lambda/${PostCitationsFunction}" + # Explicit retention time + RetentionInDays: !Ref LogRetentionDays + # Register a new DMP ID PostDmpsFunction: Type: 'AWS::Serverless::Function' @@ -645,7 +695,7 @@ Resources: CodeUri: 'functions/post_dmps/' Handler: 'app.Functions::PostDmps.process' Runtime: 'ruby2.7' - Timeout: 5 + Timeout: 15 Architectures: - 'x86_64' Layers: @@ -702,7 +752,7 @@ Resources: CodeUri: 'functions/post_narratives/' Handler: 'app.Functions::PostNarratives.process' Runtime: 'ruby2.7' - Timeout: 5 + Timeout: 15 Architectures: - 'x86_64' Layers: @@ -756,7 +806,7 @@ Resources: CodeUri: 'functions/put_dmp/' Handler: 'app.Functions::PutDmp.process' Runtime: 'ruby2.7' - Timeout: 10 + Timeout: 30 Architectures: - 'x86_64' Layers: From 5628b0085e215e6ecd43ad2c6df2300137ebacb4 Mon Sep 17 00:00:00 2001 From: briri Date: Sun, 20 Aug 2023 09:08:41 -0700 Subject: [PATCH 05/13] added spec tests for uc3-dmp-id gem's versioner and validator --- src/sam/gems/uc3-dmp-id/.rspec_status | 157 ++++++---- .../uc3-dmp-id/lib/uc3-dmp-id/validator.rb | 4 + src/sam/gems/uc3-dmp-id/spec/spec_helper.rb | 1 + .../spec/support/json_mocks/complete.json | 286 ++++++++++++++++++ .../spec/support/json_mocks/minimal.json | 79 +++++ .../gems/uc3-dmp-id/spec/support/shared.rb | 32 +- .../uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb | 25 +- .../spec/uc3-dmp-id/validator_spec.rb | 123 ++++++++ .../spec/uc3-dmp-id/versioner_spec.rb | 94 ++++++ 9 files changed, 708 insertions(+), 93 deletions(-) create mode 100644 src/sam/gems/uc3-dmp-id/spec/support/json_mocks/complete.json create mode 100644 src/sam/gems/uc3-dmp-id/spec/support/json_mocks/minimal.json create mode 100644 src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/validator_spec.rb create mode 100644 src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/versioner_spec.rb diff --git a/src/sam/gems/uc3-dmp-id/.rspec_status b/src/sam/gems/uc3-dmp-id/.rspec_status index da221d5b..07c35e34 100644 --- a/src/sam/gems/uc3-dmp-id/.rspec_status +++ b/src/sam/gems/uc3-dmp-id/.rspec_status @@ -1,65 +1,92 @@ -example_id | status | run_time | ----------------------------------------- | ------ | --------------- | -./spec/uc3-dmp-id/helper_spec.rb[1:1:1] | passed | 0.00043 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:1:2] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:2:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:2:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:3:1] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:3:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:4:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:4:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:5:1] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:5:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:5:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:6:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:6:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:6:3] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:4] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:5] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:6] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:7] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:8:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:8:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:8:3] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:8:4] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:9:1] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:9:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:9:3] | passed | 0.00484 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:9:4] | passed | 0.00008 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:10:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:10:2] | passed | 0.00008 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:11:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:11:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:11:3] | passed | 0.00064 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:11:4] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:1] | passed | 0.00005 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:2] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:4] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:5] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:6] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:7] | passed | 0.00005 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:13:1] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:13:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:13:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:14:1] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:14:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:14:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:1] | passed | 0.00008 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:2] | passed | 0.00007 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:3] | passed | 0.0001 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:4] | passed | 0.00006 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:5] | passed | 0.00055 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:6] | passed | 0.00012 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:7] | passed | 0.00011 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:8] | passed | 0.00012 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:9] | passed | 0.00012 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:16:1] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:16:2] | passed | 0.00007 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:16:3] | passed | 0.00005 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:17:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:17:2] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:18:1] | passed | 0.00003 seconds | +example_id | status | run_time | +-------------------------------------------- | ------ | --------------- | +./spec/uc3-dmp-id/helper_spec.rb[1:1:1] | passed | 0.00043 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:1:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:2:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:2:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:3:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:3:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:4:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:4:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:5:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:5:2] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:5:3] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:6:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:6:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:6:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:3] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:4] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:5] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:6] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:7] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:4] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:3] | passed | 0.00482 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:4] | passed | 0.00008 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:10:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:10:2] | passed | 0.00008 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:3] | passed | 0.00063 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:4] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:1] | passed | 0.00005 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:4] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:5] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:6] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:7] | passed | 0.00006 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:13:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:13:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:13:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:14:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:14:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:14:3] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:1] | passed | 0.00009 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:2] | passed | 0.00008 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:3] | passed | 0.0001 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:4] | passed | 0.00008 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:5] | passed | 0.00055 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:6] | passed | 0.00013 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:7] | passed | 0.00011 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:8] | passed | 0.00012 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:9] | passed | 0.00013 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:16:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:16:2] | passed | 0.00006 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:16:3] | passed | 0.00005 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:17:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:17:2] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:18:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:1] | passed | 0.00592 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:2] | passed | 0.00041 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:3] | passed | 0.00046 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:4] | passed | 0.00043 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:5] | passed | 0.00883 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:6] | passed | 0.00091 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:2:1:1] | passed | 0.00076 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:2:1:2] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:2:1:3] | passed | 0.00102 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:1] | passed | 0.00295 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:2] | passed | 0.00072 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:3] | passed | 0.00079 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:1] | passed | 0.0081 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:2] | passed | 0.00696 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:3] | passed | 0.00728 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:1:1] | passed | 0.00756 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:1:2] | passed | 0.0019 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:1] | passed | 0.00322 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:2] | passed | 0.00308 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:3] | passed | 0.00294 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:4] | passed | 0.00421 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:5] | passed | 0.00246 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:1] | passed | 0.00144 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:2] | passed | 0.00149 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:3] | passed | 0.00142 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:4] | passed | 0.00124 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:5] | passed | 0.00126 seconds | diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/validator.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/validator.rb index 45d948de..eb06cd0c 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/validator.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/validator.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'json-schema' + module Uc3DmpId class Uc3DmpIdValidatorError < StandardError; end @@ -47,6 +49,8 @@ def _load_schema(mode:) # Instatiate the matching schema schema = "Uc3DmpId::Schemas::#{mode.to_s.downcase.capitalize}".split('::').inject(Object) { |o,c| o.const_get c } schema.respond_to?(:load) ? schema.load : nil + rescue NameError + nil end end diff --git a/src/sam/gems/uc3-dmp-id/spec/spec_helper.rb b/src/sam/gems/uc3-dmp-id/spec/spec_helper.rb index c3c8be43..70b4124e 100644 --- a/src/sam/gems/uc3-dmp-id/spec/spec_helper.rb +++ b/src/sam/gems/uc3-dmp-id/spec/spec_helper.rb @@ -4,6 +4,7 @@ require 'ostruct' Dir["#{Dir.getwd}/lib/uc3-dmp-id/*.rb"].sort.each { |f| require f } +Dir["#{Dir.getwd}/lib/uc3-dmp-id/schemas/*.rb"].sort.each { |f| require f } require_relative 'support/shared' diff --git a/src/sam/gems/uc3-dmp-id/spec/support/json_mocks/complete.json b/src/sam/gems/uc3-dmp-id/spec/support/json_mocks/complete.json new file mode 100644 index 00000000..645f0ee3 --- /dev/null +++ b/src/sam/gems/uc3-dmp-id/spec/support/json_mocks/complete.json @@ -0,0 +1,286 @@ +{ + "dmp": { + "title": "Example DMP", + "description": "An exceptional example of complete DMP metadata", + "created": "2021-11-08T19:06:04Z", + "modified": "2022-01-28T17:52:14Z", + "ethical_issues_description": "We may need to anonymize user data", + "ethical_issues_exist": "yes", + "ethical_issues_report": "https://example.edu/privacy_policy", + "dmp_id": { + "type": "doi", + "identifier": "https://doi.org/10.12345/ABCDEFG" + }, + "contact": { + "name": "Jane Doe", + "mbox": "jane.doe@example.edu", + "dmproadmap_affiliation": { + "name": "Example University (example.edu)", + "affiliation_id": { + "type": "ror", + "identifier": "https://ror.org/1234567890" + } + }, + "contact_id": { + "type": "orcid", + "identifier": "https://orcid.org/0000-0000-0000-000X" + } + }, + "contributor": [ + { + "name": "Jane Doe", + "mbox": "jane.doe@example.edu", + "role": [ + "http://credit.niso.org/contributor-roles/data-curation", + "http://credit.niso.org/contributor-roles/investigation" + ], + "dmproadmap_affiliation": { + "name": "Example University (example.edu)", + "affiliation_id": { + "type": "ror", + "identifier": "https://ror.org/1234567890" + } + }, + "contributor_id": { + "type": "orcid", + "identifier": "https://orcid.org/0000-0000-0000-000X" + } + }, { + "name":"Jennifer Smith", + "role": [ + "http://credit.niso.org/contributor-roles/investigation" + ], + "dmproadmap_affiliation": { + "name": "University of Somewhere (somwhere.edu)", + "affiliation_id": { + "type": "ror", + "identifier": "https://ror.org/0987654321" + } + } + }, { + "name": "Sarah James", + "role": [ + "http://credit.niso.org/contributor-roles/project_administration" + ] + } + ], + "cost": [ + { + "currency_code": "USD", + "title": "Preservation costs", + "description": "The estimated costs for preserving our data for 20 years", + "value": 10000 + } + ], + "dataset": [ + { + "type": "dataset", + "title": "Odds and ends", + "description": "Collection of odds and ends", + "issued": "2022-03-15T00:00:00Z", + "keyword": [ + "foo" + ], + "dataset_id": { + "type": "doi", + "identifier": "http://doi.org/10.99999/8888.7777" + }, + "language": "eng", + "metadata": [ + { + "description": "The industry standard!", + "language": "eng", + "metadata_standard_id": { + "type": "url", + "identifier": "https://example.com/metadata_standards/123" + } + } + ], + "personal_data": "no", + "data_quality_assurance": [ + "We will ensure that the preserved copies are of high quality" + ], + "preservation_statement": "We are going to preserve this data for 20 years", + "security_and_privacy": [ + { + "title": "Data security", + "description": "We're going to encrypt this one." + } + ], + "sensitive_data": "yes", + "technical_resource": [ + { + "name": "Elctron microscope 1234", + "description": "A super electron microscope", + "dmproadmap_technical_resource_id": { + "type": "url", + "identifier": "https://rrids.example.org/98765" + } + } + ], + "distribution": [ + { + "title": "Distribution of 'Odds and Ends' to 'Random repo'", + "access_url": "https://example.edu/datasets/00000", + "download_url": "https://example.edu/datasets/00000.pdf", + "available_until": "2052-03-15", + "byte_size": 1234567890, + "data_access": "shared", + "format": [ + "application/vnd.ms-excel" + ], + "host": { + "title": "Random repo", + "url": "A generic data repository", + "dmproadmap_host_id": { + "type": "url", + "identifier": "https://hosts.example.org/765675" + } + }, + "license": [ + { + "license_ref": "https://licenses.example.org/zyxw", + "start_date": "2022-03-15T00:00:00Z" + } + ] + } + ] + } + ], + "dmphub_modifications": [ + { + "id": "ZYXW9876", + "provenance": "datacite", + "timestamp": "2023-07-27T15:08:32+07:00", + "note": "data received from event data", + "status": "pending", + "dmproadmap_related_identifier": { + "work_type": "dataset", + "descriptor": "references", + "type": "doi", + "identifier": "https://dx.doi.org/77.6666/H5H5H5" + } + }, { + "id": "ZYXW9878", + "provenance": "datacite", + "timestamp": "2023-07-27T15:08:52+07:00", + "note": "data received from event data", + "status": "accepted", + "dmproadmap_related_identifier": { + "work_type": "article", + "descriptor": "is_cited_by", + "type": "url", + "identifier": "https://doi.org/22.33333/pubmed.1242345234" + } + }, { + "id": "ZYXW9880", + "provenance": "datacite", + "timestamp": "2023-07-27T15:09:13+07:00", + "note": "data received from event data", + "status": "rejected", + "dmproadmap_related_identifier": { + "work_type": "paper", + "descriptor": "is_cited_by", + "type": "url", + "identifier": "https://academic.site/papers/123" + } + }, + { + "id": "ABCD1234", + "provenance": "crossref", + "timestamp": "2023-07-13T09:25:23+07:00", + "note": "data received from the Crossref Grants API", + "status": "pending", + "funding": { + "name": "National Science Foundation", + "funder_id": { + "type": "ror", + "identifier": "https://ror.org/021nxhr62" + }, + "funding_status": "granted", + "grant_id": { + "identifier": "https://doi.org/11.1111/2019.22702-3", + "type": "doi" + } + } + } + ], + "dmphub_versions": [ + { + "timestamp": "2020-03-14T10:53:49+00:00", + "url": "https://doi.org/dmps/10.12345/ABCD1234?version=2020-03-14T10:53:49+00:00" + }, + { + "timestamp": "2019-03-13T13:13:00+00:00", + "url": "https://doi.org/dmps/doi.org/10.12345/ABCD1234?version=2019-03-13T13:13:00+00:00" + } + ], + "dmproadmap_privacy": "public", + "dmproadmap_related_identifiers": [ + { + "work_type": "article", + "identifier": "https://doi.org/10.1371/journal.pcbi.1006750", + "descriptor": "is_cited_by", + "type": "handle" + }, + { + "work_type": "article", + "descriptor": "is_cited_by", + "type": "url", + "identifier": "https://doi.org/22.33333/pubmed.1242345234" + }, + { + "descriptor": "is_metadata_for", + "identifier": "https://api.dmphub.uc3dev.cdlib.net/narratives/1234567890.pdf", + "type": "url", + "work_type": "output_management_plan" + } + ], + "dmproadmap_research_facilities": [ + { + "name": "Example Research Lab", + "type": "field_station", + "facility_id": { + "type": "ror", + "identifier": "https://ror.org/03yrm5c26" + } + } + ], + "dmproadmap_template": { + "id": "38745623765", + "title": "Digital Curation Centre (português)" + }, + "language": "eng", + "project": [ + { + "title": "Example research project", + "description": "Abstract of what we're going to do.", + "start": "2015-05-12T00:00:00Z", + "end": "2024-05-24T11:32:21-07:00", + "funding": [ + { + "name": "National Funding Organization", + "funder_id": { + "type": "fundref", + "identifier": "http://dx.doi.org/10.13039/100005595" + }, + "funding_status": "granted", + "grant_id": { + "type": "url", + "identifier": "https://nfo.example.org/awards/098765" + }, + "dmproadmap_funded_affiliations": [ + { + "name": "Example University (example.edu)", + "affiliation_id": { + "type": "ror", + "identifier": "https://ror.org/1234567890" + } + } + ] + } + ] + } + ] + } +} diff --git a/src/sam/gems/uc3-dmp-id/spec/support/json_mocks/minimal.json b/src/sam/gems/uc3-dmp-id/spec/support/json_mocks/minimal.json new file mode 100644 index 00000000..21fed909 --- /dev/null +++ b/src/sam/gems/uc3-dmp-id/spec/support/json_mocks/minimal.json @@ -0,0 +1,79 @@ +{ + "author": { + "dmp": { + "title": "Example DMP", + "created": "2022-05-24T12:33:44Z", + "modified": "2022-05-24T12:33:44Z", + "contact": { + "name": "jane doe", + "mbox": "jane@example.edu", + "contact_id": { + "type": "orcid", + "identifier": "https://orcid.org/0000-0000-0000-000X" + } + }, + "dmp_id": { + "type": "doi", + "identifier": "https://doi.org/10.12345/ABCDEFG" + }, + "dataset": [], + "project": [] + } + }, + "amend-related_identifiers": { + "dmp": { + "PK": "DMP#1234567890", + "dmphub_provenance_id": "PROVENANCE#abcdefghijk", + "title": "Example DMP", + "modified": "2022-05-26T12:12:00Z", + "dmp_id": { + "type": "doi", + "identifier": "doi:10.12345/abcd1234" + }, + "dmproadmap_related_identifiers": [ + { + "type": "url", + "identifier": "http://example.edu/datasets/12345", + "descriptor": "describes", + "work_type": "dataset" + } + ] + } + }, + "amend-funding": { + "dmp": { + "PK": "DMP#1234567890", + "dmphub_provenance_id": "PROVENANCE#abcdefghijk", + "title": "Example DMP", + "modified": "2022-05-26T12:12:00Z", + "dmp_id": { + "type": "doi", + "identifier": "doi:10.12345/abcd1234" + }, + "project": [ + { + "funding": [ + { + "funder_id": { + "type": "fundref", + "identifier": "http://dx.doi.org/10.13039/100005595" + }, + "funding_status": "planned" + } + ] + } + ] + } + }, + "delete": { + "dmp": { + "PK": "DMP#1234567890", + "dmphub_provenance_id": "PROVENANCE#abcdefghijk", + "title": "Example DMP", + "dmp_id": { + "type": "doi", + "identifier": "doi:10.12345/abcd1234" + } + } + } +} diff --git a/src/sam/gems/uc3-dmp-id/spec/support/shared.rb b/src/sam/gems/uc3-dmp-id/spec/support/shared.rb index c31a1367..026db81f 100644 --- a/src/sam/gems/uc3-dmp-id/spec/support/shared.rb +++ b/src/sam/gems/uc3-dmp-id/spec/support/shared.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true require 'ostruct' - -require 'ostruct' +require 'securerandom' # Mock S3 Resources Uc3DmpDynamoClient = Struct.new('S3Client', :get_item, :put_item, :delete_item, :query) @@ -15,31 +14,38 @@ def mock_uc3_dmp_dynamo(success: true) allow(client).to receive(:query).and_return(success ? [mock_dmp] : nil) allow(Uc3DmpDynamo::Client).to receive(:new).and_return(client) + client end -def mock_dmp - JSON.parse({ - dmp: { +def mock_dmp_id + domain = ENV.fetch('DMP_ID_BASE_URL', 'doi.org').gsub(%r{https?://}, '') + "#{domain}/#{rand(10...99)}.#{rand(10000...99999)}/#{SecureRandom.hex(6)}" +end - } - }.to_json) +def mock_dmp(minimal: false) + JSON.parse(File.read("#{Dir.pwd}/spec/support/json_mocks/#{minimal ? 'minimal' : 'complete'}.json")) end # Helper to compare 2 hashes -def assert_dmps_match(obj_a: {}, obj_b: {}) +def assert_dmps_match(obj_a: {}, obj_b: {}, debug: false) pass = obj_a.class.name == obj_b.class.name - # puts "::::: #{obj_a.class.name}" - # pp obj_a - # p '-----------' - # pp obj_b + if debug + puts 'Debug from spec/support/shared.rb - assert_dmps_match' + pp obj_a + p '-----------' + pp obj_b + end if pass case obj_a.class.name when 'Array' - pass = false unless obj_a.length == obj_b.length + return false unless obj_a.length == obj_b.length + obj_a.each { |entry| pass = false unless obj_b.include?(entry) } when 'Hash' + return false unless obj_a.keys.sort { |a, b| a <=> b } == obj_b.keys.sort { |a, b| a <=> b } + obj_a.each_pair do |key, value| if %w[Array, Hash].include?(value.class.name) pass = assert_dmps_match(obj_a: value, obj_b: obj_b.nil? ? nil : obj_b[key.to_s]) diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb index 47e683b7..2f8767bd 100644 --- a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb @@ -5,11 +5,6 @@ RSpec.describe 'Uc3DmpId::Helper' do let!(:described_class) { Uc3DmpId::Helper } - before do - # mock_uc3_dmp_dynamo - # allow(described_class).to receive(:puts).and_return(true) - end - describe 'append_pk_prefix(p_key:)' do it 'appends the prefix' do key = 'foo/bar' @@ -346,7 +341,7 @@ dmphub_provenance_id: provenance['PK'] }.to_json) result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) - expect(assert_dmps_match(obj_a: result, obj_b: expected)).to be(true) + expect(assert_dmps_match(obj_a: result, obj_b: expected, debug: false)).to be(true) end it 'properly translates :dmproadmap_featured' do dmp['dmproadmap_featured'] = 'yes' @@ -363,7 +358,7 @@ dmphub_provenance_id: provenance['PK'] }.to_json) result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) - expect(assert_dmps_match(obj_a: result, obj_b: expected)).to be(true) + expect(assert_dmps_match(obj_a: result, obj_b: expected, debug: false)).to be(true) end it 'adds the expected JSON if :dmphub_provenance_identifier if not defined in the :json' do dmp['dmproadmap_featured'] = 1 @@ -383,7 +378,7 @@ dmphub_provenance_identifier: 'http://foo.bar/dmp/123' }.to_json) result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) - expect(assert_dmps_match(obj_a: expected, obj_b: result)).to be(true) + expect(assert_dmps_match(obj_a: expected, obj_b: result, debug: false)).to be(true) end it 'retains the DMP ID specified if the provenance is :seedingWithLiveDmpIds' do provenance[:seedingWithLiveDmpIds] = true @@ -399,11 +394,10 @@ dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), dmphub_owner_id: 'orcid123', dmphub_owner_org: 'ror123', - dmphub_provenance_id: provenance['PK'], - dmphub_provenance_identifier: 'http://foo.bar/dmp/123' + dmphub_provenance_id: provenance['PK'] }.to_json) result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) - expect(assert_dmps_match(obj_a: result, obj_b: expected)).to be(true) + expect(assert_dmps_match(obj_a: result, obj_b: expected, debug: false)).to be(true) end it 'does NOT retain the specified DMP ID if the provenance is not :seedingWithLiveDmpIds' do dmp['dmp_id'] = JSON.parse({ type: 'url', identifier: 'http://foo.bar/dmp/123' }.to_json) @@ -422,7 +416,7 @@ dmphub_provenance_identifier: 'http://foo.bar/dmp/123' }.to_json) result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) - expect(assert_dmps_match(obj_a: expected, obj_b: result)).to be(true) + expect(assert_dmps_match(obj_a: expected, obj_b: result, debug: false)).to be(true) end end @@ -462,7 +456,7 @@ }.to_json) result = described_class.cleanse_dmp_json(json: dmp) - expect(assert_dmps_match(obj_a: expected, obj_b: result)).to be(true) + expect(assert_dmps_match(obj_a: expected, obj_b: result, debug: false)).to be(true) end end @@ -483,7 +477,8 @@ ] }.to_json) expected = dmp['dmproadmap_related_identifiers'].reject { |id| id['identifier'] == 'http://skip.me' } - expect(assert_dmps_match(obj_a: expected, obj_b: described_class.citable_related_identifiers(dmp: dmp))).to be(true) + result = described_class.citable_related_identifiers(dmp: dmp) + expect(assert_dmps_match(obj_a: expected, obj_b: result, debug: false)).to be(true) end end @@ -499,7 +494,7 @@ } } }.to_json) - expect(assert_dmps_match(obj_a: described_class.deep_copy_dmp(obj: obj), obj_b: obj)).to be(true) + expect(assert_dmps_match(obj_a: described_class.deep_copy_dmp(obj: obj), obj_b: obj, debug: false)).to be(true) end end end diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/validator_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/validator_spec.rb new file mode 100644 index 00000000..ab6970cb --- /dev/null +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/validator_spec.rb @@ -0,0 +1,123 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Uc3DmpId::Validator' do + let!(:described_class) { Uc3DmpId::Validator } + + describe 'validate(mode:, json:)' do + let!(:mode) { 'author' } + let!(:json) { { foo: 'bar' } } + let!(:expected_error) { [described_class::MSG_EMPTY_JSON] } + let!(:schema) do + { + type: 'object', + properties: { + foo: { + '$id': '#/properties/foo', + type: 'string' + } + }, + required: [ + 'foo' + ] + } + end + + before do + allow(described_class).to receive(:_load_schema).and_return(mock_dmp(minimal: false)) + end + + it 'returns the appropriate error when no :mode is specified' do + result = described_class.validate(mode: nil, json: json) + expect(assert_dmps_match(obj_a: result, obj_b: expected_error, debug: false)).to be(true) + end + + it 'returns the appropriate error when an invalid :mode is specified' do + result = described_class.validate(mode: 'foo', json: json) + expect(assert_dmps_match(obj_a: result, obj_b: expected_error, debug: false)).to be(true) + end + + it 'returns the appropriate error when parse_json returns a nil' do + allow(Uc3DmpId::Helper).to receive(:parse_json).and_return(nil) + result = described_class.validate(mode: mode, json: {}) + expect(assert_dmps_match(obj_a: result, obj_b: expected_error, debug: false)).to be(true) + end + + it 'returns the appropriate error when load_schema returns a nil' do + allow(described_class).to receive(:_load_schema).and_return(nil) + expected_error = [described_class::MSG_NO_SCHEMA] + result = described_class.validate(mode: mode, json: json) + expect(assert_dmps_match(obj_a: result, obj_b: expected_error, debug: false)).to be(true) + end + + it 'returns the appropriate error if the :json is NOT valid' do + json = { bar: 'foo' } + allow(described_class).to receive(:_load_schema).and_return(schema) + result = described_class.validate(mode: mode, json: json) + expect(result.last.include?('did not contain a required property of \'foo\'')).to be(true) + end + + it 'returns the appropriate error if the :json is valid' do + allow(described_class).to receive(:load_schema).and_return(schema) + json = { foo: 'bar' } + result = described_class.validate(mode: mode, json: json) + expected_error = [] + expect(assert_dmps_match(obj_a: result, obj_b: expected_error, debug: false)).to be(true) + end + end + + describe 'private methods' do + describe '_load_schema(mode:)' do + it 'returns nil if :mode is not provided' do + expect(described_class._load_schema(mode: nil)).to be_nil + end + + it 'returns nil if :mode is not a valid mode' do + expect(described_class._load_schema(mode: 'foo')).to be_nil + end + + it 'returns the JSON schema' do + schema = described_class._load_schema(mode: :author) + expected = 'https://github.com/CDLUC3/dmp-hub-sam/layer/ruby/config/schemas/author.json' + expect(schema['$id']).to eql(expected) + + schema = described_class._load_schema(mode: :amend) + expected = 'https://github.com/CDLUC3/dmp-hub-sam/layer/ruby/config/schemas/amend.json' + expect(schema['$id']).to eql(expected) + end + end + end + + describe 'Ensure our JSON Schemas are working as expected' do + # The following tests are used to validate the JSON schema documents to ensure + # that a minimal metadata record and a complete metadata record are valid + describe 'spec/support/json_mocks/minimal.json' do + let!(:json) { mock_dmp(minimal: true) } + + it 'minimal author metadata is valid' do + expect(described_class.validate(mode: 'author', json: json['author'])).to eql([]) + end + + it 'minimal amend - related_identifiers metadata is valid' do + expect(described_class.validate(mode: 'amend', json: json['amend-related_identifiers'])).to eql([]) + end + + it 'minimal amend - funding metadata is valid' do + expect(described_class.validate(mode: 'amend', json: json['amend-funding'])).to eql([]) + end + end + + describe 'spec/support/json_mocks/complete.json' do + let!(:json) { mock_dmp(minimal: false) } + + # The complete JSON should pass for all modes + Uc3DmpId::Validator::VALIDATION_MODES.each do |mode| + it "is valid for mode #{mode}" do + response = described_class.validate(mode: 'author', json: json) + expect(described_class.validate(mode: 'author', json: json)).to eql([]) + end + end + end + end +end diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/versioner_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/versioner_spec.rb new file mode 100644 index 00000000..88a03270 --- /dev/null +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/versioner_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Uc3DmpId::Versioner' do + let!(:described_class) { Uc3DmpId::Versioner } + + let!(:p_key) { "#{Uc3DmpId::Helper::PK_DMP_PREFIX}#{mock_dmp_id}" } + let!(:landing_page_url) { 'http://foo.bar/dmps/' } + + before do + mock_uc3_dmp_dynamo + ENV['DMP_ID_BASE_URL'] = 'http://doi.org' + ENV['DMP_ID_LANDING_URL'] = landing_page_url + end + + describe 'get_versions(p_key:, client: nil, logger: nil)' do + it 'returns an empty array if p_key is not a valid String' do + expect(described_class.get_versions(p_key: 123)).to eql([]) + end + it 'fetches the :modified dates for each Dyanmo table entry for the PK' do + expect(described_class.get_versions(p_key: p_key)).to eql([mock_dmp]) + end + end + + describe 'generate_version(client:, latest_version:, owner:, updater:, logger: nil)' do + let!(:client) { mock_uc3_dmp_dynamo } + let!(:owner) { "PROVENANCE#foo" } + + it 'returns the :latest_version as-is if it could not determine the modified time' do + dmp = JSON.parse({ modified: '123A' }.to_json) + expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, updater: owner)).to eql(dmp) + end + it 'returns the :latest_version as-is if the owner of the DMP ID does not match the updater' do + dmp = JSON.parse({ modified: (Time.now - 7200).utc.iso8601 }.to_json) + updater = "PROVENANCE#bar" + expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, updater: updater)).to eql(dmp) + end + it 'returns the :latest_version as-is if the modified time of the latest version is within the past hour' do + dmp = JSON.parse({ modified: Time.now.utc.iso8601 }.to_json) + expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, updater: owner)).to eql(dmp) + end + it 'returns nil if it was unable to generate a version snapshot' do + allow(client).to receive(:put_item).and_return(nil) + dmp = JSON.parse({ modified: (Time.now - 72000).utc.iso8601 }.to_json) + expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, updater: owner)).to be(nil) + end + it 'generates the version snapshot and returns the :latest_version' do + tstamp = (Time.now - 72000).utc.iso8601 + dmp = JSON.parse({ modified: tstamp }.to_json) + version = JSON.parse({ SK: "#{Uc3DmpId::Helper::SK_DMP_PREFIX}#{tstamp}", modified: tstamp }.to_json) + expect(client).to receive(:put_item).with(json: version, logger: nil).once + expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, updater: owner)).to eql(dmp) + end + end + + describe 'append_versions(p_key:, dmp:, client: nil, logger: nil)' do + let!(:first_version) { JSON.parse({ modified: '2023-01-18T13:14:15Z' }.to_json) } + let!(:last_version) { JSON.parse({ modified: '2023-07-21T22:23:24Z' }.to_json) } + + let!(:dmp) { JSON.parse({ dmp: { title: 'Foo bar' } }.to_json) } + + it 'returns the :json as-is if the :p_key is not a valid String' do + expect(described_class.append_versions(p_key: 123, dmp: dmp)).to eql(dmp) + end + it 'returns the :json as-is if :json is not a Hash' do + expect(described_class.append_versions(p_key: p_key, dmp: '123')).to eql(123) + end + it 'returns the :json as-is if :json does not have a top level :dmp' do + json = JSON.parse({ title: 'Foo' }.to_json) + result = described_class.append_versions(p_key: p_key, dmp: json) + expect(assert_dmps_match(obj_a: result, obj_b: json, debug: false)).to be(true) + end + it 'does NOT append the :dmphub_versions Array if there is only one version' do + allow(described_class).to receive(:get_versions).and_return([first_version]) + result = described_class.append_versions(p_key: p_key, dmp: dmp) + expect(assert_dmps_match(obj_a: result, obj_b: dmp, debug: false)).to be(true) + end + it 'appends the :dmphub_versions Array if there are multiple versions of the DMP ID' do + allow(described_class).to receive(:get_versions).and_return([first_version, last_version]) + result = described_class.append_versions(p_key: p_key, dmp: dmp) + expected = JSON.parse({ + dmp: { + title: 'Foo bar', + dmphub_versions: [ + { timestamp: first_version['modified'], url: "#{landing_page_url}#{p_key}?version=#{first_version['modified']}" }, + { timestamp: last_version['modified'], url: "#{landing_page_url}#{p_key}?version=#{last_version['modified']}" } + ] + } + }.to_json) + expect(assert_dmps_match(obj_a: result, obj_b: expected, debug: false)).to be(true) + end + end +end From 0ce44b8e4325428111bf918a9d42e0825ba3d90b Mon Sep 17 00:00:00 2001 From: briri Date: Mon, 21 Aug 2023 11:27:08 -0700 Subject: [PATCH 06/13] Added tests for uc3-dmp-id asserter class --- src/sam/gems/uc3-dmp-id/.rspec_status | 139 ++++---- .../uc3-dmp-id/lib/uc3-dmp-id/asserter.rb | 208 +++++++----- .../spec/support/json_mocks/complete.json | 45 ++- .../spec/uc3-dmp-id/asserter_spec.rb | 314 ++++++++++++++++++ 4 files changed, 548 insertions(+), 158 deletions(-) create mode 100644 src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/asserter_spec.rb diff --git a/src/sam/gems/uc3-dmp-id/.rspec_status b/src/sam/gems/uc3-dmp-id/.rspec_status index 07c35e34..9c75cd0c 100644 --- a/src/sam/gems/uc3-dmp-id/.rspec_status +++ b/src/sam/gems/uc3-dmp-id/.rspec_status @@ -1,92 +1,119 @@ example_id | status | run_time | -------------------------------------------- | ------ | --------------- | -./spec/uc3-dmp-id/helper_spec.rb[1:1:1] | passed | 0.00043 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:1:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:2:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:2:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:3:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:1] | passed | 0.00084 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:2] | passed | 0.00026 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:3] | passed | 0.00025 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:4] | passed | 0.00025 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:5] | passed | 0.0003 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:6] | passed | 0.00028 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:7] | passed | 0.00031 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:1] | passed | 0.00064 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:2] | passed | 0.00026 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:3] | passed | 0.00026 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:4] | passed | 0.00028 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:5] | passed | 0.00028 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:1] | passed | 0.00024 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:2] | passed | 0.00025 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:3] | passed | 0.00024 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:4] | passed | 0.00028 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:5] | passed | 0.00027 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:6] | passed | 0.00028 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:1] | passed | 0.00025 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:2] | passed | 0.00025 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:3] | passed | 0.00032 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:4] | passed | 0.00028 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:5] | passed | 0.00026 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:6] | passed | 0.0003 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:5:1] | passed | 0.00027 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:5:2] | passed | 0.00028 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:5:3] | passed | 0.0003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:1:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:1:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:2:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:2:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:3:1] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:3:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:4:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:4:1] | passed | 0.00134 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:4:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:5:1] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:5:2] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:5:3] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:5:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:5:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:5:3] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:6:1] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:6:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:6:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:6:3] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:1] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:7:2] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:7:3] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:7:4] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:5] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:5] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:7:6] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:7:7] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:8:1] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:8:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:8:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:3] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:8:4] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:9:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:9:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:9:3] | passed | 0.00482 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:9:4] | passed | 0.00008 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:10:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:3] | passed | 0.00492 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:4] | passed | 0.00009 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:10:1] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:10:2] | passed | 0.00008 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:11:1] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:11:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:11:3] | passed | 0.00063 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:11:4] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:3] | passed | 0.00064 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:4] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:1] | passed | 0.00005 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:2] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:4] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:4] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:5] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:6] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:7] | passed | 0.00006 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:13:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:7] | passed | 0.00005 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:13:1] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:13:2] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:13:3] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:14:1] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:14:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:14:3] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:1] | passed | 0.00009 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:2] | passed | 0.00008 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:14:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:1] | passed | 0.00008 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:2] | passed | 0.00007 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:3] | passed | 0.0001 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:4] | passed | 0.00008 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:5] | passed | 0.00055 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:6] | passed | 0.00013 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:5] | passed | 0.00012 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:6] | passed | 0.00011 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:7] | passed | 0.00011 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:8] | passed | 0.00012 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:9] | passed | 0.00013 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:8] | passed | 0.00011 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:9] | passed | 0.00012 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:16:1] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:16:2] | passed | 0.00006 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:16:3] | passed | 0.00005 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:17:1] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:17:2] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:18:1] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:1] | passed | 0.00592 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:2] | passed | 0.00041 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:3] | passed | 0.00046 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:4] | passed | 0.00043 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:5] | passed | 0.00883 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:6] | passed | 0.00091 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:2:1:1] | passed | 0.00076 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:2:1:2] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:2:1:3] | passed | 0.00102 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:1:1] | passed | 0.00295 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:1:2] | passed | 0.00072 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:1:3] | passed | 0.00079 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:2:1] | passed | 0.0081 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:2:2] | passed | 0.00696 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:2:3] | passed | 0.00728 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:1:1] | passed | 0.00756 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:1:2] | passed | 0.0019 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:1] | passed | 0.00322 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:2] | passed | 0.00308 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:3] | passed | 0.00294 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:4] | passed | 0.00421 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:5] | passed | 0.00246 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:1] | passed | 0.00144 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:2] | passed | 0.00149 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:3] | passed | 0.00142 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:4] | passed | 0.00124 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:5] | passed | 0.00126 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:1] | passed | 0.00032 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:2] | passed | 0.00029 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:3] | passed | 0.0003 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:4] | passed | 0.00029 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:5] | passed | 0.01068 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:6] | passed | 0.00079 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:2:1:1] | passed | 0.00073 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:2:1:2] | passed | 0.00005 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:2:1:3] | passed | 0.00064 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:1] | passed | 0.00336 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:2] | passed | 0.00077 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:3] | passed | 0.00062 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:1] | passed | 0.00766 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:2] | passed | 0.00709 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:3] | passed | 0.00688 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:1:1] | passed | 0.00166 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:1:2] | passed | 0.00213 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:1] | passed | 0.0034 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:2] | passed | 0.00296 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:3] | passed | 0.00251 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:4] | passed | 0.00298 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:5] | passed | 0.00296 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:1] | passed | 0.00164 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:2] | passed | 0.00148 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:3] | passed | 0.00148 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:4] | passed | 0.00152 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:5] | passed | 0.00153 seconds | diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/asserter.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/asserter.rb index 54193db3..f5100151 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/asserter.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/asserter.rb @@ -6,54 +6,52 @@ module Uc3DmpId class AsserterError < StandardError; end class Asserter + DEFAULT_DESCRIPTOR = 'references' + DEFAULT_WORK_TYPE = 'other' + class << self # Add assertions to a DMP ID - this is performed by non-provenance systems - def add(updater:, dmp:, mods:, note: nil, logger: nil) - -puts "ADDING ASSERTIONS: #{mods}" + def add(updater:, latest_version:, modified_version:, note: nil, logger: nil) + return latest_version unless latest_version.is_a?(Hash) + owner = latest_version['dmphub_provenance_id']&.gsub('PROVENANCE#', '') # If the updater and provenance are the same just return the :dmp as-is - return dmp if updater.nil? || !dmp.is_a?(Hash) || !mods.is_a?(Hash) || - updater&.gsub('PROVENANCE#', '') == dmp['dmphub_provenance_id']&.gsub('PROVENANCE#', '') - - contact = mods['contact'] - contributor = mods.fetch('contributor', []) - project = mods.fetch('project', []) - # Return the DMP ID as-is if there are no assertable changes - return dmp if contact.nil? && contributor.empty? && project.empty? - - # Clone any existing pending_assertions on the current DMP ID so we can manipulate them - assertions = Helper.deep_copy_dmp(obj: dmp.fetch('dmphub_modifications', [])) - # Return the DMP ID as-is if the assertion is already on the record - return dmp if assertions.select { |entry| entry['provenance'] == updater && entry['pending_assertions'] == mods } - - assertions << _generate_assertion(updater: updater, mods: mods, note: note) - dmp['dmphub_modifications'] = assertions.flatten - dmp + return latest_version if updater.nil? || !latest_version.is_a?(Hash) || !modified_version.is_a?(Hash) || + updater&.gsub('PROVENANCE#', '') == owner + + # contact = modified_version['contact'] + # contributor = modified_version.fetch('contributor', []) + # project = modified_version.fetch('project', []) + funding = modified_version.fetch('project', []).first&.fetch('funding', []) + related_works = modified_version.fetch('dmproadmap_related_identifiers', []) + + if related_works.any? + latest_version = _add_related_identifier(updater: updater, latest_version: latest_version, + identifiers: related_works, note: note, logger: logger) + end + return latest_version unless !funding.nil? && funding.any? + + latest_version = _add_funding_mod(updater: updater, latest_version: latest_version, funding: funding, + note: note, logger: logger) + latest_version end - # Splice together assertions made while the user was updating the DMP ID + # Splice together assertions made by the owner of the DMP ID so that any :dmphub_modifications made to + # the record while it was being updated are not lost def splice(latest_version:, modified_version:, logger: nil) - -puts "LATEST_VERSION ASSERTIONS: #{latest_version['dmphub_modifications']}" -puts "MODIFIED_VERSION ASSERTIONS: #{modified_version['dmphub_modifications']}" - - # Return the modified_version if the timestamps are the same (meaning no new assertions were made while the - # user was working on the DMP ID) OR neither version has assertions + # Return the modified_version if the timestamps are the same OR neither version has :dmphub_modifications return modified_version if latest_version['modified'] == modified_version['modified'] || (latest_version.fetch('dmphub_modifications', []).empty? && modified_version.fetch('dmphub_modifications', []).empty?) - # Clone any existing assertions on the current DMP ID so we can manipulate them + # Clone any existing :dmphub_modifications on the current DMP ID so we can retain them existing_assertions = Helper.deep_copy_dmp(obj: latest_version.fetch('dmphub_modifications', [])) incoming_assertions = Helper.deep_copy_dmp(obj: modified_version.fetch('dmphub_modifications', [])) - logger.debug(message: "Existing assertions", details: existing_assertions) if logger.respond_to?(:debug) - logger.debug(message: "Incoming modifications", details: incoming_assertions) if logger.respond_to?(:debug) + logger.debug(message: "Existing dmphub_modifications", details: existing_assertions) if logger.respond_to?(:debug) + logger.debug(message: "Incoming dmphub_modifications", details: incoming_assertions) if logger.respond_to?(:debug) - # Keep any assetions that were made after the modified on the incoming changes - modified_version['dmphub_modifications'] = existing_assertions.select do |entry| - !entry['timestamp'].nil? && Time.parse(entry['timestamp']) > Time.parse(modified_version['modified']) - end + # Keep any :dmphub_modifications and then add the incoming to the Array + modified_version['dmphub_modifications'] = existing_assertions return modified_version unless incoming_assertions.any? # Add any of the assertions still on the incoming record back to the latest record @@ -63,6 +61,75 @@ def splice(latest_version:, modified_version:, logger: nil) private + # Verify that the DMP ID record does not already have the specified identifiers and then add them + # to the :latest_version in the :dmphub_modifications Array + def _add_related_identifier(updater:, latest_version:, identifiers:, note: '', logger: nil) + return latest_version unless updater.is_a?(String) && latest_version.is_a?(Hash) && identifiers.is_a?(Array) + + known_mods = latest_version['dmphub_modifications'].map { |mod| mod.fetch('dmproadmap_related_identifiers', []) } + known_mods = known_mods.flatten.compact.map { |mod| mod['identifier'].downcase.strip }.compact.uniq + + asserted = latest_version.fetch('dmproadmap_related_identifiers', []) + asserted = asserted.flatten.compact.map { |mod| mod['identifier'].downcase.strip }.compact.uniq + + additions = [] + identifiers.each do |related_identifier| + # Skip if there is no :type or :identifier value + next if !related_identifier.is_a?(Hash) || related_identifier['type'].nil? || related_identifier['identifier'].nil? + + id = related_identifier['identifier'].downcase.strip + # Skip if the :identifier is already listed in :dmphub_modifications or the :dmproadmap_related_identifiers Arrays + next if known_mods.include?(id) || asserted.include?(id) + + related_identifier['work_type'] = DEFAULT_WORK_TYPE if related_identifier['work_type'].nil? + related_identifier['descriptor'] = DEFAULT_DESCRIPTOR if related_identifier['descriptor'].nil? + additions << related_identifier + end + + latest_version['dmproadmap_related_identifiers'] = [] if latest_version['dmproadmap_related_identifiers'].nil? + assertion = _generate_assertion(updater: updater, note: note, + mods: JSON.parse({ dmproadmap_related_identifiers: additions }.to_json)) + logger.debug(message: 'Adding change to :dmphub_modifications.', details: assertion) if logger.respond_to?(:debug) + latest_version['dmphub_modifications'] << assertion + latest_version + end + + # Verify that the DMP ID record does not already have the specified funding change and then add it + # to the :latest_version in the :dmphub_modifications Array + def _add_funding_mod(updater:, latest_version:, funding:, note: '', logger: nil) + return latest_version unless updater.is_a?(String) && latest_version.is_a?(Hash) && funding.is_a?(Array) + + known_mods = latest_version['dmphub_modifications'].map do |mod| + next if mod.nil? + + mod.fetch('funding', {}).fetch('grant_id', {})['identifier']&.downcase&.strip + end + known_mods = known_mods.flatten.compact.uniq + + asserted = latest_version.fetch('project', [])&.map do |project| + next if project.nil? + + project&.fetch('funding', []).first&.fetch('grant_id', {})['identifier']&.downcase&.strip + end + asserted = asserted.flatten.compact.uniq + + fund = funding.reject { |entry| entry['grant_id'].nil? }.first + # Skip if there is no :grant_id + return latest_version if !fund.is_a?(Hash) || fund.fetch('grant_id', {})['identifier'].nil? + + grant_id = fund.fetch('grant_id', {})['identifier'].downcase.strip + # Skip if the :grant_id is already listed as a :dmphub_modifications or project: :funding + return latest_version if known_mods.include?(grant_id) || asserted.include?(grant_id) + + latest_version['dmphub_modifications'] = [] if latest_version['dmphub_modifications'].nil? + mod = JSON.parse({ funding: fund }.to_json) + mod['funding']['funding_status'] = 'granted' + assertion = _generate_assertion(updater: updater, mods: mod, note: note) + logger.debug(message: 'Adding change to :dmphub_modifications.', details: assertion) if logger.respond_to?(:debug) + latest_version['dmphub_modifications'] << assertion + latest_version + end + # Generate an assertion entry. For example: # # { @@ -70,56 +137,41 @@ def splice(latest_version:, modified_version:, logger: nil) # "provenance": "dmphub", # "timestamp": "2023-07-07T14:50:23+00:00", # "note": "data received from the NIH API", - # "pending_assertions": { - # "contact": { - # "name": "Wrong Person" - # }, - # "contributor": [ - # { - # "name": "Jane Doe", - # "role": ["Investigation"] - # } - # ], - # "dmproadmap_related_identifiers": [ - # { - # "work_type": "article", - # "descriptor": "is_cited_by", - # "type": "doi", - # "identifier": "https://dx.doi.org/99.9876/ZYX987.V6" - # } - # ], - # "project": [ - # { - # "start": "2024-01-01T00:00:00+07:00", - # "end": "2025-12-31T23:59:59+07:00" - # } - # ], - # "funding": [ - # { - # "funder_id": { - # "identifier": "https://doi.org/10.13039/501100001807", - # "type": "fundref" - # }, - # "funding_status": "granted", - # "grant_id": { - # "identifier": "2019/22702-3", - # "type": "other" - # } - # } - # ] + # "dmproadmap_related_identifiers": { + # "work_type": "article", + # "descriptor": "is_cited_by", + # "type": "doi", + # "identifier": "https://dx.doi.org/99.9876/ZYX987.V6" + # } + # } + # + # OR: + # + # { + # "id": "ABCD1234", + # "provenance": "dmphub", + # "timestamp": "2023-07-07T14:50:23+00:00", + # "note": "data received from the NIH API", + # "funding": { + # "funding_status": "granted", + # "grant_id": { + # "identifier": "2019/22702-3", + # "type": "other" + # } # } - # } + # } def _generate_assertion(updater:, mods:, note: '') - return nil if updater.nil? || !mod.is_a?(Hash) + return nil if updater.nil? || !mods.is_a?(Hash) - JSON.parse({ + assertion = { id: SecureRandom.hex(4).upcase, provenance: updater.gsub('PROVENANCE#', ''), timestamp: Time.now.utc.iso8601, - status: 'new', - note: note, - pending_assertions: mods - }.to_json) + status: 'pending', + note: note + } + mods.each_pair { |key, val| assertion[key] = val } + JSON.parse(assertion.to_json) end end end diff --git a/src/sam/gems/uc3-dmp-id/spec/support/json_mocks/complete.json b/src/sam/gems/uc3-dmp-id/spec/support/json_mocks/complete.json index 645f0ee3..d3fe7849 100644 --- a/src/sam/gems/uc3-dmp-id/spec/support/json_mocks/complete.json +++ b/src/sam/gems/uc3-dmp-id/spec/support/json_mocks/complete.json @@ -154,36 +154,33 @@ "timestamp": "2023-07-27T15:08:32+07:00", "note": "data received from event data", "status": "pending", - "dmproadmap_related_identifier": { - "work_type": "dataset", - "descriptor": "references", - "type": "doi", - "identifier": "https://dx.doi.org/77.6666/H5H5H5" - } + "dmproadmap_related_identifiers": [ + { + "work_type": "dataset", + "descriptor": "references", + "type": "doi", + "identifier": "https://dx.doi.org/77.6666/H5H5H5" + } + ] }, { "id": "ZYXW9878", "provenance": "datacite", "timestamp": "2023-07-27T15:08:52+07:00", "note": "data received from event data", "status": "accepted", - "dmproadmap_related_identifier": { - "work_type": "article", - "descriptor": "is_cited_by", - "type": "url", - "identifier": "https://doi.org/22.33333/pubmed.1242345234" - } - }, { - "id": "ZYXW9880", - "provenance": "datacite", - "timestamp": "2023-07-27T15:09:13+07:00", - "note": "data received from event data", - "status": "rejected", - "dmproadmap_related_identifier": { - "work_type": "paper", - "descriptor": "is_cited_by", - "type": "url", - "identifier": "https://academic.site/papers/123" - } + "dmproadmap_related_identifiers": [ + { + "work_type": "article", + "descriptor": "is_cited_by", + "type": "url", + "identifier": "https://doi.org/22.33333/pubmed.1242345234" + }, { + "work_type": "paper", + "descriptor": "is_cited_by", + "type": "url", + "identifier": "https://academic.site/papers/123" + } + ] }, { "id": "ABCD1234", diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/asserter_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/asserter_spec.rb new file mode 100644 index 00000000..28decdfb --- /dev/null +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/asserter_spec.rb @@ -0,0 +1,314 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Uc3DmpId::Asserter' do + let!(:described_class) { Uc3DmpId::Asserter } + + let!(:owner) { 'PROVENANCE#foo' } + let!(:updater) { 'PROVENANCE#bar' } + + let!(:latest_version) { mock_dmp['dmp'] } + + before do + latest_version['dmphub_provenance_id'] = owner + end + + describe 'add(updater:, latest_version:, modified_version:, note: nil, logger: nil)' do + let!(:new_mods) do + JSON.parse({ + "project": [ + "title": "foo ... testing", + "funding": [ + { + "name": "Reliable cash.net", + "grant_id": { + "type": "other", identifier: "TESTING MONEY" + } + } + ] + ], + "dmproadmap_related_identifiers": [ + { work_type: 'image', descriptor: 'annoys', type: 'other', identifier: 'testing_one' }, + { type: "other", identifier: 'testing_two' } + ] + }.to_json) + end + + it 'returns the :latest_version as-is if the :updater is not a String' do + result = described_class.add(updater: nil, latest_version: latest_version, modified_version: new_mods) + expect(result).to eql(latest_version) + end + it 'returns the :latest_version as-is if the :latest_version is not a Hash' do + result = described_class.add(updater: updater, latest_version: 123, modified_version: new_mods) + expect(result).to eql(123) + end + it 'returns the :latest_version as-is if the :modified_version is not a Hash' do + result = described_class.add(updater: updater, latest_version: latest_version, modified_version: nil) + expect(result).to eql(latest_version) + end + it 'returns the :latest_version as-is if the :updater is the owner of the DMP ID (owner doesn\'t need to assert!)' do + latest_version['dmphub_provenance_id'] = updater + result = described_class.add(updater: updater, latest_version: latest_version, modified_version: new_mods) + expect(result).to eql(latest_version) + end + it 'adds the :dmphub_modifications if there are only :dmproadmap_related_identifiers' do + new_mods.delete('project') + result = described_class.add(updater: updater, latest_version: latest_version, modified_version: new_mods) + changes = result['dmphub_modifications'].reject { |mod| mod.fetch('dmproadmap_related_identifiers', []).empty? } + changes = changes.map { |change| change['dmproadmap_related_identifiers'] }.flatten + + work_one = changes.select { |work| work['identifier'] == new_mods['dmproadmap_related_identifiers'].first['identifier'] } + work_two = changes.select { |work| work['identifier'] == new_mods['dmproadmap_related_identifiers'].last['identifier'] } + + expect(work_one.first['work_type']).to eql(new_mods['dmproadmap_related_identifiers'].first['work_type']) + expect(work_one.first['descriptor']).to eql(new_mods['dmproadmap_related_identifiers'].first['descriptor']) + expect(work_one.first['type']).to eql(new_mods['dmproadmap_related_identifiers'].first['type']) + expect(work_one.first['identifier']).to eql(new_mods['dmproadmap_related_identifiers'].first['identifier']) + + expect(work_two.first['work_type']).to eql(described_class::DEFAULT_WORK_TYPE) + expect(work_two.first['descriptor']).to eql(described_class::DEFAULT_DESCRIPTOR) + expect(work_two.first['type']).to eql(new_mods['dmproadmap_related_identifiers'].last['type']) + expect(work_two.first['identifier']).to eql(new_mods['dmproadmap_related_identifiers'].last['identifier']) + end + it 'adds the :dmphub_modifications if there is only a :grant_id' do + new_mods.delete('dmproadmap_related_identifiers') + result = described_class.add(updater: updater, latest_version: latest_version, modified_version: new_mods) + fundings = result['dmphub_modifications'].reject { |mod| mod['funding'].nil? }.map { |m| m['funding'] } + match = fundings.select { |fund| fund['grant_id'] == new_mods['project'].first['funding'].first['grant_id'] }.first + + expect(match['grant_id']).to eql(new_mods['project'].first['funding'].first['grant_id']) + expect(match['funding_status']).to eql('granted') + end + it 'adds the :dmphub_modifications if there are both :dmproadmap_related_identifiers and a :grant_id' do + result = described_class.add(updater: updater, latest_version: latest_version, modified_version: new_mods) + changes = result['dmphub_modifications'].reject { |mod| mod.fetch('dmproadmap_related_identifiers', []).empty? } + changes = changes.map { |change| change['dmproadmap_related_identifiers'] }.flatten + + fundings = result['dmphub_modifications'].reject { |mod| mod['funding'].nil? }.map { |m| m['funding'] } + match = fundings.select { |fund| fund['grant_id'] == new_mods['project'].first['funding'].first['grant_id'] }.first + + expect(match['grant_id']).to eql(new_mods['project'].first['funding'].first['grant_id']) + expect(match['funding_status']).to eql('granted') + + work_one = changes.select { |work| work['identifier'] == new_mods['dmproadmap_related_identifiers'].first['identifier'] } + work_two = changes.select { |work| work['identifier'] == new_mods['dmproadmap_related_identifiers'].last['identifier'] } + + expect(work_one.first['work_type']).to eql(new_mods['dmproadmap_related_identifiers'].first['work_type']) + expect(work_one.first['descriptor']).to eql(new_mods['dmproadmap_related_identifiers'].first['descriptor']) + expect(work_one.first['type']).to eql(new_mods['dmproadmap_related_identifiers'].first['type']) + expect(work_one.first['identifier']).to eql(new_mods['dmproadmap_related_identifiers'].first['identifier']) + + expect(work_two.first['work_type']).to eql(described_class::DEFAULT_WORK_TYPE) + expect(work_two.first['descriptor']).to eql(described_class::DEFAULT_DESCRIPTOR) + expect(work_two.first['type']).to eql(new_mods['dmproadmap_related_identifiers'].last['type']) + expect(work_two.first['identifier']).to eql(new_mods['dmproadmap_related_identifiers'].last['identifier']) + end + end + + describe 'splice(latest_version:, modified_version:, logger: nil)' do + let!(:new_mod) do + JSON.parse({ + "id": "FOOOOOOO", + "provenance": "dmphub", + "timestamp": "2023-08-20T05:06:07Z", + "note": "data for testing mods", + "dmproadmap_related_identifiers": [ + { work_type: 'image', descriptor: 'annoys', type: 'other', identifier: 'bar.jpg' } + ] + }.to_json) + end + + it 'returns the :modified_version if the :modified dates are the same' do + modified = latest_version.dup + modified['dmphub_modifications'] << new_mod + result = described_class.splice(latest_version: latest_version, modified_version: modified) + expect(assert_dmps_match(obj_a: modified, obj_b: result, debug: false)).to be(true) + end + it 'returns the :modified_version if neither has any :dmphub_modifications' do + latest_version.delete('dmphub_modifications') + modified = latest_version.dup + result = described_class.splice(latest_version: latest_version, modified_version: modified) + expect(assert_dmps_match(obj_a: modified, obj_b: result, debug: false)).to be(true) + end + it 'adds the incoming :dmphub_modifications to the latest_version when the latest_version has none' do + latest_version.delete('dmphub_modifications') + modified = latest_version.dup + modified['dmphub_modifications'] = [new_mod] + modified['modified'] = Time.now.utc.iso8601 + result = described_class.splice(latest_version: latest_version, modified_version: modified) + expect(result['dmphub_modifications'].length).to eql(1) + expect(result['dmphub_modifications'].include?(new_mod)).to be(true) + end + it 'retains the existing :dmphub_modifications if the incoming has none' do + modified = latest_version.dup + modified.delete('dmphub_modifications') + modified['modified'] = Time.now.utc.iso8601 + result = described_class.splice(latest_version: latest_version, modified_version: modified) + expect(result['dmphub_modifications'].length).to eql(3) + latest_version['dmphub_modifications'].each do |mod| + expect(result['dmphub_modifications'].include?(mod)).to be(true) + end + end + it 'appends the incoming :dmphub_modifications to the ones on the existing record' do + modified = latest_version.dup + modified.delete('dmphub_modifications') + modified['dmphub_modifications'] = [new_mod] + modified['modified'] = Time.now.utc.iso8601 + result = described_class.splice(latest_version: latest_version, modified_version: modified) + expect(result['dmphub_modifications'].length).to eql(4) + expect(result['dmphub_modifications'].include?(new_mod)).to be(true) + latest_version['dmphub_modifications'].each do |mod| + expect(result['dmphub_modifications'].include?(mod)).to be(true) + end + end + end + + describe '_add_related_identifier(updater:, latest_version:, identifiers:, note: '', logger: nil)' do + let!(:mods) do + JSON.parse([ + { + work_type: 'software', + descriptor: 'cites', + type: 'url', + identifier: 'http://granter.org/12345.testing' + }, { + type: 'doi', + identifier: 'http://99.66666/foo.testing/bar' + } + ].to_json) + end + + it 'returns :latest_version as-is if :updater is not a String' do + result = described_class.send(:_add_related_identifier, updater: 123, latest_version: latest_version, + identifiers: mods) + expect(result).to eql(latest_version) + end + it 'returns :latest_version as-is if :latest_version is not a Hash' do + result = described_class.send(:_add_related_identifier, updater: updater, latest_version: [123], identifiers: mods) + expect(result).to eql([123]) + end + it 'returns :latest_version as-is if :identifiers is not an Array' do + result = described_class.send(:_add_related_identifier, updater: updater, latest_version: latest_version, + identifiers: { 'foo': 'bar' }) + expect(result).to eql(latest_version) + end + it 'skips adding the :related_identifier if it is already in the :latest_version :dmphub_modifications Array' do + latest_version['dmphub_modifications'] << JSON.parse({ dmproadmap_related_identifiers: [mods.first] }.to_json) + result = described_class.send(:_add_related_identifier, updater: updater, latest_version: latest_version, + identifiers: mods) + expect(result['dmphub_modifications'].length).to eql(5) + + changes = result['dmphub_modifications'].reject { |mod| mod.fetch('dmproadmap_related_identifiers', []).empty? } + changes = changes.map { |change| change['dmproadmap_related_identifiers'] }.flatten + ids = changes.map { |mod| mod.fetch('identifier', '')&.downcase&.strip }.flatten.compact + expect(ids.include?(mods.first['identifier'])).to be(true) + expect(ids.select { |id| id == mods.first['identifier'] }.length).to eql(1) + end + it 'skips adding the :related_identifier if it is already in the :latest_version :dmproadmap_related_identifiers' do + tweaked_id = mods.first + tweaked_id['descriptor'] = 'documents' + latest_version['dmproadmap_related_identifiers'] << tweaked_id + result = described_class.send(:_add_related_identifier, updater: updater, latest_version: latest_version, + identifiers: mods) + expect(result['dmphub_modifications'].length).to eql(4) + + changes = result['dmphub_modifications'].reject { |mod| mod.fetch('dmproadmap_related_identifiers', []).empty? } + changes = changes.map { |change| change['dmproadmap_related_identifiers'] }.flatten + ids = changes.map { |mod| mod.fetch('identifier', '')&.downcase&.strip }.flatten.compact.uniq + expect(ids.include?(mods.first['identifier'])).to be(false) + end + it 'adds the :related_identifier assertion to the :latest_version :dmphub_modifications Array' do + result = described_class.send(:_add_related_identifier, updater: updater, latest_version: latest_version, + identifiers: mods) + expect(result['dmphub_modifications'].length).to eql(4) + + changes = result['dmphub_modifications'].reject { |mod| mod.fetch('dmproadmap_related_identifiers', []).empty? } + changes = changes.map { |change| change['dmproadmap_related_identifiers'] }.flatten + ids = changes.map { |mod| mod.fetch('identifier', '')&.downcase&.strip }.flatten.compact.uniq + mods.each { |mod| expect(ids.include?(mod['identifier'])) } + end + end + + describe '_add_funding_mod(updater:, latest_version:, funding:, note: '', logger: nil)' do + let!(:mods) do + JSON.parse([{ + status: 'granted', + grant_id: { type: 'url', identifier: 'http://granter.org/12345' } + }].to_json) + end + + it 'returns :latest_version as-is if :updater is not a String' do + result = described_class.send(:_add_funding_mod, updater: 123, latest_version: latest_version, funding: mods) + expect(result).to eql(latest_version) + end + it 'returns :latest_version as-is if :latest_version is not a Hash' do + result = described_class.send(:_add_funding_mod, updater: updater, latest_version: [123], funding: mods) + expect(result).to eql([123]) + end + it 'returns :latest_version as-is if :funding is not an Array' do + result = described_class.send(:_add_funding_mod, updater: updater, latest_version: latest_version, + funding: { 'foo': 'bar' }) + expect(result).to eql(latest_version) + end + it 'skips adding the :grant_id if it is already in the :latest_version :dmphub_modifications Array' do + latest_version['dmphub_modifications'] << JSON.parse({ funding: mods.first }.to_json) + result = described_class.send(:_add_funding_mod, updater: updater, latest_version: latest_version, funding: mods) + expect(result['dmphub_modifications'].length).to eql(4) + fundings = result['dmphub_modifications'].reject { |mod| mod['funding'].nil? }.flatten.compact.uniq + grants = fundings.map { |fund| fund.fetch('funding', {})['grant_id'] } + expect(grants.include?(mods.first)).to be(false) + end + it 'skips adding the :grant_id if it is already in the :latest_version project: :funding Array' do + latest_version['project'].first['funding'].first['grant_id'] = mods.first['grant_id'] + result = described_class.send(:_add_funding_mod, updater: updater, latest_version: latest_version, funding: mods) + expect(result['dmphub_modifications'].length).to eql(3) + fundings = result['dmphub_modifications'].reject { |mod| mod['funding'].nil? }.flatten.compact.uniq + grants = fundings.map { |fund| fund.fetch('funding', {})['grant_id'] } + expect(grants.include?(mods.first)).to be(false) + end + it 'adds the :grant_id assertion to the :latest_version :dmphub_modifications Array' do + result = described_class.send(:_add_funding_mod, updater: updater, latest_version: latest_version, funding: mods) + expect(result['dmphub_modifications'].length).to eql(4) + fundings = result['dmphub_modifications'].reject { |mod| mod['funding'].nil? }.flatten.compact.uniq + grants = fundings.map { |fund| fund.fetch('funding', {})['grant_id'] } + expect(grants.include?(mods.first['grant_id'])).to be(true) + end + end + + describe '_generate_assertion(updater:, mods:, note: '')' do + let!(:mods) do + JSON.parse({ + dmproadmap_related_identifiers: [ + { work_type: 'dataset', descriptor: 'references', type: 'doi', identifier: 'https://doi.org/11.22222/3333344' }, + { work_type: 'article', descriptor: 'is_cited_by', type: 'doi', identifier: 'https://doi.org/11.22222/journalA/1' }, + ], + funding: { + status: 'granted', + grant_id: { type: 'url', identifier: 'http://granter.org/12345' } + } + }.to_json) + end + + it 'returns nil if the :updater is nil' do + expect(described_class.send(:_generate_assertion, updater: nil, mods: mods, note: 'testing ...')).to be(nil) + end + + it 'returns nil if :mod is not a Hash' do + expect(described_class.send(:_generate_assertion, updater: updater, mods: '123', note: 'testing ...')).to be(nil) + end + + it 'returns the formatted assertion' do + result = described_class.send(:_generate_assertion, updater: updater, mods: mods, note: 'testing ...') + expect(result['id'].nil?).to be(false) + expect(result['provenance']).to eql(updater.gsub('PROVENANCE#', '')) + expect(result['timestamp'].nil?).to be(false) + expect(result['status']).to eql('pending') + expect(result['note']).to eql('testing ...') + mods['dmproadmap_related_identifiers'].each do |mod| + expect(result['dmproadmap_related_identifiers'].include?(mod)).to be(true) + end + expect(result['funding']).to eql(mods['funding']) + end + end +end From 84a6a942458206f61f8321376f6112f609cedbab Mon Sep 17 00:00:00 2001 From: briri Date: Mon, 21 Aug 2023 15:21:15 -0700 Subject: [PATCH 07/13] added spec tests for uc3-dmp-id finder class --- src/sam/gems/uc3-dmp-id/.rspec_status | 34 ++ .../gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb | 2 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/finder.rb | 47 +-- .../gems/uc3-dmp-id/spec/support/shared.rb | 11 +- .../uc3-dmp-id/spec/uc3-dmp-id/finder_spec.rb | 319 ++++++++++++++++++ 5 files changed, 388 insertions(+), 25 deletions(-) create mode 100644 src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/finder_spec.rb diff --git a/src/sam/gems/uc3-dmp-id/.rspec_status b/src/sam/gems/uc3-dmp-id/.rspec_status index 9c75cd0c..939d241b 100644 --- a/src/sam/gems/uc3-dmp-id/.rspec_status +++ b/src/sam/gems/uc3-dmp-id/.rspec_status @@ -27,6 +27,40 @@ example_id | status | run_time | ./spec/uc3-dmp-id/asserter_spec.rb[1:5:1] | passed | 0.00027 seconds | ./spec/uc3-dmp-id/asserter_spec.rb[1:5:2] | passed | 0.00028 seconds | ./spec/uc3-dmp-id/asserter_spec.rb[1:5:3] | passed | 0.0003 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:1] | passed | 0.00574 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:2] | passed | 0.00054 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:3] | passed | 0.00049 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:4] | passed | 0.00048 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:1] | passed | 0.00111 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:2] | passed | 0.0005 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:3] | passed | 0.00052 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:4] | passed | 0.00052 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:1] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:2] | passed | 0.00105 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:3] | passed | 0.00053 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:4] | passed | 0.0005 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:5] | passed | 0.0005 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:6] | passed | 0.00048 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:4:1] | passed | 0.00047 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:4:2] | passed | 0.00091 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:4:3] | passed | 0.00049 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:1] | passed | 0.0005 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:2] | passed | 0.0017 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:3] | passed | 0.0005 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:4] | passed | 0.00048 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:6:1] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:6:2] | passed | 0.00045 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:6:3] | passed | 0.00053 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:7:1] | passed | 0.00042 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:7:2] | passed | 0.00048 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:7:3] | passed | 0.00056 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:8:1] | passed | 0.00046 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:8:2] | passed | 0.00049 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:8:3] | passed | 0.00056 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:1] | passed | 0.00048 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:2] | passed | 0.00049 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:3] | passed | 0.00052 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:4] | passed | 0.00052 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:1:1] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:1:2] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:2:1] | passed | 0.00003 seconds | diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb index a13b63d4..a0d6f506 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb @@ -27,7 +27,7 @@ def create(provenance:, json:, logger: nil) # Fail if the provenance or owner affiliation are not defined raise CreatorError, MSG_NO_PROVENANCE_OWNER if provenance.nil? - # TODO: Swap this out with the Finder search once the Dynamo indexes are working + # TODO: Swap this out with the Finder.exists? once the Dynamo indexes are working # Try to find it first and Fail if found result = Finder.by_json(json: json, logger: logger) raise CreatorError, Uc3DmpId::MSG_DMP_EXISTS if result.is_a?(Hash) diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/finder.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/finder.rb index da42806c..e4e0f167 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/finder.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/finder.rb @@ -9,8 +9,8 @@ class FinderError < StandardError; end class Finder MSG_INVALID_ARGS = 'Expected JSON to be structured as `{ "dmp": { "PK": "value"} } OR \ { "dmp": { "dmp_id": { "identifier": "value", "type": "value" } }`' - MSG_INVALID_OWNER_ID = 'Invalid :owner_orcid. Expected value to start with `https://orcid.org/`.' - MSG_INVALID_OWNER_ORG = 'Invalid :owner_org_ror. Expected value to start with `https://ror.org/`.' + MSG_INVALID_OWNER_ID = 'Invalid :owner_orcid. Expected a valid ORCID id (excluding the domain)`.' + MSG_INVALID_OWNER_ORG = 'Invalid :owner_org_ror. Expected a valid ROR id (excluding the domain)`.' MSG_INVALID_MOD_DATE = 'Invalid :modification_day. Expected value to be in the `YYYY-MM-DD` format.' MSG_MISSING_PK = 'No PK was provided' MSG_MISSING_PROV_ID = 'No Provenance identifier was provided. \ @@ -20,9 +20,10 @@ class Finder class << self # TODO: Replace this with ElasticSearch def search_dmps(args:, logger: nil) - return _by_owner(owner_org: args['owner_orcid'], logger: logger) unless args['owner_orcid'].nil? - return _by_owner_org(owner_org: args['owner_org_ror'], logger: logger) unless args['owner_org_ror'].nil? - return _by_mod_day(day: args['modification_day'], logger: logger) unless args['modification_day'].nil? + client = Uc3DmpDynamo::Client.new + return _by_owner(owner_org: args['owner_orcid'], client: client, logger: logger) unless args['owner_orcid'].nil? + return _by_owner_org(owner_org: args['owner_org_ror'], client: client, logger: logger) unless args['owner_org_ror'].nil? + return _by_mod_day(day: args['modification_day'], client: client, logger: logger) unless args['modification_day'].nil? [] end @@ -30,9 +31,9 @@ def search_dmps(args:, logger: nil) # Find a DMP based on the contents of the incoming JSON # ------------------------------------------------------------------------- - def by_json(json:, cleanse: true, logger: nil) + def by_json(json:, client: nil, cleanse: true, logger: nil) json = Helper.parse_json(json: json)&.fetch('dmp', {}) - raise FinderError, MSG_INVALID_ARGS if json.nil? || (json['PK'].nil? && json['dmp_id'].nil?) + raise FinderError, MSG_INVALID_ARGS if !json.is_a?(Hash) || (json['PK'].nil? && json['dmp_id'].nil?) p_key = json['PK'] # Translate the incoming :dmp_id into a PK @@ -57,7 +58,7 @@ def by_pk(p_key:, s_key: Helper::DMP_LATEST_VERSION, client: nil, cleanse: true, resp = client.get_item( key: { PK: Helper.append_pk_prefix(p_key: p_key), - SK: s_key + SK: Helper.append_sk_prefix(s_key: s_key) }, logger: logger ) @@ -79,7 +80,7 @@ def exists?(p_key:, s_key: Helper::DMP_LATEST_VERSION, client: nil, logger: nil) client.pk_exists?( key: { PK: Helper.append_pk_prefix(p_key: p_key), - SK: s_key + SK: Helper.append_sk_prefix(s_key: s_key) }, logger: logger ) @@ -89,7 +90,10 @@ def exists?(p_key:, s_key: Helper::DMP_LATEST_VERSION, client: nil, logger: nil) # ------------------------------------------------------------------------- # rubocop:disable Metrics/AbcSize def by_provenance_identifier(json:, client: nil, cleanse: true, logger: nil) - raise FinderError, MSG_MISSING_PROV_ID if json.nil? || json.fetch('dmp_id', {})['identifier'].nil? + raise FinderError, MSG_MISSING_PROV_ID unless json.is_a?(Hash) + + json = json['dmp'] unless json['dmp'].nil? + raise FinderError, MSG_MISSING_PROV_ID if json.fetch('dmp_id', {})['identifier'].nil? args = { index_name: 'dmphub_provenance_identifier_gsi', @@ -117,17 +121,17 @@ def by_provenance_identifier(json:, client: nil, cleanse: true, logger: nil) private # Fetch the DMP IDs for the specified owner's ORCID (the owner is the :dmphub_owner_id on the DMP ID record) - def _by_owner(owner_id:, logger: nil) + def _by_owner(owner_id:, client: nil, logger: nil) regex = %r{^([0-9A-Z]{4}-){3}[0-9A-Z]{4}$} - raise FinderError, MSG_INVALID_OWNER_ID if owner_id.nil? || (owner_id.to_s.downcase =~ regex).nil? + raise FinderError, MSG_INVALID_OWNER_ID if owner_id.nil? || (owner_id.to_s =~ regex).nil? args = { index_name: 'dmphub_owner_id_gsi', key_conditions: { dmphub_owner_id: { attribute_value_list: [ - "http://orcid.org/#{owner_id.to_s.downcase}", - "https://orcid.org/#{owner_id.to_s.downcase}" + "http://orcid.org/#{owner_id.to_s}", + "https://orcid.org/#{owner_id.to_s}" ], comparison_operator: 'IN' } @@ -141,7 +145,7 @@ def _by_owner(owner_id:, logger: nil) end # Fetch the DMP IDs for the specified organization/institution (the org is the :dmphub_owner_org on the DMP ID record) - def _by_owner_org(owner_org:, logger: nil) + def _by_owner_org(owner_org:, client: nil, logger: nil) regex = %r{^[a-zA-Z0-9]+$} raise FinderError, MSG_INVALID_OWNER_ID if owner_org.nil? ||(owner_org.to_s.downcase =~ regex).nil? @@ -149,8 +153,11 @@ def _by_owner_org(owner_org:, logger: nil) index_name: 'dmphub_owner_org_gsi', key_conditions: { dmphub_owner_org: { - attribute_value_list: ["https://ror.org/#{owner_org.to_s.downcase}"], - comparison_operator: 'EQ' + attribute_value_list: [ + "https://ror.org/#{owner_org.to_s.downcase}", + "http://ror.org/#{owner_org.to_s.downcase}", + ], + comparison_operator: 'IN' } }, filter_expression: 'SK = :version', @@ -162,7 +169,7 @@ def _by_owner_org(owner_org:, logger: nil) end # Fetch the DMP IDs modified on the specified date (the date is the :dmphub_modification_day on the DMP ID record) - def _by_mod_day(day:, logger: nil) + def _by_mod_day(day:, client: nil, logger: nil) regex = %r{^[0-9]{4}(-[0-9]{2}){2}} raise FinderError, MSG_INVALID_OWNER_ID if day.nil? || (day.to_s =~ regex).nil? @@ -187,7 +194,9 @@ def _by_mod_day(day:, logger: nil) def _process_search_response(response:) return [] unless response.is_a?(Array) && response.any? - results = response.each do |item| + results = response.map do |item| + next if item.nil? + dmp = item['dmp'].nil? ? JSON.parse({ dmp: item }.to_json) : item Helper.cleanse_dmp_json(json: dmp) end diff --git a/src/sam/gems/uc3-dmp-id/spec/support/shared.rb b/src/sam/gems/uc3-dmp-id/spec/support/shared.rb index 026db81f..91ac9671 100644 --- a/src/sam/gems/uc3-dmp-id/spec/support/shared.rb +++ b/src/sam/gems/uc3-dmp-id/spec/support/shared.rb @@ -6,12 +6,13 @@ # Mock S3 Resources Uc3DmpDynamoClient = Struct.new('S3Client', :get_item, :put_item, :delete_item, :query) -def mock_uc3_dmp_dynamo(success: true) +def mock_uc3_dmp_dynamo(dmp: mock_dmp, success: true) client = Uc3DmpDynamoClient.new - allow(client).to receive(:get_item).and_return(success ? mock_dmp : nil) - allow(client).to receive(:put_item).and_return(success ? mock_dmp : nil) - allow(client).to receive(:delete_item).and_return(success ? mock_dmp : nil) - allow(client).to receive(:query).and_return(success ? [mock_dmp] : nil) + allow(client).to receive(:get_item).and_return(success ? dmp : nil) + allow(client).to receive(:put_item).and_return(success ? dmp : nil) + allow(client).to receive(:delete_item).and_return(success ? dmp : nil) + allow(client).to receive(:query).and_return(success ? [dmp] : nil) + allow(client).to receive(:pk_exists?).and_return(success) allow(Uc3DmpDynamo::Client).to receive(:new).and_return(client) client diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/finder_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/finder_spec.rb new file mode 100644 index 00000000..3f669c29 --- /dev/null +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/finder_spec.rb @@ -0,0 +1,319 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Uc3DmpId::Finder' do + let!(:described_class) { Uc3DmpId::Finder } + let!(:finder_error) { Uc3DmpId::FinderError } + + let!(:dmp) do + record = mock_dmp + record['dmp']['PK'] = "#{Uc3DmpId::Helper::PK_DMP_PREFIX}test" + record + end + let!(:client) { mock_uc3_dmp_dynamo(dmp: dmp) } + + describe 'search_dmps(args:, logger: nil)' do + it 'returns an empty Array if :args does not contain any valid query criteria' do + allow(described_class).to receive(:_by_owner).never + allow(described_class).to receive(:_by_owner_org).never + allow(described_class).to receive(:_by_mod_day).never + expect(described_class.search_dmps(args: JSON.parse({ foo: 'bar' }.to_json))).to eql([]) + end + it 'calls :_by_owner if :args includes an :owner_orcid' do + allow(described_class).to receive(:_by_owner).once + allow(described_class).to receive(:_by_owner_org).never + allow(described_class).to receive(:_by_mod_day).never + expect(described_class.search_dmps(args: JSON.parse({ foo: 'bar' }.to_json))).to eql([]) + end + it 'calls :_by_owner_org if :args includes an :owner_org_ror' do + allow(described_class).to receive(:_by_owner).never + allow(described_class).to receive(:_by_owner_org).once + allow(described_class).to receive(:_by_mod_day).never + expect(described_class.search_dmps(args: JSON.parse({ foo: 'bar' }.to_json))).to eql([]) + end + it 'calls :_by_mod_day if :args includes an :modification_day' do + allow(described_class).to receive(:_by_owner).never + allow(described_class).to receive(:_by_owner_org).never + allow(described_class).to receive(:_by_mod_day).once + expect(described_class.search_dmps(args: JSON.parse({ foo: 'bar' }.to_json))).to eql([]) + end + end + + describe 'by_json(json:, cleanse: true, logger: nil)' do + let!(:json) do + JSON.parse({ + dmp: { + dmp_id: { type: 'doi', identifier: 'http://doi.org/11.4444/HH33JJ' }, + title: 'Testing find by JSON' + } + }.to_json) + end + + it 'raises a FinderError if :json is not a Hash' do + allow(Uc3DmpId::Helper).to receive(:parse_json).and_return(nil) + expect{ described_class.by_json(json: '123', client: client) }.to raise_error(finder_error) + end + it 'raises a FinderError if :json does not contain a :PK or :dmp_id' do + json['dmp'].delete('dmp_id') + allow(Uc3DmpId::Helper).to receive(:parse_json).and_return(json) + expect{ described_class.by_json(json: '123', client: client) }.to raise_error(finder_error) + end + it 'calls :by_pk if the :json contains a :dmp_id' do + allow(Uc3DmpId::Helper).to receive(:parse_json).and_return(json) + allow(described_class).to receive(:by_pk).once + described_class.by_json(json: json, client: client) + end + it 'calls :by_pk if the :json contains a :PK' do + json['dmp']['PK'] = json['dmp']['dmp_id']['identifier'] + json['dmp'].delete('dmp_id') + allow(Uc3DmpId::Helper).to receive(:parse_json).and_return(json) + allow(Uc3DmpId::Helper).to receive(:parse_json).and_return(json) + allow(described_class).to receive(:by_pk).once + described_class.by_json(json: json, client: client) + end + end + + describe 'by_pk(p_key:, s_key: Helper::DMP_LATEST_VERSION, client: nil, cleanse: true, logger: nil)' do + it 'raises a FinderError if :p_key is nil' do + expect{ described_class.by_pk(p_key: nil, client: client) }.to raise_error(finder_error) + end + it 'uses the default SK if no :s_key is specified' do + allow(Uc3DmpId::Versioner).to receive(:append_versions).and_return(dmp) + allow(Uc3DmpId::Helper).to receive(:cleanse_dmp_json).and_return(dmp) + expect(Uc3DmpId::Versioner).to receive(:append_versions).once + expect(described_class.by_pk(p_key: 'foo', client: client).length).to eql(1) + expected = { + key: { PK: "#{Uc3DmpId::Helper::PK_DMP_PREFIX}foo", SK: Uc3DmpId::Helper::DMP_LATEST_VERSION }, + logger: nil + } + expect(client).to have_received(:get_item).with(expected) + end + it 'calls Dynamo with the expected query args' do + allow(Uc3DmpId::Versioner).to receive(:append_versions).and_return(dmp) + allow(Uc3DmpId::Helper).to receive(:cleanse_dmp_json).and_return(dmp) + expect(Uc3DmpId::Versioner).to receive(:append_versions).once + expect(described_class.by_pk(p_key: 'foo', s_key: 'bar', client: client).length).to eql(1) + expected = { + key: { PK: "#{Uc3DmpId::Helper::PK_DMP_PREFIX}foo", SK: "#{Uc3DmpId::Helper::SK_DMP_PREFIX}bar" }, + logger: nil + } + expect(client).to have_received(:get_item).with(expected) + end + it 'appends the :dmphub_versions' do + allow(Uc3DmpId::Versioner).to receive(:append_versions).and_return(dmp) + allow(Uc3DmpId::Helper).to receive(:cleanse_dmp_json).and_return(dmp) + expect(Uc3DmpId::Versioner).to receive(:append_versions).once + described_class.by_pk(p_key: 'foo', s_key: 'bar', client: client) + end + it 'cleanses the :dmphub_ prefixed attributes by default' do + allow(Uc3DmpId::Versioner).to receive(:append_versions).and_return(dmp) + allow(Uc3DmpId::Helper).to receive(:cleanse_dmp_json).and_return(dmp) + expect(Uc3DmpId::Helper).to receive(:cleanse_dmp_json).once + described_class.by_pk(p_key: 'foo', s_key: 'bar', client: client) + end + it 'does not cleanse the :dmphub_ prefixed attributes if specified' do + allow(Uc3DmpId::Versioner).to receive(:append_versions).and_return(dmp) + allow(Uc3DmpId::Helper).to receive(:cleanse_dmp_json).and_return(dmp) + expect(Uc3DmpId::Helper).to receive(:cleanse_dmp_json).never + described_class.by_pk(p_key: 'foo', s_key: 'bar', client: client, cleanse: false) + end + end + + describe 'exists?(p_key:, s_key: Helper::DMP_LATEST_VERSION, client: nil, logger: nil)' do + it 'raises a FinderError if :p_key is nil' do + expect{ described_class.exists?(p_key: nil, client: client) }.to raise_error(finder_error) + end + it 'uses the default SK if no :s_key is specified' do + expect(described_class.exists?(p_key: 'foo', client: client)).to be(true) + expected = { + key: { PK: "#{Uc3DmpId::Helper::PK_DMP_PREFIX}foo", SK: Uc3DmpId::Helper::DMP_LATEST_VERSION }, + logger: nil + } + expect(client).to have_received(:pk_exists?).with(expected) + end + it 'calls Dynamo with the expected query args' do + expect(described_class.exists?(p_key: 'foo', s_key: 'bar', client: client)).to be(true) + expected = { + key: { PK: "#{Uc3DmpId::Helper::PK_DMP_PREFIX}foo", SK: "#{Uc3DmpId::Helper::SK_DMP_PREFIX}bar" }, + logger: nil + } + expect(client).to have_received(:pk_exists?).with(expected) + end + end + + describe 'by_provenance_identifier(json:, client: nil, cleanse: true, logger: nil)' do + let!(:json) do + JSON.parse({ + title: 'Testing', + dmp_id: { type: 'url', identifier: 'http://some.org/12345' } + }.to_json) + end + + it 'raises a FinderError if :json is not a Hash' do + expect{ described_class.by_provenance_identifier(json: nil, client: client) }.to raise_error(finder_error) + end + it 'raises a FinderError if :json does not contain a :dmp_id with a :identifier' do + expect{ described_class.by_provenance_identifier(json: {}, client: client) }.to raise_error(finder_error) + end + it 'can handle it when :json has a top level :dmp' do + nested = JSON.parse({ dmp: json }.to_json) + expect(described_class.by_provenance_identifier(json: nested, client: client).length).to eql(1) + expected = { + args: { + expression_attribute_values: { ':version': Uc3DmpId::Helper::DMP_LATEST_VERSION }, + filter_expression: "SK = :version", + index_name: "dmphub_provenance_identifier_gsi", + key_conditions: { + dmphub_provenance_identifier: { + attribute_value_list: ['http://some.org/12345'], + comparison_operator: "EQ" + } + } + }, + logger: nil + } + expect(client).to have_received(:query).with(expected) + end + it 'calls Dynamo with the expected query args' do + expect(described_class.by_provenance_identifier(json: json, client: client).length).to eql(1) + expected = { + args: { + expression_attribute_values: { ':version': Uc3DmpId::Helper::DMP_LATEST_VERSION }, + filter_expression: "SK = :version", + index_name: "dmphub_provenance_identifier_gsi", + key_conditions: { + dmphub_provenance_identifier: { + attribute_value_list: ['http://some.org/12345'], + comparison_operator: "EQ" + } + } + }, + logger: nil + } + expect(client).to have_received(:query).with(expected) + end + end + + describe '_by_owner(owner_id:, logger: nil)' do + it 'raises a FinderError if :_by_owner is nil' do + expect{ described_class.send(:_by_owner, owner_id: nil) }.to raise_error(finder_error) + end + it 'raises a FinderError if :owner_org is not an ORCID id' do + expect{ described_class.send(:_by_owner, owner_id: '12345') }.to raise_error(finder_error) + end + it 'calls Dynamo with the expected query args' do + expect(described_class.send(:_by_owner, owner_id: '0000-0000-0000-TEST', client: client).length).to eql(1) + expected = { + args: { + expression_attribute_values: { ':version': Uc3DmpId::Helper::DMP_LATEST_VERSION }, + filter_expression: "SK = :version", + index_name: "dmphub_owner_id_gsi", + key_conditions: { + dmphub_owner_id: { + attribute_value_list: ["http://orcid.org/0000-0000-0000-TEST", 'https://orcid.org/0000-0000-0000-TEST'], + comparison_operator: "IN" + } + } + }, + logger: nil + } + expect(client).to have_received(:query).with(expected) + end + end + + describe '_by_owner_org(owner_org:, logger: nil)' do + it 'raises a FinderError if :owner_org is nil' do + expect{ described_class.send(:_by_owner_org, owner_org: nil) }.to raise_error(finder_error) + end + it 'raises a FinderError if :owner_org is not a ROR id' do + expect{ described_class.send(:_by_owner_org, owner_org: '536.45t245/wefwRT') }.to raise_error(finder_error) + end + it 'calls Dynamo with the expected query args' do + expect(described_class.send(:_by_owner_org, owner_org: '123abc45', client: client).length).to eql(1) + expected = { + args: { + expression_attribute_values: { ':version': Uc3DmpId::Helper::DMP_LATEST_VERSION }, + filter_expression: "SK = :version", + index_name: "dmphub_owner_org_gsi", + key_conditions: { + dmphub_owner_org: { + attribute_value_list: ['https://ror.org/123abc45', 'http://ror.org/123abc45'], + comparison_operator: "IN" + } + } + }, + logger: nil + } + expect(client).to have_received(:query).with(expected) + end + end + + describe '_by_mod_day(day:, logger: nil)' do + it 'raises a FinderError if :day is nil' do + expect{ described_class.send(:_by_mod_day, day: nil) }.to raise_error(finder_error) + end + it 'raises a FinderError if :day does not match the YYYY-MM-DD format' do + expect{ described_class.send(:_by_mod_day, day: '10/23/2020') }.to raise_error(finder_error) + end + it 'calls Dynamo with the expected query args' do + expect(described_class.send(:_by_mod_day, day: '2023-08-21', client: client).length).to eql(1) + expected = { + args: { + expression_attribute_values: { ':version': Uc3DmpId::Helper::DMP_LATEST_VERSION }, + filter_expression: "SK = :version", + index_name: "dmphub_modification_day_gsi", + key_conditions: { + dmphub_modification_day: { attribute_value_list: ['2023-08-21'], comparison_operator: "IN" } + } + }, + logger: nil + } + expect(client).to have_received(:query).with(expected) + end + end + + describe '_process_search_response(response:)' do + it 'returns an empty Array unless :response is an Array' do + expect(described_class.send(:_process_search_response, response: { foo: 'bar' })).to eql([]) + end + it 'it adds a top level :dmp to each item' do + items = JSON.parse([{ title: 'Test one' }, { title: 'Test two' }].to_json) + result = described_class.send(:_process_search_response, response: items) + expect(result.length).to eql(2) + expect(result.first).to eql(JSON.parse({ dmp: items.first }.to_json)) + expect(result.last).to eql(JSON.parse({ dmp: items.last }.to_json)) + end + it 'calls Helper.cleanse_dmp_json for each item' do + items = JSON.parse([{ + PK: 'foo', + SK: 'bar', + title: 'Test one', + dmphub_provenance_id: 'baz', + dmphub_modification_day: '2020-01-02', + dmphub_owner_id: 'orcid', + dmphub_owner_org: 'ror', + dmphub_provenance_identifier: '12345', + dmphub_test: 'should go away', + dmphub_versions: ['one', 'two'], + dmphub_modifications: ['three', 'four'] + }].to_json) + result = described_class.send(:_process_search_response, response: items) + expect(result.length).to eql(1) + + expected = JSON.parse([{ + title: 'Test one', + dmphub_versions: ['one', 'two'], + dmphub_modifications: ['three', 'four'] + }].to_json) + expect(assert_dmps_match(obj_a: result.first, obj_b: expected, debug: false)) + end + it 'removes nils and dupicates' do + items = JSON.parse([{ title: 'Test one' }, { title: 'Test two' }, nil, { title: 'Test one' }].to_json) + result = described_class.send(:_process_search_response, response: items) + expect(result.length).to eql(2) + expect(result.first).to eql(JSON.parse({ dmp: items.first }.to_json)) + expect(result.last).to eql(JSON.parse({ dmp: items[1] }.to_json)) + end + end +end From f6e538bf8b04808f4a832caca795744626c4fcd9 Mon Sep 17 00:00:00 2001 From: briri Date: Tue, 22 Aug 2023 14:59:35 -0700 Subject: [PATCH 08/13] added spec tests for uc3-dmp-id updater --- src/sam/functions/ezid_publisher/app.rb | 3 +- .../uc3-dmp-api-core/spec/support/shared.rb | 3 +- .../lib/uc3-dmp-event-bridge/publisher.rb | 3 +- .../lib/uc3-dmp-event-bridge/version.rb | 2 +- .../uc3-dmp-event-bridge-0.0.6.gem | Bin 6144 -> 0 bytes .../uc3-dmp-event-bridge-0.0.7.gem | Bin 0 -> 6144 bytes src/sam/gems/uc3-dmp-id/.rspec_status | 259 +++++++++-------- src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id.rb | 12 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb | 10 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb | 16 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb | 15 + .../gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb | 52 ++-- .../gems/uc3-dmp-id/spec/support/shared.rb | 37 ++- .../spec/uc3-dmp-id/updater_spec.rb | 265 ++++++++++++++++++ 14 files changed, 502 insertions(+), 175 deletions(-) delete mode 100644 src/sam/gems/uc3-dmp-event-bridge/uc3-dmp-event-bridge-0.0.6.gem create mode 100644 src/sam/gems/uc3-dmp-event-bridge/uc3-dmp-event-bridge-0.0.7.gem create mode 100644 src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/updater_spec.rb diff --git a/src/sam/functions/ezid_publisher/app.rb b/src/sam/functions/ezid_publisher/app.rb index bc0f9075..4fbf1f1b 100644 --- a/src/sam/functions/ezid_publisher/app.rb +++ b/src/sam/functions/ezid_publisher/app.rb @@ -57,8 +57,7 @@ class EzidPublisher # "dmphub_provenance_id": "PROVENANCE#example", # "dmproadmap_links": { # "download": "https://example.com/api/dmps/12345.pdf", - # }, - # "dmphub_updater_is_provenance": false + # } # } # } # diff --git a/src/sam/gems/uc3-dmp-api-core/spec/support/shared.rb b/src/sam/gems/uc3-dmp-api-core/spec/support/shared.rb index 01b1913b..c94a9d33 100644 --- a/src/sam/gems/uc3-dmp-api-core/spec/support/shared.rb +++ b/src/sam/gems/uc3-dmp-api-core/spec/support/shared.rb @@ -62,8 +62,7 @@ def aws_event_bridge_event(args: {}) dmphub_provenance_id: 'PROVENANCE#foo', dmproadmap_links: { download: 'https://example.com/api/dmps/12345.pdf' - }, - dmphub_updater_is_provenance: false + } } details = details.merge(args['detail']) unless args['detail'].nil? diff --git a/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/publisher.rb b/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/publisher.rb index 3273a6f6..12aab608 100644 --- a/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/publisher.rb +++ b/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/publisher.rb @@ -60,8 +60,7 @@ def _generate_detail(dmp:) PK: dmp['PK'], SK: dmp['SK'], dmphub_provenance_id: dmp.fetch('dmphub_provenance_id', nil), - dmproadmap_links: dmp.fetch('dmproadmap_links', {}), - dmphub_updater_is_provenance: dmp.fetch('dmphub_updater_is_provenance', false) + dmproadmap_links: dmp.fetch('dmproadmap_links', {}) } end diff --git a/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/version.rb b/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/version.rb index 0187fc74..1f710290 100644 --- a/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/version.rb +++ b/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Uc3DmpEventBridge - VERSION = '0.0.6' + VERSION = '0.0.7' end diff --git a/src/sam/gems/uc3-dmp-event-bridge/uc3-dmp-event-bridge-0.0.6.gem b/src/sam/gems/uc3-dmp-event-bridge/uc3-dmp-event-bridge-0.0.6.gem deleted file mode 100644 index dc7700774507d41906948e26b281f3d0cf6ccbdb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6144 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 TjH~$N;}-(I5cq|_PetHgt@C6_ literal 0 HcmV?d00001 diff --git a/src/sam/gems/uc3-dmp-id/.rspec_status b/src/sam/gems/uc3-dmp-id/.rspec_status index 939d241b..823827be 100644 --- a/src/sam/gems/uc3-dmp-id/.rspec_status +++ b/src/sam/gems/uc3-dmp-id/.rspec_status @@ -1,78 +1,78 @@ example_id | status | run_time | -------------------------------------------- | ------ | --------------- | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:1] | passed | 0.00084 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:2] | passed | 0.00026 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:3] | passed | 0.00025 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:4] | passed | 0.00025 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:5] | passed | 0.0003 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:6] | passed | 0.00028 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:7] | passed | 0.00031 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:1] | passed | 0.00064 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:2] | passed | 0.00026 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:3] | passed | 0.00026 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:4] | passed | 0.00028 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:5] | passed | 0.00028 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:1] | passed | 0.00024 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:2] | passed | 0.00025 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:3] | passed | 0.00024 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:4] | passed | 0.00028 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:5] | passed | 0.00027 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:6] | passed | 0.00028 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:1] | passed | 0.00025 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:2] | passed | 0.00025 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:3] | passed | 0.00032 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:4] | passed | 0.00028 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:5] | passed | 0.00026 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:6] | passed | 0.0003 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:5:1] | passed | 0.00027 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:5:2] | passed | 0.00028 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:5:3] | passed | 0.0003 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:1:1] | passed | 0.00574 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:1:2] | passed | 0.00054 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:1:3] | passed | 0.00049 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:1:4] | passed | 0.00048 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:2:1] | passed | 0.00111 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:2:2] | passed | 0.0005 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:2:3] | passed | 0.00052 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:2:4] | passed | 0.00052 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:1] | passed | 0.00044 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:2] | passed | 0.00105 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:3] | passed | 0.00053 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:4] | passed | 0.0005 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:5] | passed | 0.0005 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:6] | passed | 0.00048 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:4:1] | passed | 0.00047 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:4:2] | passed | 0.00091 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:4:3] | passed | 0.00049 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:5:1] | passed | 0.0005 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:5:2] | passed | 0.0017 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:5:3] | passed | 0.0005 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:5:4] | passed | 0.00048 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:6:1] | passed | 0.00044 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:6:2] | passed | 0.00045 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:6:3] | passed | 0.00053 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:7:1] | passed | 0.00042 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:7:2] | passed | 0.00048 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:7:3] | passed | 0.00056 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:8:1] | passed | 0.00046 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:8:2] | passed | 0.00049 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:8:3] | passed | 0.00056 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:9:1] | passed | 0.00048 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:9:2] | passed | 0.00049 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:9:3] | passed | 0.00052 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:9:4] | passed | 0.00052 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:1] | passed | 0.0009 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:2] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:3] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:4] | passed | 0.0002 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:5] | passed | 0.00178 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:6] | passed | 0.00024 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:7] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:1] | passed | 0.00051 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:2] | passed | 0.00023 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:3] | passed | 0.00019 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:4] | passed | 0.00019 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:5] | passed | 0.00031 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:1] | passed | 0.00025 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:2] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:3] | passed | 0.0002 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:4] | passed | 0.00026 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:5] | passed | 0.00026 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:6] | passed | 0.00024 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:1] | passed | 0.00023 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:2] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:3] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:4] | passed | 0.00024 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:5] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:6] | passed | 0.00024 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:5:1] | passed | 0.00024 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:5:2] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:5:3] | passed | 0.00025 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:1] | passed | 0.00476 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:2] | passed | 0.00055 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:3] | passed | 0.00045 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:4] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:1] | passed | 0.00096 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:2] | passed | 0.00043 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:3] | passed | 0.00047 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:4] | passed | 0.00047 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:1] | passed | 0.00039 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:2] | passed | 0.00094 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:3] | passed | 0.00051 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:4] | passed | 0.00047 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:5] | passed | 0.00045 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:6] | passed | 0.00043 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:4:1] | passed | 0.00038 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:4:2] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:4:3] | passed | 0.00039 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:1] | passed | 0.00045 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:2] | passed | 0.00037 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:3] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:4] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:6:1] | passed | 0.00042 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:6:2] | passed | 0.00038 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:6:3] | passed | 0.00048 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:7:1] | passed | 0.00041 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:7:2] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:7:3] | passed | 0.00046 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:8:1] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:8:2] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:8:3] | passed | 0.0005 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:1] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:2] | passed | 0.00041 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:3] | passed | 0.00042 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:4] | passed | 0.00045 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:1:1] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:1:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:2:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:1:2] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:2:1] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:2:2] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:3:1] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:3:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:4:1] | passed | 0.00134 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:4:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:4:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:4:2] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:5:1] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:5:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:5:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:6:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:5:3] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:6:1] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:6:2] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:6:3] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:7:1] | passed | 0.00002 seconds | @@ -82,72 +82,107 @@ example_id | status | run_time | ./spec/uc3-dmp-id/helper_spec.rb[1:7:5] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:7:6] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:7:7] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:8:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:1] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:8:2] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:8:3] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:8:4] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:9:1] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:9:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:9:3] | passed | 0.00492 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:9:4] | passed | 0.00009 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:3] | passed | 0.00006 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:4] | passed | 0.00006 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:10:1] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:10:2] | passed | 0.00008 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:11:1] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:11:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:11:3] | passed | 0.00064 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:11:4] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:4] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:1] | passed | 0.00005 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:2] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:4] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:4] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:5] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:6] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:6] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:7] | passed | 0.00005 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:13:1] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:13:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:13:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:13:2] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:13:3] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:14:1] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:14:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:14:2] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:14:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:1] | passed | 0.00008 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:2] | passed | 0.00007 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:1] | passed | 0.00007 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:2] | passed | 0.00008 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:3] | passed | 0.0001 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:4] | passed | 0.00008 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:5] | passed | 0.00012 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:6] | passed | 0.00011 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:7] | passed | 0.00011 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:6] | passed | 0.00012 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:7] | passed | 0.00012 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:8] | passed | 0.00011 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:9] | passed | 0.00012 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:9] | passed | 0.00013 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:16:1] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:16:2] | passed | 0.00006 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:16:3] | passed | 0.00005 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:17:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:16:2] | passed | 0.00007 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:16:3] | passed | 0.00006 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:17:1] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:17:2] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:18:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:18:1] | passed | 0.00005 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:1] | passed | 0.00629 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:2] | passed | 0.00095 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:3] | passed | 0.01141 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:4] | passed | 0.00353 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:5] | passed | 0.00502 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:1] | passed | 0.00095 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:2] | passed | 0.00094 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:3] | passed | 0.00094 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:4] | passed | 0.00095 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:5] | passed | 0.00103 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:6] | passed | 0.00104 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:1] | passed | 0.00145 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:2] | passed | 0.001 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:3] | passed | 0.00109 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:4] | passed | 0.00097 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:5] | passed | 0.00144 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:6] | passed | 0.00134 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:1] | passed | 0.0008 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:2] | passed | 0.00067 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:3] | passed | 0.00093 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:4] | passed | 0.00098 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:5] | passed | 0.00102 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:6] | passed | 0.001 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:5:1] | passed | 0.00093 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:5:2] | passed | 0.00093 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:5:3] | passed | 0.00092 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:1] | passed | 0.00092 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:2] | passed | 0.00093 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:3] | passed | 0.0009 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:4] | passed | 0.00088 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:5] | passed | 0.00088 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:6] | passed | 0.001 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:7] | passed | 0.00118 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:8] | passed | 0.00121 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:9] | passed | 0.00109 seconds | ./spec/uc3-dmp-id/validator_spec.rb[1:1:1] | passed | 0.00032 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:2] | passed | 0.00029 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:3] | passed | 0.0003 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:4] | passed | 0.00029 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:5] | passed | 0.01068 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:6] | passed | 0.00079 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:2:1:1] | passed | 0.00073 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:2:1:2] | passed | 0.00005 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:2:1:3] | passed | 0.00064 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:1:1] | passed | 0.00336 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:1:2] | passed | 0.00077 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:1:3] | passed | 0.00062 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:2:1] | passed | 0.00766 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:2:2] | passed | 0.00709 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:2:3] | passed | 0.00688 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:1:1] | passed | 0.00166 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:1:2] | passed | 0.00213 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:1] | passed | 0.0034 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:2] | passed | 0.00296 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:3] | passed | 0.00251 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:4] | passed | 0.00298 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:5] | passed | 0.00296 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:1] | passed | 0.00164 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:2] | passed | 0.00148 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:3] | passed | 0.00148 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:4] | passed | 0.00152 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:5] | passed | 0.00153 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:2] | passed | 0.00034 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:3] | passed | 0.00036 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:4] | passed | 0.00035 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:5] | passed | 0.01175 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:6] | passed | 0.0008 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:2:1:1] | passed | 0.00076 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:2:1:2] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:2:1:3] | passed | 0.00067 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:1] | passed | 0.00506 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:2] | passed | 0.00203 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:3] | passed | 0.00146 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:1] | passed | 0.01428 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:2] | passed | 0.00745 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:3] | passed | 0.00732 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:1:1] | passed | 0.00058 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:1:2] | passed | 0.0009 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:1] | passed | 0.0012 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:2] | passed | 0.00099 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:3] | passed | 0.00105 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:4] | passed | 0.00103 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:5] | passed | 0.00103 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:1] | passed | 0.00052 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:2] | passed | 0.00052 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:3] | passed | 0.00053 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:4] | passed | 0.00054 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:5] | passed | 0.00056 seconds | diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id.rb index 59853557..23e9dcdf 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id.rb @@ -19,16 +19,6 @@ require 'uc3-dmp-id/schemas/author' module Uc3DmpId - MSG_DMP_EXISTS = 'DMP already exists. Try :update instead.' - MSG_DMP_FORBIDDEN = 'You do not have permission.' - MSG_DMP_INVALID_DMP_ID = 'Invalid DMP ID format.' - MSG_DMP_NO_DMP_ID = 'A DMP ID could not be registered at this time.' - MSG_DMP_NO_HISTORICALS = 'You cannot modify a historical version of the DMP.' - MSG_NO_OWNER_ORG = 'Could not determine ownership of the DMP ID.' - MSG_DMP_NO_TOMBSTONE = 'Unable to tombstone the DMP ID at this time.' - MSG_DMP_NO_UPDATE = 'Unable to update the DMP ID at this time.' - MSG_DMP_NOT_FOUND = 'DMP does not exist.' - MSG_DMP_UNABLE_TO_VERSION = 'Unable to version this DMP.' - MSG_DMP_UNKNOWN = 'DMP does not exist. Try :create instead.' + end # rubocop:enable Naming/FileName diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb index a0d6f506..d5681a2d 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb @@ -17,7 +17,7 @@ def create(provenance:, json:, logger: nil) raise CreatorError, MSG_NO_BASE_URL if ENV['DMP_ID_BASE_URL'].nil? # Fail if the provenance is not defined - raise DeleterError, MSG_DMP_FORBIDDEN unless provenance.is_a?(Hash) && !provenance['PK'].nil? + raise DeleterError, Helper::MSG_DMP_FORBIDDEN unless provenance.is_a?(Hash) && !provenance['PK'].nil? # Validate the incoming JSON first json = Helper.parse_json(json: json) @@ -25,12 +25,12 @@ def create(provenance:, json:, logger: nil) raise CreatorError, errs.join(', ') if errs.is_a?(Array) && errs.any? && errs.first != Validator::MSG_VALID_JSON # Fail if the provenance or owner affiliation are not defined - raise CreatorError, MSG_NO_PROVENANCE_OWNER if provenance.nil? + raise CreatorError, Helper::MSG_NO_PROVENANCE_OWNER if provenance.nil? # TODO: Swap this out with the Finder.exists? once the Dynamo indexes are working # Try to find it first and Fail if found result = Finder.by_json(json: json, logger: logger) - raise CreatorError, Uc3DmpId::MSG_DMP_EXISTS if result.is_a?(Hash) + raise CreatorError, Helper::MSG_DMP_EXISTS if result.is_a?(Hash) # raise CreatorError, Uc3DmpId::MSG_DMP_EXISTS unless json['PK'].nil? client = Uc3DmpDynamo::Client.new @@ -48,7 +48,7 @@ def create(provenance:, json:, logger: nil) # Create the item resp = client.put_item(json: annotated, logger: logger) - raise CreatorError, Uc3DmpId::MSG_DMP_NO_DMP_ID if resp.nil? + raise CreatorError, Helper::MSG_DMP_NO_DMP_ID if resp.nil? _post_process(json: annotated, logger: logger) Helper.cleanse_dmp_json(json: JSON.parse({ dmp: annotated }.to_json)) @@ -87,8 +87,6 @@ def _preregister_dmp_id(client:, provenance:, json:, logger: nil) def _post_process(json:, logger: nil) return false unless json.is_a?(Hash) - # We are creating, so this is always true - json['dmphub_updater_is_provenance'] = true # Publish the change to the EventBridge publisher = Uc3DmpEventBridge::Publisher.new publisher.publish(source: 'DmpCreator', event_type: 'EZID update', dmp: json, logger: logger) diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb index bcdb42c4..154568d1 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb @@ -12,20 +12,20 @@ class << self # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # ------------------------------------------------------------------------- def tombstone(provenance:, p_key:, logger: nil) - raise DeleterError, MSG_DMP_INVALID_DMP_ID unless p_key.is_a?(String) && !p_key.strip.empty? + raise DeleterError, Helper::MSG_DMP_INVALID_DMP_ID unless p_key.is_a?(String) && !p_key.strip.empty? # Fail if the provenance is not defined - raise DeleterError, MSG_DMP_FORBIDDEN unless provenance.is_a?(Hash) && !provenance['PK'].nil? + raise DeleterError, Helper::MSG_DMP_FORBIDDEN unless provenance.is_a?(Hash) && !provenance['PK'].nil? # Fetch the latest version of the DMP ID by it's PK client = Uc3DmpDynamo::Client.new dmp = Finder.by_pk(p_key: p_key, client: client, logger: logger) - raise DeleterError, MSG_DMP_NOT_FOUND unless dmp.is_a?(Hash) && !dmp['dmp'].nil? + raise DeleterError, Helper::MSG_DMP_NOT_FOUND unless dmp.is_a?(Hash) && !dmp['dmp'].nil? # Only allow this if the provenance is the owner of the DMP! - raise DeleterError, MSG_DMP_FORBIDDEN if dmp['dmp']['dmphub_provenance_id'] != provenance['PK'] + raise DeleterError, Helper::MSG_DMP_FORBIDDEN if dmp['dmp']['dmphub_provenance_id'] != provenance['PK'] # Make sure they're not trying to update a historical copy of the DMP - raise DeleterError, MSG_DMP_NO_HISTORICALS if dmp['dmp']['SK'] != Helper::DMP_LATEST_VERSION + raise DeleterError, Helper::MSG_DMP_NO_HISTORICALS if dmp['dmp']['SK'] != Helper::DMP_LATEST_VERSION # Annotate the DMP ID dmp['dmp']['SK'] = Helper::DMP_TOMBSTONE_VERSION @@ -39,7 +39,7 @@ def tombstone(provenance:, p_key:, logger: nil) # Create the Tombstone version resp = client.put_item(json: dmp, logger: logger) - raise DeleterError, MSG_DMP_NO_TOMBSTONE if resp.nil? + raise DeleterError, Helper::MSG_DMP_NO_TOMBSTONE if resp.nil? # Delete the Latest version resp = client.delete_item(p_key: p_key, s_key: Helper::SK_DMP_PREFIX, logger: logger) @@ -52,7 +52,7 @@ def tombstone(provenance:, p_key:, logger: nil) Helper.cleanse_dmp_json(json: JSON.parse({ dmp: dmp }.to_json)) rescue Aws::Errors::ServiceError => e logger.error(message: e.message, details: e.backtrace) unless logger.nil? - { status: 500, error: Messages::MSG_SERVER_ERROR } + { status: 500, error: Helper::MSG_SERVER_ERROR } end # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity @@ -63,8 +63,6 @@ def tombstone(provenance:, p_key:, logger: nil) def _post_process(json:, logger: nil) return false unless json.is_a?(Hash) - # Indicate whether or not the updater is the provenance system - json['dmphub_updater_is_provenance'] = true # Publish the change to the EventBridge publisher = Uc3DmpEventBridge::Publisher.new publisher.publish(source: 'DmpDeleter', event_type: 'EZID update', dmp: json, logger: logger) diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb index c3d7bb92..d4c19d79 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb @@ -24,6 +24,21 @@ class Helper DEFAULT_API_URL = 'https://api.dmphub.uc3dev.cdlib.net/dmps/' DEFAULT_LANDING_PAGE_URL = 'https://dmphub.uc3dev.cdlib.net/dmps/' + MSG_DMP_EXISTS = 'DMP already exists. Try :update instead.' + MSG_DMP_FORBIDDEN = 'You do not have permission.' + MSG_DMP_INVALID_DMP_ID = 'Invalid DMP ID format.' + MSG_DMP_NO_DMP_ID = 'A DMP ID could not be registered at this time.' + MSG_DMP_NO_HISTORICALS = 'You cannot modify a historical version of the DMP.' + MSG_DMP_NO_TOMBSTONE = 'Unable to tombstone the DMP ID at this time.' + MSG_DMP_NO_UPDATE = 'Unable to update the DMP ID at this time.' + MSG_DMP_NOT_FOUND = 'DMP does not exist.' + MSG_DMP_UNABLE_TO_VERSION = 'Unable to version this DMP.' + MSG_DMP_UNKNOWN = 'DMP does not exist. Try :create instead.' + MSG_NO_CHANGE = 'The updated record has no changes.' + MSG_NO_OWNER_ORG = 'Could not determine ownership of the DMP ID.' + MSG_NO_PROVENANCE_OWNER = 'Unable to determine the provenance of the DMP ID.' + MSG_SERVER_ERROR = 'Something went wrong.' + class << self # Append the PK prefix for the object # ------------------------------------------------------------------------------------- diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb index 0f1a3e81..0b07b618 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb @@ -13,7 +13,7 @@ class << self # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # ------------------------------------------------------------------------- def update(provenance:, p_key:, json: {}, note: nil, logger: nil) - raise UpdaterError, MSG_DMP_INVALID_DMP_ID unless p_key.is_a?(String) && !p_key.strip.empty? + raise UpdaterError, Helper::MSG_DMP_INVALID_DMP_ID unless p_key.is_a?(String) && !p_key.strip.empty? mods = Helper.parse_json(json: json).fetch('dmp', {}) p_key = Helper.append_pk_prefix(p_key: p_key) @@ -27,17 +27,17 @@ def update(provenance:, p_key:, json: {}, note: nil, logger: nil) # Verify that the DMP ID is updateable with the info passed in errs = _updateable?(provenance: provenance, p_key: p_key, latest_version: latest_version['dmp'], mods: mods['dmp']) - logger.error(message: errs.join(', ')) if errs.is_a?(Array) && errs.any? + logger.error(message: errs.join(', ')) if logger.respond_to?(:error) && errs.is_a?(Array) && errs.any? raise UpdaterError, errs if errs.is_a?(Array) && errs.any? # Don't continue if nothing has changed! - raise UpdaterError, MSG_NO_CHANGE if Helper.eql?(dmp_a: latest_version, dmp_b: mods) + raise UpdaterError, Helper::MSG_NO_CHANGE if Helper.eql?(dmp_a: latest_version, dmp_b: mods) # Version the DMP ID record (if applicable). owner = latest_version['dmphub_provenance_id'] updater = provenance['PK'] version = Versioner.generate_version(client: client, latest_version: latest_version, owner: owner, updater: updater, logger: logger) - raise UpdaterError, MSG_DMP_UNABLE_TO_VERSION if version.nil? + raise UpdaterError, Helper::MSG_DMP_UNABLE_TO_VERSION if version.nil? # Remove the version info because we don't want to save it on the record version.delete('dmphub_versions') @@ -52,7 +52,7 @@ def update(provenance:, p_key:, json: {}, note: nil, logger: nil) # Save the changes resp = client.put_item(json: version, logger: logger) - raise UpdaterError, MSG_DMP_UNABLE_TO_VERSION if resp.nil? + raise UpdaterError, Helper::MSG_DMP_UNABLE_TO_VERSION if resp.nil? # Send the updates to EZID _post_process(provenance: provenance, json: version, logger: logger) @@ -67,14 +67,14 @@ def update(provenance:, p_key:, json: {}, note: nil, logger: nil) # Save a DMP ID's corresponding narrative PDF document to S3 and add the download URL for that # document to the DMP ID's :dmpraodmap_related_identifiers array as an `is_metadata_for` relation def attach_narrative(provenance:, p_key:, url:, logger: nil) - raise UpdaterError, MSG_DMP_INVALID_DMP_ID unless p_key.is_a?(String) && !p_key.strip.empty? + raise UpdaterError, Helper::MSG_DMP_INVALID_DMP_ID unless p_key.is_a?(String) && !p_key.strip.empty? # fetch the existing latest version of the DMP ID client = Uc3DmpDynamo::Client.new(logger: logger) dmp = Finder.by_pk(p_key: p_key, client: client, logger: logger, cleanse: false) logger.info(message: "Existing latest record", details: dmp) if logger.respond_to?(:debug) - raise UpdaterError, MSG_DMP_FORBIDDEN unless provenance.is_a?(Hash) && !provenance['PK'].nil? && - provenance['Pk'] == dmp['dmphub_provenance_id'] + raise UpdaterError, Helper::MSG_DMP_FORBIDDEN unless provenance.is_a?(Hash) && !provenance['PK'].nil? && + provenance['PK'] == dmp['dmphub_provenance_id'] # Add the download URl for the PDF as a related identifier on the DMP ID record annotated = Helper.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp['dmp']) @@ -85,7 +85,7 @@ def attach_narrative(provenance:, p_key:, url:, logger: nil) # Save the changes without creating a new version! resp = client.put_item(json: annotated, logger: logger) - raise UpdaterError, MSG_DMP_UNABLE_TO_VERSION if resp.nil? + raise UpdaterError, Helper::MSG_DMP_UNABLE_TO_VERSION if resp.nil? logger.info(message: "Added DMP ID narrative for PK: #{p_key}, Narrative: #{url}") if logger.respond_to?(:debug) true @@ -99,11 +99,11 @@ def _updateable?(provenance:, p_key:, latest_version: {}, mods: {}) errs = Validator.validate(mode: 'author', json: JSON.parse({ dmp: mods }.to_json)) return errs.join(', ') if errs.is_a?(Array) && errs.any? && errs.first != Validator::MSG_VALID_JSON # Fail if the provenance is not defined - return [MSG_DMP_FORBIDDEN] unless provenance.is_a?(Hash) && !provenance['PK'].nil? + return [Helper::MSG_DMP_FORBIDDEN] unless provenance.is_a?(Hash) && !provenance['PK'].nil? # Verify that the JSON is for the same DMP in the PK - return [MSG_DMP_FORBIDDEN] unless Helper.dmp_id_to_pk(json: mods.fetch('dmp_id', {})) == p_key + return [Helper::MSG_DMP_FORBIDDEN] unless Helper.dmp_id_to_pk(json: mods.fetch('dmp_id', {})) == p_key # Bail out if the DMP ID could not be found or the PKs do not match for some reason - return [MSG_DMP_UNKNOWN] if latest_version.nil? || latest_version.fetch['PK'] != p_key + return [Helper::MSG_DMP_UNKNOWN] unless latest_version.is_a?(Hash) && latest_version['PK'] == p_key end def _process_modifications(owner:, updater:, version:, mods:, note: nil, logger: nil) @@ -116,34 +116,40 @@ def _process_modifications(owner:, updater:, version:, mods:, note: nil, logger: # Attach the incoming changes as an assertion to the DMP ID since the updater is NOT the owner updated = Asserter.add(updater: updater, dmp: version, mods: mods, note: note, logger: logger) if owner != updater - merge_versions(latest_version: version, mods: updated, logger: logger) + _merge_versions(latest_version: version, mods: updated, logger: logger) end # We are replacing the latest version with the modifcations but want to retain the PK, SK and any dmphub_ prefixed # entries in the metadata so that we do not lose creation timestamps, provenance ids, etc. - def merge_versions(latest_version:, mods:, logger: nil) - logger.debug(message: 'Modifications before merge.', details: mods) + def _merge_versions(latest_version:, mods:, logger: nil) + return mods unless latest_version.is_a?(Hash) + + logger.debug(message: 'Modifications before merge.', details: mods) if logger.respond_to?(:debug) keys_to_retain = latest_version.keys.select do |key| - (key.start_with?('dmphub_') && !%w[dmphub_assertions].include?(key)) || + (key.start_with?('dmphub_') && !%w[dmphub_modifications dmphub_versions].include?(key)) || key.start_with?('PK') || key.start_with?('SK') end keys_to_retain.each do |key| mods[key] = latest_version[key] end - logger.debug(message: 'Modifications after merge.', details: mods) + logger.debug(message: 'Modifications after merge.', details: mods) if logger.respond_to?(:debug) mods end # Once the DMP has been updated, we need to update it's DOI metadata # ------------------------------------------------------------------------- def _post_process(provenance:, json:, logger: nil) - return false unless json.is_a?(Hash) + return false unless json.is_a?(Hash) && provenance.is_a?(Hash) && !provenance['PK'].nil? && + !json['dmphub_provenance_id'].nil? + + publishable = provenance['PK'] == json['dmphub_provenance_id'] + return true unless publishable + # TODO: we will want to send and related_identifiers in :dmphub_modifications as well!!! - # Indicate whether or not the updater is the provenance system - json['dmphub_updater_is_provenance'] = provenance['PK'] == json['dmphub_provenance_id'] - # Publish the change to the EventBridge publisher = Uc3DmpEventBridge::Publisher.new - publisher.publish(source: 'DmpUpdater', event_type: 'EZID update', dmp: json, logger: logger) + # Publish the change to the EventBridge if the updater is the owner of the DMP ID + logger.debug(message: "Sending event for EZID publication", details: json) if publishable && logger.respond_to?(:debug) + publisher.publish(source: 'DmpUpdater', event_type: 'EZID update', dmp: json, logger: logger) if publishable # Determine if there are any related identifiers that we should try to fetch a citation for citable_identifiers = Helper.citable_related_identifiers(dmp: json) @@ -155,7 +161,7 @@ def _post_process(provenance:, json:, logger: nil) SK: json['SK'], dmproadmap_related_identifiers: citable_identifiers } - logger.debug(message: "Fetching citations", details: citable_identifiers) + logger.debug(message: "Sending event to fetch citations", details: citable_identifiers) if logger.respond_to?(:debug) publisher.publish(source: 'DmpUpdater', dmp: json, event_type: 'Citation Fetch', detail: citer_detail, logger: logger) true end diff --git a/src/sam/gems/uc3-dmp-id/spec/support/shared.rb b/src/sam/gems/uc3-dmp-id/spec/support/shared.rb index 91ac9671..9ffc13b3 100644 --- a/src/sam/gems/uc3-dmp-id/spec/support/shared.rb +++ b/src/sam/gems/uc3-dmp-id/spec/support/shared.rb @@ -3,8 +3,32 @@ require 'ostruct' require 'securerandom' +require 'uc3-dmp-dynamo' +require 'uc3-dmp-event-bridge' + # Mock S3 Resources -Uc3DmpDynamoClient = Struct.new('S3Client', :get_item, :put_item, :delete_item, :query) +Uc3DmpCloudwatchLogger = Struct.new('Uc3DmpCloudwatchLogger', :new, :debug, :info, :warn, :error) +Uc3DmpDynamoClient = Struct.new('Uc3DmpDynamoClient', :get_item, :put_item, :delete_item, :query) +Uc3DmpEventBridgePublisher = Struct.new('Uc3DmpEventBridgePublisher', :publish) + +def mock_dmp_id + domain = ENV.fetch('DMP_ID_BASE_URL', 'doi.org').gsub(%r{https?://}, '') + "#{domain}/#{rand(10...99)}.#{rand(10000...99999)}/#{SecureRandom.hex(6)}" +end + +def mock_dmp(minimal: false) + JSON.parse(File.read("#{Dir.pwd}/spec/support/json_mocks/#{minimal ? 'minimal' : 'complete'}.json")) +end + +def mock_logger(success: true) + lggr = Uc3DmpCloudwatchLogger.new + allow(lggr).to receive(:debug).and_return(success) + allow(lggr).to receive(:info).and_return(success) + allow(lggr).to receive(:warn).and_return(success) + allow(lggr).to receive(:error).and_return(success) + allow(Uc3DmpCloudwatch::Logger).to receive(:new).and_return(lggr) + lggr +end def mock_uc3_dmp_dynamo(dmp: mock_dmp, success: true) client = Uc3DmpDynamoClient.new @@ -18,13 +42,12 @@ def mock_uc3_dmp_dynamo(dmp: mock_dmp, success: true) client end -def mock_dmp_id - domain = ENV.fetch('DMP_ID_BASE_URL', 'doi.org').gsub(%r{https?://}, '') - "#{domain}/#{rand(10...99)}.#{rand(10000...99999)}/#{SecureRandom.hex(6)}" -end +def mock_uc3_dmp_event_bridge(success: true) + publisher = Uc3DmpEventBridgePublisher.new + allow(publisher).to receive(:publish).and_return(success) -def mock_dmp(minimal: false) - JSON.parse(File.read("#{Dir.pwd}/spec/support/json_mocks/#{minimal ? 'minimal' : 'complete'}.json")) + allow(Uc3DmpEventBridge::Publisher).to receive(:new).and_return(publisher) + publisher end # Helper to compare 2 hashes diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/updater_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/updater_spec.rb new file mode 100644 index 00000000..a2007ece --- /dev/null +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/updater_spec.rb @@ -0,0 +1,265 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Uc3DmpId::Updater' do + let!(:described_class) { Uc3DmpId::Updater } + let!(:updater_error) { Uc3DmpId::UpdaterError } + + let!(:client) { mock_uc3_dmp_dynamo(dmp: dmp) } + let!(:publisher) { mock_uc3_dmp_event_bridge } + + let!(:owner) { JSON.parse({ PK: 'PROVENANCE#foo', SK: 'PROFILE' }.to_json) } + let!(:updater) { JSON.parse({ PK: 'PROVENANCE#bar', SK: 'PROFILE' }.to_json) } + + let!(:p_key) { "#{Uc3DmpId::Helper::PK_DMP_PREFIX}#{mock_dmp_id}" } + + let!(:dmp) do + record = mock_dmp + record['dmp']['PK'] = p_key + record['dmp']['SK'] = "#{Uc3DmpId::Helper::DMP_LATEST_VERSION}" + record['dmp']['dmphub_provenance_id'] = owner['PK'] + record + end + + let!(:transferable_keys) do + dmp['dmp'].keys.select do |key| + %w[PK SK].include?(key) || (key.start_with?('dmphub_') && !%w[dmphub_modifications dmphub_versions].include?(key)) + end + end + let!(:mods) do + record = mock_dmp + transferable_keys.each { |key| record['dmp'].delete(key) } + record['dmp']['dmp_id'] = JSON.parse({ type: 'doi', identifier: Uc3DmpId::Helper.pk_to_dmp_id(p_key: p_key) }.to_json) + record['dmp']['description'] = 'Lorem ipsum ... TESTING' + record + end + + before do + ENV['DMP_ID_BASE_URL'] = 'https://doi.org' + end + + describe 'update(provenance:, p_key:, json: {}, note: nil, logger: nil)' do + it 'raises an UpdaterError when :p_key is not a String' do + expect { described_class.update(provenance: owner, p_key: 123, json: mods) }.to raise_error(updater_error) + end + it 'raises an UpdaterError when :updateable? returns errors' do + allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) + allow(described_class).to receive(:_updateable?).and_return(['foo']) + expect { described_class.update(provenance: owner, p_key: p_key, json: mods) }.to raise_error(updater_error) + end + it 'raises an UpdaterError (no changes) when :json is equal to the existing DMP ID' do + allow(described_class).to receive(:updateable?).and_return([]) + allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) + allow(Uc3DmpId::Helper).to receive(:eql?).and_return(true) + expect { described_class.update(provenance: owner, p_key: p_key, json: dmp) }.to raise_error(updater_error) + end + it 'raises an UpdaterError when Versioner.generate_version returns a nil' do + allow(described_class).to receive(:updateable?).and_return([]) + allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) + allow(Uc3DmpId::Helper).to receive(:eql?).and_return(false) + allow(Uc3DmpId::Versioner).to receive(:generate_version).and_return(nil) + expect { described_class.update(provenance: owner, p_key: p_key, json: mods) }.to raise_error(updater_error) + end + it 'saves the changes as the new latest version' do + allow(described_class).to receive(:updateable?).and_return([]) + allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp['dmp']) + allow(Uc3DmpId::Helper).to receive(:eql?).and_return(false) + allow(Uc3DmpId::Versioner).to receive(:generate_version).and_return(mods['dmp']) + allow(described_class).to receive(:_process_modifications).and_return(mods['dmp']) + allow(client).to receive(:put_item).and_return(mods['dmp']) + allow(described_class).to receive(:_post_process) + + now = Time.now.utc.iso8601 + result = described_class.update(provenance: owner, p_key: p_key, json: mods) + expect(result['dmp']['dmphub_versions']).to be(nil) + expect(result['dmp']['modified'] >= now).to be(true) + + expect(described_class).to have_received(:_process_modifications).once + allow(client).to receive(:put_item).once + expect(described_class).to have_received(:_post_process).once + end + end + + describe 'attach_narrative(provenance:, p_key:, url:, logger: nil)' do + let!(:url) { 'http://download.me/narrative.pdf '} + + it 'raises an UpdaterError when :p_key is not a String' do + expect { described_class.attach_narrative(provenance: owner, p_key: 123, url: url) }.to raise_error(updater_error) + end + it 'raises an UpdaterError when :provenance is not a Hash' do + expect { described_class.attach_narrative(provenance: owner['PK'], p_key: p_key, url: url) }.to raise_error(updater_error) + end + it 'raises an UpdaterError when :provenance does not contain a :PK' do + owner.delete('PK') + expect { described_class.attach_narrative(provenance: owner, p_key: p_key, url: url) }.to raise_error(updater_error) + end + it 'raises an UpdaterError when :provenance does not match the :dmphub_provenance_id of the DMP ID' do + allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp['dmp']) + expect { described_class.attach_narrative(provenance: updater, p_key: p_key, url: url) }.to raise_error(updater_error) + end + it 'raises an UpdaterError if the Uc3DmpDynamo::Client is unable to save the change' do + allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp['dmp']) + allow(Uc3DmpId::Helper).to receive(:annotate_dmp_json).and_return(dmp['dmp']) + allow(client).to receive(:put_item).and_return(nil) + expect { described_class.attach_narrative(provenance: owner, p_key: p_key, url: url) }.to raise_error(updater_error) + end + it 'adds the uploaded PDF\'s access :url to the DMP ID\'s :dmproadmap_related_identifiers' do + allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp['dmp']) + allow(Uc3DmpId::Helper).to receive(:annotate_dmp_json).and_return(dmp['dmp']) + allow(client).to receive(:put_item).and_return(dmp['dmp']) + described_class.attach_narrative(provenance: owner, p_key: p_key, url: url) + + expected = dmp['dmp'].dup + expected['dmproadmap_related_identifiers'] << JSON.parse({ + work_type: 'output_management_plan', descriptor: 'is_metadata_for', type: 'url', identifier: url + }.to_json) + expect(client).to have_received(:put_item).with({ json: expected, logger: nil }) + end + end + + describe '_updateable?(provenance:, p_key:, latest_version: {}, mods: {})' do + it 'returns validation errors if the Validator.validate failed' do + allow(Uc3DmpId::Validator).to receive(:validate).and_return(['foo', 'bar']) + result = described_class.send(:_updateable?, provenance: updater, p_key: p_key, latest_version: dmp['dmp'], + mods: mods['dmp']) + expect(result).to eql('foo, bar') + end + it 'returns a Forbidden message if the :provenance is not a Hash' do + allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) + result = described_class.send(:_updateable?, provenance: updater['PK'], p_key: p_key, latest_version: dmp['dmp'], + mods: mods['dmp']) + expect(result).to eql([Uc3DmpId::Helper::MSG_DMP_FORBIDDEN]) + end + it 'returns a Forbidden message if the :provenance does not have a :PK' do + allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) + updater.delete('PK') + result = described_class.send(:_updateable?, provenance: updater, p_key: p_key, latest_version: dmp['dmp'], + mods: mods['dmp']) + expect(result).to eql([Uc3DmpId::Helper::MSG_DMP_FORBIDDEN]) + end + it 'returns a Forbidden message if the mod\'s :dmp_id does not match the :p_key' do + allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) + mods['dmp'].delete('dmp_id') + result = described_class.send(:_updateable?, provenance: updater, p_key: p_key, latest_version: dmp['dmp'], + mods: mods['dmp']) + expect(result).to eql([Uc3DmpId::Helper::MSG_DMP_FORBIDDEN]) + end + it 'returns a Not Found message if the :latest_version is not a Hash' do + allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) + result = described_class.send(:_updateable?, provenance: updater, p_key: p_key, latest_version: 123, mods: mods['dmp']) + expect(result).to eql([Uc3DmpId::Helper::MSG_DMP_UNKNOWN]) + end + it 'returns a Not Found message if the :latest_version\'s :PK does not match the :p_key' do + dmp['dmp']['PK'] = "#{Uc3DmpId::Helper::PK_DMP_PREFIX}testing9876" + allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) + result = described_class.send(:_updateable?, provenance: updater, p_key: p_key, latest_version: dmp['dmp'], + mods: mods['dmp']) + expect(result).to eql([Uc3DmpId::Helper::MSG_DMP_UNKNOWN]) + end + end + + describe '_process_modifications(owner:, updater:, version:, mods:, note: nil, logger: nil)' do + it 'returns :version if :mods is not a Hash' do + result = described_class.send(:_process_modifications, owner: owner, updater: updater, version: dmp, mods: 123) + expect(result).to eql(dmp) + end + it 'returns :version if :updater is nil' do + result = described_class.send(:_process_modifications, owner: owner, updater: nil, version: dmp, mods: mods) + expect(result).to eql(dmp) + end + it 'returns :mods if :version is not a Hash' do + result = described_class.send(:_process_modifications, owner: owner, updater: updater, version: 123, mods: mods) + expect(result).to eql(mods) + end + it 'returns :mods if :owner is nil' do + result = described_class.send(:_process_modifications, owner: nil, updater: updater, version: dmp, mods: mods) + expect(result).to eql(mods) + end + it 'calls Asserter.splice if the updater is the owner' do + allow(described_class).to receive(:_merge_versions) + allow(Uc3DmpId::Asserter).to receive(:splice) + allow(Uc3DmpId::Asserter).to receive(:add) + result = described_class.send(:_process_modifications, owner: owner, updater: owner, version: dmp, mods: mods) + expect(Uc3DmpId::Asserter).to have_received(:splice).once + expect(Uc3DmpId::Asserter).not_to have_received(:add) + end + it 'calls Asserter.add if the updater is NOT the owner' do + allow(described_class).to receive(:_merge_versions) + allow(Uc3DmpId::Asserter).to receive(:splice) + allow(Uc3DmpId::Asserter).to receive(:add) + result = described_class.send(:_process_modifications, owner: owner, updater: updater, version: dmp, mods: mods) + expect(Uc3DmpId::Asserter).not_to have_received(:splice) + expect(Uc3DmpId::Asserter).to have_received(:add).once + end + end + + describe '_merge_versions(latest_version:, mods:, logger: nil)' do + it 'returns the :mods as-is if :latest_version if not a Hash' do + expect(described_class.send(:_merge_versions, latest_version: 123, mods: mods)).to eql(mods) + end + it 'returns the :mods as-is if :latest_version does not have a :PK and :SK' do + dmp['dmp'].delete('PK') + expect(described_class.send(:_merge_versions, latest_version: dmp['dmp'], mods: mods)).to eql(mods) + end + it 'adds the attributes from the :latest_version to the :mods' do + expected = mods + transferable_keys.each { |key| expected[key] = dmp['dmp'][key] } + expect(described_class.send(:_merge_versions, latest_version: dmp['dmp'], mods: mods)).to eql(expected) + end + end + + describe '_post_process(provenance:, json:, logger: nil)' do + it 'returns false unless :json is a Hash' do + expect(described_class.send(:_post_process, provenance: owner, json: 123)).to be(false) + end + it 'returns false unless :json contains :dmphub_provenance_id' do + dmp['dmp'].delete('dmphub_provenance_id') + expect(described_class.send(:_post_process, provenance: owner, json: dmp['dmp'])).to be(false) + end + it 'returns false unless :provenance is a Hash' do + expect(described_class.send(:_post_process, provenance: 123, json: dmp['dmp'])).to be(false) + end + it 'returns false unless :provenance contains :PK' do + owner.delete('PK') + expect(described_class.send(:_post_process, provenance: owner, json: dmp['dmp'])).to be(false) + end + it 'does not publish an `EZID update` event to EventBridge if the owner of the DMP ID is NOT the one making the update' do + described_class.send(:_post_process, provenance: updater, json: dmp['dmp']) + expect(publisher).not_to have_received(:publish) + end + it 'publishes an `EZID update` event to EventBridge if the owner of the DMP ID is the one making the update' do + described_class.send(:_post_process, provenance: owner, json: dmp['dmp']) + expected = { + dmp: dmp['dmp'], + source: 'DmpUpdater', + event_type: 'EZID update', + logger: nil + } + expect(publisher).to have_received(:publish).once.with(expected) + end + it 'does not send a `Citation Fetch` event if the owner of the DMP ID is NOT the one making the update' do + allow(Uc3DmpId::Helper).to receive(:citable_related_identifiers).and_return([]) + described_class.send(:_post_process, provenance: updater, json: dmp['dmp']) + expect(publisher).not_to have_received(:publish) + end + it 'does not publish a `Citation Fetch` event to EventBridge if there are no citable identifiers' do + allow(Uc3DmpId::Helper).to receive(:citable_related_identifiers).and_return([]) + described_class.send(:_post_process, provenance: owner, json: dmp['dmp']) + expect(publisher).to have_received(:publish).once + end + it 'publishes an `Citation Fetch` event to EventBridge if there are citable identifiers' do + ids = JSON.parse([{ work_type: 'dataset', descriptor: 'references', type: 'other', identifier: 'foo' }].to_json) + allow(Uc3DmpId::Helper).to receive(:citable_related_identifiers).and_return(ids) + described_class.send(:_post_process, provenance: owner, json: dmp['dmp']) + expected = { + dmp: dmp['dmp'], + detail: { PK: dmp['dmp']['PK'], SK: dmp['dmp']['SK'], dmproadmap_related_identifiers: ids}, + source: 'DmpUpdater', + event_type: 'Citation Fetch', + logger: nil + } + expect(publisher).to have_received(:publish).once.with(expected) + end + end +end From 02dd38ade1870d503ea82c57ea15f04aa9ad5966 Mon Sep 17 00:00:00 2001 From: briri Date: Tue, 22 Aug 2023 15:35:54 -0700 Subject: [PATCH 09/13] added spec tests for uc3-dmp-id deleter --- src/sam/gems/uc3-dmp-id/.rspec_status | 305 +++++++++--------- .../gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb | 11 +- .../spec/uc3-dmp-id/deleter_spec.rb | 83 +++++ 3 files changed, 246 insertions(+), 153 deletions(-) create mode 100644 src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/deleter_spec.rb diff --git a/src/sam/gems/uc3-dmp-id/.rspec_status b/src/sam/gems/uc3-dmp-id/.rspec_status index 823827be..16fa258f 100644 --- a/src/sam/gems/uc3-dmp-id/.rspec_status +++ b/src/sam/gems/uc3-dmp-id/.rspec_status @@ -1,188 +1,197 @@ example_id | status | run_time | -------------------------------------------- | ------ | --------------- | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:1] | passed | 0.0009 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:2] | passed | 0.00022 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:3] | passed | 0.00022 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:4] | passed | 0.0002 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:5] | passed | 0.00178 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:6] | passed | 0.00024 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:7] | passed | 0.00022 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:1] | passed | 0.00051 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:2] | passed | 0.00023 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:3] | passed | 0.00019 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:4] | passed | 0.00019 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:5] | passed | 0.00031 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:1] | passed | 0.00025 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:2] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:1] | passed | 0.00054 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:2] | passed | 0.00021 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:3] | passed | 0.00173 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:4] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:5] | passed | 0.00026 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:6] | passed | 0.00023 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:7] | passed | 0.00025 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:1] | passed | 0.0005 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:2] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:3] | passed | 0.0002 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:4] | passed | 0.00023 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:5] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:1] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:2] | passed | 0.00021 seconds | ./spec/uc3-dmp-id/asserter_spec.rb[1:3:3] | passed | 0.0002 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:4] | passed | 0.00026 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:5] | passed | 0.00026 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:6] | passed | 0.00024 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:1] | passed | 0.00023 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:2] | passed | 0.00022 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:3] | passed | 0.00022 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:4] | passed | 0.00024 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:5] | passed | 0.00022 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:6] | passed | 0.00024 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:5:1] | passed | 0.00024 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:5:2] | passed | 0.00022 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:5:3] | passed | 0.00025 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:1:1] | passed | 0.00476 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:1:2] | passed | 0.00055 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:1:3] | passed | 0.00045 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:1:4] | passed | 0.00044 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:2:1] | passed | 0.00096 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:2:2] | passed | 0.00043 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:2:3] | passed | 0.00047 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:2:4] | passed | 0.00047 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:1] | passed | 0.00039 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:2] | passed | 0.00094 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:3] | passed | 0.00051 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:4] | passed | 0.00047 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:5] | passed | 0.00045 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:6] | passed | 0.00043 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:4:1] | passed | 0.00038 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:4:2] | passed | 0.0004 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:4:3] | passed | 0.00039 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:5:1] | passed | 0.00045 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:5:2] | passed | 0.00037 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:5:3] | passed | 0.0004 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:5:4] | passed | 0.0004 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:6:1] | passed | 0.00042 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:6:2] | passed | 0.00038 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:6:3] | passed | 0.00048 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:7:1] | passed | 0.00041 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:7:2] | passed | 0.0004 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:7:3] | passed | 0.00046 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:8:1] | passed | 0.0004 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:8:2] | passed | 0.0004 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:8:3] | passed | 0.0005 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:9:1] | passed | 0.0004 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:9:2] | passed | 0.00041 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:9:3] | passed | 0.00042 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:9:4] | passed | 0.00045 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:1:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:4] | passed | 0.00023 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:5] | passed | 0.00021 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:6] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:1] | passed | 0.00019 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:2] | passed | 0.00019 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:3] | passed | 0.00019 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:4] | passed | 0.0002 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:5] | passed | 0.00019 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:6] | passed | 0.0002 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:5:1] | passed | 0.0002 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:5:2] | passed | 0.00019 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:5:3] | passed | 0.00021 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:1:1] | passed | 0.00668 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:1:2] | passed | 0.00075 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:1:3] | passed | 0.00067 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:1:4] | passed | 0.00061 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:1:5] | passed | 0.00059 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:1:6] | passed | 0.00061 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:1:7] | passed | 0.0017 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:2:1] | passed | 0.0006 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:2:2] | passed | 0.00068 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:1] | passed | 0.00356 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:2] | passed | 0.00043 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:3] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:4] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:1] | passed | 0.00087 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:2] | passed | 0.00041 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:3] | passed | 0.00042 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:4] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:1] | passed | 0.00049 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:2] | passed | 0.00099 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:3] | passed | 0.00052 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:4] | passed | 0.00048 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:5] | passed | 0.00048 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:6] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:4:1] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:4:2] | passed | 0.00042 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:4:3] | passed | 0.00048 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:1] | passed | 0.00041 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:2] | passed | 0.00041 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:3] | passed | 0.00043 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:4] | passed | 0.00048 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:6:1] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:6:2] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:6:3] | passed | 0.00051 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:7:1] | passed | 0.00042 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:7:2] | passed | 0.00042 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:7:3] | passed | 0.00049 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:8:1] | passed | 0.00045 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:8:2] | passed | 0.00046 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:8:3] | passed | 0.00049 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:1] | passed | 0.00046 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:2] | passed | 0.0005 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:3] | passed | 0.00092 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:4] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:1:1] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:1:2] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:2:1] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:2:2] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:3:1] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:3:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:4:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:4:1] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:4:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:5:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:5:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:5:3] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:5:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:5:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:5:3] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:6:1] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:6:2] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:6:3] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:1] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:7:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:3] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:4] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:5] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:6] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:7] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:8:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:8:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:8:3] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:8:4] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:4] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:5] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:6] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:7] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:2] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:4] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:9:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:9:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:9:3] | passed | 0.00006 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:9:4] | passed | 0.00006 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:10:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:3] | passed | 0.00008 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:9:4] | passed | 0.00007 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:10:1] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:10:2] | passed | 0.00008 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:11:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:11:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:2] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:11:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:11:4] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:1] | passed | 0.00005 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:4] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:2] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:3] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:4] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:5] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:5] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:6] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:7] | passed | 0.00005 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:7] | passed | 0.00006 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:13:1] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:13:2] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:13:3] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:14:1] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:14:2] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:14:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:1] | passed | 0.00007 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:1] | passed | 0.00008 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:2] | passed | 0.00008 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:3] | passed | 0.0001 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:4] | passed | 0.00008 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:5] | passed | 0.00012 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:6] | passed | 0.00012 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:7] | passed | 0.00012 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:8] | passed | 0.00011 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:8] | passed | 0.00012 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:9] | passed | 0.00013 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:16:1] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:16:2] | passed | 0.00007 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:16:3] | passed | 0.00006 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:17:1] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:17:2] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:17:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:17:2] | passed | 0.00005 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:18:1] | passed | 0.00005 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:1:1] | passed | 0.00629 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:1:2] | passed | 0.00095 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:1:3] | passed | 0.01141 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:1:4] | passed | 0.00353 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:1:5] | passed | 0.00502 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:2:1] | passed | 0.00095 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:2:2] | passed | 0.00094 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:2:3] | passed | 0.00094 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:2:4] | passed | 0.00095 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:2:5] | passed | 0.00103 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:2:6] | passed | 0.00104 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:3:1] | passed | 0.00145 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:3:2] | passed | 0.001 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:3:3] | passed | 0.00109 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:3:4] | passed | 0.00097 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:3:5] | passed | 0.00144 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:3:6] | passed | 0.00134 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:4:1] | passed | 0.0008 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:4:2] | passed | 0.00067 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:4:3] | passed | 0.00093 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:4:4] | passed | 0.00098 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:4:5] | passed | 0.00102 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:4:6] | passed | 0.001 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:5:1] | passed | 0.00093 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:5:2] | passed | 0.00093 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:5:3] | passed | 0.00092 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:1] | passed | 0.00092 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:2] | passed | 0.00093 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:3] | passed | 0.0009 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:4] | passed | 0.00088 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:5] | passed | 0.00088 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:6] | passed | 0.001 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:7] | passed | 0.00118 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:8] | passed | 0.00121 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:9] | passed | 0.00109 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:1] | passed | 0.00032 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:2] | passed | 0.00034 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:3] | passed | 0.00036 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:4] | passed | 0.00035 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:5] | passed | 0.01175 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:6] | passed | 0.0008 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:2:1:1] | passed | 0.00076 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:1] | passed | 0.00078 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:2] | passed | 0.00078 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:3] | passed | 0.01095 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:4] | passed | 0.00354 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:5] | passed | 0.00401 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:1] | passed | 0.00086 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:2] | passed | 0.00082 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:3] | passed | 0.00081 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:4] | passed | 0.00079 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:5] | passed | 0.00088 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:6] | passed | 0.00093 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:1] | passed | 0.00083 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:2] | passed | 0.00082 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:3] | passed | 0.0009 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:4] | passed | 0.00101 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:5] | passed | 0.00092 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:6] | passed | 0.00085 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:1] | passed | 0.00079 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:2] | passed | 0.00078 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:3] | passed | 0.00105 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:4] | passed | 0.00076 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:5] | passed | 0.00084 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:6] | passed | 0.00083 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:5:1] | passed | 0.00082 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:5:2] | passed | 0.00073 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:5:3] | passed | 0.00076 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:1] | passed | 0.00078 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:2] | passed | 0.0008 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:3] | passed | 0.0008 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:4] | passed | 0.00078 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:5] | passed | 0.0008 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:6] | passed | 0.0009 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:7] | passed | 0.00089 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:8] | passed | 0.00082 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:9] | passed | 0.00088 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:1] | passed | 0.00034 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:2] | passed | 0.00032 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:3] | passed | 0.00035 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:4] | passed | 0.00034 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:5] | passed | 0.00064 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:6] | passed | 0.00071 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:2:1:1] | passed | 0.00171 seconds | ./spec/uc3-dmp-id/validator_spec.rb[1:2:1:2] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:2:1:3] | passed | 0.00067 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:1:1] | passed | 0.00506 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:1:2] | passed | 0.00203 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:1:3] | passed | 0.00146 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:2:1] | passed | 0.01428 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:2:2] | passed | 0.00745 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:2:3] | passed | 0.00732 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:1:1] | passed | 0.00058 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:1:2] | passed | 0.0009 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:1] | passed | 0.0012 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:2:1:3] | passed | 0.00066 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:1] | passed | 0.00428 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:2] | passed | 0.00093 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:3] | passed | 0.00082 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:1] | passed | 0.01803 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:2] | passed | 0.00695 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:3] | passed | 0.00682 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:1:1] | passed | 0.00059 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:1:2] | passed | 0.00094 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:1] | passed | 0.00119 seconds | ./spec/uc3-dmp-id/versioner_spec.rb[1:2:2] | passed | 0.00099 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:3] | passed | 0.00105 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:4] | passed | 0.00103 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:5] | passed | 0.00103 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:3] | passed | 0.00102 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:4] | passed | 0.00165 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:5] | passed | 0.00145 seconds | ./spec/uc3-dmp-id/versioner_spec.rb[1:3:1] | passed | 0.00052 seconds | ./spec/uc3-dmp-id/versioner_spec.rb[1:3:2] | passed | 0.00052 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:3] | passed | 0.00053 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:4] | passed | 0.00054 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:3] | passed | 0.00051 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:4] | passed | 0.00055 seconds | ./spec/uc3-dmp-id/versioner_spec.rb[1:3:5] | passed | 0.00056 seconds | diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb index 154568d1..b4ec1fb9 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb @@ -29,16 +29,16 @@ def tombstone(provenance:, p_key:, logger: nil) # Annotate the DMP ID dmp['dmp']['SK'] = Helper::DMP_TOMBSTONE_VERSION - dmp['dmp']['dmphub_tombstoned_at'] = Time.now.utc.iso8601 - dmp['dmp']['title'] = "OBSOLETE: #{dmp['title']}" + dmp['dmp']['title'] = "OBSOLETE: #{dmp['dmp']['title']}" logger.info(message: "Tomstoning DMP ID: #{p_key}") if logger.respond_to?(:debug) # Set the :modified timestamps now = Time.now.utc.iso8601 - dmp['modified'] = now + dmp['dmp']['modified'] = now + dmp['dmp']['dmphub_tombstoned_at'] = now # Create the Tombstone version - resp = client.put_item(json: dmp, logger: logger) + resp = client.put_item(json: dmp['dmp'], logger: logger) raise DeleterError, Helper::MSG_DMP_NO_TOMBSTONE if resp.nil? # Delete the Latest version @@ -48,8 +48,9 @@ def tombstone(provenance:, p_key:, logger: nil) # Notify EZID about the removal _post_process(json: dmp, logger: logger) + # Return the tombstoned record - Helper.cleanse_dmp_json(json: JSON.parse({ dmp: dmp }.to_json)) + Helper.cleanse_dmp_json(json: dmp) rescue Aws::Errors::ServiceError => e logger.error(message: e.message, details: e.backtrace) unless logger.nil? { status: 500, error: Helper::MSG_SERVER_ERROR } diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/deleter_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/deleter_spec.rb new file mode 100644 index 00000000..5f963273 --- /dev/null +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/deleter_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Uc3DmpId::Deleter' do + let!(:described_class) { Uc3DmpId::Deleter } + let!(:deleter_error) { Uc3DmpId::DeleterError } + + let!(:client) { mock_uc3_dmp_dynamo(dmp: dmp) } + let!(:publisher) { mock_uc3_dmp_event_bridge } + + let!(:owner) { JSON.parse({ PK: 'PROVENANCE#foo', SK: 'PROFILE' }.to_json) } + let!(:updater) { JSON.parse({ PK: 'PROVENANCE#bar', SK: 'PROFILE' }.to_json) } + + let!(:p_key) { "#{Uc3DmpId::Helper::PK_DMP_PREFIX}#{mock_dmp_id}" } + + let!(:dmp) do + record = mock_dmp + record['dmp']['PK'] = p_key + record['dmp']['SK'] = "#{Uc3DmpId::Helper::DMP_LATEST_VERSION}" + record['dmp']['dmphub_provenance_id'] = owner['PK'] + record + end + + before do + ENV['DMP_ID_BASE_URL'] = 'https://doi.org' + end + + describe 'tombstone(provenance:, p_key:, logger: nil)' do + it 'raises an DeleterError when :p_key is not a String' do + expect { described_class.tombstone(provenance: owner, p_key: 123) }.to raise_error(deleter_error) + end + it 'raises an DeleterError when :provenance is not a Hash' do + expect { described_class.tombstone(provenance: owner, p_key: p_key) }.to raise_error(deleter_error) + end + it 'raises an DeleterError when :provenance does not have a :PK' do + expect { described_class.tombstone(provenance: owner, p_key: p_key) }.to raise_error(deleter_error) + end + it 'raises an DeleterError when the DMP ID could not be found' do + allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(nil) + expect { described_class.tombstone(provenance: owner, p_key: p_key) }.to raise_error(deleter_error) + end + it 'raises an DeleterError when :provenance does not match the DMP ID\'s :dmphub_provenance_id' do + allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) + expect { described_class.tombstone(provenance: updater, p_key: p_key) }.to raise_error(deleter_error) + end + it 'raises an DeleterError when it is not the latest version of the DMP ID' do + dmp['dmp']['SK'] = "#{Uc3DmpId::Helper::SK_DMP_PREFIX}2020-03-15T11:22:33Z" + allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) + expect { described_class.tombstone(provenance: owner, p_key: p_key) }.to raise_error(deleter_error) + end + it 'tombstones the DMP ID' do + allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) + allow(client).to receive(:put_item).and_return(dmp['dmp']) + allow(client).to receive(:delete_item).and_return(dmp['dmp']) + allow(described_class).to receive(:_post_process) + + now = Time.now.utc.iso8601 + result = described_class.tombstone(provenance: owner, p_key: p_key) + expect(result['dmp']['modified'] >= now).to be(true) + expect(result['dmp']['title'].start_with?('OBSOLETE: ')).to be(true) + + expect(client).to have_received(:put_item).once + expect(client).to have_received(:delete_item).once + end + end + + describe '_post_process(json:, logger: nil)' do + it 'returns false unless :json is a Hash' do + expect(described_class.send(:_post_process, json: 123)).to be(false) + end + it 'publishes an `EZID update` event to EventBridge if the owner of the DMP ID is the one making the update' do + described_class.send(:_post_process, json: dmp['dmp']) + expected = { + dmp: dmp['dmp'], + source: 'DmpDeleter', + event_type: 'EZID update', + logger: nil + } + expect(publisher).to have_received(:publish).once.with(expected) + end + end +end \ No newline at end of file From bb6f06f5ad90e5c53be7f2b08494892392c5a900 Mon Sep 17 00:00:00 2001 From: briri Date: Wed, 23 Aug 2023 08:34:36 -0700 Subject: [PATCH 10/13] added spec tests for the uc3-dmp-id creator --- src/sam/gems/uc3-dmp-id/.rspec_status | 16 ++ .../gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb | 18 +-- .../spec/uc3-dmp-id/creator_spec.rb | 137 ++++++++++++++++++ 3 files changed, 161 insertions(+), 10 deletions(-) create mode 100644 src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/creator_spec.rb diff --git a/src/sam/gems/uc3-dmp-id/.rspec_status b/src/sam/gems/uc3-dmp-id/.rspec_status index 16fa258f..f1287843 100644 --- a/src/sam/gems/uc3-dmp-id/.rspec_status +++ b/src/sam/gems/uc3-dmp-id/.rspec_status @@ -27,6 +27,22 @@ example_id | status | run_time | ./spec/uc3-dmp-id/asserter_spec.rb[1:5:1] | passed | 0.0002 seconds | ./spec/uc3-dmp-id/asserter_spec.rb[1:5:2] | passed | 0.00019 seconds | ./spec/uc3-dmp-id/asserter_spec.rb[1:5:3] | passed | 0.00021 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:1] | passed | 0.00665 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:2] | passed | 0.00066 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:3] | passed | 0.00055 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:4] | passed | 0.00055 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:5] | passed | 0.00058 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:6] | passed | 0.00064 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:7] | passed | 0.0006 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:8] | passed | 0.00068 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:9] | passed | 0.00156 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:2:1] | passed | 0.0013 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:2:2] | passed | 0.00066 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:2:3] | passed | 0.00206 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:3:1] | passed | 0.00058 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:3:2] | passed | 0.00125 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:3:3] | passed | 0.0006 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:3:4] | passed | 0.00064 seconds | ./spec/uc3-dmp-id/deleter_spec.rb[1:1:1] | passed | 0.00668 seconds | ./spec/uc3-dmp-id/deleter_spec.rb[1:1:2] | passed | 0.00075 seconds | ./spec/uc3-dmp-id/deleter_spec.rb[1:1:3] | passed | 0.00067 seconds | diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb index d5681a2d..e3b17a50 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb @@ -17,19 +17,16 @@ def create(provenance:, json:, logger: nil) raise CreatorError, MSG_NO_BASE_URL if ENV['DMP_ID_BASE_URL'].nil? # Fail if the provenance is not defined - raise DeleterError, Helper::MSG_DMP_FORBIDDEN unless provenance.is_a?(Hash) && !provenance['PK'].nil? + raise CreatorError, Helper::MSG_DMP_FORBIDDEN unless provenance.is_a?(Hash) && !provenance['PK'].nil? # Validate the incoming JSON first json = Helper.parse_json(json: json) errs = Validator.validate(mode: 'author', json: json) raise CreatorError, errs.join(', ') if errs.is_a?(Array) && errs.any? && errs.first != Validator::MSG_VALID_JSON - # Fail if the provenance or owner affiliation are not defined - raise CreatorError, Helper::MSG_NO_PROVENANCE_OWNER if provenance.nil? - - # TODO: Swap this out with the Finder.exists? once the Dynamo indexes are working - # Try to find it first and Fail if found - result = Finder.by_json(json: json, logger: logger) + # Try to find it by the :dmp_id first and Fail if found + dmp_id = Helper.dmp_id_to_pk(json: json.fetch('dmp', {})['dmp_id']) + result = Finder.exists?(json: dmp_id, logger: logger) unless dmp_id.nil? raise CreatorError, Helper::MSG_DMP_EXISTS if result.is_a?(Hash) # raise CreatorError, Uc3DmpId::MSG_DMP_EXISTS unless json['PK'].nil? @@ -62,7 +59,8 @@ def _preregister_dmp_id(client:, provenance:, json:, logger: nil) seed_id = json.fetch('dmp', {})['dmproadmap_external_system_identifier'] # If we are seeding already registered DMP IDs from the Provenance system, then return the original DMP ID - logger.debug(message: "Seeding DMP ID with #{seed_id.gsub(%r{https?://}, '')}") if seeding && !seed_id.nil? + logger.debug(message: "Seeding DMP ID with #{seed_id.gsub(%r{https?://}, '')}") if logger.respond_to?(:debug) && + seeding && !seed_id.nil? return seed_id.gsub(%r{https?://}, '') if seeding && !seed_id.nil? #Generate a new DMP ID @@ -101,8 +99,8 @@ def _post_process(json:, logger: nil) SK: json['SK'], dmproadmap_related_identifiers: citable_identifiers } - logger.debug(message: "Fetching citations", details: citable_identifiers) - publisher.publish(source: 'DmpUpdater', dmp: json, event_type: 'Citation Fetch', detail: citer_detail, logger: logger) + logger.debug(message: "Fetching citations", details: citable_identifiers) if logger.respond_to?(:debug) + publisher.publish(source: 'DmpCreator', dmp: json, event_type: 'Citation Fetch', detail: citer_detail, logger: logger) true end end diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/creator_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/creator_spec.rb new file mode 100644 index 00000000..fc510969 --- /dev/null +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/creator_spec.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Uc3DmpId::Creator' do + let!(:described_class) { Uc3DmpId::Creator } + let!(:creator_error) { Uc3DmpId::CreatorError } + + let!(:client) { mock_uc3_dmp_dynamo(dmp: dmp) } + let!(:publisher) { mock_uc3_dmp_event_bridge } + + let!(:owner) { JSON.parse({ PK: 'PROVENANCE#foo', SK: 'PROFILE' }.to_json) } + + let!(:p_key) { "#{Uc3DmpId::Helper::PK_DMP_PREFIX}#{mock_dmp_id}" } + + let!(:dmp) { mock_dmp } + + before do + ENV['DMP_ID_SHOULDER'] = '11.22222/33' + ENV['DMP_ID_BASE_URL'] = 'https://doi.org' + end + + describe 'create(provenance:, json:, logger: nil)' do + it 'raises a CreatorError if the `DMP_ID_SHOULDER` ENV variable is not defined' do + ENV.delete('DMP_ID_SHOULDER') + expect { described_class.create(provenance: owner, json: dmp) }.to raise_error(creator_error) + end + it 'raises a CreatorError if the `DMP_ID_BASE_URL` ENV variable is not defined' do + ENV.delete('DMP_ID_BASE_URL') + expect { described_class.create(provenance: owner, json: dmp) }.to raise_error(creator_error) + end + it 'raises a CreatorError if the :provenance is not a Hash' do + expect { described_class.create(provenance: 123, json: dmp) }.to raise_error(creator_error) + end + it 'raises a CreatorError if the :provenance does not contain a :PK' do + owner.delete('PK') + expect { described_class.create(provenance: owner, json: dmp) }.to raise_error(creator_error) + end + it 'raises a CreatorError if the Uc3DmpId::Validator returns errors' do + allow(Uc3DmpId::Validator).to receive(:validate).and_return(['foo']) + expect { described_class.create(provenance: owner, json: dmp) }.to raise_error(creator_error) + end + it 'raises a CreatorError if the Uc3DmpId::Finder finds a matching :dmp_id' do + dmp['dmp']['dmp_id'] = 'https://dx.doi.org/11.1234/A1B2c3' + allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) + allow(Uc3DmpId::Finder).to receive(:exists?).and_return(true) + expect { described_class.create(provenance: owner, json: dmp) }.to raise_error(creator_error) + end + it 'raises a CreatorError if unable to generate the :PK' do + allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) + allow(Uc3DmpId::Finder).to receive(:exists?).and_return(false) + allow(described_class).to receive(:_preregister_dmp_id).and_return(nil) + expect { described_class.create(provenance: owner, json: dmp) }.to raise_error(creator_error) + end + it 'raises a CreatorError if Dynamo could not save the DMP ID record' do + allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) + allow(Uc3DmpId::Finder).to receive(:exists?).and_return(false) + allow(described_class).to receive(:_preregister_dmp_id).and_return(p_key) + allow(Uc3DmpId::Helper).to receive(:annotate_dmp_json).and_return(dmp) + allow(client).to receive(:put_item).and_return(nil) + expect { described_class.create(provenance: owner, json: dmp) }.to raise_error(creator_error) + end + it 'it creates the new DMP ID' do + allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) + allow(Uc3DmpId::Finder).to receive(:exists?).and_return(false) + allow(described_class).to receive(:_preregister_dmp_id).and_return(p_key) + allow(Uc3DmpId::Helper).to receive(:annotate_dmp_json).and_return(dmp) + allow(client).to receive(:put_item).and_return(dmp) + allow(described_class).to receive(:_post_process).and_return(true) + + now = Time.now.utc.iso8601 + result = described_class.create(provenance: owner, json: dmp) + expect(result['dmp']['created'] >= now).to be(true) + expect(result['dmp']['modified'] >= now).to be(true) + end + end + + describe '_preregister_dmp_id(client:, provenance:, json:, logger: nil)' do + it 'returns the DMP ID sent in by the provenance system if the provenance is Seeding with live DMPs' do + owner['seedingWithLiveDmpIds'] = true + dmp['dmp']['dmproadmap_external_system_identifier'] = 'http://doi.org/SEEDING-ID' + + result = described_class.send(:_preregister_dmp_id, client: client, provenance: owner, json: dmp) + expect(result).to eql('doi.org/SEEDING-ID') + end + it 'raises a CreatorError if a unique DMP ID could not be generated after 10 attempts' do + owner['seedingWithLiveDmpIds'] = false + allow(Uc3DmpId::Finder).to receive(:exists?).and_return(true) + expect { described_class.send(:_preregister_dmp_id, client: client, provenance: owner, json: dmp) }.to raise_error(creator_error) + end + it 'returns a new DMP ID' do + owner['seedingWithLiveDmpIds'] = false + allow(Uc3DmpId::Finder).to receive(:exists?).and_return(false) + result = described_class.send(:_preregister_dmp_id, client: client, provenance: owner, json: dmp) + + expected_prefix = "#{Uc3DmpId::Helper::PK_DMP_PREFIX}#{ENV['DMP_ID_BASE_URL'].gsub(%r{https?://}, '')}" + expect(result.start_with?(expected_prefix)).to be(true) + suffix = result.gsub(expected_prefix, '') + expect(suffix =~ Uc3DmpId::Helper::DOI_REGEX).to eql(1) + expect(suffix.start_with?("/#{ENV['DMP_ID_SHOULDER']}")).to be(true) + end + end + + describe '_post_process(json:, logger: nil)' do + it 'returns false unless :json is a Hash' do + expect(described_class.send(:_post_process, json: 123)).to be(false) + end + it 'publishes an `EZID update` event to EventBridge if the owner of the DMP ID is the one making the update' do + described_class.send(:_post_process, json: dmp['dmp']) + expected = { + dmp: dmp['dmp'], + source: 'DmpCreator', + event_type: 'EZID update', + logger: nil + } + expect(publisher).to have_received(:publish).once.with(expected) + end + it 'does not publish a `Citation Fetch` event to EventBridge if there are no citable identifiers' do + allow(Uc3DmpId::Helper).to receive(:citable_related_identifiers).and_return([]) + described_class.send(:_post_process, json: dmp['dmp']) + expect(publisher).to have_received(:publish).once + end + it 'publishes an `Citation Fetch` event to EventBridge if there are citable identifiers' do + ids = JSON.parse([{ work_type: 'dataset', descriptor: 'references', type: 'other', identifier: 'foo' }].to_json) + allow(Uc3DmpId::Helper).to receive(:citable_related_identifiers).and_return(ids) + described_class.send(:_post_process, json: dmp['dmp']) + expected = { + dmp: dmp['dmp'], + detail: { PK: dmp['dmp']['PK'], SK: dmp['dmp']['SK'], dmproadmap_related_identifiers: ids}, + source: 'DmpCreator', + event_type: 'Citation Fetch', + logger: nil + } + expect(publisher).to have_received(:publish).once.with(expected) + end + end +end From 1b96daa4b9523d82c4adf0cd273208952e228a9e Mon Sep 17 00:00:00 2001 From: briri Date: Thu, 24 Aug 2023 16:29:50 -0700 Subject: [PATCH 11/13] added spec tests for uc3-dmp-id integration testing and updated rubocop --- .../lib/uc3-dmp-event-bridge/publisher.rb | 2 +- src/sam/gems/uc3-dmp-id/.rspec_status | 344 +-- src/sam/gems/uc3-dmp-id/Gemfile | 5 + src/sam/gems/uc3-dmp-id/Gemfile.lock | 10 +- src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id.rb | 2 +- .../uc3-dmp-id/lib/uc3-dmp-id/asserter.rb | 54 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb | 20 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb | 10 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/finder.rb | 45 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb | 16 +- .../lib/uc3-dmp-id/schemas/amend.rb | 536 ++-- .../lib/uc3-dmp-id/schemas/author.rb | 2724 +++++++++-------- .../gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb | 67 +- .../uc3-dmp-id/lib/uc3-dmp-id/validator.rb | 10 +- .../gems/uc3-dmp-id/lib/uc3-dmp-id/version.rb | 2 +- .../uc3-dmp-id/lib/uc3-dmp-id/versioner.rb | 23 +- .../spec/support/mock_data_store.rb | 70 + .../uc3-dmp-id/spec/support/mock_event_bus.rb | 24 + .../gems/uc3-dmp-id/spec/support/shared.rb | 18 +- .../spec/uc3-dmp-id/asserter_spec.rb | 125 +- .../spec/uc3-dmp-id/creator_spec.rb | 26 +- .../spec/uc3-dmp-id/deleter_spec.rb | 14 +- .../uc3-dmp-id/spec/uc3-dmp-id/finder_spec.rb | 158 +- .../uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb | 167 +- .../spec/uc3-dmp-id/integration_spec.rb | 297 ++ .../spec/uc3-dmp-id/updater_spec.rb | 81 +- .../spec/uc3-dmp-id/validator_spec.rb | 1 - .../spec/uc3-dmp-id/versioner_spec.rb | 47 +- .../gems/uc3-dmp-id/uc3-dmp-id-0.0.140.gem | Bin 31232 -> 0 bytes src/sam/gems/uc3-dmp-id/uc3-dmp-id.gemspec | 10 +- 30 files changed, 2808 insertions(+), 2100 deletions(-) create mode 100644 src/sam/gems/uc3-dmp-id/spec/support/mock_data_store.rb create mode 100644 src/sam/gems/uc3-dmp-id/spec/support/mock_event_bus.rb create mode 100644 src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/integration_spec.rb delete mode 100644 src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.0.140.gem diff --git a/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/publisher.rb b/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/publisher.rb index 12aab608..81196675 100644 --- a/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/publisher.rb +++ b/src/sam/gems/uc3-dmp-event-bridge/lib/uc3-dmp-event-bridge/publisher.rb @@ -33,7 +33,7 @@ def publish(source:, dmp:, event_type: DEFAULT_EVENT_TYPE, detail: nil, logger: message = { entries: [{ - time: Time.now, + time: Time.now.utc.iso8601, source: "#{ENV.fetch('DOMAIN', nil)}:lambda:event_publisher", detail_type: event_type.to_s, detail: detail, diff --git a/src/sam/gems/uc3-dmp-id/.rspec_status b/src/sam/gems/uc3-dmp-id/.rspec_status index f1287843..5367dc5c 100644 --- a/src/sam/gems/uc3-dmp-id/.rspec_status +++ b/src/sam/gems/uc3-dmp-id/.rspec_status @@ -1,113 +1,113 @@ example_id | status | run_time | -------------------------------------------- | ------ | --------------- | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:1] | passed | 0.00054 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:2] | passed | 0.00021 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:3] | passed | 0.00173 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:4] | passed | 0.00022 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:5] | passed | 0.00026 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:6] | passed | 0.00023 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:1:7] | passed | 0.00025 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:1] | passed | 0.0005 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:2] | passed | 0.00022 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:3] | passed | 0.0002 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:4] | passed | 0.00023 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:2:5] | passed | 0.00022 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:1] | passed | 0.00022 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:2] | passed | 0.00021 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:3] | passed | 0.0002 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:4] | passed | 0.00023 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:5] | passed | 0.00021 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:3:6] | passed | 0.00022 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:1] | passed | 0.00019 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:2] | passed | 0.00019 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:3] | passed | 0.00019 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:4] | passed | 0.0002 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:5] | passed | 0.00019 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:4:6] | passed | 0.0002 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:5:1] | passed | 0.0002 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:5:2] | passed | 0.00019 seconds | -./spec/uc3-dmp-id/asserter_spec.rb[1:5:3] | passed | 0.00021 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:1:1] | passed | 0.00665 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:1:2] | passed | 0.00066 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:1:3] | passed | 0.00055 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:1:4] | passed | 0.00055 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:1:5] | passed | 0.00058 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:1:6] | passed | 0.00064 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:1:7] | passed | 0.0006 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:1:8] | passed | 0.00068 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:1:9] | passed | 0.00156 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:2:1] | passed | 0.0013 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:2:2] | passed | 0.00066 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:2:3] | passed | 0.00206 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:3:1] | passed | 0.00058 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:3:2] | passed | 0.00125 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:3:3] | passed | 0.0006 seconds | -./spec/uc3-dmp-id/creator_spec.rb[1:3:4] | passed | 0.00064 seconds | -./spec/uc3-dmp-id/deleter_spec.rb[1:1:1] | passed | 0.00668 seconds | -./spec/uc3-dmp-id/deleter_spec.rb[1:1:2] | passed | 0.00075 seconds | -./spec/uc3-dmp-id/deleter_spec.rb[1:1:3] | passed | 0.00067 seconds | -./spec/uc3-dmp-id/deleter_spec.rb[1:1:4] | passed | 0.00061 seconds | -./spec/uc3-dmp-id/deleter_spec.rb[1:1:5] | passed | 0.00059 seconds | -./spec/uc3-dmp-id/deleter_spec.rb[1:1:6] | passed | 0.00061 seconds | -./spec/uc3-dmp-id/deleter_spec.rb[1:1:7] | passed | 0.0017 seconds | -./spec/uc3-dmp-id/deleter_spec.rb[1:2:1] | passed | 0.0006 seconds | -./spec/uc3-dmp-id/deleter_spec.rb[1:2:2] | passed | 0.00068 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:1:1] | passed | 0.00356 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:1:2] | passed | 0.00043 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:1:3] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:1] | passed | 0.0007 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:2] | passed | 0.0005 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:3] | passed | 0.00021 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:4] | passed | 0.0002 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:5] | passed | 0.00024 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:6] | passed | 0.00019 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:1:7] | passed | 0.00021 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:1] | passed | 0.0002 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:2] | passed | 0.0002 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:3] | passed | 0.00017 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:4] | passed | 0.0002 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:2:5] | passed | 0.00024 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:1] | passed | 0.0002 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:2] | passed | 0.00016 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:3] | passed | 0.00017 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:4] | passed | 0.0002 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:5] | passed | 0.00019 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:3:6] | passed | 0.0002 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:1] | passed | 0.00016 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:2] | passed | 0.00016 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:3] | passed | 0.00015 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:4] | passed | 0.00018 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:5] | passed | 0.00017 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:4:6] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:5:1] | passed | 0.00079 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:5:2] | passed | 0.00022 seconds | +./spec/uc3-dmp-id/asserter_spec.rb[1:5:3] | passed | 0.00023 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:1] | passed | 0.00453 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:2] | passed | 0.00041 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:3] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:4] | passed | 0.00038 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:5] | passed | 0.00041 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:6] | passed | 0.00055 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:7] | passed | 0.00046 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:8] | passed | 0.0005 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:1:9] | passed | 0.00069 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:2:1] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:2:2] | passed | 0.00055 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:2:3] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:3:1] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:3:2] | passed | 0.00091 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:3:3] | passed | 0.00046 seconds | +./spec/uc3-dmp-id/creator_spec.rb[1:3:4] | passed | 0.00049 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:1:1] | passed | 0.00041 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:1:2] | passed | 0.0004 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:1:3] | passed | 0.00115 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:1:4] | passed | 0.00045 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:1:5] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:1:6] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:1:7] | passed | 0.00056 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:2:1] | passed | 0.00038 seconds | +./spec/uc3-dmp-id/deleter_spec.rb[1:2:2] | passed | 0.00046 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:1] | passed | 0.00034 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:2] | passed | 0.00035 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:1:3] | passed | 0.00048 seconds | ./spec/uc3-dmp-id/finder_spec.rb[1:1:4] | passed | 0.0004 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:2:1] | passed | 0.00087 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:2:2] | passed | 0.00041 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:2:3] | passed | 0.00042 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:2:4] | passed | 0.0004 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:1] | passed | 0.00049 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:2] | passed | 0.00099 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:3] | passed | 0.00052 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:4] | passed | 0.00048 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:3:5] | passed | 0.00048 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:1] | passed | 0.00043 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:2] | passed | 0.00042 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:3] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:2:4] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:1] | passed | 0.00037 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:2] | passed | 0.00049 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:3] | passed | 0.00047 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:4] | passed | 0.00045 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:3:5] | passed | 0.00044 seconds | ./spec/uc3-dmp-id/finder_spec.rb[1:3:6] | passed | 0.00044 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:4:1] | passed | 0.0004 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:4:2] | passed | 0.00042 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:4:3] | passed | 0.00048 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:5:1] | passed | 0.00041 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:5:2] | passed | 0.00041 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:5:3] | passed | 0.00043 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:5:4] | passed | 0.00048 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:6:1] | passed | 0.0004 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:6:2] | passed | 0.0004 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:6:3] | passed | 0.00051 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:7:1] | passed | 0.00042 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:7:2] | passed | 0.00042 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:4:1] | passed | 0.00037 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:4:2] | passed | 0.00039 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:4:3] | passed | 0.00041 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:1] | passed | 0.00039 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:2] | passed | 0.00038 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:3] | passed | 0.00041 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:5:4] | passed | 0.00042 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:6:1] | passed | 0.00041 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:6:2] | passed | 0.00041 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:6:3] | passed | 0.00053 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:7:1] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:7:2] | passed | 0.00038 seconds | ./spec/uc3-dmp-id/finder_spec.rb[1:7:3] | passed | 0.00049 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:8:1] | passed | 0.00045 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:8:2] | passed | 0.00046 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:8:3] | passed | 0.00049 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:9:1] | passed | 0.00046 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:9:2] | passed | 0.0005 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:9:3] | passed | 0.00092 seconds | -./spec/uc3-dmp-id/finder_spec.rb[1:9:4] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:8:1] | passed | 0.0005 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:8:2] | passed | 0.00041 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:8:3] | passed | 0.0005 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:1] | passed | 0.00042 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:2] | passed | 0.00043 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:3] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/finder_spec.rb[1:9:4] | passed | 0.00043 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:1:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:1:2] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:1:2] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:2:1] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:2:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:3:1] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:3:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:2:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:3:1] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:3:2] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:4:1] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:4:2] | passed | 0.00002 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:4:2] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:5:1] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:5:2] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:5:3] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:6:1] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:6:2] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:6:3] | passed | 0.00002 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:1] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:2] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:6:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:6:3] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:1] | passed | 0.00037 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:2] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:3] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:7:4] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:7:5] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:7:5] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:7:6] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:7:7] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:8:1] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:8:1] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:8:2] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:8:3] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:8:4] | passed | 0.00003 seconds | @@ -116,98 +116,98 @@ example_id | status | run_time | ./spec/uc3-dmp-id/helper_spec.rb[1:9:3] | passed | 0.00008 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:9:4] | passed | 0.00007 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:10:1] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:10:2] | passed | 0.00008 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:10:2] | passed | 0.00009 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:11:1] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:11:2] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:11:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:11:4] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:11:4] | passed | 0.00002 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:1] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:2] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:3] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:3] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:4] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:12:5] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:6] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:12:7] | passed | 0.00006 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:6] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:12:7] | passed | 0.00007 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:13:1] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:13:2] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:13:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:14:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:14:1] | passed | 0.00003 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:14:2] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:14:3] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:1] | passed | 0.00008 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:14:3] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:1] | passed | 0.00009 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:2] | passed | 0.00008 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:3] | passed | 0.0001 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:4] | passed | 0.00008 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:5] | passed | 0.00012 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:6] | passed | 0.00012 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:7] | passed | 0.00012 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:7] | passed | 0.00013 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:15:8] | passed | 0.00012 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:15:9] | passed | 0.00013 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:16:1] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:16:2] | passed | 0.00007 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:16:3] | passed | 0.00006 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:17:1] | passed | 0.00003 seconds | -./spec/uc3-dmp-id/helper_spec.rb[1:17:2] | passed | 0.00005 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:15:9] | passed | 0.00012 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:16:1] | passed | 0.00005 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:16:2] | passed | 0.00006 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:17:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/helper_spec.rb[1:17:2] | passed | 0.00004 seconds | ./spec/uc3-dmp-id/helper_spec.rb[1:18:1] | passed | 0.00005 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:1:1] | passed | 0.00078 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:1:2] | passed | 0.00078 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:1:3] | passed | 0.01095 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:1:4] | passed | 0.00354 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:1:5] | passed | 0.00401 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:2:1] | passed | 0.00086 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:2:2] | passed | 0.00082 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:2:3] | passed | 0.00081 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:2:4] | passed | 0.00079 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:2:5] | passed | 0.00088 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:2:6] | passed | 0.00093 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:3:1] | passed | 0.00083 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:3:2] | passed | 0.00082 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:3:3] | passed | 0.0009 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:3:4] | passed | 0.00101 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:3:5] | passed | 0.00092 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:3:6] | passed | 0.00085 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:4:1] | passed | 0.00079 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:4:2] | passed | 0.00078 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:4:3] | passed | 0.00105 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:4:4] | passed | 0.00076 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:4:5] | passed | 0.00084 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:4:6] | passed | 0.00083 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:5:1] | passed | 0.00082 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:5:2] | passed | 0.00073 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:5:3] | passed | 0.00076 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:1] | passed | 0.00078 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:2] | passed | 0.0008 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:3] | passed | 0.0008 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:4] | passed | 0.00078 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:5] | passed | 0.0008 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:6] | passed | 0.0009 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:7] | passed | 0.00089 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:8] | passed | 0.00082 seconds | -./spec/uc3-dmp-id/updater_spec.rb[1:6:9] | passed | 0.00088 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:1] | passed | 0.00034 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:2] | passed | 0.00032 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:3] | passed | 0.00035 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:4] | passed | 0.00034 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:5] | passed | 0.00064 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:1:6] | passed | 0.00071 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:2:1:1] | passed | 0.00171 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:2:1:2] | passed | 0.00004 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:2:1:3] | passed | 0.00066 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:1:1] | passed | 0.00428 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:1:2] | passed | 0.00093 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:1:3] | passed | 0.00082 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:2:1] | passed | 0.01803 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:2:2] | passed | 0.00695 seconds | -./spec/uc3-dmp-id/validator_spec.rb[1:3:2:3] | passed | 0.00682 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:1:1] | passed | 0.00059 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:1:2] | passed | 0.00094 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:1] | passed | 0.00119 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:2] | passed | 0.00099 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:3] | passed | 0.00102 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:4] | passed | 0.00165 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:2:5] | passed | 0.00145 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:1] | passed | 0.00052 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:2] | passed | 0.00052 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:3] | passed | 0.00051 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:4] | passed | 0.00055 seconds | -./spec/uc3-dmp-id/versioner_spec.rb[1:3:5] | passed | 0.00056 seconds | +./spec/uc3-dmp-id/integration_spec.rb[1:1] | passed | 4.08 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:1] | passed | 0.00574 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:2] | passed | 0.00274 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:3] | passed | 0.01082 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:4] | passed | 0.01158 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:1:5] | passed | 0.00758 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:1] | passed | 0.00159 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:2] | passed | 0.00151 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:3] | passed | 0.00137 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:4] | passed | 0.00141 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:5] | passed | 0.00133 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:2:6] | passed | 0.00147 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:1] | passed | 0.00147 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:2] | passed | 0.00143 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:3] | passed | 0.0014 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:4] | passed | 0.00138 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:5] | passed | 0.00144 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:3:6] | passed | 0.00132 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:1] | passed | 0.00134 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:2] | passed | 0.00155 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:3] | passed | 0.00119 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:4] | passed | 0.00117 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:5] | passed | 0.00133 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:4:6] | passed | 0.0013 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:5:1] | passed | 0.00112 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:5:2] | passed | 0.00114 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:5:3] | passed | 0.0011 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:1] | passed | 0.00113 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:2] | passed | 0.00114 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:3] | passed | 0.00117 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:4] | passed | 0.00105 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:5] | passed | 0.00112 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:6] | passed | 0.00094 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:7] | passed | 0.0011 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:8] | passed | 0.00117 seconds | +./spec/uc3-dmp-id/updater_spec.rb[1:6:9] | passed | 0.00114 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:1] | passed | 0.00044 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:2] | passed | 0.00043 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:3] | passed | 0.00046 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:4] | passed | 0.00045 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:5] | passed | 0.0008 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:1:6] | passed | 0.00088 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:2:1:1] | passed | 0.00004 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:2:1:2] | passed | 0.00003 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:2:1:3] | passed | 0.00083 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:1] | passed | 0.00364 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:2] | passed | 0.0014 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:1:3] | passed | 0.00074 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:1] | passed | 0.0042 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:2] | passed | 0.00426 seconds | +./spec/uc3-dmp-id/validator_spec.rb[1:3:2:3] | passed | 0.00406 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:1:1] | passed | 0.00058 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:1:2] | passed | 0.00091 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:1] | passed | 0.00118 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:2] | passed | 0.00103 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:3] | passed | 0.00101 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:4] | passed | 0.00113 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:2:5] | passed | 0.00146 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:1] | passed | 0.00051 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:2] | passed | 0.00051 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:3] | passed | 0.00055 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:4] | passed | 0.00057 seconds | +./spec/uc3-dmp-id/versioner_spec.rb[1:3:5] | passed | 0.00059 seconds | diff --git a/src/sam/gems/uc3-dmp-id/Gemfile b/src/sam/gems/uc3-dmp-id/Gemfile index 40d1ee9f..d5687674 100644 --- a/src/sam/gems/uc3-dmp-id/Gemfile +++ b/src/sam/gems/uc3-dmp-id/Gemfile @@ -4,3 +4,8 @@ source 'https://rubygems.org' # Specify your gem's dependencies in the gemspec gemspec + +gem 'byebug' +gem 'rspec' +gem 'rubocop' +gem 'rubocop-rspec' diff --git a/src/sam/gems/uc3-dmp-id/Gemfile.lock b/src/sam/gems/uc3-dmp-id/Gemfile.lock index 2b05295e..d1d30d23 100644 --- a/src/sam/gems/uc3-dmp-id/Gemfile.lock +++ b/src/sam/gems/uc3-dmp-id/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - uc3-dmp-id (0.0.140) + uc3-dmp-id (0.1.0) json (~> 2.6) json-schema (~> 3.0) uc3-dmp-dynamo (~> 0.0) @@ -87,10 +87,10 @@ PLATFORMS arm64-darwin-21 DEPENDENCIES - byebug (= 11.1.3) - rspec (= 3.9.0) - rubocop (= 1.50.2) - rubocop-rspec (= 2.20.0) + byebug + rspec + rubocop + rubocop-rspec uc3-dmp-id! BUNDLED WITH diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id.rb index 23e9dcdf..535b7f6b 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id.rb @@ -18,7 +18,7 @@ require 'uc3-dmp-id/schemas/amend' require 'uc3-dmp-id/schemas/author' +# Support for working with DMP IDs module Uc3DmpId - end # rubocop:enable Naming/FileName diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/asserter.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/asserter.rb index f5100151..b1046bb7 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/asserter.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/asserter.rb @@ -5,12 +5,14 @@ module Uc3DmpId class AsserterError < StandardError; end + # Class that handles changes to a DMP ID's :dmphub_modifications section class Asserter DEFAULT_DESCRIPTOR = 'references' DEFAULT_WORK_TYPE = 'other' class << self # Add assertions to a DMP ID - this is performed by non-provenance systems + # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity def add(updater:, latest_version:, modified_version:, note: nil, logger: nil) return latest_version unless latest_version.is_a?(Hash) @@ -31,13 +33,14 @@ def add(updater:, latest_version:, modified_version:, note: nil, logger: nil) end return latest_version unless !funding.nil? && funding.any? - latest_version = _add_funding_mod(updater: updater, latest_version: latest_version, funding: funding, - note: note, logger: logger) - latest_version + _add_funding_mod(updater: updater, latest_version: latest_version, funding: funding, + note: note, logger: logger) end + # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity # Splice together assertions made by the owner of the DMP ID so that any :dmphub_modifications made to # the record while it was being updated are not lost + # rubocop:disable Metrics/AbcSize def splice(latest_version:, modified_version:, logger: nil) # Return the modified_version if the timestamps are the same OR neither version has :dmphub_modifications return modified_version if latest_version['modified'] == modified_version['modified'] || @@ -47,8 +50,14 @@ def splice(latest_version:, modified_version:, logger: nil) # Clone any existing :dmphub_modifications on the current DMP ID so we can retain them existing_assertions = Helper.deep_copy_dmp(obj: latest_version.fetch('dmphub_modifications', [])) incoming_assertions = Helper.deep_copy_dmp(obj: modified_version.fetch('dmphub_modifications', [])) - logger.debug(message: "Existing dmphub_modifications", details: existing_assertions) if logger.respond_to?(:debug) - logger.debug(message: "Incoming dmphub_modifications", details: incoming_assertions) if logger.respond_to?(:debug) + if logger.respond_to?(:debug) + logger.debug(message: 'Existing dmphub_modifications', + details: existing_assertions) + end + if logger.respond_to?(:debug) + logger.debug(message: 'Incoming dmphub_modifications', + details: incoming_assertions) + end # Keep any :dmphub_modifications and then add the incoming to the Array modified_version['dmphub_modifications'] = existing_assertions @@ -58,15 +67,22 @@ def splice(latest_version:, modified_version:, logger: nil) incoming_assertions.each { |entry| modified_version['dmphub_modifications'] << entry } modified_version end + # rubocop:enable Metrics/AbcSize private # Verify that the DMP ID record does not already have the specified identifiers and then add them # to the :latest_version in the :dmphub_modifications Array + # + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity def _add_related_identifier(updater:, latest_version:, identifiers:, note: '', logger: nil) return latest_version unless updater.is_a?(String) && latest_version.is_a?(Hash) && identifiers.is_a?(Array) - known_mods = latest_version['dmphub_modifications'].map { |mod| mod.fetch('dmproadmap_related_identifiers', []) } + latest_version['dmphub_modifications'] = [] if latest_version['dmphub_modifications'].nil? + known_mods = latest_version['dmphub_modifications'].map do |mod| + mod.fetch('dmproadmap_related_identifiers', []) + end known_mods = known_mods.flatten.compact.map { |mod| mod['identifier'].downcase.strip }.compact.uniq asserted = latest_version.fetch('dmproadmap_related_identifiers', []) @@ -75,10 +91,13 @@ def _add_related_identifier(updater:, latest_version:, identifiers:, note: '', l additions = [] identifiers.each do |related_identifier| # Skip if there is no :type or :identifier value - next if !related_identifier.is_a?(Hash) || related_identifier['type'].nil? || related_identifier['identifier'].nil? + if !related_identifier.is_a?(Hash) || related_identifier['type'].nil? || related_identifier['identifier'].nil? + next + end id = related_identifier['identifier'].downcase.strip - # Skip if the :identifier is already listed in :dmphub_modifications or the :dmproadmap_related_identifiers Arrays + # Skip if the :identifier is already listed in :dmphub_modifications or the + # :dmproadmap_related_identifiers Arrays next if known_mods.include?(id) || asserted.include?(id) related_identifier['work_type'] = DEFAULT_WORK_TYPE if related_identifier['work_type'].nil? @@ -89,13 +108,21 @@ def _add_related_identifier(updater:, latest_version:, identifiers:, note: '', l latest_version['dmproadmap_related_identifiers'] = [] if latest_version['dmproadmap_related_identifiers'].nil? assertion = _generate_assertion(updater: updater, note: note, mods: JSON.parse({ dmproadmap_related_identifiers: additions }.to_json)) - logger.debug(message: 'Adding change to :dmphub_modifications.', details: assertion) if logger.respond_to?(:debug) + if logger.respond_to?(:debug) + logger.debug(message: 'Adding change to :dmphub_modifications.', + details: assertion) + end latest_version['dmphub_modifications'] << assertion latest_version end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity # Verify that the DMP ID record does not already have the specified funding change and then add it # to the :latest_version in the :dmphub_modifications Array + # + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity def _add_funding_mod(updater:, latest_version:, funding:, note: '', logger: nil) return latest_version unless updater.is_a?(String) && latest_version.is_a?(Hash) && funding.is_a?(Array) @@ -109,7 +136,7 @@ def _add_funding_mod(updater:, latest_version:, funding:, note: '', logger: nil) asserted = latest_version.fetch('project', [])&.map do |project| next if project.nil? - project&.fetch('funding', []).first&.fetch('grant_id', {})['identifier']&.downcase&.strip + project&.fetch('funding', [])&.first&.fetch('grant_id', {})&.[]('identifier')&.downcase&.strip end asserted = asserted.flatten.compact.uniq @@ -125,10 +152,15 @@ def _add_funding_mod(updater:, latest_version:, funding:, note: '', logger: nil) mod = JSON.parse({ funding: fund }.to_json) mod['funding']['funding_status'] = 'granted' assertion = _generate_assertion(updater: updater, mods: mod, note: note) - logger.debug(message: 'Adding change to :dmphub_modifications.', details: assertion) if logger.respond_to?(:debug) + if logger.respond_to?(:debug) + logger.debug(message: 'Adding change to :dmphub_modifications.', + details: assertion) + end latest_version['dmphub_modifications'] << assertion latest_version end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity # Generate an assertion entry. For example: # diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb index e3b17a50..542d820b 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/creator.rb @@ -6,12 +6,15 @@ module Uc3DmpId class CreatorError < StandardError; end + # Class that registers a new DMP ID class Creator MSG_NO_BASE_URL = 'No base URL found for DMP ID (e.g. `doi.org`)' MSG_NO_SHOULDER = 'No DOI shoulder found. (e.g. `10.12345/`)' MSG_UNABLE_TO_MINT = 'Unable to mint a unique DMP ID.' class << self + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity def create(provenance:, json:, logger: nil) raise CreatorError, MSG_NO_SHOULDER if ENV['DMP_ID_SHOULDER'].nil? raise CreatorError, MSG_NO_BASE_URL if ENV['DMP_ID_BASE_URL'].nil? @@ -26,8 +29,9 @@ def create(provenance:, json:, logger: nil) # Try to find it by the :dmp_id first and Fail if found dmp_id = Helper.dmp_id_to_pk(json: json.fetch('dmp', {})['dmp_id']) - result = Finder.exists?(json: dmp_id, logger: logger) unless dmp_id.nil? + result = Finder.exists?(p_key: dmp_id, logger: logger) unless dmp_id.nil? raise CreatorError, Helper::MSG_DMP_EXISTS if result.is_a?(Hash) + # raise CreatorError, Uc3DmpId::MSG_DMP_EXISTS unless json['PK'].nil? client = Uc3DmpDynamo::Client.new @@ -50,9 +54,12 @@ def create(provenance:, json:, logger: nil) _post_process(json: annotated, logger: logger) Helper.cleanse_dmp_json(json: JSON.parse({ dmp: annotated }.to_json)) end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity private + # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity def _preregister_dmp_id(client:, provenance:, json:, logger: nil) # Use the specified DMP ID if the provenance has permission seeding = provenance.fetch('seedingWithLiveDmpIds', false).to_s.downcase == 'true' @@ -63,11 +70,11 @@ def _preregister_dmp_id(client:, provenance:, json:, logger: nil) seeding && !seed_id.nil? return seed_id.gsub(%r{https?://}, '') if seeding && !seed_id.nil? - #Generate a new DMP ID + # Generate a new DMP ID dmp_id = '' counter = 0 while dmp_id == '' && counter <= 10 - prefix = "#{ENV['DMP_ID_SHOULDER']}#{SecureRandom.hex(2).upcase}#{SecureRandom.hex(2)}" + prefix = "#{ENV.fetch('DMP_ID_SHOULDER', nil)}#{SecureRandom.hex(2).upcase}#{SecureRandom.hex(2)}" dmp_id = prefix unless Finder.exists?(client: client, p_key: prefix) counter += 1 end @@ -78,7 +85,7 @@ def _preregister_dmp_id(client:, provenance:, json:, logger: nil) url = ENV['DMP_ID_BASE_URL'].gsub(%r{https?://}, '') "#{Helper::PK_DMP_PREFIX}#{url.end_with?('/') ? url : "#{url}/"}#{dmp_id}" end - # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # rubocop:enable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity # Once the DMP has been created, we need to register it's DMP ID # ------------------------------------------------------------------------- @@ -99,8 +106,9 @@ def _post_process(json:, logger: nil) SK: json['SK'], dmproadmap_related_identifiers: citable_identifiers } - logger.debug(message: "Fetching citations", details: citable_identifiers) if logger.respond_to?(:debug) - publisher.publish(source: 'DmpCreator', dmp: json, event_type: 'Citation Fetch', detail: citer_detail, logger: logger) + logger.debug(message: 'Fetching citations', details: citable_identifiers) if logger.respond_to?(:debug) + publisher.publish(source: 'DmpCreator', dmp: json, event_type: 'Citation Fetch', detail: citer_detail, + logger: logger) true end end diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb index b4ec1fb9..7eab4a76 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/deleter.rb @@ -19,7 +19,7 @@ def tombstone(provenance:, p_key:, logger: nil) # Fetch the latest version of the DMP ID by it's PK client = Uc3DmpDynamo::Client.new - dmp = Finder.by_pk(p_key: p_key, client: client, logger: logger) + dmp = Finder.by_pk(p_key: p_key, client: client, cleanse: false, logger: logger) raise DeleterError, Helper::MSG_DMP_NOT_FOUND unless dmp.is_a?(Hash) && !dmp['dmp'].nil? # Only allow this if the provenance is the owner of the DMP! @@ -30,7 +30,7 @@ def tombstone(provenance:, p_key:, logger: nil) # Annotate the DMP ID dmp['dmp']['SK'] = Helper::DMP_TOMBSTONE_VERSION dmp['dmp']['title'] = "OBSOLETE: #{dmp['dmp']['title']}" - logger.info(message: "Tomstoning DMP ID: #{p_key}") if logger.respond_to?(:debug) + logger.info(message: "Tombstoning DMP ID: #{p_key}") if logger.respond_to?(:debug) # Set the :modified timestamps now = Time.now.utc.iso8601 @@ -42,7 +42,7 @@ def tombstone(provenance:, p_key:, logger: nil) raise DeleterError, Helper::MSG_DMP_NO_TOMBSTONE if resp.nil? # Delete the Latest version - resp = client.delete_item(p_key: p_key, s_key: Helper::SK_DMP_PREFIX, logger: logger) + client.delete_item(p_key: p_key, s_key: Helper::DMP_LATEST_VERSION, logger: logger) # TODO: We should do a check here to see if it was successful! @@ -52,8 +52,8 @@ def tombstone(provenance:, p_key:, logger: nil) # Return the tombstoned record Helper.cleanse_dmp_json(json: dmp) rescue Aws::Errors::ServiceError => e - logger.error(message: e.message, details: e.backtrace) unless logger.nil? - { status: 500, error: Helper::MSG_SERVER_ERROR } + logger.error(message: e.message, details: e.backtrace) if logger.respond_to?(:error) + raise DeleterError, Helper::MSG_SERVER_ERROR end # rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/finder.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/finder.rb index e4e0f167..ab181359 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/finder.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/finder.rb @@ -16,21 +16,27 @@ class Finder MSG_MISSING_PROV_ID = 'No Provenance identifier was provided. \ Expected: `{ "dmp_id": { "identifier": "value", "type": "value" }`' - class << self # TODO: Replace this with ElasticSearch def search_dmps(args:, logger: nil) client = Uc3DmpDynamo::Client.new return _by_owner(owner_org: args['owner_orcid'], client: client, logger: logger) unless args['owner_orcid'].nil? - return _by_owner_org(owner_org: args['owner_org_ror'], client: client, logger: logger) unless args['owner_org_ror'].nil? - return _by_mod_day(day: args['modification_day'], client: client, logger: logger) unless args['modification_day'].nil? + + unless args['owner_org_ror'].nil? + return _by_owner_org(owner_org: args['owner_org_ror'], client: client, + logger: logger) + end + unless args['modification_day'].nil? + return _by_mod_day(day: args['modification_day'], client: client, + logger: logger) + end [] end - # rubocop:enable Metrics/MethodLength # Find a DMP based on the contents of the incoming JSON # ------------------------------------------------------------------------- + # rubocop:disable Metrics/AbcSize def by_json(json:, client: nil, cleanse: true, logger: nil) json = Helper.parse_json(json: json)&.fetch('dmp', {}) raise FinderError, MSG_INVALID_ARGS if !json.is_a?(Hash) || (json['PK'].nil? && json['dmp_id'].nil?) @@ -47,9 +53,11 @@ def by_json(json:, client: nil, cleanse: true, logger: nil) # find_by_PK p_key.nil? ? nil : by_pk(p_key: p_key, s_key: json['SK'], client: client, cleanse: cleanse, logger: logger) end + # rubocop:enable Metrics/AbcSize # Find the DMP by its PK and SK # ------------------------------------------------------------------------- + # rubocop:disable Metrics/AbcSize def by_pk(p_key:, s_key: Helper::DMP_LATEST_VERSION, client: nil, cleanse: true, logger: nil) raise FinderError, MSG_MISSING_PK if p_key.nil? @@ -67,9 +75,10 @@ def by_pk(p_key:, s_key: Helper::DMP_LATEST_VERSION, client: nil, cleanse: true, dmp = resp['dmp'].nil? ? JSON.parse({ dmp: resp }.to_json) : resp return nil if dmp['dmp']['PK'].nil? - dmp = Versioner.append_versions(p_key: dmp['dmp']['PK'], dmp: dmp, client: client, logger: logger) + dmp = Versioner.append_versions(p_key: dmp['dmp']['PK'], dmp: dmp, client: client, logger: logger) if cleanse cleanse ? Helper.cleanse_dmp_json(json: dmp) : dmp end + # rubocop:enable Metrics/AbcSize # Fetch just the PK to see if a record exists # ------------------------------------------------------------------------- @@ -122,16 +131,16 @@ def by_provenance_identifier(json:, client: nil, cleanse: true, logger: nil) # Fetch the DMP IDs for the specified owner's ORCID (the owner is the :dmphub_owner_id on the DMP ID record) def _by_owner(owner_id:, client: nil, logger: nil) - regex = %r{^([0-9A-Z]{4}-){3}[0-9A-Z]{4}$} - raise FinderError, MSG_INVALID_OWNER_ID if owner_id.nil? || (owner_id.to_s =~ regex).nil? + regex = /^([0-9A-Z]{4}-){3}[0-9A-Z]{4}$/ + raise FinderError, MSG_INVALID_OWNER_ID if owner_id.nil? || (owner_id.to_s =~ regex).nil? args = { index_name: 'dmphub_owner_id_gsi', key_conditions: { dmphub_owner_id: { attribute_value_list: [ - "http://orcid.org/#{owner_id.to_s}", - "https://orcid.org/#{owner_id.to_s}" + "http://orcid.org/#{owner_id}", + "https://orcid.org/#{owner_id}" ], comparison_operator: 'IN' } @@ -139,15 +148,16 @@ def _by_owner(owner_id:, client: nil, logger: nil) filter_expression: 'SK = :version', expression_attribute_values: { ':version': Helper::DMP_LATEST_VERSION } } - logger.info(message: "Querying _by_owner with #{args}") unless logger.nil? + logger.info(message: "Querying _by_owner with #{args}") if logger.respond_to?(:info) client = client.nil? ? Uc3DmpDynamo::Client.new : client _process_search_response(response: client.query(args: args, logger: logger)) end - # Fetch the DMP IDs for the specified organization/institution (the org is the :dmphub_owner_org on the DMP ID record) + # Fetch the DMP IDs for the specified organization/institution (the org is the :dmphub_owner_org + # on the DMP ID record) def _by_owner_org(owner_org:, client: nil, logger: nil) - regex = %r{^[a-zA-Z0-9]+$} - raise FinderError, MSG_INVALID_OWNER_ID if owner_org.nil? ||(owner_org.to_s.downcase =~ regex).nil? + regex = /^[a-zA-Z0-9]+$/ + raise FinderError, MSG_INVALID_OWNER_ID if owner_org.nil? || (owner_org.to_s.downcase =~ regex).nil? args = { index_name: 'dmphub_owner_org_gsi', @@ -155,7 +165,7 @@ def _by_owner_org(owner_org:, client: nil, logger: nil) dmphub_owner_org: { attribute_value_list: [ "https://ror.org/#{owner_org.to_s.downcase}", - "http://ror.org/#{owner_org.to_s.downcase}", + "http://ror.org/#{owner_org.to_s.downcase}" ], comparison_operator: 'IN' } @@ -163,14 +173,14 @@ def _by_owner_org(owner_org:, client: nil, logger: nil) filter_expression: 'SK = :version', expression_attribute_values: { ':version': Helper::DMP_LATEST_VERSION } } - logger.info(message: "Querying _by_owner_org with #{args}") unless logger.nil? + logger.info(message: "Querying _by_owner_org with #{args}") if logger.respond_to?(:info) client = client.nil? ? Uc3DmpDynamo::Client.new : client _process_search_response(response: client.query(args: args, logger: logger)) end # Fetch the DMP IDs modified on the specified date (the date is the :dmphub_modification_day on the DMP ID record) def _by_mod_day(day:, client: nil, logger: nil) - regex = %r{^[0-9]{4}(-[0-9]{2}){2}} + regex = /^[0-9]{4}(-[0-9]{2}){2}/ raise FinderError, MSG_INVALID_OWNER_ID if day.nil? || (day.to_s =~ regex).nil? args = { @@ -184,12 +194,11 @@ def _by_mod_day(day:, client: nil, logger: nil) filter_expression: 'SK = :version', expression_attribute_values: { ':version': Helper::DMP_LATEST_VERSION } } - logger.info(message: "Querying _by_mod_day with #{args}") unless logger.nil? + logger.info(message: "Querying _by_mod_day with #{args}") if logger.respond_to?(:info) client = client.nil? ? Uc3DmpDynamo::Client.new : client _process_search_response(response: client.query(args: args, logger: logger)) end - # Transform the search results so that we do not include any of the DMPHub specific metadata def _process_search_response(response:) return [] unless response.is_a?(Array) && response.any? diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb index d4c19d79..8e6ee1fb 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/helper.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true - # TODO: Be sure to update the API functions so that they call cleanse_dmp_json before # calling Uc3DmpApiCore::Responder.respond !!!!!!!!!! - module Uc3DmpId # Helper functions for working with DMP IDs class Helper @@ -129,6 +127,7 @@ def parse_json(json:) end # Compare the DMP IDs to see if they are the same + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def eql?(dmp_a:, dmp_b:) return dmp_a == dmp_b unless dmp_a.is_a?(Hash) && !dmp_a['dmp'].nil? && dmp_b.is_a?(Hash) && !dmp_b['dmp'].nil? @@ -167,7 +166,8 @@ def extract_owner_org(json: {}) return nil unless json.is_a?(Hash) dmp = json['dmp'].nil? ? json : json['dmp'] - owner_org = dmp.fetch('contact', {}).fetch('dmproadmap_affiliation', {}).fetch('affiliation_id', {})['identifier'] + owner_org = dmp.fetch('contact', {}).fetch('dmproadmap_affiliation', {}).fetch('affiliation_id', + {})['identifier'] return owner_org unless owner_org.nil? orgs = dmp.fetch('contributor', []).map do |contributor| @@ -175,8 +175,10 @@ def extract_owner_org(json: {}) end orgs.compact.max_by { |i| orgs.count(i) } end + # rubocop:enable Metrics/AbcSize # Add DMPHub specific fields to the DMP ID JSON + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def annotate_dmp_json(provenance:, p_key:, json:) json = parse_json(json: json) bool_vals = [1, '1', true, 'true', 'yes'] @@ -201,7 +203,7 @@ def annotate_dmp_json(provenance:, p_key:, json:) annotated['dmproadmap_featured'] = bool_vals.include?(featured.to_s.downcase) ? '1' : '0' # Update the modification timestamps - annotated['dmphub_modification_day'] = Time.now.strftime('%Y-%m-%d') + annotated['dmphub_modification_day'] = Time.now.utc.strftime('%Y-%m-%d') annotated['dmphub_owner_id'] = owner_id unless owner_id.nil? annotated['dmphub_owner_org'] = owner_org unless owner_org.nil? return annotated unless json['dmphub_provenance_id'].nil? @@ -222,6 +224,8 @@ def annotate_dmp_json(provenance:, p_key:, json:) end annotated end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # Recursive method that strips out any DMPHub related metadata from a DMP record before sending # it to the caller @@ -301,8 +305,8 @@ def format_provenance_id(provenance:, value:) # Remove the homepage or callbackUri because we will add this when needed. we just want the id val = value.downcase - .gsub(provenance.fetch('callbackUri', '').downcase, '') - .gsub(provenance.fetch('homepage', '').downcase, '') + .gsub(provenance.fetch('callbackUri', '').downcase, '') + .gsub(provenance.fetch('homepage', '').downcase, '') val = val.gsub(%r{https?://}, '') val = val[1..val.length] if val.start_with?('/') id = provenance['PK']&.gsub('PROVENANCE#', '') diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/schemas/amend.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/schemas/amend.rb index 480b271a..fe053702 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/schemas/amend.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/schemas/amend.rb @@ -3,321 +3,322 @@ module Uc3DmpId module Schemas # The JSON schema for creating a new DMP ID + # rubocop:disable Layout/LineLength, Metrics/MethodLength, Metrics/ClassLength class Amend class << self def load JSON.parse({ - "$id": "https://github.com/CDLUC3/dmp-hub-sam/layer/ruby/config/schemas/amend.json", - "title": "RDA DMP Common Standard Schema", - "description": "JSON Schema for the RDA DMP Common Standard", - "type": "object", - "properties": { - "dmp": { - "$id": "#/properties/dmp", - "type": "object", - "title": "The DMP Schema", - "properties": { - "dmp_id": { - "$id": "#/properties/dmp/properties/dmp_id", - "type": "object", - "title": "The DMP Identifier Schema", - "description": "Identifier for the DMP itself", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/dmp_id/properties/identifier", - "type": "string", - "title": "The DMP Identifier Value Schema", - "description": "Identifier for a DMP", - "examples": ["https://doi.org/10.1371/journal.pcbi.1006750"] + '$id': 'https://github.com/CDLUC3/dmp-hub-sam/layer/ruby/config/schemas/amend.json', + title: 'RDA DMP Common Standard Schema', + description: 'JSON Schema for the RDA DMP Common Standard', + type: 'object', + properties: { + dmp: { + '$id': '#/properties/dmp', + type: 'object', + title: 'The DMP Schema', + properties: { + dmp_id: { + '$id': '#/properties/dmp/properties/dmp_id', + type: 'object', + title: 'The DMP Identifier Schema', + description: 'Identifier for the DMP itself', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/dmp_id/properties/identifier', + type: 'string', + title: 'The DMP Identifier Value Schema', + description: 'Identifier for a DMP', + examples: ['https://doi.org/10.1371/journal.pcbi.1006750'] }, - "type": { - "$id": "#/properties/dmp/properties/dmp_id/properties/type", - "type": "string", - "enum": [ - "handle", - "doi", - "ark", - "url", - "other" + type: { + '$id': '#/properties/dmp/properties/dmp_id/properties/type', + type: 'string', + enum: %w[ + handle + doi + ark + url + other ], - "title": "The DMP Identifier Type Schema", - "description": "The DMP Identifier Type. Allowed values: handle, doi, ark, url, other", - "examples": ["doi"] + title: 'The DMP Identifier Type Schema', + description: 'The DMP Identifier Type. Allowed values: handle, doi, ark, url, other', + examples: ['doi'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "modified": { - "$id": "#/properties/dmp/properties/modified", - "type": "string", - "format": "date-time", - "title": "The DMP Modification Schema", - "description": "Must be set each time DMP is modified. Indicates DMP version. Encoded using the relevant ISO 8601 Date and Time compliant string.", - "examples": ["2020-03-14T10:53:49"] + modified: { + '$id': '#/properties/dmp/properties/modified', + type: 'string', + format: 'date-time', + title: 'The DMP Modification Schema', + description: 'Must be set each time DMP is modified. Indicates DMP version. Encoded using the relevant ISO 8601 Date and Time compliant string.', + examples: ['2020-03-14T10:53:49'] }, - "title": { - "$id": "#/properties/dmp/properties/title", - "type": "string", - "title": "The DMP Title Schema", - "description": "Title of a DMP", - "examples": ["DMP for our new project"] + title: { + '$id': '#/properties/dmp/properties/title', + type: 'string', + title: 'The DMP Title Schema', + description: 'Title of a DMP', + examples: ['DMP for our new project'] }, - "anyOf": [ + anyOf: [ { - "dmproadmap_related_identifiers": { - "$id": "#/properties/dmp/properties/dmproadmap_related_identifiers", - "type": "array", - "title": "Related identifiers for the DMP", - "description": "Identifiers for objects related to the DMP (e.g. datasets, publications, etc.)", - "items": { - "$id": "#/properties/dmp/properties/dmproadmap_related_identifiers/items", - "type": "object", - "title": "A related identifier", - "properties": { - "descriptor": { - "$id": "#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/descriptor", - "type": "string", - "enum": [ - "is_cited_by", - "cites", - "is_supplement_to", - "is_supplemented_by", - "is_described_by", - "describes", - "has_metadata", - "is_metadata_for", - "is_part_of", - "has_part", - "is_referenced_by", - "references", - "is_documented_by", - "documents", - "is_new_version_of", - "is_previous_version_of" + dmproadmap_related_identifiers: { + '$id': '#/properties/dmp/properties/dmproadmap_related_identifiers', + type: 'array', + title: 'Related identifiers for the DMP', + description: 'Identifiers for objects related to the DMP (e.g. datasets, publications, etc.)', + items: { + '$id': '#/properties/dmp/properties/dmproadmap_related_identifiers/items', + type: 'object', + title: 'A related identifier', + properties: { + descriptor: { + '$id': '#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/descriptor', + type: 'string', + enum: %w[ + is_cited_by + cites + is_supplement_to + is_supplemented_by + is_described_by + describes + has_metadata + is_metadata_for + is_part_of + has_part + is_referenced_by + references + is_documented_by + documents + is_new_version_of + is_previous_version_of ] }, - "identifier": { - "$id": "#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/identifier", - "type": "string", - "title": "A unique identifier for the item", - "description": "Identifier for a DMP", - "examples": ["https://doi.org/10.1371/journal.pcbi.1006750"] + identifier: { + '$id': '#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/identifier', + type: 'string', + title: 'A unique identifier for the item', + description: 'Identifier for a DMP', + examples: ['https://doi.org/10.1371/journal.pcbi.1006750'] }, - "type": { - "$id": "#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/type", - "type": "string", - "enum": [ - "handle", - "doi", - "ark", - "url", - "other" + type: { + '$id': '#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/type', + type: 'string', + enum: %w[ + handle + doi + ark + url + other ] }, - "work_type": { - "$id": "#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/work_type", - "type": "string", - "enum": [ - "article", - "book", - "dataset", - "metadata_template", - "other", - "output_management_plan", - "paper", - "preprint", - "preregistration", - "protocol", - "software", - "supplemental_information" + work_type: { + '$id': '#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/work_type', + type: 'string', + enum: %w[ + article + book + dataset + metadata_template + other + output_management_plan + paper + preprint + preregistration + protocol + software + supplemental_information ] } }, - "required": [ - "descriptor", - "identifier", - "type", - "work_type" + required: %w[ + descriptor + identifier + type + work_type ] } } }, { - "project": { - "$id": "#/properties/dmp/properties/project", - "type": "array", - "title": "The DMP Project Schema", - "description": "Project related to a DMP", - "items": { - "$id": "#/properties/dmp/properties/project/items", - "type": "object", - "title": "The DMP Project Items Schema", - "properties": { - "funding": { - "$id": "#/properties/dmp/properties/project/items/properties/funding", - "type": "array", - "title": "The DMP Project Funding Schema", - "description": "Funding related with a project", - "items": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/items", - "type": "object", - "title": "The DMP Project Funding Items Schema", - "properties": { - "dmproadmap_funded_affiliations": { - "$id": "#/properties/dmp/properties/project/items/properties/funding//items/properties/dmproadmap_funded_affiliations", - "type": "array", - "title": "Institutions named on the grant", - "description": "The institutions who received the funding", - "items": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items", - "type": "object", - "title": "An institution that received funding", - "properties": { - "affiliation_id": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/affiliation_id", - "type": "object", - "title": "The funded affiliation's ID", - "description": "Affiliation ID of the associated project", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/affiliation_id/properties/identifier", - "type": "string", - "title": "The affiliation ID", - "description": "ROR ID or URL. Recommended to use Research Organization Registry (ROR). See: https://ror.org", - "examples": ["https://ror.org/00pjdza24", "https://cdlib.org"] + project: { + '$id': '#/properties/dmp/properties/project', + type: 'array', + title: 'The DMP Project Schema', + description: 'Project related to a DMP', + items: { + '$id': '#/properties/dmp/properties/project/items', + type: 'object', + title: 'The DMP Project Items Schema', + properties: { + funding: { + '$id': '#/properties/dmp/properties/project/items/properties/funding', + type: 'array', + title: 'The DMP Project Funding Schema', + description: 'Funding related with a project', + items: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/items', + type: 'object', + title: 'The DMP Project Funding Items Schema', + properties: { + dmproadmap_funded_affiliations: { + '$id': '#/properties/dmp/properties/project/items/properties/funding//items/properties/dmproadmap_funded_affiliations', + type: 'array', + title: 'Institutions named on the grant', + description: 'The institutions who received the funding', + items: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items', + type: 'object', + title: 'An institution that received funding', + properties: { + affiliation_id: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/affiliation_id', + type: 'object', + title: "The funded affiliation's ID", + description: 'Affiliation ID of the associated project', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/affiliation_id/properties/identifier', + type: 'string', + title: 'The affiliation ID', + description: 'ROR ID or URL. Recommended to use Research Organization Registry (ROR). See: https://ror.org', + examples: ['https://ror.org/00pjdza24', 'https://cdlib.org'] }, - "type": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/affiliation_id/properties/type", - "type": "string", - "enum": [ - "doi", - "ror", - "url" + type: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/affiliation_id/properties/type', + type: 'string', + enum: %w[ + doi + ror + url ], - "title": "The affiliation ID Type Schema", - "description": "Identifier type. Allowed values: doi, ror, url", - "examples": ["ror"] + title: 'The affiliation ID Type Schema', + description: 'Identifier type. Allowed values: doi, ror, url', + examples: ['ror'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "name": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/name", - "type": "string", - "title": "The name of the instituion / organization", - "description": "Project title", - "examples": ["Our New Project"] + name: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/name', + type: 'string', + title: 'The name of the instituion / organization', + description: 'Project title', + examples: ['Our New Project'] } } } }, - "dmproadmap_opportunity_number": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/dmproadmap_opportunity_number", - "type": "string", - "title": "The funder's opportunity / award number", - "description": "The funder's number used to identify the award or call for submissions", - "examples": ["Award-123"] + dmproadmap_opportunity_number: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/dmproadmap_opportunity_number', + type: 'string', + title: "The funder's opportunity / award number", + description: "The funder's number used to identify the award or call for submissions", + examples: ['Award-123'] }, - "dmproadmap_project_number": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/dmproadmap_project_number", - "type": "string", - "title": "The funder's identifier for the research project", - "description": "The funder's identifier used to identify the research project", - "examples": ["prj-XYZ987-UCB"] + dmproadmap_project_number: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/dmproadmap_project_number', + type: 'string', + title: "The funder's identifier for the research project", + description: "The funder's identifier used to identify the research project", + examples: ['prj-XYZ987-UCB'] }, - "funder_id": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/funder_id", - "type": "object", - "title": "The Funder ID Schema", - "description": "Funder ID of the associated project", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/funder_id/properties/identifier", - "type": "string", - "title": "The Funder ID Value Schema", - "description": "Funder ID, recommended to use CrossRef Funder Registry. See: https://www.crossref.org/services/funder-registry/", - "examples": ["501100002428"] + funder_id: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/funder_id', + type: 'object', + title: 'The Funder ID Schema', + description: 'Funder ID of the associated project', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/funder_id/properties/identifier', + type: 'string', + title: 'The Funder ID Value Schema', + description: 'Funder ID, recommended to use CrossRef Funder Registry. See: https://www.crossref.org/services/funder-registry/', + examples: ['501100002428'] }, - "type": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/funder_id/properties/type", - "type": "string", - "enum": [ - "fundref", - "ror", - "url", - "other" + type: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/funder_id/properties/type', + type: 'string', + enum: %w[ + fundref + ror + url + other ], - "title": "The Funder ID Type Schema", - "description": "Identifier type. Allowed values: fundref, url, other", - "examples": ["fundref"] + title: 'The Funder ID Type Schema', + description: 'Identifier type. Allowed values: fundref, url, other', + examples: ['fundref'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "funding_status": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/funding_status", - "type": "string", - "enum": [ - "planned", - "applied", - "granted", - "rejected" + funding_status: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/funding_status', + type: 'string', + enum: %w[ + planned + applied + granted + rejected ], - "title": "The Funding Status Schema", - "description": "To express different phases of project lifecycle. Allowed values: planned, applied, granted, rejected", - "examples": ["granted"] + title: 'The Funding Status Schema', + description: 'To express different phases of project lifecycle. Allowed values: planned, applied, granted, rejected', + examples: ['granted'] }, - "grant_id": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/grant_id", - "type": "object", - "title": "The Funding Grant ID Schema", - "description": "Grant ID of the associated project", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/grant_id/properties/identifier", - "type": "string", - "title": "The Funding Grant ID Value Schema", - "description": "Grant ID", - "examples": ["776242"] + grant_id: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/grant_id', + type: 'object', + title: 'The Funding Grant ID Schema', + description: 'Grant ID of the associated project', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/grant_id/properties/identifier', + type: 'string', + title: 'The Funding Grant ID Value Schema', + description: 'Grant ID', + examples: ['776242'] }, - "type": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/grant_id/properties/type", - "type": "string", - "title": "The Funding Grant ID Type Schema", - "enum": [ - "doi", - "url", - "other" + type: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/grant_id/properties/type', + type: 'string', + title: 'The Funding Grant ID Type Schema', + enum: %w[ + doi + url + other ], - "description": "Identifier type. Allowed values: url, other", - "examples": ["other"] + description: 'Identifier type. Allowed values: url, other', + examples: ['other'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "name": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/name", - "type": "string", - "title": "The name of the funding instituion / organization", - "description": "Name", - "examples": ["National Science Foundation"] + name: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/name', + type: 'string', + title: 'The name of the funding instituion / organization', + description: 'Name', + examples: ['National Science Foundation'] } }, - "required": [ - "funding_status", - "name" + required: %w[ + funding_status + name ] } } @@ -327,18 +328,19 @@ def load } ] }, - "required": [ - "dmp_id", - "modified", - "title" + required: %w[ + dmp_id + modified + title ] } }, - "additionalProperties": false, - "required": ["dmp"] + additionalProperties: false, + required: ['dmp'] }.to_json) end end end + # rubocop:enable Layout/LineLength, Metrics/MethodLength, Metrics/ClassLength end end diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/schemas/author.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/schemas/author.rb index 4d0b4f57..e963c32a 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/schemas/author.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/schemas/author.rb @@ -3,1656 +3,1658 @@ module Uc3DmpId module Schemas # The JSON schema for creating a new DMP ID + # rubocop:disable Layout/LineLength, Metrics/MethodLength, Metrics/ClassLength class Author class << self def load JSON.parse({ - "$id": "https://github.com/CDLUC3/dmp-hub-sam/layer/ruby/config/schemas/author.json", - "title": "Data Management Plan (DMP)", - "description": "JSON Schema for a Data Management Plan (DMP)", - "type": "object", - "properties": { - "dmp": { - "$id": "#/properties/dmp", - "type": "object", - "title": "The DMP Schema", - "properties": { - "contact": { - "$id": "#/properties/dmp/properties/contact", - "type": "object", - "title": "The DMP Contact Schema", - "properties": { - "contact_id": { - "$id": "#/properties/dmp/properties/contact/properties/contact_id", - "type": "object", - "title": "The Contact ID Schema", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/contact/properties/contact_id/properties/identifier", - "type": "string", - "title": "The DMP Contact Identifier Schema", - "examples": ["https://orcid.org/0000-0000-0000-0000"] + '$id': 'https://github.com/CDLUC3/dmp-hub-sam/layer/ruby/config/schemas/author.json', + title: 'Data Management Plan (DMP)', + description: 'JSON Schema for a Data Management Plan (DMP)', + type: 'object', + properties: { + dmp: { + '$id': '#/properties/dmp', + type: 'object', + title: 'The DMP Schema', + properties: { + contact: { + '$id': '#/properties/dmp/properties/contact', + type: 'object', + title: 'The DMP Contact Schema', + properties: { + contact_id: { + '$id': '#/properties/dmp/properties/contact/properties/contact_id', + type: 'object', + title: 'The Contact ID Schema', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/contact/properties/contact_id/properties/identifier', + type: 'string', + title: 'The DMP Contact Identifier Schema', + examples: ['https://orcid.org/0000-0000-0000-0000'] }, - "type": { - "$id": "#/properties/dmp/properties/contact/properties/contact_id/properties/type", - "type": "string", - "enum": [ - "orcid", - "isni", - "openid", - "other" + type: { + '$id': '#/properties/dmp/properties/contact/properties/contact_id/properties/type', + type: 'string', + enum: %w[ + orcid + isni + openid + other ], - "title": "The DMP Contact Identifier Type Schema", - "description": "Identifier type. Allowed values: orcid, isni, openid, other", - "examples": ["orcid"] + title: 'The DMP Contact Identifier Type Schema', + description: 'Identifier type. Allowed values: orcid, isni, openid, other', + examples: ['orcid'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "dmproadmap_affiliation": { - "$id": "#/properties/dmp/properties/contact/properties/dmproadmap_affiliation", - "type": "object", - "title": "The contact's affiliation", - "properties": { - "affiliation_id": { - "$id": "#/properties/dmp/properties/contact/properties/dmproadmap_affiliation/properties/affiliation_id", - "type": "object", - "title": "The unique ID of the affiliation", - "description": "The affiliation's ROR, Crossref funder ID or URL", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/contact/properties/dmproadmap_affiliation/properties/affiliation_id/properties/identifier", - "type": "string", - "title": "The affiliation ID", - "description": "ROR ID, Crossref funder ID or URL. Recommended to use Research Organization Registry (ROR). See: https://ror.org", - "examples": ["https://ror.org/03yrm5c26", "http://dx.doi.org/10.13039/100005595", "http://www.cdlib.org/"] + dmproadmap_affiliation: { + '$id': '#/properties/dmp/properties/contact/properties/dmproadmap_affiliation', + type: 'object', + title: "The contact's affiliation", + properties: { + affiliation_id: { + '$id': '#/properties/dmp/properties/contact/properties/dmproadmap_affiliation/properties/affiliation_id', + type: 'object', + title: 'The unique ID of the affiliation', + description: "The affiliation's ROR, Crossref funder ID or URL", + properties: { + identifier: { + '$id': '#/properties/dmp/properties/contact/properties/dmproadmap_affiliation/properties/affiliation_id/properties/identifier', + type: 'string', + title: 'The affiliation ID', + description: 'ROR ID, Crossref funder ID or URL. Recommended to use Research Organization Registry (ROR). See: https://ror.org', + examples: ['https://ror.org/03yrm5c26', 'http://dx.doi.org/10.13039/100005595', 'http://www.cdlib.org/'] }, - "type": { - "$id": "#/properties/dmp/properties/contact/properties/dmproadmap_affiliation/properties/affiliation_id/properties/type", - "type": "string", - "enum": [ - "doi", - "ror", - "url" + type: { + '$id': '#/properties/dmp/properties/contact/properties/dmproadmap_affiliation/properties/affiliation_id/properties/type', + type: 'string', + enum: %w[ + doi + ror + url ], - "title": "The affiliation ID type schema", - "description": "Identifier type. Allowed values: doi, ror, url", - "examples": ["ror"] + title: 'The affiliation ID type schema', + description: 'Identifier type. Allowed values: doi, ror, url', + examples: ['ror'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "name": { - "$id": "#/properties/dmp/properties/contact/properties/dmproadmap_affiliation/properties/name", - "type": "string", - "title": "Name of the instituion/organization", - "description": "Official institution/organization name", - "examples": ["Example University"] + name: { + '$id': '#/properties/dmp/properties/contact/properties/dmproadmap_affiliation/properties/name', + type: 'string', + title: 'Name of the instituion/organization', + description: 'Official institution/organization name', + examples: ['Example University'] } } }, - "mbox": { - "$id": "#/properties/dmp/properties/contact/properties/mbox", - "type": "string", - "format": "email", - "title": "The Mailbox Schema", - "description": "Contact Person's E-mail address", - "examples": ["cc@example.com"] + mbox: { + '$id': '#/properties/dmp/properties/contact/properties/mbox', + type: 'string', + format: 'email', + title: 'The Mailbox Schema', + description: "Contact Person's E-mail address", + examples: ['cc@example.com'] }, - "name": { - "$id": "#/properties/dmp/properties/contact/properties/name", - "type": "string", - "title": "The Name Schema", - "description": "Name of the contact person as Last, First (e.g. 'Doe PhD., Jane A.' or 'Doe, Jane')", - "examples": ["Doe, Jane"] + name: { + '$id': '#/properties/dmp/properties/contact/properties/name', + type: 'string', + title: 'The Name Schema', + description: "Name of the contact person as Last, First (e.g. 'Doe PhD., Jane A.' or 'Doe, Jane')", + examples: ['Doe, Jane'] } }, - "required": [ - "contact_id", - "mbox", - "name" + required: %w[ + contact_id + mbox + name ] }, - "contributor": { - "$id": "#/properties/dmp/properties/contributor", - "type": "array", - "title": "The Contributor Schema", - "items": { - "$id": "#/properties/dmp/properties/contributor/items", - "type": "object", - "title": "The Contributor Items Schema", - "properties": { - "contributor_id": { - "$id": "#/properties/dmp/properties/contributor/items/properties/contributor_id", - "type": "object", - "title": "The Contributor_id Schema", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/contributor/items/properties/contributor_id/properties/identifier", - "type": "string", - "title": "The Contributor Identifier Schema", - "description": "Identifier for a contact person", - "examples": ["http://orcid.org/0000-0000-0000-0000"] + contributor: { + '$id': '#/properties/dmp/properties/contributor', + type: 'array', + title: 'The Contributor Schema', + items: { + '$id': '#/properties/dmp/properties/contributor/items', + type: 'object', + title: 'The Contributor Items Schema', + properties: { + contributor_id: { + '$id': '#/properties/dmp/properties/contributor/items/properties/contributor_id', + type: 'object', + title: 'The Contributor_id Schema', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/contributor/items/properties/contributor_id/properties/identifier', + type: 'string', + title: 'The Contributor Identifier Schema', + description: 'Identifier for a contact person', + examples: ['http://orcid.org/0000-0000-0000-0000'] }, - "type": { - "$id": "#/properties/dmp/properties/contributor/items/properties/contributor_id/properties/type", - "type": "string", - "enum": [ - "orcid", - "isni", - "openid", - "other" + type: { + '$id': '#/properties/dmp/properties/contributor/items/properties/contributor_id/properties/type', + type: 'string', + enum: %w[ + orcid + isni + openid + other ], - "title": "The Contributor Identifier Type Schema", - "description": "Identifier type. Allowed values: orcid, isni, openid, other", - "examples": ["orcid"] + title: 'The Contributor Identifier Type Schema', + description: 'Identifier type. Allowed values: orcid, isni, openid, other', + examples: ['orcid'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "dmproadmap_affiliation": { - "$id": "#/properties/dmp/properties/contributor/items/properties/dmproadmap_affiliation", - "type": "object", - "title": "The contributor's affiliation", - "properties": { - "affiliation_id": { - "$id": "#/properties/dmp/properties/contributor/items/properties/dmproadmap_affiliation/properties/affiliation_id", - "type": "object", - "title": "The unique ID of the affiliation", - "description": "The affiliation's ROR, Crossref funder ID or URL", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/contributor/items/properties/dmproadmap_affiliation/properties/affiliation_id/properties/identifier", - "type": "string", - "title": "The affiliation ID", - "description": "ROR ID, Crossref funder ID or URL. Recommended to use Research Organization Registry (ROR). See: https://ror.org", - "examples": ["https://ror.org/03yrm5c26", "http://dx.doi.org/10.13039/100005595", "http://www.cdlib.org/"] + dmproadmap_affiliation: { + '$id': '#/properties/dmp/properties/contributor/items/properties/dmproadmap_affiliation', + type: 'object', + title: "The contributor's affiliation", + properties: { + affiliation_id: { + '$id': '#/properties/dmp/properties/contributor/items/properties/dmproadmap_affiliation/properties/affiliation_id', + type: 'object', + title: 'The unique ID of the affiliation', + description: "The affiliation's ROR, Crossref funder ID or URL", + properties: { + identifier: { + '$id': '#/properties/dmp/properties/contributor/items/properties/dmproadmap_affiliation/properties/affiliation_id/properties/identifier', + type: 'string', + title: 'The affiliation ID', + description: 'ROR ID, Crossref funder ID or URL. Recommended to use Research Organization Registry (ROR). See: https://ror.org', + examples: ['https://ror.org/03yrm5c26', 'http://dx.doi.org/10.13039/100005595', 'http://www.cdlib.org/'] }, - "type": { - "$id": "#/properties/dmp/properties/contributor/items/properties/dmproadmap_affiliation/properties/affiliation_id/properties/type", - "type": "string", - "enum": [ - "doi", - "ror", - "url" + type: { + '$id': '#/properties/dmp/properties/contributor/items/properties/dmproadmap_affiliation/properties/affiliation_id/properties/type', + type: 'string', + enum: %w[ + doi + ror + url ], - "title": "The affiliation ID type schema", - "description": "Identifier type. Allowed values: doi, ror, url", - "examples": ["ror"] + title: 'The affiliation ID type schema', + description: 'Identifier type. Allowed values: doi, ror, url', + examples: ['ror'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "name": { - "$id": "#/properties/dmp/properties/contributor/items/properties/dmproadmap_affiliation/properties/name", - "type": "string", - "title": "Name of the instituion/organization", - "description": "Official institution/organization name", - "examples": ["Example University"] + name: { + '$id': '#/properties/dmp/properties/contributor/items/properties/dmproadmap_affiliation/properties/name', + type: 'string', + title: 'Name of the instituion/organization', + description: 'Official institution/organization name', + examples: ['Example University'] } } }, - "mbox": { - "$id": "#/properties/dmp/properties/contributor/items/properties/mbox", - "type": "string", - "title": "The Contributor Mailbox Schema", - "description": "Contributor Mail address", - "examples": ["john@smith.com"], - "format": "email" + mbox: { + '$id': '#/properties/dmp/properties/contributor/items/properties/mbox', + type: 'string', + title: 'The Contributor Mailbox Schema', + description: 'Contributor Mail address', + examples: ['john@smith.com'], + format: 'email' }, - "name": { - "$id": "#/properties/dmp/properties/contributor/items/properties/name", - "type": "string", - "title": "The Name Schema", - "description": "Name of the contributor as Last, First (e.g. 'Doe PhD., Jane A.' or 'Doe, Jane')", - "examples": ["Smith, John"] + name: { + '$id': '#/properties/dmp/properties/contributor/items/properties/name', + type: 'string', + title: 'The Name Schema', + description: "Name of the contributor as Last, First (e.g. 'Doe PhD., Jane A.' or 'Doe, Jane')", + examples: ['Smith, John'] }, - "role": { - "$id": "#/properties/dmp/properties/contributor/items/properties/role", - "type": "array", - "title": "The Role Schema", - "description": "Type of contributor", - "items": { - "$id": "#/properties/dmp/properties/contributor/items/properties/role/items", - "type": "string", - "title": "The Contributor Role(s) Items Schema", - "examples": ["Data Steward"] + role: { + '$id': '#/properties/dmp/properties/contributor/items/properties/role', + type: 'array', + title: 'The Role Schema', + description: 'Type of contributor', + items: { + '$id': '#/properties/dmp/properties/contributor/items/properties/role/items', + type: 'string', + title: 'The Contributor Role(s) Items Schema', + examples: ['Data Steward'] }, - "uniqueItems": true + uniqueItems: true } }, - "required": [ - "name", - "role" + required: %w[ + name + role ] } }, - "cost": { - "$id": "#/properties/dmp/properties/cost", - "type": "array", - "title": "The Cost Schema", - "items": { - "$id": "#/properties/dmp/properties/cost/items", - "type": "object", - "title": "The Cost Items Schema", - "properties": { - "currency_code": { - "$id": "#/properties/dmp/properties/cost/items/properties/currency_code", - "type": "string", - "enum": [ - "AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", - "BAM", "BBD", "BDT", "BGN", "BHD", "BIF", "BMD", "BND", "BOB", "BRL", - "BSD", "BTN", "BWP", "BYN", "BZD", "CAD", "CDF", "CHF", "CLP", "CNY", - "COP", "CRC", "CUC", "CUP", "CVE", "CZK", "DJF", "DKK", "DOP", "DZD", - "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GGP", "GHS", - "GIP", "GMD", "GNF", "GTQ", "GYD", "HKD", "HNL", "HRK", "HTG", "HUF", - "IDR", "ILS", "IMP", "INR", "IQD", "IRR", "ISK", "JEP", "JMD", "JOD", - "JPY", "KES", "KGS", "KHR", "KMF", "KPW", "KRW", "KWD", "KYD", "KZT", - "LAK", "LBP", "LKR", "LRD", "LSL", "LYD", "MAD", "MDL", "MGA", "MKD", - "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN", "MYR", "MZN", - "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "OMR", "PAB", "PEN", "PGK", - "PHP", "PKR", "PLN", "PYG", "QAR", "RON", "RSD", "RUB", "RWF", "SAR", - "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLL", "SOS", "SPL*","SRD", - "STN", "SVC", "SYP", "SZL", "THB", "TJS", "TMT", "TND", "TOP", "TRY", - "TTD", "TVD", "TWD", "TZS", "UAH", "UGX", "USD", "UYU", "UZS", "VEF", - "VND", "VUV", "WST", "XAF", "XCD", "XDR", "XOF", "XPF", "YER", "ZAR", - "ZMW", "ZWD" + cost: { + '$id': '#/properties/dmp/properties/cost', + type: 'array', + title: 'The Cost Schema', + items: { + '$id': '#/properties/dmp/properties/cost/items', + type: 'object', + title: 'The Cost Items Schema', + properties: { + currency_code: { + '$id': '#/properties/dmp/properties/cost/items/properties/currency_code', + type: 'string', + enum: [ + 'AED', 'AFN', 'ALL', 'AMD', 'ANG', 'AOA', 'ARS', 'AUD', 'AWG', 'AZN', + 'BAM', 'BBD', 'BDT', 'BGN', 'BHD', 'BIF', 'BMD', 'BND', 'BOB', 'BRL', + 'BSD', 'BTN', 'BWP', 'BYN', 'BZD', 'CAD', 'CDF', 'CHF', 'CLP', 'CNY', + 'COP', 'CRC', 'CUC', 'CUP', 'CVE', 'CZK', 'DJF', 'DKK', 'DOP', 'DZD', + 'EGP', 'ERN', 'ETB', 'EUR', 'FJD', 'FKP', 'GBP', 'GEL', 'GGP', 'GHS', + 'GIP', 'GMD', 'GNF', 'GTQ', 'GYD', 'HKD', 'HNL', 'HRK', 'HTG', 'HUF', + 'IDR', 'ILS', 'IMP', 'INR', 'IQD', 'IRR', 'ISK', 'JEP', 'JMD', 'JOD', + 'JPY', 'KES', 'KGS', 'KHR', 'KMF', 'KPW', 'KRW', 'KWD', 'KYD', 'KZT', + 'LAK', 'LBP', 'LKR', 'LRD', 'LSL', 'LYD', 'MAD', 'MDL', 'MGA', 'MKD', + 'MMK', 'MNT', 'MOP', 'MRU', 'MUR', 'MVR', 'MWK', 'MXN', 'MYR', 'MZN', + 'NAD', 'NGN', 'NIO', 'NOK', 'NPR', 'NZD', 'OMR', 'PAB', 'PEN', 'PGK', + 'PHP', 'PKR', 'PLN', 'PYG', 'QAR', 'RON', 'RSD', 'RUB', 'RWF', 'SAR', + 'SBD', 'SCR', 'SDG', 'SEK', 'SGD', 'SHP', 'SLL', 'SOS', 'SPL*', 'SRD', + 'STN', 'SVC', 'SYP', 'SZL', 'THB', 'TJS', 'TMT', 'TND', 'TOP', 'TRY', + 'TTD', 'TVD', 'TWD', 'TZS', 'UAH', 'UGX', 'USD', 'UYU', 'UZS', 'VEF', + 'VND', 'VUV', 'WST', 'XAF', 'XCD', 'XDR', 'XOF', 'XPF', 'YER', 'ZAR', + 'ZMW', 'ZWD' ], - "title": "The Cost Currency Code Schema", - "description": "Allowed values defined by ISO 4217", - "examples": ["EUR"] + title: 'The Cost Currency Code Schema', + description: 'Allowed values defined by ISO 4217', + examples: ['EUR'] }, - "description": { - "$id": "#/properties/dmp/properties/cost/items/properties/description", - "type": "string", - "title": "The Cost Description Schema", - "description": "Cost(s) Description", - "examples": ["Costs for maintaining..."] + description: { + '$id': '#/properties/dmp/properties/cost/items/properties/description', + type: 'string', + title: 'The Cost Description Schema', + description: 'Cost(s) Description', + examples: ['Costs for maintaining...'] }, - "title": { - "$id": "#/properties/dmp/properties/cost/items/properties/title", - "type": "string", - "title": "The Cost Title Schema", - "description": "Title", - "examples": ["Storage and Backup"] + title: { + '$id': '#/properties/dmp/properties/cost/items/properties/title', + type: 'string', + title: 'The Cost Title Schema', + description: 'Title', + examples: ['Storage and Backup'] }, - "value": { - "$id": "#/properties/dmp/properties/cost/items/properties/value", - "type": "number", - "title": "The Cost Value Schema", - "description": "Value", - "examples": [1000] + value: { + '$id': '#/properties/dmp/properties/cost/items/properties/value', + type: 'number', + title: 'The Cost Value Schema', + description: 'Value', + examples: [1000] } }, - "required": ["title"] + required: ['title'] } }, - "created": { - "$id": "#/properties/dmp/properties/created", - "type": "string", - "format": "date-time", - "title": "The DMP Creation Schema", - "description": "Date and time of the first version of a DMP. Must not be changed in subsequent DMPs. Encoded using the relevant ISO 8601 Date and Time compliant string", - "examples": ["2019-03-13T13:13:00+00:00"] + created: { + '$id': '#/properties/dmp/properties/created', + type: 'string', + format: 'date-time', + title: 'The DMP Creation Schema', + description: 'Date and time of the first version of a DMP. Must not be changed in subsequent DMPs. Encoded using the relevant ISO 8601 Date and Time compliant string', + examples: ['2019-03-13T13:13:00+00:00'] }, - "dataset": { - "$id": "#/properties/dmp/properties/dataset", - "type": "array", - "title": "The Dataset Schema", - "items": { - "$id": "#/properties/dmp/properties/dataset/items", - "type": "object", - "title": "The Dataset Items Schema", - "properties": { - "data_quality_assurance": { - "$id": "#/properties/dmp/properties/dataset/items/properties/data_quality_assurance", - "type": "array", - "title": "The Data Quality Assurance Schema", - "description": "Data Quality Assurance", - "items": { - "$id": "#/properties/dmp/properties/dataset/items/properties/data_quality_assurance/items", - "type": "string", - "title": "The Data Quality Assurance Schema", - "examples": ["We use file naming convention..."] + dataset: { + '$id': '#/properties/dmp/properties/dataset', + type: 'array', + title: 'The Dataset Schema', + items: { + '$id': '#/properties/dmp/properties/dataset/items', + type: 'object', + title: 'The Dataset Items Schema', + properties: { + data_quality_assurance: { + '$id': '#/properties/dmp/properties/dataset/items/properties/data_quality_assurance', + type: 'array', + title: 'The Data Quality Assurance Schema', + description: 'Data Quality Assurance', + items: { + '$id': '#/properties/dmp/properties/dataset/items/properties/data_quality_assurance/items', + type: 'string', + title: 'The Data Quality Assurance Schema', + examples: ['We use file naming convention...'] } }, - "dataset_id": { - "$id": "#/properties/dmp/properties/dataset/items/properties/dataset_id", - "type": "object", - "title": "The Dataset ID Schema", - "description": "Dataset ID", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/dataset/items/properties/dataset_id/properties/identifier", - "type": "string", - "title": "The Dataset Identifier Schema", - "description": "Identifier for a dataset", - "examples": ["https://hdl.handle.net/11353/10.923628"] + dataset_id: { + '$id': '#/properties/dmp/properties/dataset/items/properties/dataset_id', + type: 'object', + title: 'The Dataset ID Schema', + description: 'Dataset ID', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/dataset/items/properties/dataset_id/properties/identifier', + type: 'string', + title: 'The Dataset Identifier Schema', + description: 'Identifier for a dataset', + examples: ['https://hdl.handle.net/11353/10.923628'] }, - "type": { - "$id": "#/properties/dmp/properties/dataset/items/properties/dataset_id/properties/type", - "type": "string", - "enum": [ - "handle", - "doi", - "ark", - "url", - "other" + type: { + '$id': '#/properties/dmp/properties/dataset/items/properties/dataset_id/properties/type', + type: 'string', + enum: %w[ + handle + doi + ark + url + other ], - "title": "The Dataset Identifier Type Schema", - "description": "Dataset identifier type. Allowed values: handle, doi, ark, url, other", - "examples": ["handle"] + title: 'The Dataset Identifier Type Schema', + description: 'Dataset identifier type. Allowed values: handle, doi, ark, url, other', + examples: ['handle'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "description": { - "$id": "#/properties/dmp/properties/dataset/items/properties/description", - "type": "string", - "title": "The Dataset Description Schema", - "description": "Description is a property in both Dataset and Distribution, in compliance with W3C DCAT. In some cases these might be identical, but in most cases the Dataset represents a more abstract concept, while the distribution can point to a specific file.", - "examples": ["Field observation"] + description: { + '$id': '#/properties/dmp/properties/dataset/items/properties/description', + type: 'string', + title: 'The Dataset Description Schema', + description: 'Description is a property in both Dataset and Distribution, in compliance with W3C DCAT. In some cases these might be identical, but in most cases the Dataset represents a more abstract concept, while the distribution can point to a specific file.', + examples: ['Field observation'] }, - "distribution": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution", - "type": "array", - "title": "The Dataset Distribution Schema", - "description": "To provide technical information on a specific instance of data.", - "items": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items", - "type": "object", - "title": "The Dataset Distribution Items Schema", - "properties": { - "access_url": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/access_url", - "type": "string", - "title": "The Dataset Distribution Access URL Schema", - "description": "A URL of the resource that gives access to a distribution of the dataset. e.g. landing page.", - "examples": ["http://some.repo"] + distribution: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution', + type: 'array', + title: 'The Dataset Distribution Schema', + description: 'To provide technical information on a specific instance of data.', + items: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items', + type: 'object', + title: 'The Dataset Distribution Items Schema', + properties: { + access_url: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/access_url', + type: 'string', + title: 'The Dataset Distribution Access URL Schema', + description: 'A URL of the resource that gives access to a distribution of the dataset. e.g. landing page.', + examples: ['http://some.repo'] }, - "available_until": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/available_until", - "type": "string", - "format": "date", - "title": "The Dataset Distribution Available Until Schema", - "description": "Indicates how long this distribution will be/ should be available. Encoded using the relevant ISO 8601 Date and Time compliant string.", - "examples": ["2030-06-30"] + available_until: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/available_until', + type: 'string', + format: 'date', + title: 'The Dataset Distribution Available Until Schema', + description: 'Indicates how long this distribution will be/ should be available. Encoded using the relevant ISO 8601 Date and Time compliant string.', + examples: ['2030-06-30'] }, - "byte_size": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/byte_size", - "type": "integer", - "title": "The Dataset Distribution Byte Size Schema", - "description": "Size in bytes.", - "examples": [690000] + byte_size: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/byte_size', + type: 'integer', + title: 'The Dataset Distribution Byte Size Schema', + description: 'Size in bytes.', + examples: [690_000] }, - "data_access": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/data_access", - "type": "string", - "enum": [ - "open", - "shared", - "closed" + data_access: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/data_access', + type: 'string', + enum: %w[ + open + shared + closed ], - "title": "The Dataset Distribution Data Access Schema", - "description": "Indicates access mode for data. Allowed values: open, shared, closed", - "examples": ["open"] + title: 'The Dataset Distribution Data Access Schema', + description: 'Indicates access mode for data. Allowed values: open, shared, closed', + examples: ['open'] }, - "description": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/description", - "type": "string", - "title": "The Dataset Distribution Description Schema", - "description": "Description is a property in both Dataset and Distribution, in compliance with W3C DCAT. In some cases these might be identical, but in most cases the Dataset represents a more abstract concept, while the distribution can point to a specific file.", - "examples": ["Best quality data before resizing"] + description: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/description', + type: 'string', + title: 'The Dataset Distribution Description Schema', + description: 'Description is a property in both Dataset and Distribution, in compliance with W3C DCAT. In some cases these might be identical, but in most cases the Dataset represents a more abstract concept, while the distribution can point to a specific file.', + examples: ['Best quality data before resizing'] }, - "download_url": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/download_url", - "type": "string", - "format": "uri", - "title": "The Dataset Distribution Download URL Schema", - "description": "The URL of the downloadable file in a given format. E.g. CSV file or RDF file.", - "examples": ["http://example.com/download/abc123/download"] + download_url: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/download_url', + type: 'string', + format: 'uri', + title: 'The Dataset Distribution Download URL Schema', + description: 'The URL of the downloadable file in a given format. E.g. CSV file or RDF file.', + examples: ['http://example.com/download/abc123/download'] }, - "format": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/format", - "type": "array", - "title": "The Dataset Distribution Format Schema", - "description": "Format according to: https://www.iana.org/assignments/media-types/media-types.xhtml if appropriate, otherwise use the common name for this format.", - "items": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/format/items", - "type": "string", - "title": "The Dataset Distribution Format Items Schema", - "examples": ["image/tiff"] + format: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/format', + type: 'array', + title: 'The Dataset Distribution Format Schema', + description: 'Format according to: https://www.iana.org/assignments/media-types/media-types.xhtml if appropriate, otherwise use the common name for this format.', + items: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/format/items', + type: 'string', + title: 'The Dataset Distribution Format Items Schema', + examples: ['image/tiff'] } }, - "host": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host", - "type": "object", - "title": "The Dataset Distribution Host Schema", - "description": "To provide information on quality of service provided by infrastructure (e.g. repository) where data is stored.", - "properties": { - "availability": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/availability", - "type": "string", - "title": "The Dataset Distribution Host Availability Schema", - "description": "Availability", - "examples": ["99,5"] + host: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host', + type: 'object', + title: 'The Dataset Distribution Host Schema', + description: 'To provide information on quality of service provided by infrastructure (e.g. repository) where data is stored.', + properties: { + availability: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/availability', + type: 'string', + title: 'The Dataset Distribution Host Availability Schema', + description: 'Availability', + examples: ['99,5'] }, - "backup_frequency": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/backup_frequency", - "type": "string", - "title": "The Dataset Distribution Host Backup Frequency Schema", - "description": "Backup Frequency", - "examples": ["weekly"] + backup_frequency: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/backup_frequency', + type: 'string', + title: 'The Dataset Distribution Host Backup Frequency Schema', + description: 'Backup Frequency', + examples: ['weekly'] }, - "backup_type": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/backup_type", - "type": "string", - "title": "The Dataset Distribution Host Backup Type Schema", - "description": "Backup Type", - "examples": ["tapes"] + backup_type: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/backup_type', + type: 'string', + title: 'The Dataset Distribution Host Backup Type Schema', + description: 'Backup Type', + examples: ['tapes'] }, - "certified_with": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/certified_with", - "type": "string", - "enum": [ - "din31644", - "dini-zertifikat", - "dsa", - "iso16363", - "iso16919", - "trac", - "wds", - "coretrustseal" + certified_with: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/certified_with', + type: 'string', + enum: %w[ + din31644 + dini-zertifikat + dsa + iso16363 + iso16919 + trac + wds + coretrustseal ], - "title": "The Dataset Distribution Host Certification Type Schema", - "description": "Repository certified to a recognised standard. Allowed values: din31644, dini-zertifikat, dsa, iso16363, iso16919, trac, wds, coretrustseal", - "examples": ["coretrustseal"] + title: 'The Dataset Distribution Host Certification Type Schema', + description: 'Repository certified to a recognised standard. Allowed values: din31644, dini-zertifikat, dsa, iso16363, iso16919, trac, wds, coretrustseal', + examples: ['coretrustseal'] }, - "description": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/description", - "type": "string", - "title": "The Dataset Distribution Host Description Schema", - "description": "Description", - "examples": ["Repository hosted by..."] + description: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/description', + type: 'string', + title: 'The Dataset Distribution Host Description Schema', + description: 'Description', + examples: ['Repository hosted by...'] }, - "dmproadmap_host_id": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/host_id", - "type": "object", - "title": "The Host ID", - "description": "The unique identifier or URL for the host", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/host_id/properties/identifier", - "type": "string", - "title": "The Host Identifier", - "description": "The Host URL or identifier", - "examples": ["https://www.re3data.org/repository/r3d100000044", "https://example.host.org"] + dmproadmap_host_id: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/host_id', + type: 'object', + title: 'The Host ID', + description: 'The unique identifier or URL for the host', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/host_id/properties/identifier', + type: 'string', + title: 'The Host Identifier', + description: 'The Host URL or identifier', + examples: ['https://www.re3data.org/repository/r3d100000044', 'https://example.host.org'] }, - "type": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/host_id/properties/type", - "type": "string", - "enum": [ - "handle", - "doi", - "ark", - "url" + type: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/host_id/properties/type', + type: 'string', + enum: %w[ + handle + doi + ark + url ], - "title": "The Host Identifier Type Schema", - "description": "Host identifier type. Allowed values: handle, doi, ark, url", - "examples": ["url"] + title: 'The Host Identifier Type Schema', + description: 'Host identifier type. Allowed values: handle, doi, ark, url', + examples: ['url'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "geo_location": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/geo_location", - "type": "string", - "enum": [ - "AD", "AE", "AF", "AG", "AI", "AL", "AM", "AO", "AQ", "AR", "AS", "AT", "AU", "AW", "AX", "AZ", "BA", - "BB", "BD", "BE", "BF", "BG", "BH", "BI", "BJ", "BL", "BM", "BN", "BO", "BQ", "BR", "BS", "BT", "BV", - "BW", "BY", "BZ", "CA", "CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", - "CV", "CW", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", "EH", "ER", "ES", - "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "GA", "GB", "GD", "GE", "GF", "GG", "GH", "GI", "GL", "GM", - "GN", "GP", "GQ", "GR", "GS", "GT", "GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", - "IL", "IM", "IN", "IO", "IQ", "IR", "IS", "IT", "JE", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", - "KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", "LT", "LU", "LV", "LY", - "MA", "MC", "MD", "ME", "MF", "MG", "MH", "MK", "ML", "MM", "MN", "MO", "MP", "MQ", "MR", "MS", "MT", - "MU", "MV", "MW", "MX", "MY", "MZ", "NA", "NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", - "NZ", "OM", "PA", "PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", "QA", - "RE", "RO", "RS", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", "SJ", "SK", "SL", "SM", - "SN", "SO", "SR", "SS", "ST", "SV", "SX", "SY", "SZ", "TC", "TD", "TF", "TG", "TH", "TJ", "TK", "TL", - "TM", "TN", "TO", "TR", "TT", "TV", "TW", "TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", - "VG", "VI", "VN", "VU", "WF", "WS", "YE", "YT", "ZA", "ZM", "ZW" + geo_location: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/geo_location', + type: 'string', + enum: %w[ + AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA + BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV + BW BY BZ CA CC CD CF CG CH CI CK CL CM CN CO CR CU + CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES + ET FI FJ FK FM FO FR GA GB GD GE GF GG GH GI GL GM + GN GP GQ GR GS GT GU GW GY HK HM HN HR HT HU ID IE + IL IM IN IO IQ IR IS IT JE JM JO JP KE KG KH KI KM + KN KP KR KW KY KZ LA LB LC LI LK LR LS LT LU LV LY + MA MC MD ME MF MG MH MK ML MM MN MO MP MQ MR MS MT + MU MV MW MX MY MZ NA NC NE NF NG NI NL NO NP NR NU + NZ OM PA PE PF PG PH PK PL PM PN PR PS PT PW PY QA + RE RO RS RU RW SA SB SC SD SE SG SH SI SJ SK SL SM + SN SO SR SS ST SV SX SY SZ TC TD TF TG TH TJ TK TL + TM TN TO TR TT TV TW TZ UA UG UM US UY UZ VA VC VE + VG VI VN VU WF WS YE YT ZA ZM ZW ], - "title": "The Dataset Distribution Host Geographical Location Schema", - "description": "Physical location of the data expressed using ISO 3166-1 country code.", - "examples": ["AT"] + title: 'The Dataset Distribution Host Geographical Location Schema', + description: 'Physical location of the data expressed using ISO 3166-1 country code.', + examples: ['AT'] }, - "pid_system": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/pid_system", - "type": "array", - "title": "The Dataset Distribution Host PID System Schema", - "description": "PID system(s). Allowed values: ark, arxiv, bibcode, doi, ean13, eissn, handle, igsn, isbn, issn, istc, lissn, lsid, pmid, purl, upc, url, urn, other", - "items": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/pid_system/items", - "type": "string", - "title": "The Dataset Distribution Host PID System Items Schema", - "enum": [ - "ark", - "arxiv", - "bibcode", - "doi", - "ean13", - "eissn", - "handle", - "igsn", - "isbn", - "issn", - "istc", - "lissn", - "lsid", - "pmid", - "purl", - "upc", - "url", - "urn", - "other" + pid_system: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/pid_system', + type: 'array', + title: 'The Dataset Distribution Host PID System Schema', + description: 'PID system(s). Allowed values: ark, arxiv, bibcode, doi, ean13, eissn, handle, igsn, isbn, issn, istc, lissn, lsid, pmid, purl, upc, url, urn, other', + items: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/pid_system/items', + type: 'string', + title: 'The Dataset Distribution Host PID System Items Schema', + enum: %w[ + ark + arxiv + bibcode + doi + ean13 + eissn + handle + igsn + isbn + issn + istc + lissn + lsid + pmid + purl + upc + url + urn + other ], - "examples": ["doi"] + examples: ['doi'] } }, - "storage_type": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/storage_type", - "type": "string", - "title": "The Dataset Distribution Host Storage Type Schema", - "description": "The type of storage required", - "examples": ["External Hard Drive"] + storage_type: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/storage_type', + type: 'string', + title: 'The Dataset Distribution Host Storage Type Schema', + description: 'The type of storage required', + examples: ['External Hard Drive'] }, - "support_versioning": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/support_versioning", - "type": "string", - "enum": [ - "yes", - "no", - "unknown" + support_versioning: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/support_versioning', + type: 'string', + enum: %w[ + yes + no + unknown ], - "title": "The Dataset Distribution Host Support Versioning Schema", - "description": "If host supports versioning. Allowed values: yes, no, unknown", - "examples": ["yes"] + title: 'The Dataset Distribution Host Support Versioning Schema', + description: 'If host supports versioning. Allowed values: yes, no, unknown', + examples: ['yes'] }, - "title": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/title", - "type": "string", - "title": "The Dataset Distribution Host Title Schema", - "description": "Title", - "examples": ["Super Repository"] + title: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/title', + type: 'string', + title: 'The Dataset Distribution Host Title Schema', + description: 'Title', + examples: ['Super Repository'] }, - "url": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/url", - "type": "string", - "format": "uri", - "title": "The Dataset Distribution Host Title Schema", - "description": "The URL of the system hosting a distribution of a dataset", - "examples": ["https://zenodo.org"] + url: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/host/properties/url', + type: 'string', + format: 'uri', + title: 'The Dataset Distribution Host Title Schema', + description: 'The URL of the system hosting a distribution of a dataset', + examples: ['https://zenodo.org'] } }, - "required": [ - "title", - "url" + required: %w[ + title + url ] }, - "license": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/license", - "type": "array", - "title": "The Dataset Distribution License(s) Schema", - "description": "To list all licenses applied to a specific distribution of data.", - "items": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/license/items", - "type": "object", - "title": "The Dataset Distribution License Items", - "properties": { - "license_ref": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/license/items/properties/license_ref", - "type": "string", - "format": "uri", - "title": "The Dataset Distribution License Reference Schema", - "description": "Link to license document.", - "examples": ["https://creativecommons.org/licenses/by/4.0/"] + license: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/license', + type: 'array', + title: 'The Dataset Distribution License(s) Schema', + description: 'To list all licenses applied to a specific distribution of data.', + items: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/license/items', + type: 'object', + title: 'The Dataset Distribution License Items', + properties: { + license_ref: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/license/items/properties/license_ref', + type: 'string', + format: 'uri', + title: 'The Dataset Distribution License Reference Schema', + description: 'Link to license document.', + examples: ['https://creativecommons.org/licenses/by/4.0/'] }, - "start_date": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/license/items/properties/start_date", - "type": "string", - "format": "date-time", - "title": "The Dataset Distribution License Start Date Schema", - "description": "If date is set in the future, it indicates embargo period. Encoded using the relevant ISO 8601 Date and Time compliant string.", - "examples": ["2019-06-30"] + start_date: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/license/items/properties/start_date', + type: 'string', + format: 'date-time', + title: 'The Dataset Distribution License Start Date Schema', + description: 'If date is set in the future, it indicates embargo period. Encoded using the relevant ISO 8601 Date and Time compliant string.', + examples: ['2019-06-30'] } }, - "required": [ - "license_ref", - "start_date" + required: %w[ + license_ref + start_date ] } }, - "title": { - "$id": "#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/title", - "type": "string", - "title": "The Dataset Distribution Title Schema", - "description": "Title is a property in both Dataset and Distribution, in compliance with W3C DCAT. In some cases these might be identical, but in most cases the Dataset represents a more abstract concept, while the distribution can point to a specific file.", - "examples": ["Full resolution images"] + title: { + '$id': '#/properties/dmp/properties/dataset/items/properties/distribution/items/properties/title', + type: 'string', + title: 'The Dataset Distribution Title Schema', + description: 'Title is a property in both Dataset and Distribution, in compliance with W3C DCAT. In some cases these might be identical, but in most cases the Dataset represents a more abstract concept, while the distribution can point to a specific file.', + examples: ['Full resolution images'] } }, - "required": [ - "data_access", - "title" + required: %w[ + data_access + title ] } }, - "issued": { - "$id": "#/properties/dmp/properties/dataset/items/properties/issued", - "type": "string", - "format": "date-time", - "title": "The Dataset Date of Issue Schema", - "description": "Issued. Encoded using the relevant ISO 8601 Date and Time compliant string.", - "examples": ["2019-06-30"] + issued: { + '$id': '#/properties/dmp/properties/dataset/items/properties/issued', + type: 'string', + format: 'date-time', + title: 'The Dataset Date of Issue Schema', + description: 'Issued. Encoded using the relevant ISO 8601 Date and Time compliant string.', + examples: ['2019-06-30'] }, - "keyword": { - "$id": "#/properties/dmp/properties/dataset/items/properties/keyword", - "type": "array", - "title": "The Dataset Keyword(s) Schema", - "description": "Keywords", - "items": { - "$id": "#/properties/dmp/properties/dataset/items/properties/keyword/items", - "type": "string", - "title": "The Dataset Keyword Items Schema", - "examples": ["keyword 1, keyword 2"] + keyword: { + '$id': '#/properties/dmp/properties/dataset/items/properties/keyword', + type: 'array', + title: 'The Dataset Keyword(s) Schema', + description: 'Keywords', + items: { + '$id': '#/properties/dmp/properties/dataset/items/properties/keyword/items', + type: 'string', + title: 'The Dataset Keyword Items Schema', + examples: ['keyword 1, keyword 2'] } }, - "language": { - "$id": "#/properties/dmp/properties/dataset/items/properties/language", - "type": "string", - "enum": [ - "aar", "abk", "afr", "aka", "amh", "ara", "arg", "asm", "ava", "ave", "aym", "aze", "bak", "bam", "bel", "ben", "bih", "bis", "bod", "bos", - "bre", "bul", "cat", "ces", "cha", "che", "chu", "chv", "cor", "cos", "cre", "cym", "dan", "deu", "div", "dzo", "ell", "eng", "epo", "est", - "eus", "ewe", "fao", "fas", "fij", "fin", "fra", "fry", "ful", "gla", "gle", "glg", "glv", "grn", "guj", "hat", "hau", "hbs", "heb", "her", - "hin", "hmo", "hrv", "hun", "hye", "ibo", "ido", "iii", "iku", "ile", "ina", "ind", "ipk", "isl", "ita", "jav", "jpn", "kal", "kan", "kas", - "kat", "kau", "kaz", "khm", "kik", "kin", "kir", "kom", "kon", "kor", "kua", "kur", "lao", "lat", "lav", "lim", "lin", "lit", "ltz", "lub", - "lug", "mah", "mal", "mar", "mkd", "mlg", "mlt", "mon", "mri", "msa", "mya", "nau", "nav", "nbl", "nde", "ndo", "nep", "nld", "nno", "nob", - "nor", "nya", "oci", "oji", "ori", "orm", "oss", "pan", "pli", "pol", "por", "pus", "que", "roh", "ron", "run", "rus", "sag", "san", "sin", - "slk", "slv", "sme", "smo", "sna", "snd", "som", "sot", "spa", "sqi", "srd", "srp", "ssw", "sun", "swa", "swe", "tah", "tam", "tat", "tel", - "tgk", "tgl", "tha", "tir", "ton", "tsn", "tso", "tuk", "tur", "twi", "uig", "ukr", "urd", "uzb", "ven", "vie", "vol", "wln", "wol", "xho", - "yid", "yor", "zha", "zho", "zul" + language: { + '$id': '#/properties/dmp/properties/dataset/items/properties/language', + type: 'string', + enum: %w[ + aar abk afr aka amh ara arg asm ava ave aym aze bak bam bel ben bih bis bod bos + bre bul cat ces cha che chu chv cor cos cre cym dan deu div dzo ell eng epo est + eus ewe fao fas fij fin fra fry ful gla gle glg glv grn guj hat hau hbs heb her + hin hmo hrv hun hye ibo ido iii iku ile ina ind ipk isl ita jav jpn kal kan kas + kat kau kaz khm kik kin kir kom kon kor kua kur lao lat lav lim lin lit ltz lub + lug mah mal mar mkd mlg mlt mon mri msa mya nau nav nbl nde ndo nep nld nno nob + nor nya oci oji ori orm oss pan pli pol por pus que roh ron run rus sag san sin + slk slv sme smo sna snd som sot spa sqi srd srp ssw sun swa swe tah tam tat tel + tgk tgl tha tir ton tsn tso tuk tur twi uig ukr urd uzb ven vie vol wln wol xho + yid yor zha zho zul ], - "title": "The Dataset Language Schema", - "description": "Language of the dataset expressed using ISO 639-3.", - "examples": ["eng"] + title: 'The Dataset Language Schema', + description: 'Language of the dataset expressed using ISO 639-3.', + examples: ['eng'] }, - "metadata": { - "$id": "#/properties/dmp/properties/dataset/items/properties/metadata", - "type": "array", - "title": "The Dataset Metadata Schema", - "description": "To describe metadata standards used.", - "items": { - "$id": "#/properties/dmp/properties/dataset/items/properties/metadata/items", - "type": "object", - "title": "The Dataset Metadata Items Schema", - "properties": { - "description": { - "$id": "#/properties/dmp/properties/dataset/items/properties/metadata/items/properties/description", - "type": "string", - "title": "The Dataset Metadata Description Schema", - "description": "Description", - "examples": ["Provides taxonomy for..."] + metadata: { + '$id': '#/properties/dmp/properties/dataset/items/properties/metadata', + type: 'array', + title: 'The Dataset Metadata Schema', + description: 'To describe metadata standards used.', + items: { + '$id': '#/properties/dmp/properties/dataset/items/properties/metadata/items', + type: 'object', + title: 'The Dataset Metadata Items Schema', + properties: { + description: { + '$id': '#/properties/dmp/properties/dataset/items/properties/metadata/items/properties/description', + type: 'string', + title: 'The Dataset Metadata Description Schema', + description: 'Description', + examples: ['Provides taxonomy for...'] }, - "language": { - "$id": "#/properties/dmp/properties/dataset/items/properties/metadata/items/properties/language", - "type": "string", - "enum": [ - "aar", "abk", "afr", "aka", "amh", "ara", "arg", "asm", "ava", "ave", "aym", "aze", "bak", "bam", "bel", "ben", "bih", "bis", "bod", "bos", - "bre", "bul", "cat", "ces", "cha", "che", "chu", "chv", "cor", "cos", "cre", "cym", "dan", "deu", "div", "dzo", "ell", "eng", "epo", "est", - "eus", "ewe", "fao", "fas", "fij", "fin", "fra", "fry", "ful", "gla", "gle", "glg", "glv", "grn", "guj", "hat", "hau", "hbs", "heb", "her", - "hin", "hmo", "hrv", "hun", "hye", "ibo", "ido", "iii", "iku", "ile", "ina", "ind", "ipk", "isl", "ita", "jav", "jpn", "kal", "kan", "kas", - "kat", "kau", "kaz", "khm", "kik", "kin", "kir", "kom", "kon", "kor", "kua", "kur", "lao", "lat", "lav", "lim", "lin", "lit", "ltz", "lub", - "lug", "mah", "mal", "mar", "mkd", "mlg", "mlt", "mon", "mri", "msa", "mya", "nau", "nav", "nbl", "nde", "ndo", "nep", "nld", "nno", "nob", - "nor", "nya", "oci", "oji", "ori", "orm", "oss", "pan", "pli", "pol", "por", "pus", "que", "roh", "ron", "run", "rus", "sag", "san", "sin", - "slk", "slv", "sme", "smo", "sna", "snd", "som", "sot", "spa", "sqi", "srd", "srp", "ssw", "sun", "swa", "swe", "tah", "tam", "tat", "tel", - "tgk", "tgl", "tha", "tir", "ton", "tsn", "tso", "tuk", "tur", "twi", "uig", "ukr", "urd", "uzb", "ven", "vie", "vol", "wln", "wol", "xho", - "yid", "yor", "zha", "zho", "zul" + language: { + '$id': '#/properties/dmp/properties/dataset/items/properties/metadata/items/properties/language', + type: 'string', + enum: %w[ + aar abk afr aka amh ara arg asm ava ave aym aze bak bam bel ben bih bis bod bos + bre bul cat ces cha che chu chv cor cos cre cym dan deu div dzo ell eng epo est + eus ewe fao fas fij fin fra fry ful gla gle glg glv grn guj hat hau hbs heb her + hin hmo hrv hun hye ibo ido iii iku ile ina ind ipk isl ita jav jpn kal kan kas + kat kau kaz khm kik kin kir kom kon kor kua kur lao lat lav lim lin lit ltz lub + lug mah mal mar mkd mlg mlt mon mri msa mya nau nav nbl nde ndo nep nld nno nob + nor nya oci oji ori orm oss pan pli pol por pus que roh ron run rus sag san sin + slk slv sme smo sna snd som sot spa sqi srd srp ssw sun swa swe tah tam tat tel + tgk tgl tha tir ton tsn tso tuk tur twi uig ukr urd uzb ven vie vol wln wol xho + yid yor zha zho zul ], - "title": "The Dataset Metadata Language Schema", - "description": "Language of the metadata expressed using ISO 639-3.", - "examples": ["eng"] + title: 'The Dataset Metadata Language Schema', + description: 'Language of the metadata expressed using ISO 639-3.', + examples: ['eng'] }, - "metadata_standard_id": { - "$id": "#/properties/dmp/properties/dataset/items/properties/metadata/items/properties/metadata_standard_id", - "type": "object", - "title": "The Dataset Metadata Standard ID Schema", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/dataset/items/properties/metadata/items/properties/metadata_standard_id/identifier", - "type": "string", - "title": "The Dataset Metadata Standard Identifier Value Schema", - "description": "Identifier for the metadata standard used.", - "examples": ["http://www.dublincore.org/specifications/dublin-core/dcmi-terms/"] + metadata_standard_id: { + '$id': '#/properties/dmp/properties/dataset/items/properties/metadata/items/properties/metadata_standard_id', + type: 'object', + title: 'The Dataset Metadata Standard ID Schema', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/dataset/items/properties/metadata/items/properties/metadata_standard_id/identifier', + type: 'string', + title: 'The Dataset Metadata Standard Identifier Value Schema', + description: 'Identifier for the metadata standard used.', + examples: ['http://www.dublincore.org/specifications/dublin-core/dcmi-terms/'] }, - "type": { - "$id": "#/properties/dmp/properties/dataset/items/properties/metadata/items/properties/metadata_standard_id/type", - "type": "string", - "enum": [ - "url", - "other" + type: { + '$id': '#/properties/dmp/properties/dataset/items/properties/metadata/items/properties/metadata_standard_id/type', + type: 'string', + enum: %w[ + url + other ], - "title": "The Dataset Metadata Standard Identifier Type Schema", - "description": "Identifier type. Allowed values: url, other", - "examples": ["url"] + title: 'The Dataset Metadata Standard Identifier Type Schema', + description: 'Identifier type. Allowed values: url, other', + examples: ['url'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] } }, - "required": [ - "metadata_standard_id" + required: [ + 'metadata_standard_id' ] } }, - "personal_data": { - "$id": "#/properties/dmp/properties/dataset/items/properties/personal_data", - "type": "string", - "enum": [ - "yes", - "no", - "unknown" + personal_data: { + '$id': '#/properties/dmp/properties/dataset/items/properties/personal_data', + type: 'string', + enum: %w[ + yes + no + unknown ], - "title": "The Dataset Personal Data Schema", - "description": "If any personal data is contained. Allowed values: yes, no, unknown", - "examples": ["unknown"] + title: 'The Dataset Personal Data Schema', + description: 'If any personal data is contained. Allowed values: yes, no, unknown', + examples: ['unknown'] }, - "preservation_statement": { - "$id": "#/properties/dmp/properties/dataset/items/properties/preservation_statement", - "type": "string", - "title": "The Dataset Preservation Statement Schema", - "description": "Preservation Statement", - "examples": ["Must be preserved to enable..."] + preservation_statement: { + '$id': '#/properties/dmp/properties/dataset/items/properties/preservation_statement', + type: 'string', + title: 'The Dataset Preservation Statement Schema', + description: 'Preservation Statement', + examples: ['Must be preserved to enable...'] }, - "security_and_privacy": { - "$id": "#/properties/dmp/properties/dataset/items/properties/security_and_privacy", - "type": "array", - "title": "The Dataset Security and Policy Schema", - "description": "To list all issues and requirements related to security and privacy", - "items": { - "$id": "#/properties/dmp/properties/dataset/items/properties/security_and_privacy/items", - "type": "object", - "title": "The Dataset Security & Policy Items Schema", - "properties": { - "description": { - "$id": "#/properties/dmp/properties/dataset/items/properties/security_and_privacy/items/properties/description", - "type": "string", - "title": "The Dataset Security & Policy Description Schema", - "description": "Description", - "examples": ["Server with data must be kept in a locked room"] + security_and_privacy: { + '$id': '#/properties/dmp/properties/dataset/items/properties/security_and_privacy', + type: 'array', + title: 'The Dataset Security and Policy Schema', + description: 'To list all issues and requirements related to security and privacy', + items: { + '$id': '#/properties/dmp/properties/dataset/items/properties/security_and_privacy/items', + type: 'object', + title: 'The Dataset Security & Policy Items Schema', + properties: { + description: { + '$id': '#/properties/dmp/properties/dataset/items/properties/security_and_privacy/items/properties/description', + type: 'string', + title: 'The Dataset Security & Policy Description Schema', + description: 'Description', + examples: ['Server with data must be kept in a locked room'] }, - "title": { - "$id": "#/properties/dmp/properties/dataset/items/properties/security_and_privacy/items/properties/title", - "type": "string", - "title": "The Dataset Security & Policy Title Schema", - "description": "Title", - "examples": ["Physical access control"] + title: { + '$id': '#/properties/dmp/properties/dataset/items/properties/security_and_privacy/items/properties/title', + type: 'string', + title: 'The Dataset Security & Policy Title Schema', + description: 'Title', + examples: ['Physical access control'] } }, - "required": ["title"] + required: ['title'] } }, - "sensitive_data": { - "$id": "#/properties/dmp/properties/dataset/items/properties/sensitive_data", - "type": "string", - "enum": [ - "yes", - "no", - "unknown" + sensitive_data: { + '$id': '#/properties/dmp/properties/dataset/items/properties/sensitive_data', + type: 'string', + enum: %w[ + yes + no + unknown ], - "title": "The Dataset Sensitive Data Schema", - "description": "If any sensitive data is contained. Allowed values: yes, no, unknown", - "examples": ["unknown"] + title: 'The Dataset Sensitive Data Schema', + description: 'If any sensitive data is contained. Allowed values: yes, no, unknown', + examples: ['unknown'] }, - "technical_resource": { - "$id": "#/properties/dmp/properties/dataset/items/properties/technical_resource", - "type": "array", - "title": "The Dataset Technical Resource Schema", - "description": "To list all technical resources needed to implement a DMP", - "items": { - "$id": "#/properties/dmp/properties/dataset/items/properties/technical_resource/items", - "type": "object", - "title": "The Dataset Technical Resource Items Schema", - "properties": { - "description": { - "$id": "#/properties/dmp/properties/dataset/items/properties/technical_resource/items/description", - "type": "string", - "title": "The Dataset Technical Resource Description Schema", - "description": "Description of the technical resource", - "examples": ["Device needed to collect field data..."] + technical_resource: { + '$id': '#/properties/dmp/properties/dataset/items/properties/technical_resource', + type: 'array', + title: 'The Dataset Technical Resource Schema', + description: 'To list all technical resources needed to implement a DMP', + items: { + '$id': '#/properties/dmp/properties/dataset/items/properties/technical_resource/items', + type: 'object', + title: 'The Dataset Technical Resource Items Schema', + properties: { + description: { + '$id': '#/properties/dmp/properties/dataset/items/properties/technical_resource/items/description', + type: 'string', + title: 'The Dataset Technical Resource Description Schema', + description: 'Description of the technical resource', + examples: ['Device needed to collect field data...'] }, - "dmproadmap_technical_resource_id": { - "$id": "#/properties/dmp/properties/dataset/items/properties/technical_resource/items/dmproadmap_technical_resource_id", - "type": "object", - "title": "The Dataset Metadata Standard ID Schema", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/dataset/items/properties/technical_resource/items/dmproadmap_technical_resource_id/identifier", - "type": "string", - "title": "The Technical Resource Identifier Value Schema", - "description": "Identifier for the metadata standard used.", - "examples": ["http://www.dublincore.org/specifications/dublin-core/dcmi-terms/"] + dmproadmap_technical_resource_id: { + '$id': '#/properties/dmp/properties/dataset/items/properties/technical_resource/items/dmproadmap_technical_resource_id', + type: 'object', + title: 'The Dataset Metadata Standard ID Schema', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/dataset/items/properties/technical_resource/items/dmproadmap_technical_resource_id/identifier', + type: 'string', + title: 'The Technical Resource Identifier Value Schema', + description: 'Identifier for the metadata standard used.', + examples: ['http://www.dublincore.org/specifications/dublin-core/dcmi-terms/'] }, - "type": { - "$id": "#/properties/dmp/properties/dataset/items/properties/technical_resource/items/dmproadmap_technical_resource_id/type", - "type": "string", - "enum": [ - "ark", - "doi", - "handle", - "rrid", - "url", - "other" + type: { + '$id': '#/properties/dmp/properties/dataset/items/properties/technical_resource/items/dmproadmap_technical_resource_id/type', + type: 'string', + enum: %w[ + ark + doi + handle + rrid + url + other ], - "title": "The Technical Resource Identifier Type Schema", - "description": "Identifier type. Allowed values: url, other", - "examples": ["url"] + title: 'The Technical Resource Identifier Type Schema', + description: 'Identifier type. Allowed values: url, other', + examples: ['url'] } } }, - "name": { - "$id": "#/properties/dmp/properties/dataset/items/properties/technical_resource/items/name", - "type": "string", - "title": "The Dataset Technical Resource Name Schema", - "description": "Name of the technical resource", - "examples": ["123/45/43/AT"] + name: { + '$id': '#/properties/dmp/properties/dataset/items/properties/technical_resource/items/name', + type: 'string', + title: 'The Dataset Technical Resource Name Schema', + description: 'Name of the technical resource', + examples: ['123/45/43/AT'] } }, - "required": ["name"] + required: ['name'] } }, - "title": { - "$id": "#/properties/dmp/properties/dataset/items/properties/title", - "type": "string", - "title": "The Dataset Title Schema", - "description": "Title is a property in both Dataset and Distribution, in compliance with W3C DCAT. In some cases these might be identical, but in most cases the Dataset represents a more abstract concept, while the distribution can point to a specific file.", - "examples": ["Fast car images"] + title: { + '$id': '#/properties/dmp/properties/dataset/items/properties/title', + type: 'string', + title: 'The Dataset Title Schema', + description: 'Title is a property in both Dataset and Distribution, in compliance with W3C DCAT. In some cases these might be identical, but in most cases the Dataset represents a more abstract concept, while the distribution can point to a specific file.', + examples: ['Fast car images'] }, - "type": { - "$id": "#/properties/dmp/properties/dataset/items/properties/type", - "type": "string", - "title": "The Dataset Type Schema", - "description": "If appropriate, type according to: DataCite and/or COAR dictionary. Otherwise use the common name for the type, e.g. raw data, software, survey, etc. https://schema.datacite.org/meta/kernel-4.1/doc/DataCite-MetadataKernel_v4.1.pdf http://vocabularies.coar-repositories.org/pubby/resource_type.html", - "examples": ["image"] + type: { + '$id': '#/properties/dmp/properties/dataset/items/properties/type', + type: 'string', + title: 'The Dataset Type Schema', + description: 'If appropriate, type according to: DataCite and/or COAR dictionary. Otherwise use the common name for the type, e.g. raw data, software, survey, etc. https://schema.datacite.org/meta/kernel-4.1/doc/DataCite-MetadataKernel_v4.1.pdf http://vocabularies.coar-repositories.org/pubby/resource_type.html', + examples: ['image'] } }, - "required": [ - "title" + required: [ + 'title' ] } }, - "description": { - "$id": "#/properties/dmp/properties/description", - "type": "string", - "title": "The DMP Description Schema", - "description": "To provide any free-form text information on a DMP", - "examples": ["This DMP is for our new project"] + description: { + '$id': '#/properties/dmp/properties/description', + type: 'string', + title: 'The DMP Description Schema', + description: 'To provide any free-form text information on a DMP', + examples: ['This DMP is for our new project'] }, - "dmp_id": { - "$id": "#/properties/dmp/properties/dmp_id", - "type": "object", - "title": "The DMP Identifier Schema", - "description": "Identifier for the DMP itself", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/dmp_id/properties/identifier", - "type": "string", - "title": "The DMP Identifier Value Schema", - "description": "Identifier for a DMP", - "examples": ["https://doi.org/10.1371/journal.pcbi.1006750"] + dmp_id: { + '$id': '#/properties/dmp/properties/dmp_id', + type: 'object', + title: 'The DMP Identifier Schema', + description: 'Identifier for the DMP itself', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/dmp_id/properties/identifier', + type: 'string', + title: 'The DMP Identifier Value Schema', + description: 'Identifier for a DMP', + examples: ['https://doi.org/10.1371/journal.pcbi.1006750'] }, - "type": { - "$id": "#/properties/dmp/properties/dmp_id/properties/type", - "type": "string", - "enum": [ - "handle", - "doi", - "ark", - "url", - "other", - "file" + type: { + '$id': '#/properties/dmp/properties/dmp_id/properties/type', + type: 'string', + enum: %w[ + handle + doi + ark + url + other + file ], - "title": "The DMP Identifier Type Schema", - "description": "The DMP Identifier Type. Allowed values: handle, doi, ark, url, other, file (note: file is used by DMPHub to handle new PDF uploads)", - "examples": ["doi"] + title: 'The DMP Identifier Type Schema', + description: 'The DMP Identifier Type. Allowed values: handle, doi, ark, url, other, file (note: file is used by DMPHub to handle new PDF uploads)', + examples: ['doi'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "dmphub_modifications": { - "$id": "#/properties/dmp/properties/dmphub_modifications", - "type": "array", - "title": "External modifications", - "description": "Modifications made by an external system that does not own the DMP ID", - "items": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items", - "type": "object", - "title": "An external modification", - "properties": { - "id": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/id", - "type": "string", - "title": "Modification identifier", - "examples": ["12345ABCD"] + dmphub_modifications: { + '$id': '#/properties/dmp/properties/dmphub_modifications', + type: 'array', + title: 'External modifications', + description: 'Modifications made by an external system that does not own the DMP ID', + items: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items', + type: 'object', + title: 'An external modification', + properties: { + id: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/id', + type: 'string', + title: 'Modification identifier', + examples: ['12345ABCD'] }, - "provenance": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/provenance", - "type": "string", - "title": "Modifier", - "examples": ["datacite"] + provenance: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/provenance', + type: 'string', + title: 'Modifier', + examples: ['datacite'] }, - "timestamp": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/timestamp", - "type": "string", - "format": "date-time", - "title": "The modification date and time", - "examples": ["2023-07-27T15:08:32Z"] + timestamp: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/timestamp', + type: 'string', + format: 'date-time', + title: 'The modification date and time', + examples: ['2023-07-27T15:08:32Z'] }, - "note": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/note", - "type": "string", - "title": "Descriptive note", - "examples": ["data received from event data"] + note: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/note', + type: 'string', + title: 'Descriptive note', + examples: ['data received from event data'] }, - "status": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/status", - "type": "string", - "title": "Modification status", - "enum": [ - "accepted", - "pending", - "rejected" + status: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/status', + type: 'string', + title: 'Modification status', + enum: %w[ + accepted + pending + rejected ] }, - "dmproadmap_related_identifier": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier", - "type": "object", - "title": "A related identifier", - "properties": { - "descriptor": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier/properties/descriptor", - "type": "string", - "enum": [ - "is_cited_by", - "cites", - "is_supplement_to", - "is_supplemented_by", - "is_described_by", - "describes", - "has_metadata", - "is_metadata_for", - "is_part_of", - "has_part", - "is_referenced_by", - "references", - "is_documented_by", - "documents", - "is_new_version_of", - "is_previous_version_of" + dmproadmap_related_identifier: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier', + type: 'object', + title: 'A related identifier', + properties: { + descriptor: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier/properties/descriptor', + type: 'string', + enum: %w[ + is_cited_by + cites + is_supplement_to + is_supplemented_by + is_described_by + describes + has_metadata + is_metadata_for + is_part_of + has_part + is_referenced_by + references + is_documented_by + documents + is_new_version_of + is_previous_version_of ] }, - "identifier": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier/properties/identifier", - "type": "string", - "title": "A unique identifier for the item", - "description": "Identifier for a DMP", - "examples": ["https://doi.org/10.1371/journal.pcbi.1006750"] + identifier: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier/properties/identifier', + type: 'string', + title: 'A unique identifier for the item', + description: 'Identifier for a DMP', + examples: ['https://doi.org/10.1371/journal.pcbi.1006750'] }, - "type": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier/properties/type", - "type": "string", - "enum": [ - "handle", - "doi", - "ark", - "url", - "other" + type: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier/properties/type', + type: 'string', + enum: %w[ + handle + doi + ark + url + other ] }, - "work_type": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier/properties/work_type", - "type": "string" + work_type: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/dmproadmap_related_identifier/properties/work_type', + type: 'string' } }, - "required": [ - "descriptor", - "identifier", - "type", - "work_type" + required: %w[ + descriptor + identifier + type + work_type ] }, - "funding": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding", - "type": "object", - "title": "A modification to Funding", - "properties": { - "dmproadmap_project_number": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/dmproadmap_project_number", - "type": "string", - "title": "The funder's identifier for the research project", - "description": "The funder's identifier used to identify the research project", - "examples": ["prj-XYZ987-UCB"] + funding: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/funding', + type: 'object', + title: 'A modification to Funding', + properties: { + dmproadmap_project_number: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/dmproadmap_project_number', + type: 'string', + title: "The funder's identifier for the research project", + description: "The funder's identifier used to identify the research project", + examples: ['prj-XYZ987-UCB'] }, - "funder_id": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/funder_id", - "type": "object", - "title": "The Funder ID Schema", - "description": "Funder ID of the associated project", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/funder_id/properties/identifier", - "type": "string", - "title": "The Funder ID Value Schema", - "description": "Funder ID, recommended to use CrossRef Funder Registry. See: https://www.crossref.org/services/funder-registry/", - "examples": ["501100002428"] + funder_id: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/funder_id', + type: 'object', + title: 'The Funder ID Schema', + description: 'Funder ID of the associated project', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/funder_id/properties/identifier', + type: 'string', + title: 'The Funder ID Value Schema', + description: 'Funder ID, recommended to use CrossRef Funder Registry. See: https://www.crossref.org/services/funder-registry/', + examples: ['501100002428'] }, - "type": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/funder_id/properties/type", - "type": "string", - "enum": [ - "fundref", - "ror", - "url", - "other" + type: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/funder_id/properties/type', + type: 'string', + enum: %w[ + fundref + ror + url + other ], - "title": "The Funder ID Type Schema", - "description": "Identifier type. Allowed values: fundref, url, other", - "examples": ["fundref"] + title: 'The Funder ID Type Schema', + description: 'Identifier type. Allowed values: fundref, url, other', + examples: ['fundref'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "funding_status": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/funding_status", - "type": "string", - "enum": [ - "planned", - "applied", - "granted", - "rejected" + funding_status: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/funding_status', + type: 'string', + enum: %w[ + planned + applied + granted + rejected ], - "title": "The Funding Status Schema", - "description": "To express different phases of project lifecycle. Allowed values: planned, applied, granted, rejected", - "examples": ["granted"] + title: 'The Funding Status Schema', + description: 'To express different phases of project lifecycle. Allowed values: planned, applied, granted, rejected', + examples: ['granted'] }, - "grant_id": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/grant_id", - "type": "object", - "title": "The Funding Grant ID Schema", - "description": "Grant ID of the associated project", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/grant_id/properties/identifier", - "type": "string", - "title": "The Funding Grant ID Value Schema", - "description": "Grant ID", - "examples": ["776242"] + grant_id: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/grant_id', + type: 'object', + title: 'The Funding Grant ID Schema', + description: 'Grant ID of the associated project', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/grant_id/properties/identifier', + type: 'string', + title: 'The Funding Grant ID Value Schema', + description: 'Grant ID', + examples: ['776242'] }, - "type": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/grant_id/properties/type", - "type": "string", - "title": "The Funding Grant ID Type Schema", - "enum": [ - "doi", - "url", - "other" + type: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/grant_id/properties/type', + type: 'string', + title: 'The Funding Grant ID Type Schema', + enum: %w[ + doi + url + other ], - "description": "Identifier type. Allowed values: url, other", - "examples": ["other"] + description: 'Identifier type. Allowed values: url, other', + examples: ['other'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "name": { - "$id": "#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/name", - "type": "string", - "title": "The name of the funding instituion / organization", - "description": "Name", - "examples": ["National Science Foundation"] + name: { + '$id': '#/properties/dmp/properties/dmphub_modifications/items/properties/funding/properties/name', + type: 'string', + title: 'The name of the funding instituion / organization', + description: 'Name', + examples: ['National Science Foundation'] } }, - "required": [ - "funding_status", - "name" + required: %w[ + funding_status + name ] }, - "project": { - "$id": "#/properties/dmp/properties/dmphub_modifications/project", - "type": "object", - "title": "The DMP Project Items Schema", - "properties": { - "description": { - "$id": "#/properties/dmp/properties/dmphub_modifications/project/properties/description", - "type": "string", - "title": "The DMP Project Description Schema", - "description": "Project description", - "examples": ["Project develops novel..."] + project: { + '$id': '#/properties/dmp/properties/dmphub_modifications/project', + type: 'object', + title: 'The DMP Project Items Schema', + properties: { + description: { + '$id': '#/properties/dmp/properties/dmphub_modifications/project/properties/description', + type: 'string', + title: 'The DMP Project Description Schema', + description: 'Project description', + examples: ['Project develops novel...'] }, - "end": { - "$id": "#/properties/dmp/properties/dmphub_modifications/project/properties/end", - "type": "string", - "format": "date-time", - "title": "The DMP Project End Date Schema", - "description": "Project end date. Encoded using the relevant ISO 8601 Date and Time compliant string.", - "examples": ["2020-03-31T00:00:00Z"] + end: { + '$id': '#/properties/dmp/properties/dmphub_modifications/project/properties/end', + type: 'string', + format: 'date-time', + title: 'The DMP Project End Date Schema', + description: 'Project end date. Encoded using the relevant ISO 8601 Date and Time compliant string.', + examples: ['2020-03-31T00:00:00Z'] }, - "start": { - "$id": "#/properties/dmp/properties/dmphub_modifications/project/properties/start", - "type": "string", - "format": "date-time", - "title": "The DMP Project Start Date Schema", - "description": "Project start date. Encoded using the relevant ISO 8601 Date and Time compliant string.", - "examples": ["2019-04-01T00:00:00Z"] + start: { + '$id': '#/properties/dmp/properties/dmphub_modifications/project/properties/start', + type: 'string', + format: 'date-time', + title: 'The DMP Project Start Date Schema', + description: 'Project start date. Encoded using the relevant ISO 8601 Date and Time compliant string.', + examples: ['2019-04-01T00:00:00Z'] }, - "title": { - "$id": "#/properties/dmp/properties/dmphub_modifications/project/properties/title", - "type": "string", - "title": "The DMP Project Title Schema", - "description": "Project title", - "examples": ["Our New Project"] + title: { + '$id': '#/properties/dmp/properties/dmphub_modifications/project/properties/title', + type: 'string', + title: 'The DMP Project Title Schema', + description: 'Project title', + examples: ['Our New Project'] } }, - "required": [ - "title" + required: [ + 'title' ] } } }, - "required": [ - "id", - "provenance", - "status", - "timestamp" + required: %w[ + id + provenance + status + timestamp ] }, - "dmphub_versions": { - "$id": "#/properties/dmp/properties/dmphub_versions", - "type": "array", - "title": "DMP ID versions", - "description": "Links to all of the DMPs versions", - "items": { - "$id": "#/properties/dmp/properties/dmphub_versions/items", - "type": "object", - "title": "DMP version", - "properties": { - "timestamp": { - "$id": "#/properties/dmp/properties/dmphub_versions/items/properties/timestamp", - "type": "string", - "format": "date-time", - "title": "The version date and time", - "examples": ["2023-08-17T16:14:39Z"] + dmphub_versions: { + '$id': '#/properties/dmp/properties/dmphub_versions', + type: 'array', + title: 'DMP ID versions', + description: 'Links to all of the DMPs versions', + items: { + '$id': '#/properties/dmp/properties/dmphub_versions/items', + type: 'object', + title: 'DMP version', + properties: { + timestamp: { + '$id': '#/properties/dmp/properties/dmphub_versions/items/properties/timestamp', + type: 'string', + format: 'date-time', + title: 'The version date and time', + examples: ['2023-08-17T16:14:39Z'] }, - "url": { - "$id": "#/properties/dmp/properties/dmphub_versions/items/properties/url", - "type": "string", - "format": "uri", - "title": "The URL to retrieve the specified version", - "examples": ["https://somesite.org/dmps/doi.org/10.1234/A1B2C3D4?version=2023-08-17T16:14:39Z"] + url: { + '$id': '#/properties/dmp/properties/dmphub_versions/items/properties/url', + type: 'string', + format: 'uri', + title: 'The URL to retrieve the specified version', + examples: ['https://somesite.org/dmps/doi.org/10.1234/A1B2C3D4?version=2023-08-17T16:14:39Z'] } } }, - "required": [ - "timestamp", - "url" + required: %w[ + timestamp + url ] }, - "dmproadmap_related_identifiers": { - "$id": "#/properties/dmp/properties/dmproadmap_related_identifiers", - "type": "array", - "title": "Related identifiers for the DMP", - "description": "Identifiers for objects related to the DMP (e.g. datasets, publications, etc.)", - "items": { - "$id": "#/properties/dmp/properties/dmproadmap_related_identifiers/items", - "type": "object", - "title": "A related identifier", - "properties": { - "descriptor": { - "$id": "#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/descriptor", - "type": "string", - "enum": [ - "is_cited_by", - "cites", - "is_supplement_to", - "is_supplemented_by", - "is_described_by", - "describes", - "has_metadata", - "is_metadata_for", - "is_part_of", - "has_part", - "is_referenced_by", - "references", - "is_documented_by", - "documents", - "is_new_version_of", - "is_previous_version_of" + dmproadmap_related_identifiers: { + '$id': '#/properties/dmp/properties/dmproadmap_related_identifiers', + type: 'array', + title: 'Related identifiers for the DMP', + description: 'Identifiers for objects related to the DMP (e.g. datasets, publications, etc.)', + items: { + '$id': '#/properties/dmp/properties/dmproadmap_related_identifiers/items', + type: 'object', + title: 'A related identifier', + properties: { + descriptor: { + '$id': '#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/descriptor', + type: 'string', + enum: %w[ + is_cited_by + cites + is_supplement_to + is_supplemented_by + is_described_by + describes + has_metadata + is_metadata_for + is_part_of + has_part + is_referenced_by + references + is_documented_by + documents + is_new_version_of + is_previous_version_of ] }, - "identifier": { - "$id": "#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/identifier", - "type": "string", - "title": "A unique identifier for the item", - "description": "Identifier for a DMP", - "examples": ["https://doi.org/10.1371/journal.pcbi.1006750"] + identifier: { + '$id': '#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/identifier', + type: 'string', + title: 'A unique identifier for the item', + description: 'Identifier for a DMP', + examples: ['https://doi.org/10.1371/journal.pcbi.1006750'] }, - "type": { - "$id": "#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/type", - "type": "string", - "enum": [ - "handle", - "doi", - "ark", - "url", - "other" + type: { + '$id': '#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/type', + type: 'string', + enum: %w[ + handle + doi + ark + url + other ] }, - "work_type": { - "$id": "#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/work_type", - "type": "string" + work_type: { + '$id': '#/properties/dmp/properties/dmproadmap_related_identifiers/items/properties/work_type', + type: 'string' } }, - "required": [ - "descriptor", - "identifier", - "type", - "work_type" + required: %w[ + descriptor + identifier + type + work_type ] } }, - "dmproadmap_research_facilities": { - "$id": "#/properties/dmp/properties/dmproadmap_research_facilities", - "type": "array", - "title": "Facilities", - "description": "Facilities (e.g. labs and research stations) that will be used to collect/process research data", - "items": { - "$id": "#/properties/dmp/properties/dmproadmap_research_facilities/items", - "type": "object", - "title": "A research facility", - "properties": { - "facility_id": { - "$id": "#/properties/dmp/properties/dmproadmap_research_facilities/items/properties/facility_id", - "type": "object", - "title": "The unique ID of the facility", - "description": "The facility's ROR, DOI or URL", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/dmproadmap_research_facilities/items/properties/facility_id/properties/identifier", - "type": "string", - "title": "The facility ID", - "description": "ROR ID, DOI or URL. Recommended to use Research Organization Registry (ROR) or DOI when available. See: https://ror.org", - "examples": ["https://ror.org/03yrm5c26", "http://doi.org/10.13039/100005595", "http://www.cdlib.org/"] + dmproadmap_research_facilities: { + '$id': '#/properties/dmp/properties/dmproadmap_research_facilities', + type: 'array', + title: 'Facilities', + description: 'Facilities (e.g. labs and research stations) that will be used to collect/process research data', + items: { + '$id': '#/properties/dmp/properties/dmproadmap_research_facilities/items', + type: 'object', + title: 'A research facility', + properties: { + facility_id: { + '$id': '#/properties/dmp/properties/dmproadmap_research_facilities/items/properties/facility_id', + type: 'object', + title: 'The unique ID of the facility', + description: "The facility's ROR, DOI or URL", + properties: { + identifier: { + '$id': '#/properties/dmp/properties/dmproadmap_research_facilities/items/properties/facility_id/properties/identifier', + type: 'string', + title: 'The facility ID', + description: 'ROR ID, DOI or URL. Recommended to use Research Organization Registry (ROR) or DOI when available. See: https://ror.org', + examples: ['https://ror.org/03yrm5c26', 'http://doi.org/10.13039/100005595', 'http://www.cdlib.org/'] }, - "type": { - "$id": "#/properties/dmp/properties/dmproadmap_research_facilities/items/properties/facility_id/properties/type", - "type": "string", - "enum": [ - "doi", - "ror", - "url" + type: { + '$id': '#/properties/dmp/properties/dmproadmap_research_facilities/items/properties/facility_id/properties/type', + type: 'string', + enum: %w[ + doi + ror + url ], - "title": "The facility ID type schema", - "description": "Identifier type. Allowed values: doi, ror, url", - "examples": ["ror"] + title: 'The facility ID type schema', + description: 'Identifier type. Allowed values: doi, ror, url', + examples: ['ror'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "name": { - "$id": "#/properties/dmp/properties/dmproadmap_research_facilities/items/properties/name", - "type": "string", - "title": "Name of the facility", - "description": "Official facility name", - "examples": ["Example Research Lab"] + name: { + '$id': '#/properties/dmp/properties/dmproadmap_research_facilities/items/properties/name', + type: 'string', + title: 'Name of the facility', + description: 'Official facility name', + examples: ['Example Research Lab'] }, - "type": { - "$id": "#/properties/dmp/properties/dmproadmap_research_facilities/items/properties/type", - "type": "string", - "enum": [ - "field_station", - "laboratory" + type: { + '$id': '#/properties/dmp/properties/dmproadmap_research_facilities/items/properties/type', + type: 'string', + enum: %w[ + field_station + laboratory ], - "title": "The type of facility", - "examples": ["field_station"] + title: 'The type of facility', + examples: ['field_station'] } }, - "required": [ - "name", - "type" + required: %w[ + name + type ] } }, - "ethical_issues_description": { - "$id": "#/properties/dmp/properties/ethical_issues_description", - "type": "string", - "title": "The DMP Ethical Issues Description Schema", - "description": "To describe ethical issues directly in a DMP", - "examples": ["There are ethical issues, because..."] + ethical_issues_description: { + '$id': '#/properties/dmp/properties/ethical_issues_description', + type: 'string', + title: 'The DMP Ethical Issues Description Schema', + description: 'To describe ethical issues directly in a DMP', + examples: ['There are ethical issues, because...'] }, - "ethical_issues_exist": { - "$id": "#/properties/dmp/properties/ethical_issues_exist", - "type": "string", - "enum": [ - "yes", - "no", - "unknown" + ethical_issues_exist: { + '$id': '#/properties/dmp/properties/ethical_issues_exist', + type: 'string', + enum: %w[ + yes + no + unknown ], - "title": "The DMP Ethical Issues Exist Schema", - "description": "To indicate whether there are ethical issues related to data that this DMP describes. Allowed values: yes, no, unknown", - "examples": ["yes"] + title: 'The DMP Ethical Issues Exist Schema', + description: 'To indicate whether there are ethical issues related to data that this DMP describes. Allowed values: yes, no, unknown', + examples: ['yes'] }, - "ethical_issues_report": { - "$id": "#/properties/dmp/properties/ethical_issues_report", - "type": "string", - "format": "uri", - "title": "The DMP Ethical Issues Report Schema", - "description": "To indicate where a protocol from a meeting with an ethical commitee can be found", - "examples": ["http://report.location"] + ethical_issues_report: { + '$id': '#/properties/dmp/properties/ethical_issues_report', + type: 'string', + format: 'uri', + title: 'The DMP Ethical Issues Report Schema', + description: 'To indicate where a protocol from a meeting with an ethical commitee can be found', + examples: ['http://report.location'] }, - "language": { - "$id": "#/properties/dmp/properties/language", - "type": "string", - "enum": [ - "aar", "abk", "afr", "aka", "amh", "ara", "arg", "asm", "ava", "ave", "aym", "aze", "bak", "bam", "bel", "ben", "bih", "bis", "bod", "bos", - "bre", "bul", "cat", "ces", "cha", "che", "chu", "chv", "cor", "cos", "cre", "cym", "dan", "deu", "div", "dzo", "ell", "eng", "epo", "est", - "eus", "ewe", "fao", "fas", "fij", "fin", "fra", "fry", "ful", "gla", "gle", "glg", "glv", "grn", "guj", "hat", "hau", "hbs", "heb", "her", - "hin", "hmo", "hrv", "hun", "hye", "ibo", "ido", "iii", "iku", "ile", "ina", "ind", "ipk", "isl", "ita", "jav", "jpn", "kal", "kan", "kas", - "kat", "kau", "kaz", "khm", "kik", "kin", "kir", "kom", "kon", "kor", "kua", "kur", "lao", "lat", "lav", "lim", "lin", "lit", "ltz", "lub", - "lug", "mah", "mal", "mar", "mkd", "mlg", "mlt", "mon", "mri", "msa", "mya", "nau", "nav", "nbl", "nde", "ndo", "nep", "nld", "nno", "nob", - "nor", "nya", "oci", "oji", "ori", "orm", "oss", "pan", "pli", "pol", "por", "pus", "que", "roh", "ron", "run", "rus", "sag", "san", "sin", - "slk", "slv", "sme", "smo", "sna", "snd", "som", "sot", "spa", "sqi", "srd", "srp", "ssw", "sun", "swa", "swe", "tah", "tam", "tat", "tel", - "tgk", "tgl", "tha", "tir", "ton", "tsn", "tso", "tuk", "tur", "twi", "uig", "ukr", "urd", "uzb", "ven", "vie", "vol", "wln", "wol", "xho", - "yid", "yor", "zha", "zho", "zul" + language: { + '$id': '#/properties/dmp/properties/language', + type: 'string', + enum: %w[ + aar abk afr aka amh ara arg asm ava ave aym aze bak bam bel ben bih bis bod bos + bre bul cat ces cha che chu chv cor cos cre cym dan deu div dzo ell eng epo est + eus ewe fao fas fij fin fra fry ful gla gle glg glv grn guj hat hau hbs heb her + hin hmo hrv hun hye ibo ido iii iku ile ina ind ipk isl ita jav jpn kal kan kas + kat kau kaz khm kik kin kir kom kon kor kua kur lao lat lav lim lin lit ltz lub + lug mah mal mar mkd mlg mlt mon mri msa mya nau nav nbl nde ndo nep nld nno nob + nor nya oci oji ori orm oss pan pli pol por pus que roh ron run rus sag san sin + slk slv sme smo sna snd som sot spa sqi srd srp ssw sun swa swe tah tam tat tel + tgk tgl tha tir ton tsn tso tuk tur twi uig ukr urd uzb ven vie vol wln wol xho + yid yor zha zho zul ], - "title": "The DMP Language Schema", - "description": "Language of the DMP expressed using ISO 639-3.", - "examples": ["eng"] + title: 'The DMP Language Schema', + description: 'Language of the DMP expressed using ISO 639-3.', + examples: ['eng'] }, - "modified": { - "$id": "#/properties/dmp/properties/modified", - "type": "string", - "format": "date-time", - "title": "The DMP Modification Schema", - "description": "Must be set each time DMP is modified. Indicates DMP version. Encoded using the relevant ISO 8601 Date and Time compliant string.", - "examples": ["2020-03-14T10:53:49+00:00"] + modified: { + '$id': '#/properties/dmp/properties/modified', + type: 'string', + format: 'date-time', + title: 'The DMP Modification Schema', + description: 'Must be set each time DMP is modified. Indicates DMP version. Encoded using the relevant ISO 8601 Date and Time compliant string.', + examples: ['2020-03-14T10:53:49+00:00'] }, - "project": { - "$id": "#/properties/dmp/properties/project", - "type": "array", - "title": "The DMP Project Schema", - "description": "Project related to a DMP", - "items": { - "$id": "#/properties/dmp/properties/project/items", - "type": "object", - "title": "The DMP Project Items Schema", - "properties": { - "description": { - "$id": "#/properties/dmp/properties/project/items/properties/description", - "type": "string", - "title": "The DMP Project Description Schema", - "description": "Project description", - "examples": ["Project develops novel..."] + project: { + '$id': '#/properties/dmp/properties/project', + type: 'array', + title: 'The DMP Project Schema', + description: 'Project related to a DMP', + items: { + '$id': '#/properties/dmp/properties/project/items', + type: 'object', + title: 'The DMP Project Items Schema', + properties: { + description: { + '$id': '#/properties/dmp/properties/project/items/properties/description', + type: 'string', + title: 'The DMP Project Description Schema', + description: 'Project description', + examples: ['Project develops novel...'] }, - "end": { - "$id": "#/properties/dmp/properties/project/items/properties/end", - "type": "string", - "format": "date-time", - "title": "The DMP Project End Date Schema", - "description": "Project end date. Encoded using the relevant ISO 8601 Date and Time compliant string.", - "examples": ["2020-03-31"] + end: { + '$id': '#/properties/dmp/properties/project/items/properties/end', + type: 'string', + format: 'date-time', + title: 'The DMP Project End Date Schema', + description: 'Project end date. Encoded using the relevant ISO 8601 Date and Time compliant string.', + examples: ['2020-03-31'] }, - "funding": { - "$id": "#/properties/dmp/properties/project/items/properties/funding", - "type": "array", - "title": "The DMP Project Funding Schema", - "description": "Funding related with a project", - "items": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/items", - "type": "object", - "title": "The DMP Project Funding Items Schema", - "properties": { - "dmproadmap_funded_affiliations": { - "$id": "#/properties/dmp/properties/project/items/properties/funding//items/properties/dmproadmap_funded_affiliations", - "type": "array", - "title": "Institutions named on the grant", - "description": "The institutions who received the funding", - "items": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items", - "type": "object", - "title": "An institution that received funding", - "properties": { - "affiliation_id": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/affiliation_id", - "type": "object", - "title": "The funded affiliation's ID", - "description": "Affiliation ID of the associated project", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/affiliation_id/properties/identifier", - "type": "string", - "title": "The affiliation ID", - "description": "ROR ID or URL. Recommended to use Research Organization Registry (ROR). See: https://ror.org", - "examples": ["https://ror.org/00pjdza24", "https://cdlib.org"] + funding: { + '$id': '#/properties/dmp/properties/project/items/properties/funding', + type: 'array', + title: 'The DMP Project Funding Schema', + description: 'Funding related with a project', + items: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/items', + type: 'object', + title: 'The DMP Project Funding Items Schema', + properties: { + dmproadmap_funded_affiliations: { + '$id': '#/properties/dmp/properties/project/items/properties/funding//items/properties/dmproadmap_funded_affiliations', + type: 'array', + title: 'Institutions named on the grant', + description: 'The institutions who received the funding', + items: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items', + type: 'object', + title: 'An institution that received funding', + properties: { + affiliation_id: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/affiliation_id', + type: 'object', + title: "The funded affiliation's ID", + description: 'Affiliation ID of the associated project', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/affiliation_id/properties/identifier', + type: 'string', + title: 'The affiliation ID', + description: 'ROR ID or URL. Recommended to use Research Organization Registry (ROR). See: https://ror.org', + examples: ['https://ror.org/00pjdza24', 'https://cdlib.org'] }, - "type": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/affiliation_id/properties/type", - "type": "string", - "enum": [ - "doi", - "ror", - "url" + type: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/affiliation_id/properties/type', + type: 'string', + enum: %w[ + doi + ror + url ], - "title": "The affiliation ID Type Schema", - "description": "Identifier type. Allowed values: doi, ror, url", - "examples": ["ror"] + title: 'The affiliation ID Type Schema', + description: 'Identifier type. Allowed values: doi, ror, url', + examples: ['ror'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "name": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/name", - "type": "string", - "title": "The name of the instituion / organization", - "description": "Project title", - "examples": ["Our New Project"] + name: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/items/properties/dmproadmap_funded_affiliations/items/properties/name', + type: 'string', + title: 'The name of the instituion / organization', + description: 'Project title', + examples: ['Our New Project'] } } } }, - "dmproadmap_opportunity_number": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/dmproadmap_opportunity_number", - "type": "string", - "title": "The funder's opportunity / award number", - "description": "The funder's number used to identify the award or call for submissions", - "examples": ["Award-123"] + dmproadmap_opportunity_number: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/dmproadmap_opportunity_number', + type: 'string', + title: "The funder's opportunity / award number", + description: "The funder's number used to identify the award or call for submissions", + examples: ['Award-123'] }, - "dmproadmap_project_number": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/dmproadmap_project_number", - "type": "string", - "title": "The funder's identifier for the research project", - "description": "The funder's identifier used to identify the research project", - "examples": ["prj-XYZ987-UCB"] + dmproadmap_project_number: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/dmproadmap_project_number', + type: 'string', + title: "The funder's identifier for the research project", + description: "The funder's identifier used to identify the research project", + examples: ['prj-XYZ987-UCB'] }, - "funder_id": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/funder_id", - "type": "object", - "title": "The Funder ID Schema", - "description": "Funder ID of the associated project", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/funder_id/properties/identifier", - "type": "string", - "title": "The Funder ID Value Schema", - "description": "Funder ID, recommended to use CrossRef Funder Registry. See: https://www.crossref.org/services/funder-registry/", - "examples": ["501100002428"] + funder_id: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/funder_id', + type: 'object', + title: 'The Funder ID Schema', + description: 'Funder ID of the associated project', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/funder_id/properties/identifier', + type: 'string', + title: 'The Funder ID Value Schema', + description: 'Funder ID, recommended to use CrossRef Funder Registry. See: https://www.crossref.org/services/funder-registry/', + examples: ['501100002428'] }, - "type": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/funder_id/properties/type", - "type": "string", - "enum": [ - "fundref", - "ror", - "url", - "other" + type: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/funder_id/properties/type', + type: 'string', + enum: %w[ + fundref + ror + url + other ], - "title": "The Funder ID Type Schema", - "description": "Identifier type. Allowed values: fundref, url, other", - "examples": ["fundref"] + title: 'The Funder ID Type Schema', + description: 'Identifier type. Allowed values: fundref, url, other', + examples: ['fundref'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "funding_status": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/funding_status", - "type": "string", - "enum": [ - "planned", - "applied", - "granted", - "rejected" + funding_status: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/funding_status', + type: 'string', + enum: %w[ + planned + applied + granted + rejected ], - "title": "The Funding Status Schema", - "description": "To express different phases of project lifecycle. Allowed values: planned, applied, granted, rejected", - "examples": ["granted"] + title: 'The Funding Status Schema', + description: 'To express different phases of project lifecycle. Allowed values: planned, applied, granted, rejected', + examples: ['granted'] }, - "grant_id": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/grant_id", - "type": "object", - "title": "The Funding Grant ID Schema", - "description": "Grant ID of the associated project", - "properties": { - "identifier": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/grant_id/properties/identifier", - "type": "string", - "title": "The Funding Grant ID Value Schema", - "description": "Grant ID", - "examples": ["776242"] + grant_id: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/grant_id', + type: 'object', + title: 'The Funding Grant ID Schema', + description: 'Grant ID of the associated project', + properties: { + identifier: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/grant_id/properties/identifier', + type: 'string', + title: 'The Funding Grant ID Value Schema', + description: 'Grant ID', + examples: ['776242'] }, - "type": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/grant_id/properties/type", - "type": "string", - "title": "The Funding Grant ID Type Schema", - "enum": [ - "doi", - "url", - "other" + type: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/grant_id/properties/type', + type: 'string', + title: 'The Funding Grant ID Type Schema', + enum: %w[ + doi + url + other ], - "description": "Identifier type. Allowed values: url, other", - "examples": ["other"] + description: 'Identifier type. Allowed values: url, other', + examples: ['other'] } }, - "required": [ - "identifier", - "type" + required: %w[ + identifier + type ] }, - "name": { - "$id": "#/properties/dmp/properties/project/items/properties/funding/properties/name", - "type": "string", - "title": "The name of the funding instituion / organization", - "description": "Name", - "examples": ["National Science Foundation"] + name: { + '$id': '#/properties/dmp/properties/project/items/properties/funding/properties/name', + type: 'string', + title: 'The name of the funding instituion / organization', + description: 'Name', + examples: ['National Science Foundation'] } }, - "required": [ - "funding_status", - "name" + required: %w[ + funding_status + name ] } }, - "start": { - "$id": "#/properties/dmp/properties/project/items/properties/start", - "type": "string", - "format": "date-time", - "title": "The DMP Project Start Date Schema", - "description": "Project start date. Encoded using the relevant ISO 8601 Date and Time compliant string.", - "examples": ["2019-04-01"] + start: { + '$id': '#/properties/dmp/properties/project/items/properties/start', + type: 'string', + format: 'date-time', + title: 'The DMP Project Start Date Schema', + description: 'Project start date. Encoded using the relevant ISO 8601 Date and Time compliant string.', + examples: ['2019-04-01'] }, - "title": { - "$id": "#/properties/dmp/properties/project/items/properties/title", - "type": "string", - "title": "The DMP Project Title Schema", - "description": "Project title", - "examples": ["Our New Project"] + title: { + '$id': '#/properties/dmp/properties/project/items/properties/title', + type: 'string', + title: 'The DMP Project Title Schema', + description: 'Project title', + examples: ['Our New Project'] } }, - "required": [ - "title" + required: [ + 'title' ] } }, - "title": { - "$id": "#/properties/dmp/properties/title", - "type": "string", - "title": "The DMP Title Schema", - "description": "Title of a DMP", - "examples": ["DMP for our new project"] + title: { + '$id': '#/properties/dmp/properties/title', + type: 'string', + title: 'The DMP Title Schema', + description: 'Title of a DMP', + examples: ['DMP for our new project'] } }, - "required": [ - "contact", - "created", - "dataset", - "dmp_id", - "modified", - "project", - "title" + required: %w[ + contact + created + dataset + dmp_id + modified + project + title ] } }, - "additionalProperties": false, - "required": ["dmp"] + additionalProperties: false, + required: ['dmp'] }.to_json) end end end + # rubocop:enable Layout/LineLength, Metrics/MethodLength, Metrics/ClassLength end end diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb index 0b07b618..bd9f6112 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/updater.rb @@ -6,6 +6,7 @@ module Uc3DmpId class UpdaterError < StandardError; end + # Class that handles updating a DMP ID class Updater class << self # Update a DMP ID @@ -26,7 +27,8 @@ def update(provenance:, p_key:, json: {}, note: nil, logger: nil) logger.debug(message: "Latest version for PK #{p_key}", details: latest_version) if logger.respond_to?(:debug) # Verify that the DMP ID is updateable with the info passed in - errs = _updateable?(provenance: provenance, p_key: p_key, latest_version: latest_version['dmp'], mods: mods['dmp']) + errs = _updateable?(provenance: provenance, p_key: p_key, latest_version: latest_version['dmp'], + mods: mods['dmp']) logger.error(message: errs.join(', ')) if logger.respond_to?(:error) && errs.is_a?(Array) && errs.any? raise UpdaterError, errs if errs.is_a?(Array) && errs.any? # Don't continue if nothing has changed! @@ -35,8 +37,8 @@ def update(provenance:, p_key:, json: {}, note: nil, logger: nil) # Version the DMP ID record (if applicable). owner = latest_version['dmphub_provenance_id'] updater = provenance['PK'] - version = Versioner.generate_version(client: client, latest_version: latest_version, owner: owner, updater: updater, - logger: logger) + version = Versioner.generate_version(client: client, latest_version: latest_version, owner: owner, + updater: updater, logger: logger) raise UpdaterError, Helper::MSG_DMP_UNABLE_TO_VERSION if version.nil? # Remove the version info because we don't want to save it on the record @@ -45,10 +47,10 @@ def update(provenance:, p_key:, json: {}, note: nil, logger: nil) # Splice the assertions version = _process_modifications(owner: owner, updater: updater, version: version, mods: mods, note: note, logger: logger) - # Set the :modified timestamps - now = Time.now.utc.iso8601 - version['modified'] = now + now = Time.now.utc + version['modified'] = now.iso8601 + version['dmphub_modification_day'] = now.strftime('%Y-%m-%d') # Save the changes resp = client.put_item(json: version, logger: logger) @@ -59,29 +61,34 @@ def update(provenance:, p_key:, json: {}, note: nil, logger: nil) # Return the new version record logger.info(message: "Updated DMP ID: #{p_key}") if logger.respond_to?(:debug) - Helper.cleanse_dmp_json(json: JSON.parse({ dmp: version }.to_json)) + + # Append the :dmphub_versions Array + json = JSON.parse({ dmp: version }.to_json) + json = Versioner.append_versions(p_key: p_key, dmp: json, client: client, logger: logger) + Helper.cleanse_dmp_json(json: json) end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # Save a DMP ID's corresponding narrative PDF document to S3 and add the download URL for that # document to the DMP ID's :dmpraodmap_related_identifiers array as an `is_metadata_for` relation + # rubocop:disable Metrics/AbcSize def attach_narrative(provenance:, p_key:, url:, logger: nil) raise UpdaterError, Helper::MSG_DMP_INVALID_DMP_ID unless p_key.is_a?(String) && !p_key.strip.empty? # fetch the existing latest version of the DMP ID client = Uc3DmpDynamo::Client.new(logger: logger) dmp = Finder.by_pk(p_key: p_key, client: client, logger: logger, cleanse: false) - logger.info(message: "Existing latest record", details: dmp) if logger.respond_to?(:debug) + logger.info(message: 'Existing latest record', details: dmp) if logger.respond_to?(:debug) raise UpdaterError, Helper::MSG_DMP_FORBIDDEN unless provenance.is_a?(Hash) && !provenance['PK'].nil? && - provenance['PK'] == dmp['dmphub_provenance_id'] + provenance['PK'] == dmp['dmp']['dmphub_provenance_id'] # Add the download URl for the PDF as a related identifier on the DMP ID record annotated = Helper.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp['dmp']) annotated['dmproadmap_related_identifiers'] = [] if annotated['dmproadmap_related_identifiers'].nil? - annotated['dmproadmap_related_identifiers'] << { + annotated['dmproadmap_related_identifiers'] << JSON.parse({ descriptor: 'is_metadata_for', work_type: 'output_management_plan', type: 'url', identifier: url - } + }.to_json) # Save the changes without creating a new version! resp = client.put_item(json: annotated, logger: logger) @@ -90,10 +97,12 @@ def attach_narrative(provenance:, p_key:, url:, logger: nil) logger.info(message: "Added DMP ID narrative for PK: #{p_key}, Narrative: #{url}") if logger.respond_to?(:debug) true end + # rubocop:enable Metrics/AbcSize private # Check to make sure the incoming JSON is valid, the DMP ID requested matches the DMP ID in the JSON + # rubocop:disable Metrics/AbcSize def _updateable?(provenance:, p_key:, latest_version: {}, mods: {}) # Validate the incoming JSON first errs = Validator.validate(mode: 'author', json: JSON.parse({ dmp: mods }.to_json)) @@ -105,22 +114,29 @@ def _updateable?(provenance:, p_key:, latest_version: {}, mods: {}) # Bail out if the DMP ID could not be found or the PKs do not match for some reason return [Helper::MSG_DMP_UNKNOWN] unless latest_version.is_a?(Hash) && latest_version['PK'] == p_key end + # rubocop:enable Metrics/AbcSize + # rubocop:disable Metrics/ParameterLists def _process_modifications(owner:, updater:, version:, mods:, note: nil, logger: nil) return version unless mods.is_a?(Hash) && !updater.nil? return mods unless version.is_a?(Hash) && !owner.nil? - # Splice together any assertions that may have been made while the user was editing the DMP ID - updated = Asserter.splice(latest_version: version, modified_version: mods, logger: logger) if owner == updater - - # Attach the incoming changes as an assertion to the DMP ID since the updater is NOT the owner - updated = Asserter.add(updater: updater, dmp: version, mods: mods, note: note, logger: logger) if owner != updater + updated = if owner == updater + # Splice together any assertions that may have been made while the user was editing the DMP ID + Asserter.splice(latest_version: version, modified_version: mods, logger: logger) + else + # Attach the incoming changes as an assertion to the DMP ID since the updater is NOT the owner + Asserter.add(updater: updater, latest_version: version, modified_version: mods, note: note, + logger: logger) + end _merge_versions(latest_version: version, mods: updated, logger: logger) end + # rubocop:enable Metrics/ParameterLists # We are replacing the latest version with the modifcations but want to retain the PK, SK and any dmphub_ prefixed # entries in the metadata so that we do not lose creation timestamps, provenance ids, etc. + # rubocop:disable Metrics/AbcSize def _merge_versions(latest_version:, mods:, logger: nil) return mods unless latest_version.is_a?(Hash) @@ -135,20 +151,27 @@ def _merge_versions(latest_version:, mods:, logger: nil) logger.debug(message: 'Modifications after merge.', details: mods) if logger.respond_to?(:debug) mods end + # rubocop:enable Metrics/AbcSize # Once the DMP has been updated, we need to update it's DOI metadata # ------------------------------------------------------------------------- + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity def _post_process(provenance:, json:, logger: nil) return false unless json.is_a?(Hash) && provenance.is_a?(Hash) && !provenance['PK'].nil? && !json['dmphub_provenance_id'].nil? publishable = provenance['PK'] == json['dmphub_provenance_id'] return true unless publishable + # TODO: we will want to send and related_identifiers in :dmphub_modifications as well!!! publisher = Uc3DmpEventBridge::Publisher.new # Publish the change to the EventBridge if the updater is the owner of the DMP ID - logger.debug(message: "Sending event for EZID publication", details: json) if publishable && logger.respond_to?(:debug) + if publishable && logger.respond_to?(:debug) + logger.debug(message: 'Sending event for EZID publication', + details: json) + end publisher.publish(source: 'DmpUpdater', event_type: 'EZID update', dmp: json, logger: logger) if publishable # Determine if there are any related identifiers that we should try to fetch a citation for @@ -161,10 +184,16 @@ def _post_process(provenance:, json:, logger: nil) SK: json['SK'], dmproadmap_related_identifiers: citable_identifiers } - logger.debug(message: "Sending event to fetch citations", details: citable_identifiers) if logger.respond_to?(:debug) - publisher.publish(source: 'DmpUpdater', dmp: json, event_type: 'Citation Fetch', detail: citer_detail, logger: logger) + if logger.respond_to?(:debug) + logger.debug(message: 'Sending event to fetch citations', + details: citable_identifiers) + end + publisher.publish(source: 'DmpUpdater', dmp: json, event_type: 'Citation Fetch', detail: citer_detail, + logger: logger) true end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity end end end diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/validator.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/validator.rb index eb06cd0c..50f44161 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/validator.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/validator.rb @@ -5,6 +5,7 @@ module Uc3DmpId class Uc3DmpIdValidatorError < StandardError; end + # Method that compares incoming JSON against our JSON Schema and provides detailed errors class Validator # Valid Validation modes are: # - :author --> system of provenance is attempting to create or update @@ -20,8 +21,7 @@ class Validator class << self # Validate the specified DMP's :json against the schema for the specified :mode - # - # ------------------------------------------------------------------------------------ + # rubocop:disable Metrics/AbcSize def validate(mode:, json:) json = Helper.parse_json(json: json) return [MSG_EMPTY_JSON] if json.nil? || !VALIDATION_MODES.include?(mode) @@ -38,6 +38,7 @@ def validate(mode:, json:) rescue JSON::Schema::ValidationError => e ["#{MSG_INVALID_JSON} - #{e.message}"] end + # rubocop:enable Metrics/AbcSize # ------------------------------------------------------------------------------------ # METHODS BELOW ARE ONLY MEANT TO BE INVOKED FROM WITHIN THIS MODULE @@ -47,12 +48,13 @@ def validate(mode:, json:) # ------------------------------------------------------------------------------------ def _load_schema(mode:) # Instatiate the matching schema - schema = "Uc3DmpId::Schemas::#{mode.to_s.downcase.capitalize}".split('::').inject(Object) { |o,c| o.const_get c } + schema = "Uc3DmpId::Schemas::#{mode.to_s.downcase.capitalize}".split('::').inject(Object) do |o, c| + o.const_get c + end schema.respond_to?(:load) ? schema.load : nil rescue NameError nil end - end end end diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/version.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/version.rb index d75f02d4..45ff8b03 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/version.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Uc3DmpId - VERSION = '0.0.140' + VERSION = '0.1.0' end diff --git a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/versioner.rb b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/versioner.rb index 11344835..7fbbece6 100644 --- a/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/versioner.rb +++ b/src/sam/gems/uc3-dmp-id/lib/uc3-dmp-id/versioner.rb @@ -6,11 +6,11 @@ module Uc3DmpId class VersionerError < StandardError; end + # Logic to handle the versioning of DMP IDs and to retrieve the versions for a PK class Versioner SOURCE = 'Uc3DmpId::Versioner' class << self - # Find the DMP ID's versions # ------------------------------------------------------------------------- def get_versions(p_key:, client: nil, logger: nil) @@ -29,11 +29,12 @@ def get_versions(p_key:, client: nil, logger: nil) # Generate a snapshot of the current latest version of the DMP ID using the existing :modified as # the new SK. - # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity def generate_version(client:, latest_version:, owner:, updater:, logger: nil) # Only create a version if the Updater is not the Owner OR the changes have happened on a different day mod_time = Time.parse(latest_version.fetch('modified', Time.now.utc.iso8601)) - now = Time.now + now = Time.now.utc if mod_time.nil? || !(now - mod_time).is_a?(Float) logger.error(message: "#{SOURCE} unable to determine mod time: #{mod_time}") if logger.respond_to?(:debug) return latest_version @@ -42,9 +43,11 @@ def generate_version(client:, latest_version:, owner:, updater:, logger: nil) # Only allow a new version if the owner and updater are the same and it has been at least one hour since # the last version was created same_hour = (now - mod_time).round <= 3600 - if owner != updater || (owner == updater && same_hour) + if owner == updater && same_hour logger.debug(message: "#{SOURCE} same owner and updater? #{owner == updater}") if logger.respond_to?(:debug) - logger.debug(message: "#{SOURCE} already updated within the past hour? #{same_hour}") if logger.respond_to?(:debug) + if logger.respond_to?(:debug) + logger.debug(message: "#{SOURCE} already updated within the past hour? #{same_hour}") + end return latest_version end @@ -52,6 +55,7 @@ def generate_version(client:, latest_version:, owner:, updater:, logger: nil) # We essentially make a snapshot of the record before making changes prior = Helper.deep_copy_dmp(obj: latest_version) prior['SK'] = "#{Helper::SK_DMP_PREFIX}#{latest_version['modified'] || Time.now.utc.iso8601}" + # Create the prior version record () client = client.nil? ? Uc3DmpDynamo::Client.new : client resp = client.put_item(json: prior, logger: logger) @@ -61,7 +65,8 @@ def generate_version(client:, latest_version:, owner:, updater:, logger: nil) logger.info(message: msg, details: prior) if logger.respond_to?(:debug) latest_version end - # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity # Build the :dmphub_versions array and attach it to the DMP JSON # rubocop:disable Metrics/AbcSize @@ -72,11 +77,15 @@ def append_versions(p_key:, dmp:, client: nil, logger: nil) results = get_versions(p_key: p_key, client: client, logger: logger) return json unless results.length > 1 + # TODO: we may want to include milliseconds in the future if we get increased volume so that + # we don't end up with duplicate URLs if versions are made within the same second versions = results.map do |ver| next if ver['modified'].nil? + + base_url = "#{Helper.landing_page_url}#{Helper.remove_pk_prefix(p_key: p_key)}" { timestamp: ver['modified'], - url: "#{Helper.landing_page_url}#{Helper.remove_pk_prefix(p_key: p_key)}?version=#{ver['modified']}" + url: dmp['dmp']['modified'] == ver['modified'] ? base_url : "#{base_url}?version=#{ver['modified']}" } end json['dmp']['dmphub_versions'] = JSON.parse(versions.to_json) diff --git a/src/sam/gems/uc3-dmp-id/spec/support/mock_data_store.rb b/src/sam/gems/uc3-dmp-id/spec/support/mock_data_store.rb new file mode 100644 index 00000000..4f099ac2 --- /dev/null +++ b/src/sam/gems/uc3-dmp-id/spec/support/mock_data_store.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +module Uc3DmpDynamo + # Mock The Uc3DmpDynamo Client functionality so we can inspect what the DMP ID looks like + # rubocop:disable Lint/UnusedMethodArgument + class Client + attr_accessor :data_store + + def initialize(**_args) + @data_store = [] + end + + def pk_exists?(key:, logger: nil) + return false unless @data_store.any? + + @data_store.any? { |rec| rec['PK'] == key['PK'] } + end + + def get_item(key:, logger: nil) + return nil unless @data_store.any? + + @data_store.find { |rec| rec['PK'] == key[:PK] && rec['SK'] == key[:SK] } + end + + # rubocop:disable Metrics/AbcSize + def query(args:, logger: nil) + dmps = [] + + conds = args.fetch(:key_conditions, {}) + unless conds[:owner_orcid].nil? + dmps = @data_store.find { |rec| rec['dmphub_owner_id'] == conds[:owner_orcid][:attribute_value_list].first } + end + unless conds[:owner_org_ror].nil? + dmps = @data_store.find { |rec| rec['dmphub_owner_org'] == conds[:owner_org_ror][:attribute_value_list].first } + end + unless conds[:modification_day].nil? + dmps = @data_store.find do |rec| + rec['dmphub_modification_day'] == conds[:modification_day][:attribute_value_list].first + end + end + dmps = @data_store.select { |rec| rec['PK'] == conds[:PK][:attribute_value_list].first } unless conds[:PK].nil? + dmps + end + # rubocop:enable Metrics/AbcSize + + def put_item(json:, logger: nil) + rec = @data_store.find { |r| r['PK'] == json['PK'] && r['SK'] == json['SK'] } + delete_item(p_key: json['PK'], s_key: json['SK']) unless rec.nil? + + @data_store << json + end + + def delete_item(p_key:, s_key:, logger: nil) + @data_store = @data_store.reject { |rec| rec['PK'] == p_key && rec['SK'] == s_key } + end + + def change_timestamps(p_key:, tstamp:) + rec = @data_store.find { |r| r['PK'] == p_key && r['SK'] == Uc3DmpId::Helper::DMP_LATEST_VERSION } + return false if rec.nil? + + json = rec.dup + delete_item(p_key: p_key, s_key: rec['SK']) + json['created'] = tstamp + json['modified'] = tstamp + json['dmphub_modification_day'] = Time.parse(tstamp).strftime('%Y-%,-%d') + @data_store << json + end + end + # rubocop:enable Lint/UnusedMethodArgument +end diff --git a/src/sam/gems/uc3-dmp-id/spec/support/mock_event_bus.rb b/src/sam/gems/uc3-dmp-id/spec/support/mock_event_bus.rb new file mode 100644 index 00000000..5152b053 --- /dev/null +++ b/src/sam/gems/uc3-dmp-id/spec/support/mock_event_bus.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Uc3DmpEventBridge + # Mock replacement of the EventBridge so we can check message structures + class Publisher + attr_accessor :event_bus + + def initialize(**_args) + @event_bus = [] + end + + # rubocop:disable Lint/UnusedMethodArgument + def publish(source:, dmp:, event_type: DEFAULT_EVENT_TYPE, detail: nil, logger: nil) + @event_bus << { + time: Time.now.utc, + source: "#{ENV.fetch('DOMAIN', nil)}:lambda:event_publisher", + detail_type: event_type.to_s, + detail: detail, + event_bus_name: ENV.fetch('EVENT_BUS_NAME', nil) + } + end + # rubocop:enable Lint/UnusedMethodArgument + end +end diff --git a/src/sam/gems/uc3-dmp-id/spec/support/shared.rb b/src/sam/gems/uc3-dmp-id/spec/support/shared.rb index 9ffc13b3..2b99e5a3 100644 --- a/src/sam/gems/uc3-dmp-id/spec/support/shared.rb +++ b/src/sam/gems/uc3-dmp-id/spec/support/shared.rb @@ -13,13 +13,14 @@ def mock_dmp_id domain = ENV.fetch('DMP_ID_BASE_URL', 'doi.org').gsub(%r{https?://}, '') - "#{domain}/#{rand(10...99)}.#{rand(10000...99999)}/#{SecureRandom.hex(6)}" + "#{domain}/#{rand(10...99)}.#{rand(10_000...99_999)}/#{SecureRandom.hex(6)}" end def mock_dmp(minimal: false) JSON.parse(File.read("#{Dir.pwd}/spec/support/json_mocks/#{minimal ? 'minimal' : 'complete'}.json")) end +# rubocop:disable Metrics/AbcSize def mock_logger(success: true) lggr = Uc3DmpCloudwatchLogger.new allow(lggr).to receive(:debug).and_return(success) @@ -29,7 +30,9 @@ def mock_logger(success: true) allow(Uc3DmpCloudwatch::Logger).to receive(:new).and_return(lggr) lggr end +# rubocop:enable Metrics/AbcSize +# rubocop:disable Metrics/AbcSize def mock_uc3_dmp_dynamo(dmp: mock_dmp, success: true) client = Uc3DmpDynamoClient.new allow(client).to receive(:get_item).and_return(success ? dmp : nil) @@ -41,6 +44,7 @@ def mock_uc3_dmp_dynamo(dmp: mock_dmp, success: true) allow(Uc3DmpDynamo::Client).to receive(:new).and_return(client) client end +# rubocop:enable Metrics/AbcSize def mock_uc3_dmp_event_bridge(success: true) publisher = Uc3DmpEventBridgePublisher.new @@ -51,8 +55,10 @@ def mock_uc3_dmp_event_bridge(success: true) end # Helper to compare 2 hashes +# rubocop:disable Metrics/MethodLength, Metrics/AbcSize +# rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity def assert_dmps_match(obj_a: {}, obj_b: {}, debug: false) - pass = obj_a.class.name == obj_b.class.name + pass = obj_a.instance_of?(obj_b.class) if debug puts 'Debug from spec/support/shared.rb - assert_dmps_match' @@ -71,12 +77,14 @@ def assert_dmps_match(obj_a: {}, obj_b: {}, debug: false) return false unless obj_a.keys.sort { |a, b| a <=> b } == obj_b.keys.sort { |a, b| a <=> b } obj_a.each_pair do |key, value| - if %w[Array, Hash].include?(value.class.name) + # rubocop:disable Metrics/BlockNesting + if %w[Array Hash].include?(value.class.name) pass = assert_dmps_match(obj_a: value, obj_b: obj_b.nil? ? nil : obj_b[key.to_s]) else - #puts "Hash item #{key} not a sub Hash/Array #{value} == #{obj_b.nil? ? nil : obj_b[key.to_s]}" + # puts "Hash item #{key} not a sub Hash/Array #{value} == #{obj_b.nil? ? nil : obj_b[key.to_s]}" pass = false unless value == obj_b.nil? ? nil : obj_b[key.to_s] end + # rubocop:enable Metrics/BlockNesting end else # puts "#{obj_a} == #{obj_b}" @@ -85,3 +93,5 @@ def assert_dmps_match(obj_a: {}, obj_b: {}, debug: false) end pass end +# rubocop:enable Metrics/MethodLength, Metrics/AbcSize +# rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/asserter_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/asserter_spec.rb index 28decdfb..46f6182b 100644 --- a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/asserter_spec.rb +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/asserter_spec.rb @@ -17,20 +17,20 @@ describe 'add(updater:, latest_version:, modified_version:, note: nil, logger: nil)' do let!(:new_mods) do JSON.parse({ - "project": [ - "title": "foo ... testing", - "funding": [ + project: [ + title: 'foo ... testing', + funding: [ { - "name": "Reliable cash.net", - "grant_id": { - "type": "other", identifier: "TESTING MONEY" + name: 'Reliable cash.net', + grant_id: { + type: 'other', identifier: 'TESTING MONEY' } } ] ], - "dmproadmap_related_identifiers": [ + dmproadmap_related_identifiers: [ { work_type: 'image', descriptor: 'annoys', type: 'other', identifier: 'testing_one' }, - { type: "other", identifier: 'testing_two' } + { type: 'other', identifier: 'testing_two' } ] }.to_json) end @@ -39,27 +39,36 @@ result = described_class.add(updater: nil, latest_version: latest_version, modified_version: new_mods) expect(result).to eql(latest_version) end + it 'returns the :latest_version as-is if the :latest_version is not a Hash' do result = described_class.add(updater: updater, latest_version: 123, modified_version: new_mods) - expect(result).to eql(123) + expect(result).to be(123) end + it 'returns the :latest_version as-is if the :modified_version is not a Hash' do result = described_class.add(updater: updater, latest_version: latest_version, modified_version: nil) expect(result).to eql(latest_version) end - it 'returns the :latest_version as-is if the :updater is the owner of the DMP ID (owner doesn\'t need to assert!)' do + + it 'returns the :latest_version as-is if the :updater is the owner of the DMP ID (owner do not assert!)' do latest_version['dmphub_provenance_id'] = updater result = described_class.add(updater: updater, latest_version: latest_version, modified_version: new_mods) expect(result).to eql(latest_version) end + + # rubocop:disable RSpec/MultipleExpectations, RSpec/ExampleLength it 'adds the :dmphub_modifications if there are only :dmproadmap_related_identifiers' do new_mods.delete('project') result = described_class.add(updater: updater, latest_version: latest_version, modified_version: new_mods) changes = result['dmphub_modifications'].reject { |mod| mod.fetch('dmproadmap_related_identifiers', []).empty? } changes = changes.map { |change| change['dmproadmap_related_identifiers'] }.flatten - work_one = changes.select { |work| work['identifier'] == new_mods['dmproadmap_related_identifiers'].first['identifier'] } - work_two = changes.select { |work| work['identifier'] == new_mods['dmproadmap_related_identifiers'].last['identifier'] } + work_one = changes.select do |work| + work['identifier'] == new_mods['dmproadmap_related_identifiers'].first['identifier'] + end + work_two = changes.select do |work| + work['identifier'] == new_mods['dmproadmap_related_identifiers'].last['identifier'] + end expect(work_one.first['work_type']).to eql(new_mods['dmproadmap_related_identifiers'].first['work_type']) expect(work_one.first['descriptor']).to eql(new_mods['dmproadmap_related_identifiers'].first['descriptor']) @@ -71,28 +80,40 @@ expect(work_two.first['type']).to eql(new_mods['dmproadmap_related_identifiers'].last['type']) expect(work_two.first['identifier']).to eql(new_mods['dmproadmap_related_identifiers'].last['identifier']) end + # rubocop:enable RSpec/MultipleExpectations, RSpec/ExampleLength + it 'adds the :dmphub_modifications if there is only a :grant_id' do new_mods.delete('dmproadmap_related_identifiers') result = described_class.add(updater: updater, latest_version: latest_version, modified_version: new_mods) fundings = result['dmphub_modifications'].reject { |mod| mod['funding'].nil? }.map { |m| m['funding'] } - match = fundings.select { |fund| fund['grant_id'] == new_mods['project'].first['funding'].first['grant_id'] }.first + match = fundings.select do |fund| + fund['grant_id'] == new_mods['project'].first['funding'].first['grant_id'] + end.first expect(match['grant_id']).to eql(new_mods['project'].first['funding'].first['grant_id']) expect(match['funding_status']).to eql('granted') end + + # rubocop:disable RSpec/MultipleExpectations, RSpec/ExampleLength it 'adds the :dmphub_modifications if there are both :dmproadmap_related_identifiers and a :grant_id' do result = described_class.add(updater: updater, latest_version: latest_version, modified_version: new_mods) changes = result['dmphub_modifications'].reject { |mod| mod.fetch('dmproadmap_related_identifiers', []).empty? } changes = changes.map { |change| change['dmproadmap_related_identifiers'] }.flatten fundings = result['dmphub_modifications'].reject { |mod| mod['funding'].nil? }.map { |m| m['funding'] } - match = fundings.select { |fund| fund['grant_id'] == new_mods['project'].first['funding'].first['grant_id'] }.first + match = fundings.select do |fund| + fund['grant_id'] == new_mods['project'].first['funding'].first['grant_id'] + end.first expect(match['grant_id']).to eql(new_mods['project'].first['funding'].first['grant_id']) expect(match['funding_status']).to eql('granted') - work_one = changes.select { |work| work['identifier'] == new_mods['dmproadmap_related_identifiers'].first['identifier'] } - work_two = changes.select { |work| work['identifier'] == new_mods['dmproadmap_related_identifiers'].last['identifier'] } + work_one = changes.select do |work| + work['identifier'] == new_mods['dmproadmap_related_identifiers'].first['identifier'] + end + work_two = changes.select do |work| + work['identifier'] == new_mods['dmproadmap_related_identifiers'].last['identifier'] + end expect(work_one.first['work_type']).to eql(new_mods['dmproadmap_related_identifiers'].first['work_type']) expect(work_one.first['descriptor']).to eql(new_mods['dmproadmap_related_identifiers'].first['descriptor']) @@ -104,16 +125,17 @@ expect(work_two.first['type']).to eql(new_mods['dmproadmap_related_identifiers'].last['type']) expect(work_two.first['identifier']).to eql(new_mods['dmproadmap_related_identifiers'].last['identifier']) end + # rubocop:enable RSpec/MultipleExpectations, RSpec/ExampleLength end describe 'splice(latest_version:, modified_version:, logger: nil)' do let!(:new_mod) do JSON.parse({ - "id": "FOOOOOOO", - "provenance": "dmphub", - "timestamp": "2023-08-20T05:06:07Z", - "note": "data for testing mods", - "dmproadmap_related_identifiers": [ + id: 'FOOOOOOO', + provenance: 'dmphub', + timestamp: '2023-08-20T05:06:07Z', + note: 'data for testing mods', + dmproadmap_related_identifiers: [ { work_type: 'image', descriptor: 'annoys', type: 'other', identifier: 'bar.jpg' } ] }.to_json) @@ -125,38 +147,42 @@ result = described_class.splice(latest_version: latest_version, modified_version: modified) expect(assert_dmps_match(obj_a: modified, obj_b: result, debug: false)).to be(true) end + it 'returns the :modified_version if neither has any :dmphub_modifications' do latest_version.delete('dmphub_modifications') modified = latest_version.dup result = described_class.splice(latest_version: latest_version, modified_version: modified) expect(assert_dmps_match(obj_a: modified, obj_b: result, debug: false)).to be(true) end + it 'adds the incoming :dmphub_modifications to the latest_version when the latest_version has none' do latest_version.delete('dmphub_modifications') modified = latest_version.dup modified['dmphub_modifications'] = [new_mod] modified['modified'] = Time.now.utc.iso8601 result = described_class.splice(latest_version: latest_version, modified_version: modified) - expect(result['dmphub_modifications'].length).to eql(1) + expect(result['dmphub_modifications'].length).to be(1) expect(result['dmphub_modifications'].include?(new_mod)).to be(true) end + it 'retains the existing :dmphub_modifications if the incoming has none' do modified = latest_version.dup modified.delete('dmphub_modifications') modified['modified'] = Time.now.utc.iso8601 result = described_class.splice(latest_version: latest_version, modified_version: modified) - expect(result['dmphub_modifications'].length).to eql(3) + expect(result['dmphub_modifications'].length).to be(3) latest_version['dmphub_modifications'].each do |mod| expect(result['dmphub_modifications'].include?(mod)).to be(true) end end + it 'appends the incoming :dmphub_modifications to the ones on the existing record' do modified = latest_version.dup modified.delete('dmphub_modifications') modified['dmphub_modifications'] = [new_mod] modified['modified'] = Time.now.utc.iso8601 result = described_class.splice(latest_version: latest_version, modified_version: modified) - expect(result['dmphub_modifications'].length).to eql(4) + expect(result['dmphub_modifications'].length).to be(4) expect(result['dmphub_modifications'].include?(new_mod)).to be(true) latest_version['dmphub_modifications'].each do |mod| expect(result['dmphub_modifications'].include?(mod)).to be(true) @@ -164,7 +190,7 @@ end end - describe '_add_related_identifier(updater:, latest_version:, identifiers:, note: '', logger: nil)' do + describe '_add_related_identifier(updater:, latest_version:, identifiers:, note: \'\', logger: nil)' do let!(:mods) do JSON.parse([ { @@ -184,53 +210,59 @@ identifiers: mods) expect(result).to eql(latest_version) end + it 'returns :latest_version as-is if :latest_version is not a Hash' do - result = described_class.send(:_add_related_identifier, updater: updater, latest_version: [123], identifiers: mods) + result = described_class.send(:_add_related_identifier, updater: updater, latest_version: [123], + identifiers: mods) expect(result).to eql([123]) end + it 'returns :latest_version as-is if :identifiers is not an Array' do result = described_class.send(:_add_related_identifier, updater: updater, latest_version: latest_version, - identifiers: { 'foo': 'bar' }) + identifiers: { foo: 'bar' }) expect(result).to eql(latest_version) end + it 'skips adding the :related_identifier if it is already in the :latest_version :dmphub_modifications Array' do latest_version['dmphub_modifications'] << JSON.parse({ dmproadmap_related_identifiers: [mods.first] }.to_json) result = described_class.send(:_add_related_identifier, updater: updater, latest_version: latest_version, identifiers: mods) - expect(result['dmphub_modifications'].length).to eql(5) + expect(result['dmphub_modifications'].length).to be(5) changes = result['dmphub_modifications'].reject { |mod| mod.fetch('dmproadmap_related_identifiers', []).empty? } changes = changes.map { |change| change['dmproadmap_related_identifiers'] }.flatten ids = changes.map { |mod| mod.fetch('identifier', '')&.downcase&.strip }.flatten.compact expect(ids.include?(mods.first['identifier'])).to be(true) - expect(ids.select { |id| id == mods.first['identifier'] }.length).to eql(1) + expect(ids.select { |id| id == mods.first['identifier'] }.length).to be(1) end + it 'skips adding the :related_identifier if it is already in the :latest_version :dmproadmap_related_identifiers' do tweaked_id = mods.first tweaked_id['descriptor'] = 'documents' latest_version['dmproadmap_related_identifiers'] << tweaked_id result = described_class.send(:_add_related_identifier, updater: updater, latest_version: latest_version, identifiers: mods) - expect(result['dmphub_modifications'].length).to eql(4) + expect(result['dmphub_modifications'].length).to be(4) changes = result['dmphub_modifications'].reject { |mod| mod.fetch('dmproadmap_related_identifiers', []).empty? } changes = changes.map { |change| change['dmproadmap_related_identifiers'] }.flatten ids = changes.map { |mod| mod.fetch('identifier', '')&.downcase&.strip }.flatten.compact.uniq expect(ids.include?(mods.first['identifier'])).to be(false) end + it 'adds the :related_identifier assertion to the :latest_version :dmphub_modifications Array' do result = described_class.send(:_add_related_identifier, updater: updater, latest_version: latest_version, identifiers: mods) - expect(result['dmphub_modifications'].length).to eql(4) + expect(result['dmphub_modifications'].length).to be(4) changes = result['dmphub_modifications'].reject { |mod| mod.fetch('dmproadmap_related_identifiers', []).empty? } changes = changes.map { |change| change['dmproadmap_related_identifiers'] }.flatten ids = changes.map { |mod| mod.fetch('identifier', '')&.downcase&.strip }.flatten.compact.uniq - mods.each { |mod| expect(ids.include?(mod['identifier'])) } + mods.each { |mod| expect(ids.include?(mod['identifier'])).to be(true) } end end - describe '_add_funding_mod(updater:, latest_version:, funding:, note: '', logger: nil)' do + describe '_add_funding_mod(updater:, latest_version:, funding:, note: \'\', logger: nil)' do let!(:mods) do JSON.parse([{ status: 'granted', @@ -242,46 +274,53 @@ result = described_class.send(:_add_funding_mod, updater: 123, latest_version: latest_version, funding: mods) expect(result).to eql(latest_version) end + it 'returns :latest_version as-is if :latest_version is not a Hash' do result = described_class.send(:_add_funding_mod, updater: updater, latest_version: [123], funding: mods) expect(result).to eql([123]) end + it 'returns :latest_version as-is if :funding is not an Array' do result = described_class.send(:_add_funding_mod, updater: updater, latest_version: latest_version, - funding: { 'foo': 'bar' }) + funding: { foo: 'bar' }) expect(result).to eql(latest_version) end + it 'skips adding the :grant_id if it is already in the :latest_version :dmphub_modifications Array' do latest_version['dmphub_modifications'] << JSON.parse({ funding: mods.first }.to_json) result = described_class.send(:_add_funding_mod, updater: updater, latest_version: latest_version, funding: mods) - expect(result['dmphub_modifications'].length).to eql(4) + expect(result['dmphub_modifications'].length).to be(4) fundings = result['dmphub_modifications'].reject { |mod| mod['funding'].nil? }.flatten.compact.uniq grants = fundings.map { |fund| fund.fetch('funding', {})['grant_id'] } expect(grants.include?(mods.first)).to be(false) end + it 'skips adding the :grant_id if it is already in the :latest_version project: :funding Array' do latest_version['project'].first['funding'].first['grant_id'] = mods.first['grant_id'] result = described_class.send(:_add_funding_mod, updater: updater, latest_version: latest_version, funding: mods) - expect(result['dmphub_modifications'].length).to eql(3) + expect(result['dmphub_modifications'].length).to be(3) fundings = result['dmphub_modifications'].reject { |mod| mod['funding'].nil? }.flatten.compact.uniq grants = fundings.map { |fund| fund.fetch('funding', {})['grant_id'] } expect(grants.include?(mods.first)).to be(false) end + it 'adds the :grant_id assertion to the :latest_version :dmphub_modifications Array' do result = described_class.send(:_add_funding_mod, updater: updater, latest_version: latest_version, funding: mods) - expect(result['dmphub_modifications'].length).to eql(4) + expect(result['dmphub_modifications'].length).to be(4) fundings = result['dmphub_modifications'].reject { |mod| mod['funding'].nil? }.flatten.compact.uniq grants = fundings.map { |fund| fund.fetch('funding', {})['grant_id'] } expect(grants.include?(mods.first['grant_id'])).to be(true) end end - describe '_generate_assertion(updater:, mods:, note: '')' do + describe '_generate_assertion(updater:, mods:, note: \'\')' do let!(:mods) do JSON.parse({ dmproadmap_related_identifiers: [ - { work_type: 'dataset', descriptor: 'references', type: 'doi', identifier: 'https://doi.org/11.22222/3333344' }, - { work_type: 'article', descriptor: 'is_cited_by', type: 'doi', identifier: 'https://doi.org/11.22222/journalA/1' }, + { work_type: 'dataset', descriptor: 'references', type: 'doi', + identifier: 'https://doi.org/11.22222/3333344' }, + { work_type: 'article', descriptor: 'is_cited_by', type: 'doi', + identifier: 'https://doi.org/11.22222/journalA/1' } ], funding: { status: 'granted', @@ -291,13 +330,14 @@ end it 'returns nil if the :updater is nil' do - expect(described_class.send(:_generate_assertion, updater: nil, mods: mods, note: 'testing ...')).to be(nil) + expect(described_class.send(:_generate_assertion, updater: nil, mods: mods, note: 'testing ...')).to be_nil end it 'returns nil if :mod is not a Hash' do - expect(described_class.send(:_generate_assertion, updater: updater, mods: '123', note: 'testing ...')).to be(nil) + expect(described_class.send(:_generate_assertion, updater: updater, mods: '123', note: 'testing ...')).to be_nil end + # rubocop:disable RSpec/MultipleExpectations it 'returns the formatted assertion' do result = described_class.send(:_generate_assertion, updater: updater, mods: mods, note: 'testing ...') expect(result['id'].nil?).to be(false) @@ -310,5 +350,6 @@ end expect(result['funding']).to eql(mods['funding']) end + # rubocop:enable RSpec/MultipleExpectations end end diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/creator_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/creator_spec.rb index fc510969..c9231208 100644 --- a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/creator_spec.rb +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/creator_spec.rb @@ -25,33 +25,40 @@ ENV.delete('DMP_ID_SHOULDER') expect { described_class.create(provenance: owner, json: dmp) }.to raise_error(creator_error) end + it 'raises a CreatorError if the `DMP_ID_BASE_URL` ENV variable is not defined' do ENV.delete('DMP_ID_BASE_URL') expect { described_class.create(provenance: owner, json: dmp) }.to raise_error(creator_error) end + it 'raises a CreatorError if the :provenance is not a Hash' do expect { described_class.create(provenance: 123, json: dmp) }.to raise_error(creator_error) end + it 'raises a CreatorError if the :provenance does not contain a :PK' do owner.delete('PK') expect { described_class.create(provenance: owner, json: dmp) }.to raise_error(creator_error) end + it 'raises a CreatorError if the Uc3DmpId::Validator returns errors' do allow(Uc3DmpId::Validator).to receive(:validate).and_return(['foo']) expect { described_class.create(provenance: owner, json: dmp) }.to raise_error(creator_error) end + it 'raises a CreatorError if the Uc3DmpId::Finder finds a matching :dmp_id' do dmp['dmp']['dmp_id'] = 'https://dx.doi.org/11.1234/A1B2c3' allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) allow(Uc3DmpId::Finder).to receive(:exists?).and_return(true) expect { described_class.create(provenance: owner, json: dmp) }.to raise_error(creator_error) end + it 'raises a CreatorError if unable to generate the :PK' do allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) allow(Uc3DmpId::Finder).to receive(:exists?).and_return(false) allow(described_class).to receive(:_preregister_dmp_id).and_return(nil) expect { described_class.create(provenance: owner, json: dmp) }.to raise_error(creator_error) end + it 'raises a CreatorError if Dynamo could not save the DMP ID record' do allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) allow(Uc3DmpId::Finder).to receive(:exists?).and_return(false) @@ -60,7 +67,8 @@ allow(client).to receive(:put_item).and_return(nil) expect { described_class.create(provenance: owner, json: dmp) }.to raise_error(creator_error) end - it 'it creates the new DMP ID' do + + it 'creates the new DMP ID' do allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) allow(Uc3DmpId::Finder).to receive(:exists?).and_return(false) allow(described_class).to receive(:_preregister_dmp_id).and_return(p_key) @@ -83,11 +91,16 @@ result = described_class.send(:_preregister_dmp_id, client: client, provenance: owner, json: dmp) expect(result).to eql('doi.org/SEEDING-ID') end + it 'raises a CreatorError if a unique DMP ID could not be generated after 10 attempts' do owner['seedingWithLiveDmpIds'] = false allow(Uc3DmpId::Finder).to receive(:exists?).and_return(true) - expect { described_class.send(:_preregister_dmp_id, client: client, provenance: owner, json: dmp) }.to raise_error(creator_error) + expect do + described_class.send(:_preregister_dmp_id, client: client, provenance: owner, + json: dmp) + end.to raise_error(creator_error) end + it 'returns a new DMP ID' do owner['seedingWithLiveDmpIds'] = false allow(Uc3DmpId::Finder).to receive(:exists?).and_return(false) @@ -96,8 +109,8 @@ expected_prefix = "#{Uc3DmpId::Helper::PK_DMP_PREFIX}#{ENV['DMP_ID_BASE_URL'].gsub(%r{https?://}, '')}" expect(result.start_with?(expected_prefix)).to be(true) suffix = result.gsub(expected_prefix, '') - expect(suffix =~ Uc3DmpId::Helper::DOI_REGEX).to eql(1) - expect(suffix.start_with?("/#{ENV['DMP_ID_SHOULDER']}")).to be(true) + expect(suffix =~ Uc3DmpId::Helper::DOI_REGEX).to be(1) + expect(suffix.start_with?("/#{ENV.fetch('DMP_ID_SHOULDER', nil)}")).to be(true) end end @@ -105,6 +118,7 @@ it 'returns false unless :json is a Hash' do expect(described_class.send(:_post_process, json: 123)).to be(false) end + it 'publishes an `EZID update` event to EventBridge if the owner of the DMP ID is the one making the update' do described_class.send(:_post_process, json: dmp['dmp']) expected = { @@ -115,18 +129,20 @@ } expect(publisher).to have_received(:publish).once.with(expected) end + it 'does not publish a `Citation Fetch` event to EventBridge if there are no citable identifiers' do allow(Uc3DmpId::Helper).to receive(:citable_related_identifiers).and_return([]) described_class.send(:_post_process, json: dmp['dmp']) expect(publisher).to have_received(:publish).once end + it 'publishes an `Citation Fetch` event to EventBridge if there are citable identifiers' do ids = JSON.parse([{ work_type: 'dataset', descriptor: 'references', type: 'other', identifier: 'foo' }].to_json) allow(Uc3DmpId::Helper).to receive(:citable_related_identifiers).and_return(ids) described_class.send(:_post_process, json: dmp['dmp']) expected = { dmp: dmp['dmp'], - detail: { PK: dmp['dmp']['PK'], SK: dmp['dmp']['SK'], dmproadmap_related_identifiers: ids}, + detail: { PK: dmp['dmp']['PK'], SK: dmp['dmp']['SK'], dmproadmap_related_identifiers: ids }, source: 'DmpCreator', event_type: 'Citation Fetch', logger: nil diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/deleter_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/deleter_spec.rb index 5f963273..30642a62 100644 --- a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/deleter_spec.rb +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/deleter_spec.rb @@ -17,7 +17,7 @@ let!(:dmp) do record = mock_dmp record['dmp']['PK'] = p_key - record['dmp']['SK'] = "#{Uc3DmpId::Helper::DMP_LATEST_VERSION}" + record['dmp']['SK'] = Uc3DmpId::Helper::DMP_LATEST_VERSION record['dmp']['dmphub_provenance_id'] = owner['PK'] record end @@ -30,25 +30,32 @@ it 'raises an DeleterError when :p_key is not a String' do expect { described_class.tombstone(provenance: owner, p_key: 123) }.to raise_error(deleter_error) end + it 'raises an DeleterError when :provenance is not a Hash' do - expect { described_class.tombstone(provenance: owner, p_key: p_key) }.to raise_error(deleter_error) + expect { described_class.tombstone(provenance: 123, p_key: p_key) }.to raise_error(deleter_error) end + it 'raises an DeleterError when :provenance does not have a :PK' do + owner.delete('PK') expect { described_class.tombstone(provenance: owner, p_key: p_key) }.to raise_error(deleter_error) end + it 'raises an DeleterError when the DMP ID could not be found' do allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(nil) expect { described_class.tombstone(provenance: owner, p_key: p_key) }.to raise_error(deleter_error) end + it 'raises an DeleterError when :provenance does not match the DMP ID\'s :dmphub_provenance_id' do allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) expect { described_class.tombstone(provenance: updater, p_key: p_key) }.to raise_error(deleter_error) end + it 'raises an DeleterError when it is not the latest version of the DMP ID' do dmp['dmp']['SK'] = "#{Uc3DmpId::Helper::SK_DMP_PREFIX}2020-03-15T11:22:33Z" allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) expect { described_class.tombstone(provenance: owner, p_key: p_key) }.to raise_error(deleter_error) end + it 'tombstones the DMP ID' do allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) allow(client).to receive(:put_item).and_return(dmp['dmp']) @@ -69,6 +76,7 @@ it 'returns false unless :json is a Hash' do expect(described_class.send(:_post_process, json: 123)).to be(false) end + it 'publishes an `EZID update` event to EventBridge if the owner of the DMP ID is the one making the update' do described_class.send(:_post_process, json: dmp['dmp']) expected = { @@ -80,4 +88,4 @@ expect(publisher).to have_received(:publish).once.with(expected) end end -end \ No newline at end of file +end diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/finder_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/finder_spec.rb index 3f669c29..5a935946 100644 --- a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/finder_spec.rb +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/finder_spec.rb @@ -15,26 +15,20 @@ describe 'search_dmps(args:, logger: nil)' do it 'returns an empty Array if :args does not contain any valid query criteria' do - allow(described_class).to receive(:_by_owner).never - allow(described_class).to receive(:_by_owner_org).never - allow(described_class).to receive(:_by_mod_day).never expect(described_class.search_dmps(args: JSON.parse({ foo: 'bar' }.to_json))).to eql([]) end + it 'calls :_by_owner if :args includes an :owner_orcid' do allow(described_class).to receive(:_by_owner).once - allow(described_class).to receive(:_by_owner_org).never - allow(described_class).to receive(:_by_mod_day).never expect(described_class.search_dmps(args: JSON.parse({ foo: 'bar' }.to_json))).to eql([]) end + it 'calls :_by_owner_org if :args includes an :owner_org_ror' do - allow(described_class).to receive(:_by_owner).never allow(described_class).to receive(:_by_owner_org).once - allow(described_class).to receive(:_by_mod_day).never expect(described_class.search_dmps(args: JSON.parse({ foo: 'bar' }.to_json))).to eql([]) end + it 'calls :_by_mod_day if :args includes an :modification_day' do - allow(described_class).to receive(:_by_owner).never - allow(described_class).to receive(:_by_owner_org).never allow(described_class).to receive(:_by_mod_day).once expect(described_class.search_dmps(args: JSON.parse({ foo: 'bar' }.to_json))).to eql([]) end @@ -52,78 +46,89 @@ it 'raises a FinderError if :json is not a Hash' do allow(Uc3DmpId::Helper).to receive(:parse_json).and_return(nil) - expect{ described_class.by_json(json: '123', client: client) }.to raise_error(finder_error) + expect { described_class.by_json(json: '123', client: client) }.to raise_error(finder_error) end + it 'raises a FinderError if :json does not contain a :PK or :dmp_id' do json['dmp'].delete('dmp_id') allow(Uc3DmpId::Helper).to receive(:parse_json).and_return(json) - expect{ described_class.by_json(json: '123', client: client) }.to raise_error(finder_error) + expect { described_class.by_json(json: '123', client: client) }.to raise_error(finder_error) end + it 'calls :by_pk if the :json contains a :dmp_id' do allow(Uc3DmpId::Helper).to receive(:parse_json).and_return(json) - allow(described_class).to receive(:by_pk).once + allow(described_class).to receive(:by_pk).and_return(json) described_class.by_json(json: json, client: client) + expect(described_class).to have_received(:by_pk).once end + it 'calls :by_pk if the :json contains a :PK' do json['dmp']['PK'] = json['dmp']['dmp_id']['identifier'] json['dmp'].delete('dmp_id') allow(Uc3DmpId::Helper).to receive(:parse_json).and_return(json) allow(Uc3DmpId::Helper).to receive(:parse_json).and_return(json) - allow(described_class).to receive(:by_pk).once + allow(described_class).to receive(:by_pk).and_return(json) described_class.by_json(json: json, client: client) + expect(described_class).to have_received(:by_pk).once end end describe 'by_pk(p_key:, s_key: Helper::DMP_LATEST_VERSION, client: nil, cleanse: true, logger: nil)' do it 'raises a FinderError if :p_key is nil' do - expect{ described_class.by_pk(p_key: nil, client: client) }.to raise_error(finder_error) + expect { described_class.by_pk(p_key: nil, client: client) }.to raise_error(finder_error) end + it 'uses the default SK if no :s_key is specified' do allow(Uc3DmpId::Versioner).to receive(:append_versions).and_return(dmp) allow(Uc3DmpId::Helper).to receive(:cleanse_dmp_json).and_return(dmp) - expect(Uc3DmpId::Versioner).to receive(:append_versions).once - expect(described_class.by_pk(p_key: 'foo', client: client).length).to eql(1) + expect(described_class.by_pk(p_key: 'foo', client: client).length).to be(1) + expect(Uc3DmpId::Versioner).to have_received(:append_versions).once expected = { key: { PK: "#{Uc3DmpId::Helper::PK_DMP_PREFIX}foo", SK: Uc3DmpId::Helper::DMP_LATEST_VERSION }, logger: nil } expect(client).to have_received(:get_item).with(expected) end + it 'calls Dynamo with the expected query args' do allow(Uc3DmpId::Versioner).to receive(:append_versions).and_return(dmp) allow(Uc3DmpId::Helper).to receive(:cleanse_dmp_json).and_return(dmp) - expect(Uc3DmpId::Versioner).to receive(:append_versions).once - expect(described_class.by_pk(p_key: 'foo', s_key: 'bar', client: client).length).to eql(1) + expect(described_class.by_pk(p_key: 'foo', s_key: 'bar', client: client).length).to be(1) + expect(Uc3DmpId::Versioner).to have_received(:append_versions).once expected = { key: { PK: "#{Uc3DmpId::Helper::PK_DMP_PREFIX}foo", SK: "#{Uc3DmpId::Helper::SK_DMP_PREFIX}bar" }, logger: nil } expect(client).to have_received(:get_item).with(expected) end + it 'appends the :dmphub_versions' do allow(Uc3DmpId::Versioner).to receive(:append_versions).and_return(dmp) allow(Uc3DmpId::Helper).to receive(:cleanse_dmp_json).and_return(dmp) - expect(Uc3DmpId::Versioner).to receive(:append_versions).once described_class.by_pk(p_key: 'foo', s_key: 'bar', client: client) + expect(Uc3DmpId::Versioner).to have_received(:append_versions).once end + it 'cleanses the :dmphub_ prefixed attributes by default' do allow(Uc3DmpId::Versioner).to receive(:append_versions).and_return(dmp) allow(Uc3DmpId::Helper).to receive(:cleanse_dmp_json).and_return(dmp) - expect(Uc3DmpId::Helper).to receive(:cleanse_dmp_json).once described_class.by_pk(p_key: 'foo', s_key: 'bar', client: client) + expect(Uc3DmpId::Helper).to have_received(:cleanse_dmp_json).once end + it 'does not cleanse the :dmphub_ prefixed attributes if specified' do allow(Uc3DmpId::Versioner).to receive(:append_versions).and_return(dmp) allow(Uc3DmpId::Helper).to receive(:cleanse_dmp_json).and_return(dmp) - expect(Uc3DmpId::Helper).to receive(:cleanse_dmp_json).never described_class.by_pk(p_key: 'foo', s_key: 'bar', client: client, cleanse: false) + expect(Uc3DmpId::Helper).not_to have_received(:cleanse_dmp_json) end end describe 'exists?(p_key:, s_key: Helper::DMP_LATEST_VERSION, client: nil, logger: nil)' do it 'raises a FinderError if :p_key is nil' do - expect{ described_class.exists?(p_key: nil, client: client) }.to raise_error(finder_error) + expect { described_class.exists?(p_key: nil, client: client) }.to raise_error(finder_error) end + it 'uses the default SK if no :s_key is specified' do expect(described_class.exists?(p_key: 'foo', client: client)).to be(true) expected = { @@ -132,6 +137,7 @@ } expect(client).to have_received(:pk_exists?).with(expected) end + it 'calls Dynamo with the expected query args' do expect(described_class.exists?(p_key: 'foo', s_key: 'bar', client: client)).to be(true) expected = { @@ -151,23 +157,26 @@ end it 'raises a FinderError if :json is not a Hash' do - expect{ described_class.by_provenance_identifier(json: nil, client: client) }.to raise_error(finder_error) + expect { described_class.by_provenance_identifier(json: nil, client: client) }.to raise_error(finder_error) end + it 'raises a FinderError if :json does not contain a :dmp_id with a :identifier' do - expect{ described_class.by_provenance_identifier(json: {}, client: client) }.to raise_error(finder_error) + expect { described_class.by_provenance_identifier(json: {}, client: client) }.to raise_error(finder_error) end + + # rubocop:disable RSpec/ExampleLength it 'can handle it when :json has a top level :dmp' do nested = JSON.parse({ dmp: json }.to_json) - expect(described_class.by_provenance_identifier(json: nested, client: client).length).to eql(1) + expect(described_class.by_provenance_identifier(json: nested, client: client).length).to be(1) expected = { args: { expression_attribute_values: { ':version': Uc3DmpId::Helper::DMP_LATEST_VERSION }, - filter_expression: "SK = :version", - index_name: "dmphub_provenance_identifier_gsi", + filter_expression: 'SK = :version', + index_name: 'dmphub_provenance_identifier_gsi', key_conditions: { dmphub_provenance_identifier: { attribute_value_list: ['http://some.org/12345'], - comparison_operator: "EQ" + comparison_operator: 'EQ' } } }, @@ -175,17 +184,20 @@ } expect(client).to have_received(:query).with(expected) end + # rubocop:enable RSpec/ExampleLength + + # rubocop:disable RSpec/ExampleLength it 'calls Dynamo with the expected query args' do - expect(described_class.by_provenance_identifier(json: json, client: client).length).to eql(1) + expect(described_class.by_provenance_identifier(json: json, client: client).length).to be(1) expected = { args: { expression_attribute_values: { ':version': Uc3DmpId::Helper::DMP_LATEST_VERSION }, - filter_expression: "SK = :version", - index_name: "dmphub_provenance_identifier_gsi", + filter_expression: 'SK = :version', + index_name: 'dmphub_provenance_identifier_gsi', key_conditions: { dmphub_provenance_identifier: { attribute_value_list: ['http://some.org/12345'], - comparison_operator: "EQ" + comparison_operator: 'EQ' } } }, @@ -193,26 +205,30 @@ } expect(client).to have_received(:query).with(expected) end + # rubocop:enable RSpec/ExampleLength end describe '_by_owner(owner_id:, logger: nil)' do it 'raises a FinderError if :_by_owner is nil' do - expect{ described_class.send(:_by_owner, owner_id: nil) }.to raise_error(finder_error) + expect { described_class.send(:_by_owner, owner_id: nil) }.to raise_error(finder_error) end + it 'raises a FinderError if :owner_org is not an ORCID id' do - expect{ described_class.send(:_by_owner, owner_id: '12345') }.to raise_error(finder_error) + expect { described_class.send(:_by_owner, owner_id: '12345') }.to raise_error(finder_error) end + + # rubocop:disable RSpec/ExampleLength it 'calls Dynamo with the expected query args' do - expect(described_class.send(:_by_owner, owner_id: '0000-0000-0000-TEST', client: client).length).to eql(1) + expect(described_class.send(:_by_owner, owner_id: '0000-0000-0000-TEST', client: client).length).to be(1) expected = { args: { expression_attribute_values: { ':version': Uc3DmpId::Helper::DMP_LATEST_VERSION }, - filter_expression: "SK = :version", - index_name: "dmphub_owner_id_gsi", + filter_expression: 'SK = :version', + index_name: 'dmphub_owner_id_gsi', key_conditions: { dmphub_owner_id: { - attribute_value_list: ["http://orcid.org/0000-0000-0000-TEST", 'https://orcid.org/0000-0000-0000-TEST'], - comparison_operator: "IN" + attribute_value_list: ['http://orcid.org/0000-0000-0000-TEST', 'https://orcid.org/0000-0000-0000-TEST'], + comparison_operator: 'IN' } } }, @@ -220,26 +236,30 @@ } expect(client).to have_received(:query).with(expected) end + # rubocop:enable RSpec/ExampleLength end describe '_by_owner_org(owner_org:, logger: nil)' do it 'raises a FinderError if :owner_org is nil' do - expect{ described_class.send(:_by_owner_org, owner_org: nil) }.to raise_error(finder_error) + expect { described_class.send(:_by_owner_org, owner_org: nil) }.to raise_error(finder_error) end + it 'raises a FinderError if :owner_org is not a ROR id' do - expect{ described_class.send(:_by_owner_org, owner_org: '536.45t245/wefwRT') }.to raise_error(finder_error) + expect { described_class.send(:_by_owner_org, owner_org: '536.45t245/wefwRT') }.to raise_error(finder_error) end + + # rubocop:disable RSpec/ExampleLength it 'calls Dynamo with the expected query args' do - expect(described_class.send(:_by_owner_org, owner_org: '123abc45', client: client).length).to eql(1) + expect(described_class.send(:_by_owner_org, owner_org: '123abc45', client: client).length).to be(1) expected = { args: { expression_attribute_values: { ':version': Uc3DmpId::Helper::DMP_LATEST_VERSION }, - filter_expression: "SK = :version", - index_name: "dmphub_owner_org_gsi", + filter_expression: 'SK = :version', + index_name: 'dmphub_owner_org_gsi', key_conditions: { dmphub_owner_org: { attribute_value_list: ['https://ror.org/123abc45', 'http://ror.org/123abc45'], - comparison_operator: "IN" + comparison_operator: 'IN' } } }, @@ -247,43 +267,51 @@ } expect(client).to have_received(:query).with(expected) end + # rubocop:enable RSpec/ExampleLength end describe '_by_mod_day(day:, logger: nil)' do it 'raises a FinderError if :day is nil' do - expect{ described_class.send(:_by_mod_day, day: nil) }.to raise_error(finder_error) + expect { described_class.send(:_by_mod_day, day: nil) }.to raise_error(finder_error) end + it 'raises a FinderError if :day does not match the YYYY-MM-DD format' do - expect{ described_class.send(:_by_mod_day, day: '10/23/2020') }.to raise_error(finder_error) + expect { described_class.send(:_by_mod_day, day: '10/23/2020') }.to raise_error(finder_error) end + + # rubocop:disable RSpec/ExampleLength it 'calls Dynamo with the expected query args' do - expect(described_class.send(:_by_mod_day, day: '2023-08-21', client: client).length).to eql(1) + expect(described_class.send(:_by_mod_day, day: '2023-08-21', client: client).length).to be(1) expected = { args: { expression_attribute_values: { ':version': Uc3DmpId::Helper::DMP_LATEST_VERSION }, - filter_expression: "SK = :version", - index_name: "dmphub_modification_day_gsi", + filter_expression: 'SK = :version', + index_name: 'dmphub_modification_day_gsi', key_conditions: { - dmphub_modification_day: { attribute_value_list: ['2023-08-21'], comparison_operator: "IN" } + dmphub_modification_day: { attribute_value_list: ['2023-08-21'], comparison_operator: 'IN' } } }, logger: nil } expect(client).to have_received(:query).with(expected) end + # rubocop:enable RSpec/ExampleLength end describe '_process_search_response(response:)' do it 'returns an empty Array unless :response is an Array' do expect(described_class.send(:_process_search_response, response: { foo: 'bar' })).to eql([]) end - it 'it adds a top level :dmp to each item' do + + it 'adds a top level :dmp to each item' do items = JSON.parse([{ title: 'Test one' }, { title: 'Test two' }].to_json) result = described_class.send(:_process_search_response, response: items) - expect(result.length).to eql(2) + expect(result.length).to be(2) expect(result.first).to eql(JSON.parse({ dmp: items.first }.to_json)) expect(result.last).to eql(JSON.parse({ dmp: items.last }.to_json)) end + + # rubocop:disable RSpec/ExampleLength it 'calls Helper.cleanse_dmp_json for each item' do items = JSON.parse([{ PK: 'foo', @@ -295,23 +323,27 @@ dmphub_owner_org: 'ror', dmphub_provenance_identifier: '12345', dmphub_test: 'should go away', - dmphub_versions: ['one', 'two'], - dmphub_modifications: ['three', 'four'] + dmphub_versions: %w[one two], + dmphub_modifications: %w[three four] }].to_json) result = described_class.send(:_process_search_response, response: items) - expect(result.length).to eql(1) + expect(result.length).to be(1) - expected = JSON.parse([{ - title: 'Test one', - dmphub_versions: ['one', 'two'], - dmphub_modifications: ['three', 'four'] - }].to_json) - expect(assert_dmps_match(obj_a: result.first, obj_b: expected, debug: false)) + expected = JSON.parse({ + dmp: { + title: 'Test one', + dmphub_versions: %w[one two], + dmphub_modifications: %w[three four] + } + }.to_json) + expect(assert_dmps_match(obj_a: result.first, obj_b: expected, debug: false)).to be(true) end + # rubocop:enable RSpec/ExampleLength + it 'removes nils and dupicates' do items = JSON.parse([{ title: 'Test one' }, { title: 'Test two' }, nil, { title: 'Test one' }].to_json) result = described_class.send(:_process_search_response, response: items) - expect(result.length).to eql(2) + expect(result.length).to be(2) expect(result.first).to eql(JSON.parse({ dmp: items.first }.to_json)) expect(result.last).to eql(JSON.parse({ dmp: items[1] }.to_json)) end diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb index 2f8767bd..ecb4739b 100644 --- a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/helper_spec.rb @@ -24,7 +24,7 @@ end it 'removes the prefix' do - key = "foo/bar" + key = 'foo/bar' expect(described_class.remove_pk_prefix(p_key: "#{described_class::PK_DMP_PREFIX}#{key}")).to eql(key) end end @@ -48,7 +48,7 @@ end it 'removes the prefix' do - key = "foo/bar" + key = 'foo/bar' expect(described_class.remove_sk_prefix(s_key: "#{described_class::SK_DMP_PREFIX}#{key}")).to eql(key) end end @@ -58,13 +58,15 @@ ENV.delete('DMP_ID_BASE_URL') expect(described_class.dmp_id_base_url).to eql(described_class::DEFAULT_LANDING_PAGE_URL) end + it 'returns the ENV[\'DMP_ID_BASE_URL\']' do ENV['DMP_ID_BASE_URL'] = 'http://foo.bar/' - expect(described_class.dmp_id_base_url).to eql(ENV['DMP_ID_BASE_URL']) + expect(described_class.dmp_id_base_url).to eql(ENV.fetch('DMP_ID_BASE_URL', nil)) end + it 'appends a trailing \'/\' if necessary' do ENV['DMP_ID_BASE_URL'] = 'http://foo.bar' - expect(described_class.dmp_id_base_url).to eql("#{ENV['DMP_ID_BASE_URL']}/") + expect(described_class.dmp_id_base_url).to eql("#{ENV.fetch('DMP_ID_BASE_URL', nil)}/") end end @@ -73,45 +75,53 @@ ENV.delete('DMP_ID_LANDING_URL') expect(described_class.landing_page_url).to eql(described_class::DEFAULT_LANDING_PAGE_URL) end + it 'returns the ENV[\'DMP_ID_LANDING_URL\']' do ENV['DMP_ID_LANDING_URL'] = 'http://foo.bar/' - expect(described_class.landing_page_url).to eql(ENV['DMP_ID_LANDING_URL']) + expect(described_class.landing_page_url).to eql(ENV.fetch('DMP_ID_LANDING_URL', nil)) end + it 'appends a trailing \'/\' if necessary' do ENV['DMP_ID_LANDING_URL'] = 'http://foo.bar' - expect(described_class.landing_page_url).to eql("#{ENV['DMP_ID_LANDING_URL']}/") + expect(described_class.landing_page_url).to eql("#{ENV.fetch('DMP_ID_LANDING_URL', nil)}/") end end describe 'format_dmp_id(value:, with_protocol: false)' do - let!(:dmp_id) { 'doi.org/11.2222/3333.444'} + let!(:dmp_id) { 'doi.org/11.2222/3333.444' } before do ENV['DMP_ID_BASE_URL'] = 'https://doi.org' end it 'returns nil if the value is not a DOI' do - expect(described_class.format_dmp_id(value: 'https://dmptool.org')).to eql(nil) + expect(described_class.format_dmp_id(value: 'https://dmptool.org')).to be_nil end + it 'removes the protocol from the value by default' do - val = "#{ENV['DMP_ID_BASE_URL']}/#{dmp_id}" + val = "#{ENV.fetch('DMP_ID_BASE_URL', nil)}/#{dmp_id}" expect(described_class.format_dmp_id(value: val)).to eql("doi.org/#{dmp_id}") end + it 'returns the value as is if it starts with a protocol' do - val = "#{ENV['DMP_ID_BASE_URL']}/#{dmp_id}" + val = "#{ENV.fetch('DMP_ID_BASE_URL', nil)}/#{dmp_id}" expect(described_class.format_dmp_id(value: val, with_protocol: true)).to eql(val) end + it 'removes the `doi:` prefix' do val = "doi:#{dmp_id}" expect(described_class.format_dmp_id(value: val)).to eql(dmp_id) end + it 'removes preceding `/` character' do val = "/#{dmp_id}" expect(described_class.format_dmp_id(value: val)).to eql(dmp_id) end + it 'does not include the protocol by default' do expect(described_class.format_dmp_id(value: dmp_id)).to eql(dmp_id) end + it 'includes the protocol if we specify :with_protocol' do expected = "https://#{dmp_id}" expect(described_class.format_dmp_id(value: dmp_id, with_protocol: true)).to eql(expected) @@ -124,14 +134,19 @@ end it 'returns nil if param is not a string' do - expect(described_class.path_parameter_to_pk(param: 123)).to eql(nil) + expect(described_class.path_parameter_to_pk(param: 123)).to be_nil end + it 'handles URI escaped characters' do - expect(described_class.path_parameter_to_pk(param: 'doi%3A11%2E2222%2F33333')).to eql('DMP#doi.org/11.2222/33333') + result = described_class.path_parameter_to_pk(param: 'doi%3A11%2E2222%2F33333') + expect(result).to eql('DMP#doi.org/11.2222/33333') end + it 'replaces a domain with our base domain' do - expect(described_class.path_parameter_to_pk(param: 'doi:foo.bar/11.2222/33333')).to eql('DMP#doi.org/11.2222/33333') + result = described_class.path_parameter_to_pk(param: 'doi:foo.bar/11.2222/33333') + expect(result).to eql('DMP#doi.org/11.2222/33333') end + it 'returns the DMP ID as a PK' do expect(described_class.path_parameter_to_pk(param: '11.2222/33333')).to eql('DMP#doi.org/11.2222/33333') end @@ -139,29 +154,34 @@ describe 'dmp_id_to_pk(json:)' do it 'returns nil if :json is not a Hash' do - expect(described_class.dmp_id_to_pk(json: 123)).to eql(nil) + expect(described_class.dmp_id_to_pk(json: 123)).to be_nil end + it 'returns nil if :json does not contain :identifier' do - expect(described_class.dmp_id_to_pk(json: JSON.parse({ foo: 'bar' }.to_json))).to eql(nil) + expect(described_class.dmp_id_to_pk(json: JSON.parse({ foo: 'bar' }.to_json))).to be_nil end + it 'returns nil if :format_dmp_id returns nil' do allow(described_class).to receive(:format_dmp_id).and_return(nil) - expect(described_class.dmp_id_to_pk(json: JSON.parse({ identifier: '11.2222/12345' }.to_json))).to eql(nil) + expect(described_class.dmp_id_to_pk(json: JSON.parse({ identifier: '11.2222/12345' }.to_json))).to be_nil end + it 'formats the PK as expected' do val = 'doi.org/11.2222/12345' allow(described_class).to receive(:format_dmp_id).and_return(val) - expect(described_class.dmp_id_to_pk(json: JSON.parse({ identifier: '11.2222/12345' }.to_json))).to eql("DMP##{val}") + result = described_class.dmp_id_to_pk(json: JSON.parse({ identifier: '11.2222/12345' }.to_json)) + expect(result).to eql("DMP##{val}") end end describe 'pk_to_dmp_id(p_key:)' do it 'returns nil if :p_key is nil' do - expect(described_class.pk_to_dmp_id(p_key: nil)).to eql(nil) + expect(described_class.pk_to_dmp_id(p_key: nil)).to be_nil end + it 'returns the expected Hash' do ENV['DMP_ID_BASE_URL'] = 'https://doi.org' - expected = { type: 'doi', identifier: 'https://doi.org/11.2222.12345'} + expected = { type: 'doi', identifier: 'https://doi.org/11.2222.12345' } allow(described_class).to receive(:remove_pk_prefix).and_return('doi.org/11.2222.12345') allow(described_class).to receive(:format_dmp_id).and_return(expected[:identifier]) expect(described_class.pk_to_dmp_id(p_key: 'DMP#doi.org/11.2222.12345')).to eql(expected) @@ -172,12 +192,15 @@ it 'returns the :json as-is if it is already a Hash' do expect(described_class.parse_json(json: { foo: 'bar' })).to eql({ foo: 'bar' }) end + it 'returns nil if :json is not a String or Hash' do - expect(described_class.parse_json(json: 123)).to eql(nil) + expect(described_class.parse_json(json: 123)).to be_nil end + it 'raises a JSON::ParserError if the :json String is invalid' do expect { described_class.parse_json(json: 'foo: bar') }.to raise_error(JSON::ParserError) end + it 'parses the String into a Hash' do expect(described_class.parse_json(json: '{"foo":"bar"}')).to eql(JSON.parse({ foo: 'bar' }.to_json)) end @@ -194,7 +217,7 @@ title: 'Foo bar', created: now, modified: now, - dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), + dmphub_modification_day: Time.now.utc.strftime('%Y-%m-%d'), dmphub_versions: [ { timestamp: now, url: 'http://foo.bar/foo' }, { timestamp: now, url: 'http://foo.bar/foo?version=2000-01-01T00:00:00+00:00' } @@ -204,26 +227,32 @@ end it 'just compares the 2 values as-is if :dmp_a or :dmp_b is not a Hash' do - expect(described_class.eql?(dmp_a: 'foo', dmp_b: { dmp: { bar: 'foo' } })).to eql(false) + expect(described_class.eql?(dmp_a: 'foo', dmp_b: { dmp: { bar: 'foo' } })).to be(false) end + it 'just compares the 2 values as-is if :dmp_a or dmp_b does not have a top level :dmp' do - expect(described_class.eql?(dmp_a: { foo: 'bar' }, dmp_b: { dmp: { bar: 'foo' } })).to eql(false) + expect(described_class.eql?(dmp_a: { foo: 'bar' }, dmp_b: { dmp: { bar: 'foo' } })).to be(false) end + it 'returns true if :dmp_a and :dmp_b are identical' do - expect(described_class.eql?(dmp_a: dmp, dmp_b: dmp)).to eql(true) + expect(described_class.eql?(dmp_a: dmp, dmp_b: dmp)).to be(true) end + it 'returns false if :dmp_a PK does not start with the DMP ID :PK prefix' do dmp_a = dmp.clone[:PK] = 'FOO' - expect(described_class.eql?(dmp_a: dmp_a, dmp_b: dmp)).to eql(false) + expect(described_class.eql?(dmp_a: dmp_a, dmp_b: dmp)).to be(false) end + it 'returns false if :dmp_b PK does not start with the DMP ID :PK prefix' do dmp_b = dmp.clone[:PK] = 'FOO' - expect(described_class.eql?(dmp_a: dmp, dmp_b: dmp_b)).to eql(false) + expect(described_class.eql?(dmp_a: dmp, dmp_b: dmp_b)).to be(false) end + it 'returns false if :dmp_a and :dmp_b :PKs do not match' do dmp_a = dmp.clone[:PK] = "#{described_class::PK_DMP_PREFIX}FOO" - expect(described_class.eql?(dmp_a: dmp_a, dmp_b: dmp)).to eql(false) + expect(described_class.eql?(dmp_a: dmp_a, dmp_b: dmp)).to be(false) end + it 'ignores :SK, :created, :modified, :dmphub_modification_day and :dmphub_versions' do dmp_a = JSON.parse({ dmp: { @@ -236,7 +265,7 @@ dmphub_versions: [] } }.to_json) - expect(described_class.eql?(dmp_a: dmp_a, dmp_b: dmp)).to eql(true) + expect(described_class.eql?(dmp_a: dmp_a, dmp_b: dmp)).to be(true) end end @@ -258,11 +287,13 @@ end it 'returns nil if :json is not a Hash' do - expect(described_class.extract_owner_id(json: 123)).to eql(nil) + expect(described_class.extract_owner_id(json: 123)).to be_nil end + it 'returns the :contact_id if available' do expect(described_class.extract_owner_id(json: dmp)).to eql('contact') end + it 'returns the first :contributor_id if :contact_id is not available' do dmp['dmp'].delete('contact') expect(described_class.extract_owner_id(json: dmp)).to eql('first') @@ -281,18 +312,20 @@ contributor: [ { dmproadmap_affiliation: { affiliation_id: { type: 'ror', identifier: 'first' } } }, { dmproadmap_affiliation: { affiliation_id: { type: 'ror', identifier: 'last' } } }, - { dmproadmap_affiliation: {affiliation_id: { type: 'ror', identifier: 'last' } } } + { dmproadmap_affiliation: { affiliation_id: { type: 'ror', identifier: 'last' } } } ] } }.to_json) end it 'returns nil if :json is not a Hash' do - expect(described_class.extract_owner_org(json: 123)).to eql(nil) + expect(described_class.extract_owner_org(json: 123)).to be_nil end + it 'returns the :contact affiliation if available' do expect(described_class.extract_owner_org(json: dmp)).to eql('contact') end + it 'returns the most common :contributor affiliation if :contact is not available' do dmp['dmp'].delete('contact') expect(described_class.extract_owner_org(json: dmp)).to eql('last') @@ -317,17 +350,22 @@ it 'returns the :json as-is if :provenance is nil' do expect(described_class.annotate_dmp_json(provenance: nil, p_key: p_key, json: dmp)).to eql(dmp) end + it 'returns the :json as-is if :p_key is nil' do expect(described_class.annotate_dmp_json(provenance: provenance, p_key: nil, json: dmp)).to eql(dmp) end + it 'returns nil if :json is not parseable' do allow(described_class).to receive(:parse_json).and_return(nil) - expect(described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: 123)).to eql(nil) + expect(described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: 123)).to be_nil end + it 'returns the :json as-is if :p_key does not match the :dmp_id' do dmp['PK'] = 'DMP#doi.org/99.9999/99999' expect(described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp)).to eql(dmp) end + + # rubocop:disable RSpec/ExampleLength it 'returns the expected JSON if :dmphub_provenance_id if not defined in the :json' do expected = JSON.parse({ title: 'Foo bar', @@ -335,7 +373,7 @@ SK: described_class::DMP_LATEST_VERSION, dmp_id: { type: 'doi', identifier: "https://#{p_key.gsub(described_class::PK_DMP_PREFIX, '')}" }, dmproadmap_featured: '0', - dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), + dmphub_modification_day: Time.now.utc.strftime('%Y-%m-%d'), dmphub_owner_id: 'orcid123', dmphub_owner_org: 'ror123', dmphub_provenance_id: provenance['PK'] @@ -343,6 +381,9 @@ result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) expect(assert_dmps_match(obj_a: result, obj_b: expected, debug: false)).to be(true) end + # rubocop:enable RSpec/ExampleLength + + # rubocop:disable RSpec/ExampleLength it 'properly translates :dmproadmap_featured' do dmp['dmproadmap_featured'] = 'yes' @@ -352,7 +393,7 @@ SK: described_class::DMP_LATEST_VERSION, dmp_id: { type: 'doi', identifier: "https://#{p_key.gsub(described_class::PK_DMP_PREFIX, '')}" }, dmproadmap_featured: '1', - dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), + dmphub_modification_day: Time.now.utc.strftime('%Y-%m-%d'), dmphub_owner_id: 'orcid123', dmphub_owner_org: 'ror123', dmphub_provenance_id: provenance['PK'] @@ -360,6 +401,9 @@ result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) expect(assert_dmps_match(obj_a: result, obj_b: expected, debug: false)).to be(true) end + # rubocop:enable RSpec/ExampleLength + + # rubocop:disable RSpec/ExampleLength it 'adds the expected JSON if :dmphub_provenance_identifier if not defined in the :json' do dmp['dmproadmap_featured'] = 1 dmp['dmphub_provenance_identifier'] = 'http://foo.bar/dmp/123' @@ -369,9 +413,9 @@ title: 'Foo bar', PK: p_key, SK: described_class::DMP_LATEST_VERSION, - dmp_id: { type: "doi", identifier: "https://#{p_key.gsub(described_class::PK_DMP_PREFIX, '')}" }, + dmp_id: { type: 'doi', identifier: "https://#{p_key.gsub(described_class::PK_DMP_PREFIX, '')}" }, dmproadmap_featured: '1', - dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), + dmphub_modification_day: Time.now.utc.strftime('%Y-%m-%d'), dmphub_owner_id: 'orcid123', dmphub_owner_org: 'ror123', dmphub_provenance_id: provenance['PK'], @@ -380,6 +424,9 @@ result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) expect(assert_dmps_match(obj_a: expected, obj_b: result, debug: false)).to be(true) end + # rubocop:enable RSpec/ExampleLength + + # rubocop:disable RSpec/ExampleLength it 'retains the DMP ID specified if the provenance is :seedingWithLiveDmpIds' do provenance[:seedingWithLiveDmpIds] = true dmp['dmp_id'] = 'http://foo.bar/dmp/123' @@ -389,9 +436,9 @@ title: 'Foo bar', PK: "#{described_class::PK_DMP_PREFIX}foo.bar/dmp/123", SK: described_class::DMP_LATEST_VERSION, - dmp_id: { type: "doi", identifier: 'http://foo.bar/dmp/123' }, + dmp_id: { type: 'doi', identifier: 'http://foo.bar/dmp/123' }, dmproadmap_featured: '1', - dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), + dmphub_modification_day: Time.now.utc.strftime('%Y-%m-%d'), dmphub_owner_id: 'orcid123', dmphub_owner_org: 'ror123', dmphub_provenance_id: provenance['PK'] @@ -399,6 +446,9 @@ result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) expect(assert_dmps_match(obj_a: result, obj_b: expected, debug: false)).to be(true) end + # rubocop:enable RSpec/ExampleLength + + # rubocop:disable RSpec/ExampleLength it 'does NOT retain the specified DMP ID if the provenance is not :seedingWithLiveDmpIds' do dmp['dmp_id'] = JSON.parse({ type: 'url', identifier: 'http://foo.bar/dmp/123' }.to_json) dmp['dmproadmap_featured'] = '1' @@ -407,9 +457,9 @@ title: 'Foo bar', PK: p_key, SK: described_class::DMP_LATEST_VERSION, - dmp_id: { type: "doi", identifier: "https://#{p_key.gsub(described_class::PK_DMP_PREFIX, '')}" }, + dmp_id: { type: 'doi', identifier: "https://#{p_key.gsub(described_class::PK_DMP_PREFIX, '')}" }, dmproadmap_featured: '1', - dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), + dmphub_modification_day: Time.now.utc.strftime('%Y-%m-%d'), dmphub_owner_id: 'orcid123', dmphub_owner_org: 'ror123', dmphub_provenance_id: provenance['PK'], @@ -418,6 +468,7 @@ result = described_class.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp) expect(assert_dmps_match(obj_a: expected, obj_b: result, debug: false)).to be(true) end + # rubocop:enable RSpec/ExampleLength end describe 'cleanse_dmp_json(json:)' do @@ -426,33 +477,30 @@ title: 'Foo bar', PK: 'FOO', SK: described_class::DMP_LATEST_VERSION, - dmp_id: { type: "doi", identifier: "https://FOO" }, + dmp_id: { type: 'doi', identifier: 'https://FOO' }, dmproadmap_featured: '1', - dmphub_modification_day: Time.now.strftime('%Y-%m-%d'), + dmphub_modification_day: Time.now.utc.strftime('%Y-%m-%d'), dmphub_owner_id: 'orcid123', dmphub_owner_org: 'ror123', dmphub_provenance_id: 'fooo', dmphub_provenance_identifier: 'http://foo.bar/dmp/123', dmphub_foo: 'bar', dmphub_modifications: 'still here', - dmphub_versions: { result: 'still here'} + dmphub_versions: { result: 'still here' } }.to_json) end it 'returns :json as-is if it is not a Hash or Array' do - expect(described_class.cleanse_dmp_json(json: 123)).to eql(123) - end - it 'calls itself for each item if :json is an array' do - allow(described_class).to receive(:cleanse_dmp_json).twice - described_class.cleanse_dmp_json(json: [dmp, dmp]) + expect(described_class.cleanse_dmp_json(json: 123)).to be(123) end + it 'returns the cleansed :json' do expected = JSON.parse({ title: 'Foo bar', - dmp_id: { type: "doi", identifier: "https://FOO" }, + dmp_id: { type: 'doi', identifier: 'https://FOO' }, dmproadmap_featured: '1', dmphub_modifications: 'still here', - dmphub_versions: { result: 'still here'} + dmphub_versions: { result: 'still here' } }.to_json) result = described_class.cleanse_dmp_json(json: dmp) @@ -464,33 +512,38 @@ it 'returns an empty array if :dmp is not a Hash' do expect(described_class.citable_related_identifiers(dmp: 123)).to eql([]) end + + # rubocop:disable RSpec/ExampleLength it 'returns the expected :dmproadmap_related_identifiers' do dmp = JSON.parse({ title: 'Foo bar', - dmp_id: { type: "doi", identifier: "https://FOO" }, + dmp_id: { type: 'doi', identifier: 'https://FOO' }, dmproadmap_related_identifiers: [ - { work_type: 'output_management_plan', descriptor: 'is_metadata_for', identifier: 'http://skip.me', type: 'url' }, + { work_type: 'output_management_plan', descriptor: 'is_metadata_for', identifier: 'http://skip.me', + type: 'url' }, { work_type: 'article', descriptor: 'cites', identifier: 'http://skip.me', type: 'doi', citation: 'fooooo' }, - { work_type: 'dataset', descriptor: 'references', identifier: 'http://keep.me', type: 'url', citation: 'baaaarr' }, + { work_type: 'dataset', descriptor: 'references', identifier: 'http://keep.me', type: 'url', + citation: 'baaaarr' }, { work_type: 'dataset', descriptor: 'references', identifier: 'http://keep.me', type: 'url' }, - { work_type: 'software', descriptor: 'cites', identifier: 'http://keep.me', type: 'url' }, + { work_type: 'software', descriptor: 'cites', identifier: 'http://keep.me', type: 'url' } ] }.to_json) expected = dmp['dmproadmap_related_identifiers'].reject { |id| id['identifier'] == 'http://skip.me' } result = described_class.citable_related_identifiers(dmp: dmp) expect(assert_dmps_match(obj_a: expected, obj_b: result, debug: false)).to be(true) end + # rubocop:enable RSpec/ExampleLength end describe 'deep_copy_dmp(obj:)' do it 'makes a copy of the object' do obj = JSON.parse({ foo: { - array_one: ['a', 'b', 'c'], + array_one: %w[a b c], key_one: 'value one', nested: { - array_two: ['Z', 'Y', 'X'], - key_two: 'value two', + array_two: %w[Z Y X], + key_two: 'value two' } } }.to_json) diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/integration_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/integration_spec.rb new file mode 100644 index 00000000..4a1ed048 --- /dev/null +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/integration_spec.rb @@ -0,0 +1,297 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Load the Mock Data Store that will monkey patch the Uc3DmpDyanmo::Client and Uc3DmpEventBridge::Publisher +require_relative '../support/mock_data_store' +require_relative '../support/mock_event_bus' + +# rubocop:disable RSpec/DescribeClass +RSpec.describe 'Full Integration Tests' do + # TODO: Add in checks for EventBridge messages + # let!(:publisher) { mock_uc3_dmp_event_bridge } + + let!(:owner) { JSON.parse({ PK: 'PROVENANCE#foo', SK: 'PROFILE', name: 'foo' }.to_json) } + let!(:external_updater) { JSON.parse({ PK: 'PROVENANCE#bar', SK: 'PROFILE', name: 'bar' }.to_json) } + + let!(:p_key) { "#{Uc3DmpId::Helper::PK_DMP_PREFIX}#{mock_dmp_id}" } + let!(:provenance_identifier) { JSON.parse({ type: 'url', identifier: 'https://some.site.edu/dmps/12345' }.to_json) } + + let!(:dynamo_client) { Uc3DmpDynamo::Client.new } + + before do + ENV['DOMAIN'] = 'https://test.org' + ENV['DMP_ID_SHOULDER'] = '11.22222/33' + ENV['DMP_ID_BASE_URL'] = 'https://doi.org' + ENV['EVENT_BUS_NAME'] = 'test-bus' + + allow(Uc3DmpDynamo::Client).to receive(:new).and_return(dynamo_client) + mock_uc3_dmp_event_bridge + end + + # rubocop:disable RSpec/MultipleExpectations, RSpec/ExampleLength + it 'succeeds in all the things' do + original_dmp = mocked_dmp(pkey: p_key) + original_dmp['dmp']['dmp_id'] = provenance_identifier + + # Test seeding a DMP ID + owner['seedingWithLiveDmpIds'] = true + original_dmp['dmp']['dmproadmap_external_system_identifier'] = mock_dmp_id + new_dmp = Uc3DmpId::Creator.create(provenance: owner, json: original_dmp) + expect(dynamo_client.data_store.length).to be(1) + new_dmp_tests(original: original_dmp, new_dmp: new_dmp, dynamo_rec: dynamo_client.data_store.last, seeding: true) + + # Register a new DMP ID + owner['seedingWithLiveDmpIds'] = false + new_dmp = Uc3DmpId::Creator.create(provenance: owner, json: original_dmp) + expect(dynamo_client.data_store.length).to be(2) + new_dmp_tests(original: original_dmp, new_dmp: new_dmp, dynamo_rec: dynamo_client.data_store.last) + pk = Uc3DmpId::Helper.dmp_id_to_pk(json: new_dmp['dmp']['dmp_id']) + + # pp dynamo_client.data_store.map { |rec| { PK: rec['PK'], SK: rec['SK'], modified: rec['modified'] } } + + # Attaches the narrative PDF document + url = 'http://test.edu/docs/123.pdf' + expect(Uc3DmpId::Updater.attach_narrative(provenance: owner, p_key: pk, url: url)).to be(true) + expect(dynamo_client.data_store.length).to be(2) + test_attachment(url: url, prior_dmp: new_dmp, dmp: Uc3DmpId::Finder.by_pk(p_key: pk), + dynamo_rec: dynamo_client.data_store.last) + + # Update the DMP ID (as the system of provenance) + modified = JSON.parse({ dmp: new_dmp['dmp'].dup }.to_json) + modified['dmp']['description'] = 'Testing update of DMP ID.' + expect(modified['dmp']['description']).not_to eql(new_dmp['dmp']['description']) + updated_dmp = Uc3DmpId::Updater.update(provenance: owner, p_key: pk, json: modified) + expect(dynamo_client.data_store.length).to be(2) + expect(updated_dmp['dmp']['description']).to eql('Testing update of DMP ID.') + expect(updated_dmp['dmp']['dmphub_versions'].nil?).to be(true) + expect(updated_dmp['dmp']['dmphub_modifications'].nil?).to be(true) + updated_dmp_id_tests(prior_dmp: new_dmp, dmp: updated_dmp, dynamo_rec: dynamo_client.data_store.last) + + # Force the DMP ID modified date to something in the past so we can test versioning + new_tstamp = (Time.parse(updated_dmp['dmp']['modified']) - 720_000).utc.iso8601 + dynamo_client.change_timestamps(p_key: pk, tstamp: new_tstamp) + updated_dmp['dmp']['created'] = new_tstamp + updated_dmp['dmp']['modified'] = new_tstamp + + # Versions the DMP ID as if the update happens several days later + modified_again = JSON.parse({ dmp: updated_dmp['dmp'].dup }.to_json) + modified_again['dmp']['description'] = 'Testing update of DMP ID again to create a new version.' + expect(modified_again['dmp']['description']).not_to eql(updated_dmp['dmp']['description']) + updated_again = Uc3DmpId::Updater.update(provenance: owner, p_key: pk, json: modified_again) + expect(dynamo_client.data_store.length).to be(3) + expect(updated_again['dmp']['description']).to eql('Testing update of DMP ID again to create a new version.') + # There are no :dmphub_modifications at this point + expect(updated_again['dmp']['dmphub_versions'].length).to be(2) + expect(updated_again['dmp']['dmphub_modifications'].nil?).to be(true) + updated_dmp_id_tests(prior_dmp: updated_dmp, dmp: updated_again, dynamo_rec: dynamo_client.data_store.last, + expect_versioned: true) + + # Update the DMP ID (as an external system) wait a few seconds so the timestamps are different + sleep(1) + external_mod = JSON.parse({ dmp: updated_again['dmp'].dup }.to_json) + # Test a change to a field that an external system cannot make! + external_mod['dmp']['description'] = 'This change should be illegal!' + expect(external_mod['dmp']['description']).not_to eql(updated_again['dmp']['description']) + # Test a discovered :grant_id + external_mod['dmp']['project'].first['funding'].first['grant_id'] = JSON.parse({ + type: 'url', identifier: 'http:grants.example.org/my/test' + }.to_json) + # Test the discovery of new related works + if external_mod['dmp']['dmproadmap_related_identifiers'].nil? + external_mod['dmp']['dmproadmap_related_identifiers'] = + [] + end + external_mod['dmp']['dmproadmap_related_identifiers'] << JSON.parse({ + work_type: 'article', descriptor: 'is_cited_by', type: 'doi', identifier: 'http://doi.org/55.66666/some.journal/123' + }.to_json) + external_mod['dmp']['dmproadmap_related_identifiers'] << JSON.parse({ + work_type: 'dataset', descriptor: 'references', type: 'doi', identifier: 'http://dx.doi.org/33.4444/ABC555.34' + }.to_json) + external_update = Uc3DmpId::Updater.update(provenance: external_updater, p_key: pk, json: external_mod) + expect(dynamo_client.data_store.length).to be(4) + external_mod_tests(prior_dmp: updated_again, dmp: external_update, dynamo_rec: dynamo_client.data_store.last) + + # Retains the :dmphub_modifications after another update from the system of provenance + sleep(1) + final_mod = JSON.parse({ dmp: external_update['dmp'].dup }.to_json) + final_mod['dmp']['description'] = 'Final update test' + expect(final_mod['dmp']['description']).not_to eql(external_update['dmp']['description']) + last_update = Uc3DmpId::Updater.update(provenance: owner, p_key: pk, json: final_mod) + expect(dynamo_client.data_store.length).to be(4) + expect(last_update['dmp']['description']).to eql('Final update test') + expect(last_update['dmp']['dmphub_versions'].length).to be(3) + expect(last_update['dmp']['dmphub_modifications']).to eql(external_update['dmp']['dmphub_modifications']) + updated_dmp_id_tests(prior_dmp: external_update, dmp: last_update, dynamo_rec: dynamo_client.data_store.last, + expect_versioned: false) + + # Retains the :dmphub_modifications after another external system finds mods + # Update the DMP ID (as an external system) wait a few seconds so the timestamps are different + sleep(1) + external_mod2 = JSON.parse({ dmp: last_update['dmp'].dup }.to_json) + # Test a change to a field that an external system cannot make! + external_mod2['dmp']['description'] = 'This change should be illegal!' + expect(external_mod2['dmp']['description']).not_to eql(last_update['dmp']['description']) + # Test the discovery of new related works + new_id = JSON.parse({ work_type: 'software', descriptor: 'references', type: 'url', + identifier: 'http://github.com/test/project123' }.to_json) + external_mod2['dmp']['dmproadmap_related_identifiers'] << new_id + + other_updater = JSON.parse({ PK: 'PROVENANCE#baz', SK: 'PROFILE', name: 'baz' }.to_json) + + external_update2 = Uc3DmpId::Updater.update(provenance: other_updater, p_key: pk, json: external_mod2) + expect(dynamo_client.data_store.length).to be(5) + # Was unable to change the description + expect(external_update2['dmp']['description']).to eql(last_update['dmp']['description']) + # Retained all of the old :dmphub_modifications + last_update['dmp']['dmphub_modifications'].each do |mod| + expect(external_update2['dmp']['dmphub_modifications'].include?(mod)).to be(true), "Expected #{mod}" + end + # Added the new :related_identifiers to the :dmphub_modifications + new_one = external_update2['dmp']['dmphub_modifications'].select do |mod| + mod.fetch('dmproadmap_related_identifiers', []).include?(new_id) + end + expect(new_one.nil?).to be(false) + + # Tombstones the DMP ID + sleep(1) + tombstoned = Uc3DmpId::Deleter.tombstone(provenance: owner, p_key: pk) + # pp dynamo_client.data_store.map { |rec| { PK: rec['PK'], SK: rec['SK'], modified: rec['modified'] } } + + expect(dynamo_client.data_store.length).to be(5) + dynamo_rec = dynamo_client.data_store.last + + expect(tombstoned['dmp']['title']).to eql("OBSOLETE: #{external_update2['dmp']['title']}") + expect(tombstoned['dmp']['modified'] >= external_update2['dmp']['modified']).to be(true) + + expect(dynamo_client.get_item(key: { PK: pk, SK: Uc3DmpId::Helper::DMP_LATEST_VERSION })).to be_nil + expect(dynamo_rec['SK']).to eql(Uc3DmpId::Helper::DMP_TOMBSTONE_VERSION) + expect(dynamo_rec['dmphub_tombstoned_at'] >= external_update2['dmp']['modified']).to be(true) + end + # rubocop:enable RSpec/MultipleExpectations, RSpec/ExampleLength + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def new_dmp_tests(original:, new_dmp:, dynamo_rec:, seeding: false) + # Validate the record that was returned to the caller + expect(new_dmp['dmp'].nil?).to be(false) + # Validate the structure of the DMP ID (unless we are seeding because then it may not have our EZID shoulder) + validate_dmp_id(dmp_id: new_dmp['dmp']['dmp_id']) unless seeding + # The generated DMP ID should NOT match the original (unless we are seeding) + expect(new_dmp['dmp']['dmp_id']['identifier'] != original['dmp']['dmp_id']['identifier']) unless seeding + expect(new_dmp['dmp']['dmp_id']['identifier'] == original['dmp']['dmp_id']['identifier']) if seeding + # There should be no :dmphub_versions since this is a new record + expect(new_dmp['dmp']['dmphub_versions'].nil?).to be(true) + # There are no :dmphub_modifications at this point + expect(new_dmp['dmp']['dmphub_modifications'].nil?).to be(true) + # Replaced the incoming :created and :modified timestamps + expect(new_dmp['dmp']['created'] > original['dmp']['created']).to be(true) + expect(new_dmp['dmp']['modified'] > original['dmp']['modified']).to be(true) + + # Validate the record sent to the Dynamo data store + expect(dynamo_rec['dmp'].nil?).to be(true) + # Validate the structure of the DMP ID (unless we are seeding because then it may not have our EZID shoulder) + validate_dmp_id(dmp_id: dynamo_rec['dmp_id']) unless seeding + # Validate that the :PK matches the :dmp_id + pk = Uc3DmpId::Helper.dmp_id_to_pk(json: dynamo_rec['dmp_id']) + expect(dynamo_rec['PK']).to eql(pk) + expect(dynamo_rec['SK']).to eql(Uc3DmpId::Helper::DMP_LATEST_VERSION) + expect(dynamo_rec['dmphub_provenance_id']).to eql(owner['PK']) + expected = original.fetch('dmp', {}).fetch('dmp_id', {})['identifier']&.gsub(%r{https?://}, "#{owner['name']}#") + expect(dynamo_rec['dmphub_provenance_identifier']).to eql(expected) + mod_date = Time.parse(dynamo_rec['modified']) + expect(dynamo_rec['dmphub_modification_day']).to eql(mod_date.strftime('%Y-%m-%d')) + expected = original.fetch('dmp', {}).fetch('contact', {}).fetch('contact_id', {})['identifier'] + expect(dynamo_rec['dmphub_owner_id']).to eql(expected) + expected = original.fetch('dmp', {}).fetch('contact', {}).fetch('dmproadmap_affiliation', {}) + .fetch('affiliation_id', {})['identifier'] + expect(dynamo_rec['dmphub_owner_org']).to eql(expected) + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + # rubocop:disable Metrics/AbcSize + def test_attachment(url:, prior_dmp:, dmp:, dynamo_rec:) + expected = JSON.parse({ + work_type: 'output_management_plan', descriptor: 'is_metadata_for', type: 'url', identifier: url + }.to_json) + expect(dmp['dmp']['dmproadmap_related_identifiers'].any? { |id| id == expected }).to be(true) + # Expect the timestamps NOT to change + expect(dmp['dmp']['created']).to eql(prior_dmp['dmp']['created']) + expect(dmp['dmp']['modified']).to eql(prior_dmp['dmp']['modified']) + expect(dmp['dmp']['dmphub_versions'].nil?).to be(true) + expect(dmp['dmp']['dmphub_modifications'].nil?).to be(true) + expected = Time.parse(prior_dmp['dmp']['modified']).strftime('%Y-%m-%d') + expect(dynamo_rec['dmphub_modification_day']).to eql(expected) + end + # rubocop:enable Metrics/AbcSize + + # Validate the JSON that was returned + # rubocop:disable Metrics/AbcSize + def updated_dmp_id_tests(prior_dmp:, dmp:, dynamo_rec:, expect_versioned: false) + # The returned record should be wrapped in a top level :dmp + expect(dmp['dmp'].nil?).to be(false) + # Validate the structure of the DMP ID + validate_dmp_id(dmp_id: dmp['dmp']['dmp_id']) + # Replaced the incoming :created and :modified timestamps + expect(dmp['dmp']['created'] == prior_dmp['dmp']['created']).to be(true) + + if expect_versioned + expected = Time.parse(dmp['dmp']['modified']).strftime('%Y-%m-%d') + expect(dmp['dmp']['modified'] > prior_dmp['dmp']['modified']).to be(true) + expect(dynamo_rec['dmphub_modification_day']).to eql(expected) + expect(dmp['dmp']['dmphub_versions'].first['timestamp']).to eql(prior_dmp['dmp']['modified']) + expect(dmp['dmp']['dmphub_versions'].last['timestamp']).to eql(dmp['dmp']['modified']) + first_url = dmp['dmp']['dmphub_versions'].first['url'] + expect(first_url.end_with?("?version=#{prior_dmp['dmp']['modified']}")).to be(true) + expect(dmp['dmp']['dmphub_versions'].last['url'].include?('?version=')).to be(false) + else + same_hour = (Time.now.utc - Time.parse(dmp['dmp']['modified'])).round <= 3600 + expected = Time.parse(prior_dmp['dmp']['modified']).strftime('%Y-%m-%d') + expect(dmp['dmp']['modified'] >= prior_dmp['dmp']['modified'] && same_hour).to be(true) + expect(dynamo_rec['dmphub_modification_day']).to eql(expected) + end + end + # rubocop:enable Metrics/AbcSize + + # rubocop:disable Metrics/AbcSize + def external_mod_tests(prior_dmp:, dmp:, dynamo_rec:) + # Expect a new version to have been created + expect(dmp['dmp']['dmphub_versions'].length).to be(3) + expect(dmp['dmp']['modified'] > prior_dmp['dmp']['modified']).to be(true) + expected = Time.parse(dmp['dmp']['modified']).strftime('%Y-%m-%d') + expect(dynamo_rec['dmphub_modification_day']).to eql(expected) + expect(dmp['dmp']['dmphub_versions'].first['timestamp'] < prior_dmp['dmp']['modified']).to be(true) + expect(dmp['dmp']['dmphub_versions'][1]['timestamp']).to eql(prior_dmp['dmp']['modified']) + expect(dmp['dmp']['dmphub_versions'].last['timestamp']).to eql(dmp['dmp']['modified']) + expect(dmp['dmp']['dmphub_versions'].first['url'].end_with?("?version=#{prior_dmp['dmp']['created']}")).to be(true) + expect(dmp['dmp']['dmphub_versions'].last['url'].include?('?version=')).to be(false) + + # Expect the description to not have been changed because it is not allowed! + # We currently only allow :grant_id and :dmproadmap_related_identifiers to be changed by external systems + expect(dmp['dmp']['description']).to eql(prior_dmp['dmp']['description']) + # Expect the original :grant id and original :dmproadmap_related_identifiers to be unchanged + expected = prior_dmp['dmp']['project'].first['funding'].first['grant_id'] + expect(dmp['dmp']['project'].first['funding'].first['grant_id']).to eql(expected) + expect(dmp['dmp']['dmproadmap_related_identifiers']).to eql(prior_dmp['dmp']['dmproadmap_related_identifiers']) + # Expect the changes to have been added to the :dmphub_modifications Array + end + # rubocop:enable Metrics/AbcSize + + # rubocop:disable Metrics/AbcSize + def validate_dmp_id(dmp_id:) + pk = dmp_id['identifier'] + expect(pk.start_with?(ENV.fetch('DMP_ID_BASE_URL', nil))).to be(true) + suffix = pk.gsub(ENV.fetch('DMP_ID_BASE_URL', nil), '') + expect(suffix =~ Uc3DmpId::Helper::DOI_REGEX).to be(1) + expect(suffix.start_with?("/#{ENV.fetch('DMP_ID_SHOULDER', nil)}")).to be(true) + end + # rubocop:enable Metrics/AbcSize + + def mocked_dmp(pkey:) + record = mock_dmp + scrubbable_keys = record['dmp'].keys.select { |key| %w[PK SK].include?(key) || key.start_with?('dmphub_') } + scrubbable_keys.each { |key| record['dmp'].delete(key) } + record['dmp']['dmp_id'] = Uc3DmpId::Helper.pk_to_dmp_id(p_key: pkey) + record + end +end +# rubocop:enable RSpec/DescribeClass diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/updater_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/updater_spec.rb index a2007ece..e04fe002 100644 --- a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/updater_spec.rb +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/updater_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' +# rubocop:disable RSpec/MultipleMemoizedHelpers RSpec.describe 'Uc3DmpId::Updater' do let!(:described_class) { Uc3DmpId::Updater } let!(:updater_error) { Uc3DmpId::UpdaterError } @@ -9,15 +10,15 @@ let!(:client) { mock_uc3_dmp_dynamo(dmp: dmp) } let!(:publisher) { mock_uc3_dmp_event_bridge } - let!(:owner) { JSON.parse({ PK: 'PROVENANCE#foo', SK: 'PROFILE' }.to_json) } - let!(:updater) { JSON.parse({ PK: 'PROVENANCE#bar', SK: 'PROFILE' }.to_json) } + let!(:owner) { JSON.parse({ PK: 'PROVENANCE#foo', SK: 'PROFILE', name: 'foo' }.to_json) } + let!(:updater) { JSON.parse({ PK: 'PROVENANCE#bar', SK: 'PROFILE', name: 'bar' }.to_json) } let!(:p_key) { "#{Uc3DmpId::Helper::PK_DMP_PREFIX}#{mock_dmp_id}" } let!(:dmp) do record = mock_dmp record['dmp']['PK'] = p_key - record['dmp']['SK'] = "#{Uc3DmpId::Helper::DMP_LATEST_VERSION}" + record['dmp']['SK'] = Uc3DmpId::Helper::DMP_LATEST_VERSION record['dmp']['dmphub_provenance_id'] = owner['PK'] record end @@ -30,7 +31,8 @@ let!(:mods) do record = mock_dmp transferable_keys.each { |key| record['dmp'].delete(key) } - record['dmp']['dmp_id'] = JSON.parse({ type: 'doi', identifier: Uc3DmpId::Helper.pk_to_dmp_id(p_key: p_key) }.to_json) + record['dmp']['dmp_id'] = + JSON.parse({ type: 'doi', identifier: Uc3DmpId::Helper.pk_to_dmp_id(p_key: p_key) }.to_json) record['dmp']['description'] = 'Lorem ipsum ... TESTING' record end @@ -43,17 +45,20 @@ it 'raises an UpdaterError when :p_key is not a String' do expect { described_class.update(provenance: owner, p_key: 123, json: mods) }.to raise_error(updater_error) end + it 'raises an UpdaterError when :updateable? returns errors' do allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) allow(described_class).to receive(:_updateable?).and_return(['foo']) expect { described_class.update(provenance: owner, p_key: p_key, json: mods) }.to raise_error(updater_error) end + it 'raises an UpdaterError (no changes) when :json is equal to the existing DMP ID' do allow(described_class).to receive(:updateable?).and_return([]) allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) allow(Uc3DmpId::Helper).to receive(:eql?).and_return(true) expect { described_class.update(provenance: owner, p_key: p_key, json: dmp) }.to raise_error(updater_error) end + it 'raises an UpdaterError when Versioner.generate_version returns a nil' do allow(described_class).to receive(:updateable?).and_return([]) allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) @@ -61,6 +66,8 @@ allow(Uc3DmpId::Versioner).to receive(:generate_version).and_return(nil) expect { described_class.update(provenance: owner, p_key: p_key, json: mods) }.to raise_error(updater_error) end + + # rubocop:disable RSpec/ExampleLength it 'saves the changes as the new latest version' do allow(described_class).to receive(:updateable?).and_return([]) allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp['dmp']) @@ -72,40 +79,54 @@ now = Time.now.utc.iso8601 result = described_class.update(provenance: owner, p_key: p_key, json: mods) - expect(result['dmp']['dmphub_versions']).to be(nil) + expect(result['dmp']['dmphub_versions']).to be_nil expect(result['dmp']['modified'] >= now).to be(true) expect(described_class).to have_received(:_process_modifications).once allow(client).to receive(:put_item).once expect(described_class).to have_received(:_post_process).once end + # rubocop:enable RSpec/ExampleLength end describe 'attach_narrative(provenance:, p_key:, url:, logger: nil)' do - let!(:url) { 'http://download.me/narrative.pdf '} + let!(:url) { 'http://download.me/narrative.pdf ' } it 'raises an UpdaterError when :p_key is not a String' do expect { described_class.attach_narrative(provenance: owner, p_key: 123, url: url) }.to raise_error(updater_error) end + it 'raises an UpdaterError when :provenance is not a Hash' do - expect { described_class.attach_narrative(provenance: owner['PK'], p_key: p_key, url: url) }.to raise_error(updater_error) + expect do + described_class.attach_narrative(provenance: owner['PK'], p_key: p_key, url: url) + end.to raise_error(updater_error) end + it 'raises an UpdaterError when :provenance does not contain a :PK' do owner.delete('PK') - expect { described_class.attach_narrative(provenance: owner, p_key: p_key, url: url) }.to raise_error(updater_error) + expect do + described_class.attach_narrative(provenance: owner, p_key: p_key, url: url) + end.to raise_error(updater_error) end + it 'raises an UpdaterError when :provenance does not match the :dmphub_provenance_id of the DMP ID' do - allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp['dmp']) - expect { described_class.attach_narrative(provenance: updater, p_key: p_key, url: url) }.to raise_error(updater_error) + allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) + expect do + described_class.attach_narrative(provenance: updater, p_key: p_key, url: url) + end.to raise_error(updater_error) end + it 'raises an UpdaterError if the Uc3DmpDynamo::Client is unable to save the change' do - allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp['dmp']) + allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) allow(Uc3DmpId::Helper).to receive(:annotate_dmp_json).and_return(dmp['dmp']) allow(client).to receive(:put_item).and_return(nil) - expect { described_class.attach_narrative(provenance: owner, p_key: p_key, url: url) }.to raise_error(updater_error) + expect do + described_class.attach_narrative(provenance: owner, p_key: p_key, url: url) + end.to raise_error(updater_error) end + it 'adds the uploaded PDF\'s access :url to the DMP ID\'s :dmproadmap_related_identifiers' do - allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp['dmp']) + allow(Uc3DmpId::Finder).to receive(:by_pk).and_return(dmp) allow(Uc3DmpId::Helper).to receive(:annotate_dmp_json).and_return(dmp['dmp']) allow(client).to receive(:put_item).and_return(dmp['dmp']) described_class.attach_narrative(provenance: owner, p_key: p_key, url: url) @@ -120,17 +141,19 @@ describe '_updateable?(provenance:, p_key:, latest_version: {}, mods: {})' do it 'returns validation errors if the Validator.validate failed' do - allow(Uc3DmpId::Validator).to receive(:validate).and_return(['foo', 'bar']) + allow(Uc3DmpId::Validator).to receive(:validate).and_return(%w[foo bar]) result = described_class.send(:_updateable?, provenance: updater, p_key: p_key, latest_version: dmp['dmp'], mods: mods['dmp']) expect(result).to eql('foo, bar') end + it 'returns a Forbidden message if the :provenance is not a Hash' do allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) result = described_class.send(:_updateable?, provenance: updater['PK'], p_key: p_key, latest_version: dmp['dmp'], mods: mods['dmp']) expect(result).to eql([Uc3DmpId::Helper::MSG_DMP_FORBIDDEN]) end + it 'returns a Forbidden message if the :provenance does not have a :PK' do allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) updater.delete('PK') @@ -138,6 +161,7 @@ mods: mods['dmp']) expect(result).to eql([Uc3DmpId::Helper::MSG_DMP_FORBIDDEN]) end + it 'returns a Forbidden message if the mod\'s :dmp_id does not match the :p_key' do allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) mods['dmp'].delete('dmp_id') @@ -145,11 +169,14 @@ mods: mods['dmp']) expect(result).to eql([Uc3DmpId::Helper::MSG_DMP_FORBIDDEN]) end + it 'returns a Not Found message if the :latest_version is not a Hash' do allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) - result = described_class.send(:_updateable?, provenance: updater, p_key: p_key, latest_version: 123, mods: mods['dmp']) + result = described_class.send(:_updateable?, provenance: updater, p_key: p_key, latest_version: 123, + mods: mods['dmp']) expect(result).to eql([Uc3DmpId::Helper::MSG_DMP_UNKNOWN]) end + it 'returns a Not Found message if the :latest_version\'s :PK does not match the :p_key' do dmp['dmp']['PK'] = "#{Uc3DmpId::Helper::PK_DMP_PREFIX}testing9876" allow(Uc3DmpId::Validator).to receive(:validate).and_return([]) @@ -164,31 +191,36 @@ result = described_class.send(:_process_modifications, owner: owner, updater: updater, version: dmp, mods: 123) expect(result).to eql(dmp) end + it 'returns :version if :updater is nil' do result = described_class.send(:_process_modifications, owner: owner, updater: nil, version: dmp, mods: mods) expect(result).to eql(dmp) end + it 'returns :mods if :version is not a Hash' do result = described_class.send(:_process_modifications, owner: owner, updater: updater, version: 123, mods: mods) expect(result).to eql(mods) end + it 'returns :mods if :owner is nil' do result = described_class.send(:_process_modifications, owner: nil, updater: updater, version: dmp, mods: mods) expect(result).to eql(mods) end + it 'calls Asserter.splice if the updater is the owner' do allow(described_class).to receive(:_merge_versions) allow(Uc3DmpId::Asserter).to receive(:splice) allow(Uc3DmpId::Asserter).to receive(:add) - result = described_class.send(:_process_modifications, owner: owner, updater: owner, version: dmp, mods: mods) + described_class.send(:_process_modifications, owner: owner, updater: owner, version: dmp, mods: mods) expect(Uc3DmpId::Asserter).to have_received(:splice).once expect(Uc3DmpId::Asserter).not_to have_received(:add) end + it 'calls Asserter.add if the updater is NOT the owner' do allow(described_class).to receive(:_merge_versions) allow(Uc3DmpId::Asserter).to receive(:splice) allow(Uc3DmpId::Asserter).to receive(:add) - result = described_class.send(:_process_modifications, owner: owner, updater: updater, version: dmp, mods: mods) + described_class.send(:_process_modifications, owner: owner, updater: updater, version: dmp, mods: mods) expect(Uc3DmpId::Asserter).not_to have_received(:splice) expect(Uc3DmpId::Asserter).to have_received(:add).once end @@ -198,10 +230,12 @@ it 'returns the :mods as-is if :latest_version if not a Hash' do expect(described_class.send(:_merge_versions, latest_version: 123, mods: mods)).to eql(mods) end + it 'returns the :mods as-is if :latest_version does not have a :PK and :SK' do dmp['dmp'].delete('PK') expect(described_class.send(:_merge_versions, latest_version: dmp['dmp'], mods: mods)).to eql(mods) end + it 'adds the attributes from the :latest_version to the :mods' do expected = mods transferable_keys.each { |key| expected[key] = dmp['dmp'][key] } @@ -213,21 +247,26 @@ it 'returns false unless :json is a Hash' do expect(described_class.send(:_post_process, provenance: owner, json: 123)).to be(false) end + it 'returns false unless :json contains :dmphub_provenance_id' do dmp['dmp'].delete('dmphub_provenance_id') expect(described_class.send(:_post_process, provenance: owner, json: dmp['dmp'])).to be(false) end + it 'returns false unless :provenance is a Hash' do expect(described_class.send(:_post_process, provenance: 123, json: dmp['dmp'])).to be(false) end + it 'returns false unless :provenance contains :PK' do owner.delete('PK') expect(described_class.send(:_post_process, provenance: owner, json: dmp['dmp'])).to be(false) end - it 'does not publish an `EZID update` event to EventBridge if the owner of the DMP ID is NOT the one making the update' do + + it 'doesn\'t publish an `EZID update` event to EventBridge if the owner of the DMP ID is NOT the updater' do described_class.send(:_post_process, provenance: updater, json: dmp['dmp']) expect(publisher).not_to have_received(:publish) end + it 'publishes an `EZID update` event to EventBridge if the owner of the DMP ID is the one making the update' do described_class.send(:_post_process, provenance: owner, json: dmp['dmp']) expected = { @@ -238,23 +277,26 @@ } expect(publisher).to have_received(:publish).once.with(expected) end + it 'does not send a `Citation Fetch` event if the owner of the DMP ID is NOT the one making the update' do allow(Uc3DmpId::Helper).to receive(:citable_related_identifiers).and_return([]) described_class.send(:_post_process, provenance: updater, json: dmp['dmp']) expect(publisher).not_to have_received(:publish) end + it 'does not publish a `Citation Fetch` event to EventBridge if there are no citable identifiers' do allow(Uc3DmpId::Helper).to receive(:citable_related_identifiers).and_return([]) described_class.send(:_post_process, provenance: owner, json: dmp['dmp']) expect(publisher).to have_received(:publish).once end + it 'publishes an `Citation Fetch` event to EventBridge if there are citable identifiers' do ids = JSON.parse([{ work_type: 'dataset', descriptor: 'references', type: 'other', identifier: 'foo' }].to_json) allow(Uc3DmpId::Helper).to receive(:citable_related_identifiers).and_return(ids) described_class.send(:_post_process, provenance: owner, json: dmp['dmp']) expected = { dmp: dmp['dmp'], - detail: { PK: dmp['dmp']['PK'], SK: dmp['dmp']['SK'], dmproadmap_related_identifiers: ids}, + detail: { PK: dmp['dmp']['PK'], SK: dmp['dmp']['SK'], dmproadmap_related_identifiers: ids }, source: 'DmpUpdater', event_type: 'Citation Fetch', logger: nil @@ -263,3 +305,4 @@ end end end +# rubocop:enable RSpec/MultipleMemoizedHelpers diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/validator_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/validator_spec.rb index ab6970cb..2f7b6f96 100644 --- a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/validator_spec.rb +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/validator_spec.rb @@ -114,7 +114,6 @@ # The complete JSON should pass for all modes Uc3DmpId::Validator::VALIDATION_MODES.each do |mode| it "is valid for mode #{mode}" do - response = described_class.validate(mode: 'author', json: json) expect(described_class.validate(mode: 'author', json: json)).to eql([]) end end diff --git a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/versioner_spec.rb b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/versioner_spec.rb index 88a03270..6ba3cf31 100644 --- a/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/versioner_spec.rb +++ b/src/sam/gems/uc3-dmp-id/spec/uc3-dmp-id/versioner_spec.rb @@ -18,6 +18,7 @@ it 'returns an empty array if p_key is not a valid String' do expect(described_class.get_versions(p_key: 123)).to eql([]) end + it 'fetches the :modified dates for each Dyanmo table entry for the PK' do expect(described_class.get_versions(p_key: p_key)).to eql([mock_dmp]) end @@ -25,32 +26,41 @@ describe 'generate_version(client:, latest_version:, owner:, updater:, logger: nil)' do let!(:client) { mock_uc3_dmp_dynamo } - let!(:owner) { "PROVENANCE#foo" } + let!(:owner) { 'PROVENANCE#foo' } it 'returns the :latest_version as-is if it could not determine the modified time' do dmp = JSON.parse({ modified: '123A' }.to_json) - expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, updater: owner)).to eql(dmp) + expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, + updater: owner)).to eql(dmp) end + it 'returns the :latest_version as-is if the owner of the DMP ID does not match the updater' do - dmp = JSON.parse({ modified: (Time.now - 7200).utc.iso8601 }.to_json) - updater = "PROVENANCE#bar" - expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, updater: updater)).to eql(dmp) + dmp = JSON.parse({ modified: (Time.now.utc - 7200).utc.iso8601 }.to_json) + updater = 'PROVENANCE#bar' + expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, + updater: updater)).to eql(dmp) end + it 'returns the :latest_version as-is if the modified time of the latest version is within the past hour' do dmp = JSON.parse({ modified: Time.now.utc.iso8601 }.to_json) - expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, updater: owner)).to eql(dmp) + expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, + updater: owner)).to eql(dmp) end + it 'returns nil if it was unable to generate a version snapshot' do allow(client).to receive(:put_item).and_return(nil) - dmp = JSON.parse({ modified: (Time.now - 72000).utc.iso8601 }.to_json) - expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, updater: owner)).to be(nil) + dmp = JSON.parse({ modified: (Time.now.utc - 72_000).utc.iso8601 }.to_json) + expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, + updater: owner)).to be_nil end + it 'generates the version snapshot and returns the :latest_version' do - tstamp = (Time.now - 72000).utc.iso8601 + tstamp = (Time.now.utc - 72_000).utc.iso8601 dmp = JSON.parse({ modified: tstamp }.to_json) version = JSON.parse({ SK: "#{Uc3DmpId::Helper::SK_DMP_PREFIX}#{tstamp}", modified: tstamp }.to_json) - expect(client).to receive(:put_item).with(json: version, logger: nil).once - expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, updater: owner)).to eql(dmp) + expect(described_class.generate_version(client: client, latest_version: dmp, owner: owner, + updater: owner)).to eql(dmp) + expect(client).to have_received(:put_item).with(json: version, logger: nil).once end end @@ -63,32 +73,41 @@ it 'returns the :json as-is if the :p_key is not a valid String' do expect(described_class.append_versions(p_key: 123, dmp: dmp)).to eql(dmp) end + it 'returns the :json as-is if :json is not a Hash' do - expect(described_class.append_versions(p_key: p_key, dmp: '123')).to eql(123) + expect(described_class.append_versions(p_key: p_key, dmp: '123')).to be(123) end + it 'returns the :json as-is if :json does not have a top level :dmp' do json = JSON.parse({ title: 'Foo' }.to_json) result = described_class.append_versions(p_key: p_key, dmp: json) expect(assert_dmps_match(obj_a: result, obj_b: json, debug: false)).to be(true) end + it 'does NOT append the :dmphub_versions Array if there is only one version' do allow(described_class).to receive(:get_versions).and_return([first_version]) result = described_class.append_versions(p_key: p_key, dmp: dmp) expect(assert_dmps_match(obj_a: result, obj_b: dmp, debug: false)).to be(true) end + + # rubocop:disable RSpec/ExampleLength it 'appends the :dmphub_versions Array if there are multiple versions of the DMP ID' do allow(described_class).to receive(:get_versions).and_return([first_version, last_version]) result = described_class.append_versions(p_key: p_key, dmp: dmp) + pk = Uc3DmpId::Helper.remove_pk_prefix(p_key: p_key) expected = JSON.parse({ dmp: { title: 'Foo bar', dmphub_versions: [ - { timestamp: first_version['modified'], url: "#{landing_page_url}#{p_key}?version=#{first_version['modified']}" }, - { timestamp: last_version['modified'], url: "#{landing_page_url}#{p_key}?version=#{last_version['modified']}" } + { timestamp: first_version['modified'], + url: "#{landing_page_url}#{pk}?version=#{first_version['modified']}" }, + { timestamp: last_version['modified'], + url: "#{landing_page_url}#{pk}?version=#{last_version['modified']}" } ] } }.to_json) expect(assert_dmps_match(obj_a: result, obj_b: expected, debug: false)).to be(true) end + # rubocop:enable RSpec/ExampleLength end end diff --git a/src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.0.140.gem b/src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.0.140.gem deleted file mode 100644 index 60bc7532b992bb11d5a6f2e89af478fa8c79542e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31232 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 2.6') @@ -25,11 +25,5 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency('uc3-dmp-dynamo', '~> 0.0') spec.add_runtime_dependency('uc3-dmp-event-bridge', '~> 0.0') - # Requirements for running RSpec - spec.add_development_dependency('byebug', '11.1.3') - spec.add_development_dependency('rspec', '3.9.0') - spec.add_development_dependency('rubocop', '1.50.2') - spec.add_development_dependency('rubocop-rspec', '2.20.0') - - spec.metadata['rubygems_mfa_required'] = 'false' + spec.metadata['rubygems_mfa_required'] = 'true' end From cea06f79bff7f6d67a4f3645aa5d6973d6bc4d99 Mon Sep 17 00:00:00 2001 From: briri Date: Thu, 24 Aug 2023 16:30:24 -0700 Subject: [PATCH 12/13] issued the official v0.1 --- src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.1.0.gem | Bin 0 -> 31744 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.1.0.gem diff --git a/src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.1.0.gem b/src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.1.0.gem new file mode 100644 index 0000000000000000000000000000000000000000..966d5b9382bf52f7447f8fcb9ba97e306de880ce GIT binary patch literal 31744 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 literal 0 HcmV?d00001 From ebdeaeb7f468b63ae5826b4455bfadbb80eef248 Mon Sep 17 00:00:00 2001 From: briri Date: Fri, 25 Aug 2023 05:35:04 -0700 Subject: [PATCH 13/13] updated uc3-dmp-id gem readme. fixed issue with landing page not displaying the download PDF link --- src/landing_page/src/pages/landing/landing.js | 4 ++-- src/sam/gems/uc3-dmp-id/README.md | 9 +++++++++ src/sam/gems/uc3-dmp-id/uc3-dmp-id-0.1.0.gem | Bin 31744 -> 31744 bytes src/sam/gems/uc3-dmp-id/uc3-dmp-id.gemspec | 2 +- src/sam/layers/Gemfile.lock | 14 +++++++------- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/landing_page/src/pages/landing/landing.js b/src/landing_page/src/pages/landing/landing.js index 35ed19eb..e6870175 100644 --- a/src/landing_page/src/pages/landing/landing.js +++ b/src/landing_page/src/pages/landing/landing.js @@ -85,7 +85,7 @@ function Landing() { contact: getValue(dmp, "contact", {}), contributors: getValue(dmp, "contributor", []), datasets: getValue(dmp, "dataset", []), - related_identifiers: filterWorks(getValue(dmp, "dmproadmap_related_identifiers", [])), + related_identifiers: getValue(dmp, "dmproadmap_related_identifiers", []), versions: getValue(dmp, "dmphub_versions", []), }); } else { @@ -190,7 +190,7 @@ function Landing() { } {(formData.related_identifiers && formData.related_identifiers.length > 0) && - + }