This repository has been archived by the owner on May 11, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #446 from sul-dlss/tag-service
Extract TagService from Identifiable
- Loading branch information
Showing
5 changed files
with
216 additions
and
105 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
# frozen_string_literal: true | ||
|
||
module Dor | ||
# Manage tags on an object | ||
class TagService | ||
def self.add(item, tag) | ||
new(item).add(tag) | ||
end | ||
|
||
def self.remove(item, tag) | ||
new(item).remove(tag) | ||
end | ||
|
||
def self.update(item, old_tag, new_tag) | ||
new(item).update(old_tag, new_tag) | ||
end | ||
|
||
def initialize(item) | ||
@item = item | ||
end | ||
|
||
# Add an administrative tag to an item, you will need to seperately save the item to write it to fedora | ||
# @param tag [string] The tag you wish to add | ||
def add(tag) | ||
normalized_tag = validate_and_normalize_tag(tag, identity_metadata.tags) | ||
identity_metadata.add_value(:tag, normalized_tag) | ||
end | ||
|
||
def remove(tag) | ||
normtag = normalize_tag(tag) | ||
tag_nodes | ||
.select { |node| normalize_tag(node.content) == normtag } | ||
.each { identity_metadata.ng_xml_will_change! } | ||
.each(&:remove) | ||
.any? | ||
end | ||
|
||
def update(old_tag, new_tag) | ||
normtag = normalize_tag(old_tag) | ||
tag_nodes | ||
.select { |node| normalize_tag(node.content) == normtag } | ||
.each { identity_metadata.ng_xml_will_change! } | ||
.each { |node| node.content = normalize_tag(new_tag) } | ||
.any? | ||
end | ||
|
||
private | ||
|
||
attr_reader :item | ||
def identity_metadata | ||
item.identityMetadata | ||
end | ||
|
||
def tag_nodes | ||
identity_metadata.ng_xml.search('//tag') | ||
end | ||
|
||
# turns a tag string into an array with one element per tag part. | ||
# split on ":", disregard leading and trailing whitespace on tokens. | ||
def split_tag_to_arr(tag_str) | ||
tag_str.split(':').map(&:strip) | ||
end | ||
|
||
# turn a tag array back into a tag string with a standard format | ||
def normalize_tag_arr(tag_arr) | ||
tag_arr.join(' : ') | ||
end | ||
|
||
# take a tag string and return a normalized tag string | ||
def normalize_tag(tag_str) | ||
normalize_tag_arr(split_tag_to_arr(tag_str)) | ||
end | ||
|
||
# take a proposed tag string and a list of the existing tags for the object being edited. if | ||
# the proposed tag is valid, return it in normalized form. if not, raise an exception with an | ||
# explanatory message. | ||
def validate_and_normalize_tag(tag_str, existing_tag_list) | ||
tag_arr = validate_tag_format(tag_str) | ||
|
||
# note that the comparison for duplicate tags is case-insensitive, but we don't change case as part of the normalized version | ||
# we return, because we want to preserve the user's intended case. | ||
normalized_tag = normalize_tag_arr(tag_arr) | ||
dupe_existing_tag = existing_tag_list.detect { |existing_tag| normalize_tag(existing_tag).casecmp(normalized_tag) == 0 } | ||
raise "An existing tag (#{dupe_existing_tag}) is the same, consider using update_tag?" if dupe_existing_tag | ||
|
||
normalized_tag | ||
end | ||
|
||
# Ensure that an administrative tag meets the proper mininum format | ||
# @param tag_str [String] the tag | ||
# @return [Array] the tag split into an array via ':' | ||
def validate_tag_format(tag_str) | ||
tag_arr = split_tag_to_arr(tag_str) | ||
raise ArgumentError, "Invalid tag structure: tag '#{tag_str}' must have at least 2 elements" if tag_arr.length < 2 | ||
raise ArgumentError, "Invalid tag structure: tag '#{tag_str}' contains empty elements" if tag_arr.detect(&:empty?) | ||
|
||
tag_arr | ||
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
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,96 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'spec_helper' | ||
|
||
RSpec.describe Dor::TagService do | ||
let(:item) do | ||
item = instantiate_fixture('druid:ab123cd4567', Dor::Item) | ||
allow(item).to receive(:new?).and_return(false) | ||
ds = item.identityMetadata | ||
ds.instance_variable_set(:@datastream_content, item.identityMetadata.content) | ||
allow(ds).to receive(:new?).and_return(false) | ||
item | ||
end | ||
|
||
# when looking for tags after addition/update/removal, check for the normalized form. | ||
# when doing the add/update/removal, specify the tag in non-normalized form so that the | ||
# normalization mechanism actually gets tested. | ||
describe '.add' do | ||
subject(:add) { described_class.add(item, 'sometag:someval') } | ||
|
||
it 'adds a new tag' do | ||
add | ||
expect(item.identityMetadata.tags.include?('sometag : someval')).to be_truthy | ||
expect(item.identityMetadata).to be_changed | ||
end | ||
|
||
it 'raises an exception if there is an existing tag like it' do | ||
described_class.add(item, 'sometag:someval') | ||
expect(item.identityMetadata.tags.include?('sometag : someval')).to be_truthy | ||
expect { add }.to raise_error(RuntimeError) | ||
end | ||
end | ||
|
||
describe '.update' do | ||
subject(:update) { described_class.update(item, 'sometag :someval', 'new :tag') } | ||
|
||
it 'updates a tag' do | ||
described_class.add(item, 'sometag:someval') | ||
expect(item.identityMetadata.tags.include?('sometag : someval')).to be_truthy | ||
expect(update).to be_truthy | ||
expect(item.identityMetadata.tags.include?('sometag : someval')).to be_falsey | ||
expect(item.identityMetadata.tags.include?('new : tag')).to be_truthy | ||
expect(item.identityMetadata).to be_changed | ||
end | ||
|
||
it 'returns false if there is no matching tag to update' do | ||
expect(update).to be_falsey | ||
expect(item.identityMetadata).not_to be_changed | ||
end | ||
end | ||
|
||
describe '.remove' do | ||
subject(:remove) { described_class.remove(item, 'sometag:someval') } | ||
|
||
it 'deletes a tag' do | ||
described_class.add(item, 'sometag:someval') | ||
expect(item.identityMetadata.tags.include?('sometag : someval')).to be_truthy | ||
expect(remove).to be_truthy | ||
expect(item.identityMetadata.tags.include?('sometag : someval')).to be_falsey | ||
expect(item.identityMetadata).to be_changed | ||
end | ||
end | ||
|
||
describe '#validate_and_normalize_tag' do | ||
let(:service) { described_class.new(item) } | ||
subject(:invoke) { service.send(:validate_and_normalize_tag, tag_str, existing_tags) } | ||
let(:existing_tags) { [] } | ||
|
||
context 'when the tag has too few elements' do | ||
let(:tag_str) { 'just one part' } | ||
|
||
it 'throws an exception' do | ||
expect { invoke }.to raise_error(ArgumentError, "Invalid tag structure: tag '#{tag_str}' must have at least 2 elements") | ||
end | ||
end | ||
|
||
context 'when the tag has empty elements' do | ||
let(:tag_str) { 'test part1 : : test part3' } | ||
|
||
it 'throws an exception' do | ||
expect { invoke }.to raise_error(ArgumentError, "Invalid tag structure: tag '#{tag_str}' contains empty elements") | ||
end | ||
end | ||
|
||
context 'when the tag is the same as an existing tag' do | ||
let(:tag_str) { 'another:multi:part:test' } | ||
let(:existing_tags) { ['test part1 : test part2', 'Another : Multi : Part : Test', 'one : last_tag'] } | ||
|
||
it 'throws an exception' do | ||
# note that tag_str should match existing_tags[1] because the comparison should happen after normalization, and it should | ||
# be case-insensitive. | ||
expect { invoke }.to raise_error(RuntimeError, "An existing tag (#{existing_tags[1]}) is the same, consider using update_tag?") | ||
end | ||
end | ||
end | ||
end |