Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a way to do virtual merge through an API
- Loading branch information
Showing
10 changed files
with
290 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
# frozen_string_literal: true | ||
|
||
# Adds a constituent relationship between a parent work and child works | ||
# by taking the followin actions: | ||
# 1. altering the contentMD of the parent | ||
# 2. add isConstituentOf assertions to the RELS-EXT of the children | ||
# 3. saving the parent and the children | ||
class ConstituentService | ||
# @param [String] parent_druid the identifier of the parent object | ||
def initialize(parent_druid:) | ||
@parent_druid = parent_druid | ||
end | ||
|
||
# @param [Array<String>] child_druids the identifiers of the child objects | ||
def add(child_druids:) | ||
ResetContentMetadataService.new(druid: parent_druid).reset | ||
|
||
child_druids.each do |child_druid| | ||
add_constituent(child_druid: child_druid) | ||
end | ||
parent.save! | ||
end | ||
|
||
private | ||
|
||
attr_reader :parent_druid | ||
|
||
def add_constituent(child_druid:) | ||
child = DROQueryService.find_modifiable_work(child_druid) | ||
child.contentMetadata.ng_xml.search('//resource').each do |resource| | ||
parent.contentMetadata.add_virtual_resource(child.id, resource) | ||
end | ||
child.add_relationship :is_constituent_of, parent | ||
child.save! | ||
end | ||
|
||
def parent | ||
@parent ||= DROQueryService.find_modifiable_work(parent_druid) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# frozen_string_literal: true | ||
|
||
# Responsible for retrieving information based on the given work (Dor::Item). | ||
class DROQueryService | ||
# @param [String] id - The id of the work | ||
# @param [#exists?, #find] work_relation - How we will query some of the related information | ||
def initialize(id:, work_relation: default_work_relation) | ||
@id = id | ||
@work_relation = work_relation | ||
end | ||
|
||
delegate :allows_modification?, to: :work | ||
|
||
# @raises [RuntimeError] if the item is not modifiable | ||
def self.find_modifiable_work(druid) | ||
query_service = DROQueryService.new(id: druid) | ||
query_service.work do |work| | ||
raise "Item #{work.pid} is not open for modification" unless query_service.allows_modification? | ||
end | ||
end | ||
|
||
def work(&block) | ||
@work ||= work_relation.find(id) | ||
return @work unless block_given? | ||
|
||
@work.tap(&block) | ||
end | ||
|
||
private | ||
|
||
attr_reader :id, :work_relation | ||
|
||
def default_work_relation | ||
Dor::Item | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# frozen_string_literal: true | ||
|
||
# Clears the contentMetadata datastream to the default, wiping out any members. | ||
class ResetContentMetadataService | ||
def initialize(druid:, type: 'image') | ||
@druid = druid | ||
@type = type | ||
end | ||
|
||
def reset | ||
work = DROQueryService.find_modifiable_work(@druid) | ||
work.contentMetadata.content = "<contentMetadata objectId='#{work.id}' type='#{@type}'/>" | ||
work.save! | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rails_helper' | ||
|
||
RSpec.describe 'Virtual merge of objects' do | ||
let(:payload) { { sub: 'argo' } } | ||
let(:jwt) { JWT.encode(payload, Settings.dor.hmac_secret, 'HS256') } | ||
let(:parent_id) { 'druid:mk420bs7601' } | ||
let(:child1_id) { 'druid:child1' } | ||
let(:child2_id) { 'druid:child2' } | ||
|
||
let(:object) { Dor::Item.new(pid: parent_id) } | ||
let(:service) { instance_double(ConstituentService, add: true) } | ||
|
||
before do | ||
allow(Dor).to receive(:find).and_return(object) | ||
allow(ConstituentService).to receive(:new).with(parent_druid: parent_id).and_return(service) | ||
end | ||
|
||
context 'when constituent_ids is provided' do | ||
it 'merges the objects' do | ||
put "/v1/objects/#{parent_id}", | ||
params: { constituent_ids: [child1_id, child2_id] }, | ||
headers: { 'X-Auth' => "Bearer #{jwt}" } | ||
expect(service).to have_received(:add).with(child_druids: [child1_id, child2_id]) | ||
expect(response).to be_successful | ||
end | ||
end | ||
|
||
context 'when constituent_ids is not provided' do | ||
it 'renders an error' do | ||
put "/v1/objects/#{parent_id}", | ||
params: { title: 'New name' }, | ||
headers: { 'X-Auth' => "Bearer #{jwt}" } | ||
expect(service).not_to have_received(:add) | ||
expect(response).to be_bad_request | ||
json = JSON.parse(response.body) | ||
expect(json['errors'][0]['detail']).to eq 'param is missing or the value is empty: constituent_ids' | ||
end | ||
end | ||
|
||
context 'when constituent_ids is not an array' do | ||
it 'renders an error' do | ||
put "/v1/objects/#{parent_id}", | ||
params: { constituent_ids: child1_id }, | ||
headers: { 'X-Auth' => "Bearer #{jwt}" } | ||
expect(service).not_to have_received(:add) | ||
expect(response).to be_bad_request | ||
json = JSON.parse(response.body) | ||
expect(json['errors'][0]['detail']).to eq 'param is missing or the value is empty: constituent_ids must be an array' | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'rails_helper' | ||
|
||
RSpec.describe ConstituentService do | ||
let(:parent) do | ||
Dor::Item.new.tap do |item| | ||
item.contentMetadata.content = <<~XML | ||
<contentMetadata></contentMetadata> | ||
XML | ||
end | ||
end | ||
|
||
let(:child1) do | ||
Dor::Item.new.tap do |item| | ||
item.contentMetadata.content = <<~XML | ||
<contentMetadata> | ||
<resource id="bb000kg4251_1" sequence="1" type="image"> | ||
<file id="bb000kg4251.jpg" mimetype="image/jpeg" size="1347965" preserve="yes" publish="no" shelve="no"> | ||
</file> | ||
</resource> | ||
</contentMetadata> | ||
XML | ||
end | ||
end | ||
|
||
let(:child2) do | ||
Dor::Item.new.tap do |item| | ||
item.contentMetadata.content = <<~XML | ||
<contentMetadata> | ||
<resource id="bb000ff1111_1" sequence="1" type="image"> | ||
<file id="bb000ff1111.jpg" mimetype="image/jpeg" size="999" preserve="yes" publish="no" shelve="no"> | ||
</file> | ||
</resource> | ||
</contentMetadata> | ||
XML | ||
end | ||
end | ||
|
||
describe '#add' do | ||
subject(:add) { instance.add(child_druids: [child1.id, child2.id]) } | ||
|
||
let(:instance) do | ||
described_class.new(parent_druid: parent.id) | ||
end | ||
let(:namespaceless) { parent.id.sub('druid:', '') } | ||
let(:client) { instance_double(Dor::Services::Client::Object, version: version_client) } | ||
let(:version_client) { instance_double(Dor::Services::Client::ObjectVersion, close: true) } | ||
|
||
before do | ||
allow(parent).to receive_messages(id: 'druid:parent1', save!: true) | ||
allow(child1).to receive_messages(id: 'druid:child1', save!: true) | ||
allow(child2).to receive_messages(id: 'druid:child2', save!: true) | ||
|
||
# Used in ContentMetadataDS#add_virtual_resource | ||
allow(parent.contentMetadata).to receive(:pid).and_return('druid:parent1') | ||
|
||
allow(DROQueryService).to receive(:find_modifiable_work).with('druid:parent1').and_return(parent) | ||
allow(DROQueryService).to receive(:find_modifiable_work).with('druid:child1').and_return(child1) | ||
allow(DROQueryService).to receive(:find_modifiable_work).with('druid:child2').and_return(child2) | ||
|
||
allow(Dor::Services::Client).to receive(:object).and_return(client) | ||
end | ||
|
||
context 'when the parent is open for modification' do | ||
before do | ||
add | ||
end | ||
|
||
it 'merges objects' do | ||
expect(parent.contentMetadata.content).to be_equivalent_to <<~XML | ||
<contentMetadata objectId="druid:parent1" type="image"> | ||
<resource sequence="1" id="#{namespaceless}_1" type="image"> | ||
<relationship type="alsoAvailableAs" objectId="#{child1.id}"/> | ||
</resource> | ||
<resource sequence="2" id="#{namespaceless}_2" type="image"> | ||
<relationship type="alsoAvailableAs" objectId="#{child2.id}"/> | ||
</resource> | ||
</contentMetadata> | ||
XML | ||
expect(child1.object_relations[:is_constituent_of]).to eq [parent] | ||
expect(child2.object_relations[:is_constituent_of]).to eq [parent] | ||
end | ||
end | ||
|
||
context 'when the parent is closed for modification' do | ||
before do | ||
allow(DROQueryService).to receive(:find_modifiable_work).with(parent.id).and_raise('nope') | ||
end | ||
|
||
it 'merges nothing' do | ||
expect { add }.to raise_error RuntimeError | ||
expect(parent.contentMetadata.content).to be_equivalent_to <<~XML | ||
<contentMetadata /> | ||
XML | ||
expect(child1.object_relations[:is_constituent_of]).to be_empty | ||
end | ||
end | ||
|
||
context 'when the child is closed for modification' do | ||
before do | ||
allow(DROQueryService).to receive(:find_modifiable_work).with(child2.id).and_raise('not modifiable') | ||
end | ||
|
||
it 'merges nothing' do | ||
expect { add }.to raise_error RuntimeError | ||
expect(parent.contentMetadata.content).to be_equivalent_to <<~XML | ||
<contentMetadata objectId="druid:parent1" type="image"> | ||
<resource sequence="1" id="parent1_1" type="image"> | ||
<relationship type="alsoAvailableAs" objectId="druid:child1"/> | ||
</resource> | ||
</contentMetadata> | ||
XML | ||
expect(child1.object_relations[:is_constituent_of]).to eq [parent] | ||
expect(child2.object_relations[:is_constituent_of]).to be_empty | ||
end | ||
end | ||
end | ||
end |