diff --git a/.rubocop.yml b/.rubocop.yml index f7996f0af4..f6d7e9fd6b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -35,3 +35,7 @@ RSpec/MultipleExpectations: RSpec/ExpectActual: Exclude: - 'spec/routing/**' + +RSpec/DescribeClass: + Exclude: + - 'spec/requests/**' diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index bd12e4cd0c..650abeb9da 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,6 +1,6 @@ # This configuration was generated by # `rubocop --auto-gen-config` -# on 2019-06-04 08:56:27 -0500 using RuboCop version 0.65.0. +# on 2019-09-05 13:08:27 -0500 using RuboCop version 0.65.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new @@ -23,13 +23,20 @@ Lint/MissingCopEnableDirective: Exclude: - 'spec/dor/update_marc_record_service_spec.rb' +# Offense count: 2 +# Cop supports --auto-correct. +# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods. +Lint/UnusedMethodArgument: + Exclude: + - 'app/services/public_desc_metadata_service.rb' + # Offense count: 2 Lint/UriEscapeUnescape: Exclude: - 'app/controllers/sdr_controller.rb' - 'spec/controllers/sdr_controller_spec.rb' -# Offense count: 22 +# Offense count: 23 Metrics/AbcSize: Max: 91 @@ -38,7 +45,7 @@ Metrics/AbcSize: Metrics/ClassLength: Max: 124 -# Offense count: 4 +# Offense count: 5 Metrics/CyclomaticComplexity: Max: 11 @@ -47,7 +54,7 @@ Metrics/CyclomaticComplexity: Metrics/MethodLength: Max: 43 -# Offense count: 3 +# Offense count: 4 Metrics/PerceivedComplexity: Max: 11 @@ -166,7 +173,7 @@ RSpec/InstanceVariable: - 'spec/services/cleanup_reset_service_spec.rb' - 'spec/services/registration_service_spec.rb' -# Offense count: 39 +# Offense count: 41 # Configuration parameters: EnforcedStyle. # SupportedStyles: have_received, receive RSpec/MessageSpies: @@ -357,7 +364,7 @@ Style/StringLiterals: - 'spec/rails_helper.rb' - 'spec/spec_helper.rb' -# Offense count: 4 +# Offense count: 6 # Cop supports --auto-correct. # Configuration parameters: . # SupportedStyles: percent, brackets diff --git a/Gemfile b/Gemfile index f35722314a..d41e20e91c 100644 --- a/Gemfile +++ b/Gemfile @@ -25,6 +25,7 @@ gem 'honeybadger' gem 'okcomputer' gem 'faraday' +gem 'jbuilder' gem 'jwt' gem 'marc' gem 'rest-client' @@ -36,6 +37,8 @@ gem 'uuidtools', '~> 2.1.4' # DLSS/domain-specific dependencies gem 'dor-services', '~> 7.0' +gem 'dry-struct' +gem 'dry-types' group :test, :development do gem 'coveralls', '~> 0.8', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 43848f0077..a79e3a828a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -173,6 +173,11 @@ GEM dry-initializer (~> 3.0) dry-logic (~> 1.0) dry-types (~> 1.0) + dry-struct (1.0.0) + dry-core (~> 0.4, >= 0.4.3) + dry-equalizer (~> 0.2) + dry-types (~> 1.0) + ice_nine (~> 0.11) dry-types (1.1.1) concurrent-ruby (~> 1.0) dry-container (~> 0.3) @@ -205,8 +210,11 @@ GEM domain_name (~> 0.5) i18n (1.6.0) concurrent-ruby (~> 1.0) + ice_nine (0.11.2) iso-639 (0.2.8) jaro_winkler (1.5.3) + jbuilder (2.9.1) + activesupport (>= 4.2.0) json (2.2.0) jwt (2.2.1) link_header (0.0.8) @@ -448,9 +456,12 @@ DEPENDENCIES coveralls (~> 0.8) dlss-capistrano dor-services (~> 7.0) + dry-struct + dry-types equivalent-xml faraday honeybadger + jbuilder jwt listen (~> 3.0.5) marc diff --git a/app/controllers/content_controller.rb b/app/controllers/content_controller.rb index a7dedac0a0..2857bdad09 100644 --- a/app/controllers/content_controller.rb +++ b/app/controllers/content_controller.rb @@ -2,13 +2,9 @@ # API to retrieve file listings and file content from the DOR workspace class ContentController < ApplicationController - rescue_from ActionController::MissingFile do - render status: :not_found - end - def read location = druid_tools.find(:content, params[:path]) - return render status: :not_found unless location + return not_found(location) unless location send_file location end @@ -16,7 +12,7 @@ def read def list location = druid_tools.content_dir(false) - raise ActionController::MissingFile, location unless Dir.exist? location + return not_found(location) unless Dir.exist? location render json: { items: Dir.glob(File.join(location, '**', '*')).map do |file| @@ -32,6 +28,10 @@ def list private + def not_found(location) + render status: :not_found, plain: "Unable to locate file #{location}" + end + def druid_tools DruidTools::Druid.new(params[:id], Settings.content.content_base_dir) end diff --git a/app/controllers/objects_controller.rb b/app/controllers/objects_controller.rb index b9ca3b22c8..e02fa218ad 100644 --- a/app/controllers/objects_controller.rb +++ b/app/controllers/objects_controller.rb @@ -46,6 +46,10 @@ def update head :no_content end + def show + render json: Cocina::Mapper.build(@item) + end + def publish PublishMetadataService.publish(@item) head :created diff --git a/app/controllers/queries_controller.rb b/app/controllers/queries_controller.rb new file mode 100644 index 0000000000..15796ee505 --- /dev/null +++ b/app/controllers/queries_controller.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Responds to queries about objects +class QueriesController < ApplicationController + before_action :load_item, only: [:collections] + + # Returns a list of collections this object is in. + def collections + # If we move to Valkyrie this can be find_inverse_references_by + @collections = @item.collections.map { |collection| Cocina::Mapper.build(collection) } + end +end diff --git a/app/models/cocina/dro.rb b/app/models/cocina/dro.rb new file mode 100644 index 0000000000..1ade40cef5 --- /dev/null +++ b/app/models/cocina/dro.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module Cocina + # A digital repository object. See https://github.com/sul-dlss-labs/taco/blob/master/maps/DRO.json + class DRO < Dry::Struct + attribute :externalIdentifier, Types::Strict::String + attribute :type, Types::Strict::String + attribute :label, Types::Strict::String + + def as_json(*) + { + externalIdentifier: externalIdentifier, + type: type, + label: label + } + end + end +end diff --git a/app/models/cocina/types.rb b/app/models/cocina/types.rb new file mode 100644 index 0000000000..01c5cdbb72 --- /dev/null +++ b/app/models/cocina/types.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Cocina + # This defines the data types supported by the model + module Types + include Dry.Types() + end +end diff --git a/app/services/cocina/mapper.rb b/app/services/cocina/mapper.rb new file mode 100644 index 0000000000..cae3e3a5f3 --- /dev/null +++ b/app/services/cocina/mapper.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Cocina + # Maps Dor::Items to Cocina objects + class Mapper + def self.build(item) + new(item).build + end + + def initialize(item) + @item = item + end + + def build + Cocina::DRO.new(externalIdentifier: item.pid, + type: type, + label: item.label) + end + + private + + attr_reader :item + + # @todo This should have more speicific type such as found in identityMetadata.objectType + def type + case item + when Dor::Item + 'object' + when Dor::Collection + 'collection' + else + raise "Unknown type for #{item.class}" + end + end + end +end diff --git a/app/services/public_desc_metadata_service.rb b/app/services/public_desc_metadata_service.rb index 1a9a88f583..1cd4f91e76 100644 --- a/app/services/public_desc_metadata_service.rb +++ b/app/services/public_desc_metadata_service.rb @@ -16,7 +16,7 @@ def doc end # @return [String] Public descriptive medatada XML - def to_xml(include_access_conditions: true) + def to_xml(include_access_conditions: true, prefixes: nil, template: nil) ng_xml(include_access_conditions: include_access_conditions).to_xml end diff --git a/app/views/queries/collections.json.jbuilder b/app/views/queries/collections.json.jbuilder new file mode 100644 index 0000000000..30b5f07fde --- /dev/null +++ b/app/views/queries/collections.json.jbuilder @@ -0,0 +1,3 @@ +# frozen_string_literal: true + +json.collections @collections, :externalIdentifier diff --git a/config/routes.rb b/config/routes.rb index d19ee63b6a..918c870937 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -20,7 +20,7 @@ get 'catkey', to: 'marcxml#catkey' end - resources :objects, only: [:create, :update] do + resources :objects, only: [:create, :update, :show] do member do post 'publish' post 'update_embargo' @@ -33,6 +33,12 @@ get 'contents/*path', to: 'content#read', format: false, as: :read_content end + resource :query, only: [], defaults: { format: :json } do + collection do + get 'collections' + end + end + resource :workspace, only: [:create, :destroy] resources :metadata, only: [] do diff --git a/spec/requests/collections_for_object_spec.rb b/spec/requests/collections_for_object_spec.rb new file mode 100644 index 0000000000..492d6f69f3 --- /dev/null +++ b/spec/requests/collections_for_object_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Get the object' do + let(:payload) { { sub: 'argo' } } + let(:jwt) { JWT.encode(payload, Settings.dor.hmac_secret, 'HS256') } + let(:basic_auth) { ActionController::HttpAuthentication::Basic.encode_credentials(user, password) } + let(:object) { instance_double(Dor::Item, collections: [collection]) } + let(:collection_id) { 'druid:999123' } + let(:collection) do + Dor::Collection.new(pid: collection_id, label: 'collection #1') + end + + before do + allow(Dor).to receive(:find).and_return(object) + end + + describe 'as used by WAS crawl seed registration' do + it 'returns (at a minimum) the identifiers of the collections ' do + get '/v1/objects/druid:mk420bs7601/query/collections', + headers: { 'X-Auth' => "Bearer #{jwt}" } + expect(response).to be_successful + expect(response.body).to eq '{"collections":[{"externalIdentifier":"druid:999123"}]}' + end + end +end diff --git a/spec/requests/show_object_spec.rb b/spec/requests/show_object_spec.rb new file mode 100644 index 0000000000..d2c57c9320 --- /dev/null +++ b/spec/requests/show_object_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Get the object' do + let(:payload) { { sub: 'argo' } } + let(:jwt) { JWT.encode(payload, Settings.dor.hmac_secret, 'HS256') } + let(:basic_auth) { ActionController::HttpAuthentication::Basic.encode_credentials(user, password) } + let(:object) { Dor::Item.new(pid: 'druid:1234') } + + before do + allow(Dor).to receive(:find).and_return(object) + end + + context 'when the object exists' do + before do + object.descMetadata.title_info.main_title = 'Hello' + object.label = 'foo' + end + + it 'returns the object' do + get '/v1/objects/druid:mk420bs7601', + headers: { 'X-Auth' => "Bearer #{jwt}" } + expect(response).to be_successful + expect(response.body).to eq '{"externalIdentifier":"druid:1234","type":"object","label":"foo"}' + end + end + + describe 'as used by WAS crawl seed registration' do + # In this case it's necessary that the URL for the crawl be in the label field + # and the collection id is passed + + let(:collection_id) { 'druid:999123' } + let(:collection) { instance_double(Dor::Collection, is_a?: true, new_record?: false, pid: collection_id) } + + before do + # Stubbing out the internals of ActiveFedora here: + allow(Dor::Collection).to receive(:find).and_return([collection]) + + object.descMetadata.title_info.main_title = 'Hello' + object.label = 'foo' + object.collection_ids = [collection_id] + end + + it 'returns minimally the label (url) and collection id' do + get '/v1/objects/druid:mk420bs7601', + headers: { 'X-Auth' => "Bearer #{jwt}" } + expect(response).to be_successful + expect(response.body).to eq '{"externalIdentifier":"druid:1234","type":"object","label":"foo"}' + end + end +end