-
Notifications
You must be signed in to change notification settings - Fork 124
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Provide patterns and infrastructure for assigning remote identifiers …
…(DOI, Handle, etc.) There are three patterns provided: dispatcher, registrar, and builder Dispatcher - assigns registered iddentifer to a given object Registrar - handles communication with external identifier service Builder - constructs identifer to submit to external identifier service Registrar implementations just need to implement the `registrar!` method. They can be tested with the provided shared spec and then registered with Hyrax by the identifier_registrars configuration (which is generated commented out in the hyrax initializer). identifier_registrars should be a Hash with Symbol keys and Class values. A custom builder implementation can be injected into your registrar by overriding the registrar's initialize method setting the custom builder as the default value for the builder keyword argument. def initialize(builder: MyCustomBuilder.new) super(builder: builder) end With this infrastructure in place, a new remote identifier can be assigned to a work by calling the dispatcher with the work object. Assuming a :datacite registrar has been registered in Hyrax's configuration then this would look like: Hyrax::Identifier::Dispatcher.for(:datacite).assign_for!(object: work) This will set the remote identifier in the work's identifier attribute and save the work. To avoid saving the object use `assign_for` instead. If a different attribute is desired the pass the attribute as a symbol in the :attribute keywork argument to `assign_for!`. This work is ported from mahonia (which had parts ported from epigaea). Both of those implementations were done by @no-reply. Co-authored-by: Tom Johnson <johnson.tom@gmail.com>
- Loading branch information
Showing
11 changed files
with
335 additions
and
0 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# frozen_string_literal: true | ||
module Hyrax | ||
module Identifier | ||
## | ||
# Builds an identifier string. | ||
# | ||
# Implementations must accept a `prefix:` to `#initialize`, and a `hint:` to | ||
# `#build`. Either or both may be used at the preference of the specific | ||
# implementer or ignored entirely when `#build` is called. | ||
# | ||
# @example | ||
# builder = Hyrax::Identifier::Builder.new(prefix: 'moomin') | ||
# builder.build(hint: '1') # => "moomin/1" | ||
class Builder | ||
## | ||
# @!attribute prefix [rw] | ||
# @return [String] the prefix to use when building identifiers | ||
attr_accessor :prefix | ||
|
||
## | ||
# @param prefix [String] the prefix to use when building identifiers | ||
def initialize(prefix: 'pfx') | ||
@prefix = prefix | ||
end | ||
|
||
## | ||
# @note this default builder requires a `hint` which it appends to the | ||
# prefix to generate the identifier string. | ||
# | ||
# @param hint [#to_s] a string-able object which may be used by the builder | ||
# to generate an identifier. Hints may be required by some builders, while | ||
# others may ignore them to generate an identifier by other means. | ||
# | ||
# @return [String] | ||
# @raise [ArgumentError] if an identifer can't be built from the provided | ||
# hint. | ||
def build(hint: nil) | ||
raise(ArgumentError, "No hint provided to #{self.class}#build") if | ||
hint.nil? | ||
|
||
"#{prefix}/#{hint}" | ||
end | ||
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,61 @@ | ||
# frozen_string_literal: true | ||
module Hyrax | ||
module Identifier | ||
class Dispatcher | ||
## | ||
# @!attribute [rw] registrar | ||
# @return [Hyrax::Identifier::Registrar] | ||
attr_accessor :registrar | ||
|
||
## | ||
# @param registrar [Hyrax::Identifier::Registrar] | ||
def initialize(registrar:) | ||
@registrar = registrar | ||
end | ||
|
||
class << self | ||
## | ||
# @param type [Symbol] | ||
# @param registrar_opts [Hash] | ||
# @option registrar_opts [Hyrax::Identifier::Builder] :builder | ||
# | ||
# @return [Hyrax::Identifier::Dispatcher] a dispatcher with an registrar for the | ||
# given type | ||
# @see IdentifierRegistrar.for | ||
def for(type, **registrar_opts) | ||
new(registrar: Hyrax::Identifier::Registrar.for(type, **registrar_opts)) | ||
end | ||
end | ||
|
||
## | ||
# Assigns an identifier to the object. | ||
# | ||
# This involves two steps: | ||
# - Registering the identifier with the registrar service via `registrar`. | ||
# - Storing the new identifier on the object, in the provided `attribute`. | ||
# | ||
# @note the attribute for identifier storage must be multi-valued, and will | ||
# be overwritten during assignment. | ||
# | ||
# @param attribute [Symbol] the attribute in which to store the identifier. | ||
# This attribute will be overwritten during assignment. | ||
# @param object [ActiveFedora::Base, Hyrax::Resource] the object to assign an identifier. | ||
# | ||
# @return [ActiveFedora::Base, Hyrax::Resource] object | ||
def assign_for(object:, attribute: :identifier) | ||
record = registrar.register!(object: object) | ||
object.public_send("#{attribute}=".to_sym, [record.identifier]) | ||
object | ||
end | ||
|
||
## | ||
# Assigns an identifier and saves the object. | ||
# | ||
# @see #assign_for | ||
def assign_for!(object:, attribute: :identifier) | ||
assign_for(object: object, attribute: attribute).save! | ||
object | ||
end | ||
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,41 @@ | ||
# frozen_string_literal: true | ||
module Hyrax | ||
module Identifier | ||
class Registrar | ||
class << self | ||
## | ||
# @param type [Symbol] | ||
# @param opts [Hash] | ||
# @option opts [Hyrax::Identifier::Builder] :builder | ||
# | ||
# @return [Hyrax::Identifier::Registrar] a registrar for the given type | ||
def for(type, **opts) | ||
return Hyrax.config.identifier_registrars[type].new(**opts) if Hyrax.config.identifier_registrars.include?(type) | ||
raise ArgumentError, "Hyrax::Identifier::Registrar not found to handle #{type}" | ||
end | ||
end | ||
|
||
## | ||
# @!attribute builder [rw] | ||
# @return [Hyrax::Identifier::Builder] | ||
attr_accessor :builder | ||
|
||
## | ||
# @param builder [Hyrax::Identifier::Builder] | ||
def initialize(builder:) | ||
@builder = builder | ||
end | ||
|
||
## | ||
# @abstract | ||
# | ||
# @param object [#id] | ||
# | ||
# @return [#identifier] | ||
# @raise [NotImplementedError] when the method is abstract | ||
def register!(*) | ||
raise NotImplementedError | ||
end | ||
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
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 |
---|---|---|
@@ -1,2 +1,3 @@ | ||
require 'hyrax/specs/shared_specs/derivative_service' | ||
require 'hyrax/specs/shared_specs/identifiers' | ||
require 'hyrax/specs/shared_specs/workflow_method' |
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,27 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.shared_examples 'a Hyrax::Identifier::Builder' do | ||
subject(:builder) { described_class.new } | ||
|
||
describe '#build' do | ||
it 'returns an identifier string' do | ||
expect(builder.build(hint: 'moomin')) | ||
.to respond_to :to_str | ||
end | ||
end | ||
end | ||
|
||
RSpec.shared_examples 'a Hyrax::Identifier::Registrar' do | ||
subject(:registrar) { described_class.new(builder: builder) } | ||
let(:builder) { instance_double(Hyrax::Identifier::Builder, build: 'moomin') } | ||
let(:object) { instance_double(GenericWork, id: 'moomin_id') } | ||
|
||
it { is_expected.to have_attributes(builder: builder) } | ||
|
||
describe '#register!' do | ||
it 'creates an identifier record' do | ||
expect(registrar.register!(object: object).identifier) | ||
.to respond_to :to_str | ||
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,44 @@ | ||
# frozen_string_literal: true | ||
|
||
require 'hyrax/specs/shared_specs' | ||
|
||
RSpec.describe Hyrax::Identifier::Builder do | ||
subject(:builder) { described_class.new } | ||
|
||
it_behaves_like 'a Hyrax::Identifier::Builder' | ||
|
||
describe '#prefix' do | ||
it 'has a default prefix' do | ||
expect(builder.prefix).not_to be_empty | ||
end | ||
|
||
it 'accepts a prefix' do | ||
prefix = 'my_pfx' | ||
builder = described_class.new(prefix: prefix) | ||
expect(builder.prefix).to eq prefix | ||
end | ||
end | ||
|
||
describe '#build' do | ||
it 'uses the prefix' do | ||
expect(builder.build(hint: 'blah')).to start_with "#{builder.prefix}/" | ||
end | ||
|
||
context 'with a custom prefix' do | ||
subject(:builder) { described_class.new(prefix: prefix) } | ||
let(:prefix) { 'fake_prefix' } | ||
|
||
it 'uses the prefix' do | ||
expect(builder.build(hint: 'blah')).to start_with "#{prefix}/" | ||
end | ||
end | ||
|
||
it 'raises an error with no hint' do | ||
expect { builder.build }.to raise_error ArgumentError | ||
end | ||
|
||
it 'uses the hint exactly, cast to uppercase' do | ||
expect(builder.build(hint: 'moomin')).to eq 'pfx/moomin' | ||
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,68 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.describe Hyrax::Identifier::Dispatcher do | ||
subject(:dispatcher) { described_class.new(registrar: fake_registrar.new) } | ||
let(:identifier) { 'moomin/123/abc' } | ||
let(:object) { build(:generic_work) } | ||
|
||
let(:fake_registrar) do | ||
Class.new do | ||
def initialize(*); end | ||
|
||
def register!(*) | ||
Struct.new(:identifier).new('moomin/123/abc') | ||
end | ||
end | ||
end | ||
|
||
shared_examples 'performs identifier assignment' do |method| | ||
it 'returns the same object' do | ||
expect(dispatcher.public_send(method, object: object)).to eql object | ||
end | ||
|
||
it 'assigns to the identifier attribute by default' do | ||
dispatcher.public_send(method, object: object) | ||
expect(object.identifier).to contain_exactly(identifier) | ||
end | ||
|
||
it 'assigns to specified attribute when requested' do | ||
dispatcher.public_send(method, object: object, attribute: :keyword) | ||
expect(object.keyword).to contain_exactly(identifier) | ||
end | ||
end | ||
|
||
it 'has a registrar' do | ||
expect(dispatcher.registrar).to be_a fake_registrar | ||
end | ||
|
||
describe '.for' do | ||
before do | ||
allow(Hyrax.config).to receive(:identifier_registrars).and_return({ moomin: fake_registrar }) | ||
end | ||
|
||
it 'chooses the right registrar type' do | ||
expect(described_class.for(:moomin).registrar) | ||
.to be_a fake_registrar | ||
end | ||
|
||
it 'raises an error when a fake registrar type is passes' do | ||
expect { described_class.for(:NOT_A_REAL_TYPE) } | ||
.to raise_error ArgumentError | ||
end | ||
end | ||
|
||
describe '#assign_for' do | ||
include_examples 'performs identifier assignment', :assign_for | ||
end | ||
|
||
describe '#assign_for!' do | ||
include_examples 'performs identifier assignment', :assign_for! | ||
|
||
it 'saves the object' do | ||
expect { dispatcher.assign_for!(object: object) } | ||
.to change { object.new_record? } | ||
.from(true) | ||
.to(false) | ||
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,37 @@ | ||
# frozen_string_literal: true | ||
|
||
RSpec.describe Hyrax::Identifier::Registrar do | ||
subject(:registrar) { described_class.new(builder: :NOT_A_REAL_BUILDER) } | ||
|
||
it 'is abstract' do | ||
expect { registrar.register!(object: :NOT_A_REAL_OBJECT) } | ||
.to raise_error NotImplementedError | ||
end | ||
|
||
describe '.for' do | ||
let(:builder) { instance_double(Hyrax::Identifier::Builder, build: 'moomin') } | ||
let(:fake_registrar) do | ||
Class.new do | ||
def initialize(*); end | ||
|
||
def register!(*) | ||
Struct.new(:identifier).new('moomin/123/abc') | ||
end | ||
end | ||
end | ||
|
||
before do | ||
allow(Hyrax.config).to receive(:identifier_registrars).and_return({ moomin: fake_registrar }) | ||
end | ||
|
||
it 'raises an error when a fake registrar type is passes' do | ||
expect { described_class.for(:NOT_A_REAL_TYPE, builder: builder) } | ||
.to raise_error ArgumentError | ||
end | ||
|
||
it 'chooses the right registrar type' do | ||
expect(described_class.for(:moomin, builder: builder)) | ||
.to be_a fake_registrar | ||
end | ||
end | ||
end |