uc3-dmp-id 0.0.140 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ require 'time'
6
6
  module Uc3DmpId
7
7
  class UpdaterError < StandardError; end
8
8
 
9
+ # Class that handles updating a DMP ID
9
10
  class Updater
10
11
  class << self
11
12
  # Update a DMP ID
@@ -13,7 +14,7 @@ module Uc3DmpId
13
14
  # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
14
15
  # -------------------------------------------------------------------------
15
16
  def update(provenance:, p_key:, json: {}, note: nil, logger: nil)
16
- raise UpdaterError, MSG_DMP_INVALID_DMP_ID unless p_key.is_a?(String) && !p_key.strip.empty?
17
+ raise UpdaterError, Helper::MSG_DMP_INVALID_DMP_ID unless p_key.is_a?(String) && !p_key.strip.empty?
17
18
 
18
19
  mods = Helper.parse_json(json: json).fetch('dmp', {})
19
20
  p_key = Helper.append_pk_prefix(p_key: p_key)
@@ -26,18 +27,19 @@ module Uc3DmpId
26
27
  logger.debug(message: "Latest version for PK #{p_key}", details: latest_version) if logger.respond_to?(:debug)
27
28
 
28
29
  # Verify that the DMP ID is updateable with the info passed in
29
- errs = _updateable?(provenance: provenance, p_key: p_key, latest_version: latest_version['dmp'], mods: mods['dmp'])
30
- logger.error(message: errs.join(', ')) if errs.is_a?(Array) && errs.any?
30
+ errs = _updateable?(provenance: provenance, p_key: p_key, latest_version: latest_version['dmp'],
31
+ mods: mods['dmp'])
32
+ logger.error(message: errs.join(', ')) if logger.respond_to?(:error) && errs.is_a?(Array) && errs.any?
31
33
  raise UpdaterError, errs if errs.is_a?(Array) && errs.any?
32
34
  # Don't continue if nothing has changed!
33
- raise UpdaterError, MSG_NO_CHANGE if Helper.eql?(dmp_a: latest_version, dmp_b: mods)
35
+ raise UpdaterError, Helper::MSG_NO_CHANGE if Helper.eql?(dmp_a: latest_version, dmp_b: mods)
34
36
 
35
37
  # Version the DMP ID record (if applicable).
36
38
  owner = latest_version['dmphub_provenance_id']
37
39
  updater = provenance['PK']
38
- version = Versioner.generate_version(client: client, latest_version: latest_version, owner: owner, updater: updater,
39
- logger: logger)
40
- raise UpdaterError, MSG_DMP_UNABLE_TO_VERSION if version.nil?
40
+ version = Versioner.generate_version(client: client, latest_version: latest_version, owner: owner,
41
+ updater: updater, logger: logger)
42
+ raise UpdaterError, Helper::MSG_DMP_UNABLE_TO_VERSION if version.nil?
41
43
 
42
44
  # Remove the version info because we don't want to save it on the record
43
45
  version.delete('dmphub_versions')
@@ -45,105 +47,132 @@ module Uc3DmpId
45
47
  # Splice the assertions
46
48
  version = _process_modifications(owner: owner, updater: updater, version: version, mods: mods, note: note,
47
49
  logger: logger)
48
-
49
50
  # Set the :modified timestamps
50
- now = Time.now.utc.iso8601
51
- version['modified'] = now
51
+ now = Time.now.utc
52
+ version['modified'] = now.iso8601
53
+ version['dmphub_modification_day'] = now.strftime('%Y-%m-%d')
52
54
 
53
55
  # Save the changes
54
56
  resp = client.put_item(json: version, logger: logger)
55
- raise UpdaterError, MSG_DMP_UNABLE_TO_VERSION if resp.nil?
57
+ raise UpdaterError, Helper::MSG_DMP_UNABLE_TO_VERSION if resp.nil?
56
58
 
57
59
  # Send the updates to EZID
58
60
  _post_process(provenance: provenance, json: version, logger: logger)
