From e20252ac84d92665c217afdc6158a1d328388056 Mon Sep 17 00:00:00 2001 From: Tom Johnson Date: Mon, 30 Jan 2017 16:52:30 -0800 Subject: [PATCH] Open Lamprey to configuration This makes Lamprey configurable, beginning with a simple repository type configuration option. Adds command line arguments with `OptionParser`. --- app/lamprey.rb | 78 +++++++++++++- bin/lamprey | 13 +++ spec/app/lamprey_spec.rb | 142 +++++++++++++++++++------ spec/integration/ldp_testsuite_spec.rb | 2 + 4 files changed, 197 insertions(+), 38 deletions(-) diff --git a/app/lamprey.rb b/app/lamprey.rb index 5e1ab6b..f583689 100644 --- a/app/lamprey.rb +++ b/app/lamprey.rb @@ -9,11 +9,6 @@ class RDF::Lamprey < Sinatra::Base use Rack::ConditionalGet use Rack::LDP::Requests - # Set defaults in case user has not configured values - configure do - set :repository, RDF::Repository.new - end - get '/*' do RDF::LDP::Container.new(RDF::URI(request.url), settings.repository) .create(StringIO.new, 'text/turtle') if settings.repository.empty? @@ -50,5 +45,78 @@ class RDF::Lamprey < Sinatra::Base RDF::LDP::Resource.find(RDF::URI(request.url), settings.repository) end + ## + # @example Configuring Lamprey server + # Lamprey::Config + # .register_repository!(:my_repo, RDF::Repository) + # + # Lamprey::Config.configure!(repository: :my_repo) + class Config + ## + # @see #new + # @see #configure! + def self.configure!(**options) + self.new(**options).configure! + end + + ## + # Registers a repository for use with the {#build_repository} method. + # + # @example Registering a custom repository + # MyRepository = Class.new(RDF::Repository) + # + # Lamprey::Config.register_repository!(:my_repo, MyRepository) + # + # @param name [Symbol] + # @param klass [Class] + # @return [void] + def self.register_repository!(name, klass) + @@repositories[name] = klass + end + + ## + # @!attribute [rw] options + attr_accessor :options + + @@repositories = { default: RDF::Repository } + + ## + # @param repository [RDF::Repository] + def initialize(repository: :default) + @options = {} + @options[:repository] = repository + end + + ## + # Builds the repository as given in the configuration. + # + # @return [RDF::Repository] a repository instance + def build_repository + @@repositories.fetch(options[:repository]) { + warn "#{options[:repository]} is not a configured repository. Use "\ + "`Lamprey::Config.register_repository!` to register it before "\ + "configuration. Falling back on the default: " \ + "#{@@repositories[:default]}." + @@repositories[:default] + }.new + end + + ## + # Configures {RDF::Lamprey} with {#options}. + # + # @return [void] + def configure! + repository = build_repository + warn "#{repository} is not a persistent repository. "\ + "Data will be lost on server shutdown." unless repository.persistent? + + RDF::Lamprey.configure { |config| config.set :repository, repository } + end + end + + # Set defaults in case user does not configure values + Config.configure! + run! if app_file == $0 end + diff --git a/bin/lamprey b/bin/lamprey index 0eb2e6c..36b9929 100755 --- a/bin/lamprey +++ b/bin/lamprey @@ -1,5 +1,18 @@ #!/usr/bin/env ruby $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), '..', 'app'))) require 'lamprey' +require 'optparse' + +options = {} + +OptionParser.new do |opts| + opts.banner = 'Usage: lamprey [options]' + + opts.on("-rREPO", "--repository=REPO", 'Set the repository') do |repo| + options[:repository] = repo.to_sym + end +end.parse! + +RDF::Lamprey::Config.configure!(**options) RDF::Lamprey.run! diff --git a/spec/app/lamprey_spec.rb b/spec/app/lamprey_spec.rb index 7487de0..5f7b473 100644 --- a/spec/app/lamprey_spec.rb +++ b/spec/app/lamprey_spec.rb @@ -7,7 +7,7 @@ include ::Rack::Test::Methods let(:app) { RDF::Lamprey } - describe 'base container /' do + describe 'base container /' do describe 'GET' do it 'has default content type "text/turtle"' do get '/' @@ -18,21 +18,21 @@ get '/' expect(last_response.header['Etag']).to be_a String end - + context 'when resource exists' do let(:graph) { RDF::Graph.new } before do - graph << RDF::Statement(RDF::URI('http://example.org/moomin'), + graph << RDF::Statement(RDF::URI('http://example.org/moomin'), RDF::Vocab::DC.title, 'mummi') - + graph_str = graph.dump(:ntriples) post '/', graph_str, 'CONTENT_TYPE' => 'application/n-triples' @uri = last_response.header['Location'] end - + it 'can get the resource' do get @uri returned = RDF::Reader.for(:ttl).new(last_response.body).statements.to_a @@ -68,7 +68,7 @@ get @uri time = last_response.header['Last-Modified'] get @uri, '', 'HTTP_IF_MODIFIED_SINCE' => time - + expect(last_response.body).to be_empty end end @@ -122,7 +122,7 @@ patch '/', '---blah---', 'CONTENT_TYPE' => 'text/ldpatch' expect(last_response.status).to eq 400 end - + it 'returns 400 on improper SPARQL Update document' do patch '/', '---blah---', 'CONTENT_TYPE' => 'application/sparql-update' expect(last_response.status).to eq 400 @@ -139,7 +139,7 @@ patch '/', update, 'CONTENT_TYPE' => 'application/sparql-update' expect(last_response.status).to eq 200 end - + it 'properly handles null relative IRIs' do post '/', '<> "foo" .', 'CONTENT_TYPE' => 'text/turtle' resource_path = URI.parse(last_response['Location']).path @@ -157,7 +157,7 @@ let(:graph) { RDF::Graph.new } before do - graph << RDF::Statement(RDF::URI('http://example.org/moomin'), + graph << RDF::Statement(RDF::URI('http://example.org/moomin'), RDF::Vocab::DC.title, 'mummi') end @@ -190,23 +190,23 @@ context 'with Slug' do it 'accepts a Slug' do - post '/', graph.dump(:ttl), - 'CONTENT_TYPE' => 'text/turtle', + post '/', graph.dump(:ttl), + 'CONTENT_TYPE' => 'text/turtle', 'HTTP_SLUG' => 'moominpapa' expect(last_response.header['Location']) .to eq 'http://example.org/moominpapa' end it 'rejects slugs with #' do - post '/', graph.dump(:ttl), - 'CONTENT_TYPE' => 'text/turtle', + post '/', graph.dump(:ttl), + 'CONTENT_TYPE' => 'text/turtle', 'HTTP_SLUG' => 'moomin#papa' expect(last_response.status).to eq 406 end it 'gives Conflict if slug is taken' do - post '/', graph.dump(:ttl), - 'CONTENT_TYPE' => 'text/turtle', + post '/', graph.dump(:ttl), + 'CONTENT_TYPE' => 'text/turtle', 'HTTP_SLUG' => 'moomin' expect(last_response.status).to eq 409 end @@ -218,8 +218,8 @@ context 'with existing resource' do before do - post '/', graph.dump(:ttl), - 'CONTENT_TYPE' => 'text/turtle', + post '/', graph.dump(:ttl), + 'CONTENT_TYPE' => 'text/turtle', 'HTTP_SLUG' => 'moomin' end @@ -227,7 +227,7 @@ put '/moomin', '', 'CONTENT_TYPE' => 'text/turtle' expect(last_response.header['Etag']).to be_a String end - + it 'updates ETag' do get '/moomin' etag = last_response.header['Etag'] @@ -238,7 +238,7 @@ expect(last_response.header['Etag']).not_to eq etag end end - + context 'creating a resource' do it 'returns 201' do put '/put_source', '', 'CONTENT_TYPE' => 'text/turtle' @@ -250,29 +250,29 @@ links = LinkHeader.parse(last_response.header['Link']).links expect(links.map(&:href)) - .to contain_exactly(RDF::Vocab::LDP.Resource.to_s, + .to contain_exactly(RDF::Vocab::LDP.Resource.to_s, RDF::Vocab::LDP.RDFSource.to_s) - + end it 'creates an BasicContainer when using Container model' do - put '/put_container', '', 'CONTENT_TYPE' => 'text/turtle', + put '/put_container', '', 'CONTENT_TYPE' => 'text/turtle', 'HTTP_LINK' => "#{RDF::Vocab::LDP.Container.to_base};rel=\"type\"" links = LinkHeader.parse(last_response.header['Link']).links expect(links.map(&:href)) - .to include(RDF::Vocab::LDP.Resource.to_s, + .to include(RDF::Vocab::LDP.Resource.to_s, RDF::Vocab::LDP.RDFSource.to_s, RDF::Vocab::LDP.BasicContainer.to_s) end it 'creates an BasicContainer when using BasicContainer model' do - put '/put_container', '', 'CONTENT_TYPE' => 'text/turtle', + put '/put_container', '', 'CONTENT_TYPE' => 'text/turtle', 'HTTP_LINK' => "#{RDF::Vocab::LDP.BasicContainer.to_base};rel=\"type\"" links = LinkHeader.parse(last_response.header['Link']).links expect(links.map(&:href)) - .to include(RDF::Vocab::LDP.Resource.to_s, + .to include(RDF::Vocab::LDP.Resource.to_s, RDF::Vocab::LDP.RDFSource.to_s, RDF::Vocab::LDP.BasicContainer.to_s) end @@ -280,12 +280,12 @@ it 'creates an DirectContainer' do uri = RDF::Vocab::LDP.DirectContainer.to_base - put '/put_direct_container', '', 'CONTENT_TYPE' => 'text/turtle', + put '/put_direct_container', '', 'CONTENT_TYPE' => 'text/turtle', 'HTTP_LINK' => "#{uri};rel=\"type\"" links = LinkHeader.parse(last_response.header['Link']).links expect(links.map(&:href)) - .to include(RDF::Vocab::LDP.Resource.to_s, + .to include(RDF::Vocab::LDP.Resource.to_s, RDF::Vocab::LDP.RDFSource.to_s, RDF::Vocab::LDP.DirectContainer.to_s) end @@ -293,32 +293,108 @@ it 'creates an IndirectContainer' do uri = RDF::Vocab::LDP.IndirectContainer.to_base - put '/put_indirect_container', '', 'CONTENT_TYPE' => 'text/turtle', + put '/put_indirect_container', '', 'CONTENT_TYPE' => 'text/turtle', 'HTTP_LINK' => "#{uri};rel=\"type\"" links = LinkHeader.parse(last_response.header['Link']).links expect(links.map(&:href)) - .to include(RDF::Vocab::LDP.Resource.to_s, + .to include(RDF::Vocab::LDP.Resource.to_s, RDF::Vocab::LDP.RDFSource.to_s, RDF::Vocab::LDP.IndirectContainer.to_s) end it 'creates a NonRDFSource' do uri = RDF::Vocab::LDP.NonRDFSource.to_base - put '/put_nonrdf_source', '', 'CONTENT_TYPE' => 'text/turtle', + put '/put_nonrdf_source', '', 'CONTENT_TYPE' => 'text/turtle', 'HTTP_LINK' => "#{uri};rel=\"type\"" links = LinkHeader.parse(last_response.header['Link']).links .select { |link| link.attr_pairs.first.include? 'type' } expect(links.map(&:href)) - .to contain_exactly(RDF::Vocab::LDP.Resource.to_s, + .to contain_exactly(RDF::Vocab::LDP.Resource.to_s, RDF::Vocab::LDP.NonRDFSource.to_s) - + end end end - + describe 'DELETE' do end end end + +describe RDF::Lamprey::Config do + # Reset configuration to default + after(:context) { RDF::Lamprey::Config.configure! } + + shared_context 'with a registered repository' do + subject { described_class.new(repository: name) } + let(:name) { :new_repo } + let(:klass) { Class.new(RDF::Repository) } + + before { described_class.register_repository!(name, klass) } + end + + describe '.configure!' do + it 'configures :repository' do + expect { described_class.configure! } + .to change { RDF::Lamprey.repository } + end + + it 'falls back on default repository' do + expect { described_class.configure!(repository: :fake) } + .to change { RDF::Lamprey.repository } + .to an_instance_of(RDF::Repository) + end + + context 'with a registered repository' do + include_context 'with a registered repository' + + it 'configures the registered repository' do + expect { described_class.configure!(repository: name) } + .to change { RDF::Lamprey.repository } + .to an_instance_of(klass) + end + end + end + + describe '.register_repository!' do + let(:name) { :new_repo } + let(:klass) { Class.new(RDF::Repository) } + + it 'does not raise an error' do + expect { described_class.register_repository!(name, klass) } + .not_to raise_error + end + end + + describe '#build_repository' do + it 'gives basic repository instance by default' do + expect(subject.build_repository).to be_a RDF::Repository + end + + context 'with a registered repository' do + include_context 'with a registered repository' + + it 'configures the registered repository' do + expect(subject.build_repository).to be_a klass + end + end + end + + describe '#configure!' do + it 'changes RDF::Lamprey.repository' do + expect { subject.configure! }.to change { RDF::Lamprey.repository } + end + + context 'with a registered repository' do + include_context 'with a registered repository' + + it 'configures the registered repository' do + expect { subject.configure! } + .to change { RDF::Lamprey.repository } + .to an_instance_of(klass) + end + end + end +end diff --git a/spec/integration/ldp_testsuite_spec.rb b/spec/integration/ldp_testsuite_spec.rb index 6af2415..d7968b2 100644 --- a/spec/integration/ldp_testsuite_spec.rb +++ b/spec/integration/ldp_testsuite_spec.rb @@ -6,6 +6,8 @@ require 'ldp_testsuite_wrapper' require 'ldp_testsuite_wrapper/rspec' +require 'lamprey' + describe 'LDP Test Suite', integration: true do before(:all) do # use custom fork to work around https://github.com/w3c/ldp-testsuite/pull/227