59
61
 
60
62
  # Return the new version record
61
63
  logger.info(message: "Updated DMP ID: #{p_key}") if logger.respond_to?(:debug)
62
- Helper.cleanse_dmp_json(json: JSON.parse({ dmp: version }.to_json))
64
+
65
+ # Append the :dmphub_versions Array
66
+ json = JSON.parse({ dmp: version }.to_json)
67
+ json = Versioner.append_versions(p_key: p_key, dmp: json, client: client, logger: logger)
68
+ Helper.cleanse_dmp_json(json: json)
63
69
  end
64
70
  # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
65
71
  # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
66
72
 
67
73
  # Save a DMP ID's corresponding narrative PDF document to S3 and add the download URL for that
68
74
  # document to the DMP ID's :dmpraodmap_related_identifiers array as an `is_metadata_for` relation
75
+ # rubocop:disable Metrics/AbcSize
69
76
  def attach_narrative(provenance:, p_key:, url:, logger: nil)
70
- raise UpdaterError, MSG_DMP_INVALID_DMP_ID unless p_key.is_a?(String) && !p_key.strip.empty?
77
+ raise UpdaterError, Helper::MSG_DMP_INVALID_DMP_ID unless p_key.is_a?(String) && !p_key.strip.empty?
71
78
 
72
79
  # fetch the existing latest version of the DMP ID
73
80
  client = Uc3DmpDynamo::Client.new(logger: logger)
74
81
  dmp = Finder.by_pk(p_key: p_key, client: client, logger: logger, cleanse: false)
75
- logger.info(message: "Existing latest record", details: dmp) if logger.respond_to?(:debug)
76
- raise UpdaterError, MSG_DMP_FORBIDDEN unless provenance.is_a?(Hash) && !provenance['PK'].nil? &&
77
- provenance['Pk'] == dmp['dmphub_provenance_id']
82
+ logger.info(message: 'Existing latest record', details: dmp) if logger.respond_to?(:debug)
83
+ raise UpdaterError, Helper::MSG_DMP_FORBIDDEN unless provenance.is_a?(Hash) && !provenance['PK'].nil? &&
84
+ provenance['PK'] == dmp['dmp']['dmphub_provenance_id']
78
85
 
79
86
  # Add the download URl for the PDF as a related identifier on the DMP ID record
80
87
  annotated = Helper.annotate_dmp_json(provenance: provenance, p_key: p_key, json: dmp['dmp'])
81
88
  annotated['dmproadmap_related_identifiers'] = [] if annotated['dmproadmap_related_identifiers'].nil?
82
- annotated['dmproadmap_related_identifiers'] << {
89
+ annotated['dmproadmap_related_identifiers'] << JSON.parse({
83
90
  descriptor: 'is_metadata_for', work_type: 'output_management_plan', type: 'url', identifier: url
84
- }
91
+ }.to_json)
85
92
 
86
93
  # Save the changes without creating a new version!
87
94
  resp = client.put_item(json: annotated, logger: logger)
88
- raise UpdaterError, MSG_DMP_UNABLE_TO_VERSION if resp.nil?
95
+ raise UpdaterError, Helper::MSG_DMP_UNABLE_TO_VERSION if resp.nil?
89
96
 
90
97
  logger.info(message: "Added DMP ID narrative for PK: #{p_key}, Narrative: #{url}") if logger.respond_to?(:debug)
91
98
  true
92
99
  end
100
+ # rubocop:enable Metrics/AbcSize
93
101
 
94
102
  private
95
103
 
96
104
  # Check to make sure the incoming JSON is valid, the DMP ID requested matches the DMP ID in the JSON
105
+ # rubocop:disable Metrics/AbcSize
97
106
  def _updateable?(provenance:, p_key:, latest_version: {}, mods: {})
98
107
  # Validate the incoming JSON first
99
108
  errs = Validator.validate(mode: 'author', json: JSON.parse({ dmp: mods }.to_json))
100
109
  return errs.join(', ') if errs.is_a?(Array) && errs.any? && errs.first != Validator::MSG_VALID_JSON
101
110
  # Fail if the provenance is not defined
102
- return [MSG_DMP_FORBIDDEN] unless provenance.is_a?(Hash) && !provenance['PK'].nil?
111
+ return [Helper::MSG_DMP_FORBIDDEN] unless provenance.is_a?(Hash) && !provenance['PK'].nil?
103
112
  # Verify that the JSON is for the same DMP in the PK
104
- return [MSG_DMP_FORBIDDEN] unless Helper.dmp_id_to_pk(json: mods.fetch('dmp_id', {})) == p_key
113
+ return [Helper::MSG_DMP_FORBIDDEN] unless Helper.dmp_id_to_pk(json: mods.fetch('dmp_id', {})) == p_key
105
114
  # Bail out if the DMP ID could not be found or the PKs do not match for some reason
106
- return [MSG_DMP_UNKNOWN] if latest_version.nil? || latest_version.fetch['PK'] != p_key
115
+ return [Helper::MSG_DMP_UNKNOWN] unless latest_version.is_a?(Hash) && latest_version['PK'] == p_key
107
116
  end
117
+ # rubocop:enable Metrics/AbcSize
108
118
 
119
+ # rubocop:disable Metrics/ParameterLists
109
120
  def _process_modifications(owner:, updater:, version:, mods:, note: nil, logger: nil)
110
121
  return version unless mods.is_a?(Hash) && !updater.nil?
111
122
  return mods unless version.is_a?(Hash) && !owner.nil?
112
123
 
113
- # Splice together any assertions that may have been made while the user was editing the DMP ID
114
- updated = Asserter.splice(latest_version: version, modified_version: mods, logger: logger) if owner == updater
124
+ updated = if owner == updater
125
+ # Splice together any assertions that may have been made while the user was editing the DMP ID
126
+ Asserter.splice(latest_version: version, modified_version: mods, logger: logger)
127
+ else
128
+ # Attach the incoming changes as an assertion to the DMP ID since the updater is NOT the owner
129
+ Asserter.add(updater: updater, latest_version: version, modified_version: mods, note: note,
130
+ logger: logger)
131
+ end
115
132
 
116
- # Attach the incoming changes as an assertion to the DMP ID since the updater is NOT the owner
117
- updated = Asserter.add(updater: updater, dmp: version, mods: mods, note: note, logger: logger) if owner != updater
118
-
119
- merge_versions(latest_version: version, mods: updated, logger: logger)
133
+ _merge_versions(latest_version: version, mods: updated, logger: logger)
120
134
  end
135
+ # rubocop:enable Metrics/ParameterLists
121
136
 
122
137
  # We are replacing the latest version with the modifcations but want to retain the PK, SK and any dmphub_ prefixed
123
138
  # entries in the metadata so that we do not lose creation timestamps, provenance ids, etc.
124
- def merge_versions(latest_version:, mods:, logger: nil)
125
- logger.debug(message: 'Modifications before merge.', details: mods)
139
+ # rubocop:disable Metrics/AbcSize
140
+ def _merge_versions(latest_version:, mods:, logger: nil)
141
+ return mods unless latest_version.is_a?(Hash)
142
+
143
+ logger.debug(message: 'Modifications before merge.', details: mods) if logger.respond_to?(:debug)
126
144
  keys_to_retain = latest_version.keys.select do |key|
127
- (key.start_with?('dmphub_') && !%w[dmphub_assertions].include?(key)) ||
145
+ (key.start_with?('dmphub_') && !%w[dmphub_modifications dmphub_versions].include?(key)) ||
128
146
  key.start_with?('PK') || key.start_with?('SK')
129
147
  end
130
148
  keys_to_retain.each do |key|
131
149
  mods[key] = latest_version[key]
132
150
  end
133
- logger.debug(message: 'Modifications after merge.', details: mods)
151
+ logger.debug(message: 'Modifications after merge.', details: mods) if logger.respond_to?(:debug)
134
152
  mods
135
153
  end
154
+ # rubocop:enable Metrics/AbcSize
136
155
 
137
156
  # Once the DMP has been updated, we need to update it's DOI metadata
138
157
  # -------------------------------------------------------------------------
158
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
159
+ # rubocop:disable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
139
160
  def _post_process(provenance:, json:, logger: nil)
140
- return false unless json.is_a?(Hash)
161
+ return false unless json.is_a?(Hash) && provenance.is_a?(Hash) && !provenance['PK'].nil? &&
162
+ !json['dmphub_provenance_id'].nil?
163
+
164
+ publishable = provenance['PK'] == json['dmphub_provenance_id']
165
+ return true unless publishable
166
+
167
+ # TODO: we will want to send and related_identifiers in :dmphub_modifications as well!!!
141
168
 
142
- # Indicate whether or not the updater is the provenance system
143
- json['dmphub_updater_is_provenance'] = provenance['PK'] == json['dmphub_provenance_id']
144
- # Publish the change to the EventBridge
145
169
  publisher = Uc3DmpEventBridge::Publisher.new
146
- publisher.publish(source: 'DmpUpdater', event_type: 'EZID update', dmp: json, logger: logger)
170
+ # Publish the change to the EventBridge if the updater is the owner of the DMP ID
171
+ if publishable && logger.respond_to?(:debug)
172
+ logger.debug(message: 'Sending event for EZID publication',
173
+ details: json)
174
+ end
175
+ publisher.publish(source: 'DmpUpdater', event_type: 'EZID update', dmp: json, logger: logger) if publishable
147
176
 
148
177
  # Determine if there are any related identifiers that we should try to fetch a citation for
149
178
  citable_identifiers = Helper.citable_related_identifiers(dmp: json)
@@ -155,10 +184,16 @@ module Uc3DmpId
155
184
  SK: json['SK'],
156
185
  dmproadmap_related_identifiers: citable_identifiers
157
186
  }
158
- logger.debug(message: "Fetching citations", details: citable_identifiers)
159
- publisher.publish(source: 'DmpUpdater', dmp: json, event_type: 'Citation Fetch', detail: citer_detail, logger: logger)
187
+ if logger.respond_to?(:debug)
188
+ logger.debug(message: 'Sending event to fetch citations',
189
+ details: citable_identifiers)
190
+ end
191
+ publisher.publish(source: 'DmpUpdater', dmp: json, event_type: 'Citation Fetch', detail: citer_detail,
192
+ logger: logger)
160
193
  true
161
194
  end
195
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
196
+ # rubocop:enable Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity
162
197
  end
163
198
  end
164
199
  end
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json-schema'
4
+
3
5
  module Uc3DmpId
4
6
  class Uc3DmpIdValidatorError < StandardError; end
5
7
 
8
+ # Method that compares incoming JSON against our JSON Schema and provides detailed errors
6
9
  class Validator
7
10
  # Valid Validation modes are:
8
11
  # - :author --> system of provenance is attempting to create or update
@@ -18,8 +21,7 @@ module Uc3DmpId
18
21
 
19
22
  class << self
20
23
  # Validate the specified DMP's :json against the schema for the specified :mode
21
- #
22
- # ------------------------------------------------------------------------------------
24
+ # rubocop:disable Metrics/AbcSize
23
25
  def validate(mode:, json:)
24
26
  json = Helper.parse_json(json: json)
25
27
  return [MSG_EMPTY_JSON] if json.nil? || !VALIDATION_MODES.include?(mode)
@@ -36,6 +38,7 @@ module Uc3DmpId
36
38
  rescue JSON::Schema::ValidationError => e
37
39
  ["#{MSG_INVALID_JSON} - #{e.message}"]
38
40
  end
41
+ # rubocop:enable Metrics/AbcSize
39
42
 
40
43
  # ------------------------------------------------------------------------------------
41
44
  # METHODS BELOW ARE ONLY MEANT TO BE INVOKED FROM WITHIN THIS MODULE
@@ -45,10 +48,13 @@ module Uc3DmpId
45
48
  # ------------------------------------------------------------------------------------
46
49
  def _load_schema(mode:)
47
50
  # Instatiate the matching schema
48
- schema = "Uc3DmpId::Schemas::#{mode.to_s.downcase.capitalize}".split('::').inject(Object) { |o,c| o.const_get c }
51
+ schema = "Uc3DmpId::Schemas::#{mode.to_s.downcase.capitalize}".split('::').inject(Object) do |o, c|
52
+ o.const_get c
53
+ end
49
54
  schema.respond_to?(:load) ? schema.load : nil
55
+ rescue NameError
56
+ nil
50
57
  end
51
-
52
58
  end
53
59
  end
54
60
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Uc3DmpId
4
- VERSION = '0.0.140'
4
+ VERSION = '0.1.0'
5
5
  end
@@ -6,11 +6,11 @@ require 'time'
6
6
  module Uc3DmpId
7
7
  class VersionerError < StandardError; end
8
8
 
9
+ # Logic to handle the versioning of DMP IDs and to retrieve the versions for a PK
9
10
  class Versioner
10
11
  SOURCE = 'Uc3DmpId::Versioner'
11
12
 
12
13
  class << self
13
-
14
14
  # Find the DMP ID's versions
15
15
  # -------------------------------------------------------------------------
16
16
  def get_versions(p_key:, client: nil, logger: nil)
@@ -29,11 +29,12 @@ module Uc3DmpId
29
29
 
30
30
  # Generate a snapshot of the current latest version of the DMP ID using the existing :modified as
31
31
  # the new SK.
32
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
32
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength,
33
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
33
34
  def generate_version(client:, latest_version:, owner:, updater:, logger: nil)
34
35
  # Only create a version if the Updater is not the Owner OR the changes have happened on a different day
35
36
  mod_time = Time.parse(latest_version.fetch('modified', Time.now.utc.iso8601))
36
- now = Time.now
37
+ now = Time.now.utc
37
38
  if mod_time.nil? || !(now - mod_time).is_a?(Float)
38
39
  logger.error(message: "#{SOURCE} unable to determine mod time: #{mod_time}") if logger.respond_to?(:debug)
39
40
  return latest_version
@@ -42,9 +43,11 @@ module Uc3DmpId
42
43
  # Only allow a new version if the owner and updater are the same and it has been at least one hour since
43
44
  # the last version was created
44
45
  same_hour = (now - mod_time).round <= 3600
45
- if owner != updater || (owner == updater && same_hour)
46
+ if owner == updater && same_hour
46
47
  logger.debug(message: "#{SOURCE} same owner and updater? #{owner == updater}") if logger.respond_to?(:debug)
47
- logger.debug(message: "#{SOURCE} already updated within the past hour? #{same_hour}") if logger.respond_to?(:debug)
48
+ if logger.respond_to?(:debug)
49
+ logger.debug(message: "#{SOURCE} already updated within the past hour? #{same_hour}")
50
+ end
48
51
  return latest_version
49
52
  end
50
53
 
@@ -52,6 +55,7 @@ module Uc3DmpId
52
55
  # We essentially make a snapshot of the record before making changes
53
56
  prior = Helper.deep_copy_dmp(obj: latest_version)
54
57
  prior['SK'] = "#{Helper::SK_DMP_PREFIX}#{latest_version['modified'] || Time.now.utc.iso8601}"
58
+
55
59
  # Create the prior version record ()
56
60
  client = client.nil? ? Uc3DmpDynamo::Client.new : client
57
61
  resp = client.put_item(json: prior, logger: logger)
@@ -61,7 +65,8 @@ module Uc3DmpId
61
65
  logger.info(message: msg, details: prior) if logger.respond_to?(:debug)
62
66
  latest_version
63
67
  end
64
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
68
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength,
69
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
65
70
 
66
71
  # Build the :dmphub_versions array and attach it to the DMP JSON
67
72
  # rubocop:disable Metrics/AbcSize
@@ -72,11 +77,15 @@ module Uc3DmpId
72
77
  results = get_versions(p_key: p_key, client: client, logger: logger)
73
78
  return json unless results.length > 1
74
79
 
80
+ # TODO: we may want to include milliseconds in the future if we get increased volume so that
81
+ # we don't end up with duplicate URLs if versions are made within the same second
75
82
  versions = results.map do |ver|
76
83
  next if ver['modified'].nil?
84
+
85
+ base_url = "#{Helper.landing_page_url}#{Helper.remove_pk_prefix(p_key: p_key)}"
77
86
  {
78
87
  timestamp: ver['modified'],
79
- url: "#{Helper.landing_page_url}#{Helper.remove_pk_prefix(p_key: p_key)}?version=#{ver['modified']}"
88
+ url: dmp['dmp']['modified'] == ver['modified'] ? base_url : "#{base_url}?version=#{ver['modified']}"
80
89
  }
81
90
  end
82
91
  json['dmp']['dmphub_versions'] = JSON.parse(versions.to_json)
data/lib/uc3-dmp-id.rb CHANGED
@@ -18,17 +18,7 @@ require 'uc3-dmp-id/versioner'
18
18
  require 'uc3-dmp-id/schemas/amend'
19
19
  require 'uc3-dmp-id/schemas/author'
20
20
 
21
+ # Support for working with DMP IDs
21
22
  module Uc3DmpId
22
- MSG_DMP_EXISTS = 'DMP already exists. Try :update instead.'
23
- MSG_DMP_FORBIDDEN = 'You do not have permission.'
24
- MSG_DMP_INVALID_DMP_ID = 'Invalid DMP ID format.'
25
- MSG_DMP_NO_DMP_ID = 'A DMP ID could not be registered at this time.'
26
- MSG_DMP_NO_HISTORICALS = 'You cannot modify a historical version of the DMP.'
27
- MSG_NO_OWNER_ORG = 'Could not determine ownership of the DMP ID.'
28
- MSG_DMP_NO_TOMBSTONE = 'Unable to tombstone the DMP ID at this time.'
29
- MSG_DMP_NO_UPDATE = 'Unable to update the DMP ID at this time.'
30
- MSG_DMP_NOT_FOUND = 'DMP does not exist.'
31
- MSG_DMP_UNABLE_TO_VERSION = 'Unable to version this DMP.'
32
- MSG_DMP_UNKNOWN = 'DMP does not exist. Try :create instead.'
33
23
  end
34
24
  # rubocop:enable Naming/FileName
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uc3-dmp-id
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.140
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian Riley
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-17 00:00:00.000000000 Z
11
+ date: 2023-08-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -66,62 +66,6 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0.0'
69
- - !ruby/object:Gem::Dependency
70
- name: byebug
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - '='
74
- - !ruby/object:Gem::Version
75
- version: 11.1.3
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - '='
81
- - !ruby/object:Gem::Version
82
- version: 11.1.3
83
- - !ruby/object:Gem::Dependency
84
- name: rspec
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - '='
88
- - !ruby/object:Gem::Version
89
- version: 3.9.0
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - '='
95
- - !ruby/object:Gem::Version
96
- version: 3.9.0
97
- - !ruby/object:Gem::Dependency
98
- name: rubocop
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - '='
102
- - !ruby/object:Gem::Version
103
- version: 1.50.2
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - '='
109
- - !ruby/object:Gem::Version
110
- version: 1.50.2
111
- - !ruby/object:Gem::Dependency
112
- name: rubocop-rspec
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - '='
116
- - !ruby/object:Gem::Version
117
- version: 2.20.0
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - '='
123
- - !ruby/object:Gem::Version
124
- version: 2.20.0
125
69
  description: Helpers for working with JSON that represents a DMP ID
126
70
  email:
127
71
  - brian.riley@ucop.edu