diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e3f65c8 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,22 @@ +name: Run Tests + +on: push + +jobs: + test: + runs-on: ubuntu-latest + name: Ruby ${{ matrix.ruby }} + strategy: + matrix: + ruby: [2.7, 3.0, 3.1, 3.2] + steps: + - uses: actions/checkout@v3 + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + - name: Run linter for Ruby + run: bundle exec standardrb + - name: Run tests + run: bundle exec rspec diff --git a/.gitignore b/.gitignore index b04a8c8..0d12207 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ # rspec failure tracking .rspec_status +Gemfile.lock +vendor diff --git a/Gemfile b/Gemfile index 7d4c3ea..f319c3f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,10 @@ source "https://rubygems.org" -git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } +git_source(:github) { |repo_name| "https://github.com/#{repo_name}" } # Specify your gem's dependencies in ht-pairtree.gemspec gemspec + +group :development do + gem "pry" +end diff --git a/README.md b/README.md index 2bd1a7c..ee46ab8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # HathiTrust::Pairtree +[![Tests](https://github.com/hathitrust/ht-pairtree/actions/workflows/tests.yml/badge.svg)](https://github.com/hathitrust/ht-pairtree/actions/workflows/tests.yml) + Deal with a [Pairtree](https://github.com/mlibrary/pairtree) given an HTID. Allows both reading and creation of the underlying pairtree directories @@ -39,9 +41,9 @@ pairtree = pt[id] ### Create a new pairtree object (directory) newthing = pt.create('one.two3four') -#=> Pairtree::PathError (because there's no pairtree at .../obj/one) +#=> HathiTrust::Pairtree::NamespaceDoesNotExist (because there's no pairtree at .../obj/one) -newthing = pt.create('one.two3four', create_new_namespace: true) +newthing = pt.create('one.two3four', new_namespace_allowed: true) #=> Pairtree::Obj ``` @@ -69,7 +71,7 @@ function htcd() { Add this line to your application's Gemfile: ```ruby -gem 'ht-rpairtree' +gem 'ht-pairtree' ``` And then execute: diff --git a/Rakefile b/Rakefile index b7e9ed5..c92b11e 100644 --- a/Rakefile +++ b/Rakefile @@ -3,4 +3,4 @@ require "rspec/core/rake_task" RSpec::Core::RakeTask.new(:spec) -task :default => :spec +task default: :spec diff --git a/ht-pairtree.gemspec b/ht-pairtree.gemspec index cf27f3c..c18587b 100644 --- a/ht-pairtree.gemspec +++ b/ht-pairtree.gemspec @@ -1,18 +1,16 @@ - lib = File.expand_path("../lib", __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require "ht/pairtree/version" Gem::Specification.new do |spec| - spec.name = "ht-pairtree" - spec.version = HathiTrust::Pairtree::VERSION - spec.authors = ["Bill Dueber"] - spec.email = ["bill@dueber.com"] - # - spec.summary = %q{Pairtree with extra sugar for the HathiTrust environment} + spec.name = "ht-pairtree" + spec.version = HathiTrust::Pairtree::VERSION + spec.authors = ["Bill Dueber"] + spec.email = ["bill@dueber.com"] + spec.summary = "Pairtree with extra sugar for the HathiTrust environment" # spec.description = %q{TODO: Write a longer description or delete this line.} - spec.homepage = "https://github.com/hathitrust/ht-pairtree" - spec.license = "MIT" + spec.homepage = "https://github.com/hathitrust/ht-pairtree" + spec.license = "MIT" # # # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' # # to allow pushing to a single host or delete this section to allow pushing to any host. @@ -20,7 +18,7 @@ Gem::Specification.new do |spec| spec.metadata["allowed_push_host"] = "https://gems.www.lib.umich.edu" spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage - spec.metadata["changelog_uri"] = spec.homepage + '/CHANGELOG.md' + spec.metadata["changelog_uri"] = spec.homepage + "/CHANGELOG.md" else raise "RubyGems 2.0 or newer is required to protect against " \ "public gem pushes." @@ -28,19 +26,16 @@ Gem::Specification.new do |spec| # Specify which files should be added to the gem when it is released. # The `git ls-files -z` loads the files in the RubyGem that have been added into git. - spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do + spec.files = Dir.chdir(File.expand_path("..", __FILE__)) do `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end - spec.bindir = "exe" - spec.executables = %w[htdir] + spec.bindir = "exe" + spec.executables = %w[htdir] spec.require_paths = ["lib"] - # The 'pairtree' gem seems to be abandoned. Use the mlibrary - # clone - spec.add_dependency 'rpairtree' - - + spec.add_dependency "pairtree", "~> 0.3" spec.add_development_dependency "bundler", "~> 2.0" spec.add_development_dependency "rake", "~> 12.0" spec.add_development_dependency "rspec", "~> 3.0" + spec.add_development_dependency "standardrb" end diff --git a/lib/ht/pairtree.rb b/lib/ht/pairtree.rb index 475e82b..894903a 100644 --- a/lib/ht/pairtree.rb +++ b/lib/ht/pairtree.rb @@ -1,26 +1,24 @@ -# coding: utf-8 require "ht/pairtree/version" # Pairtree is ancient and throws warnings so we suppress them on require oldverbose = $VERBOSE $VERBOSE = nil -require 'pairtree' +require "pairtree" $VERBOSE = oldverbose -require 'fileutils' +require "fileutils" module HathiTrust # A simple Pairtree implementation to make it easier to work with # the HathiTrust pairtree structure. class Pairtree - - class PairtreeError < StandardError; + class PairtreeError < StandardError end class NamespaceDoesNotExist < PairtreeError end - SDR_DATA_ROOT_DEFAULT = (ENV["SDRDATAROOT"] || '/sdr1') + '/obj' + SDR_DATA_ROOT_DEFAULT = (ENV["SDRDATAROOT"] || "/sdr1") + "/obj" # Create a new pairtree object rooted at the given directory # @param [String] root The root of the über-pairtree (takes `ENV['SDRDATAROOT'] + 'obj'` by default) @@ -28,8 +26,6 @@ def initialize(root: SDR_DATA_ROOT_DEFAULT) @root = Pathname.new(root) end - - # Get a Pathname object corresponding to the directory holding data for the given HTID # @param [String] htid The HathiTrust ID for an object # @return [Pathname] Path to the given object's directory @@ -40,58 +36,47 @@ def path_for(htid) alias_method :dir, :path_for alias_method :path_to, :path_for - # Get the underlying pairtree for the given obj. # @param [String] htid The HathiTrust ID for an object + # @param [Boolean] create Whether to create the pairtree if it doesn't exist # @return [Pairtree] the pairtree for that object - def pairtree_for(htid) - pairtree_root(htid) + def pairtree_for(htid, create: false) + pairtree_root(htid, create: create) end - # Create a pairtree for the given htid. Allow namespace creation - # only if told to. + # Create the pairtree directory for the given htid. Allow namespace + # creation only if told to. # @param htid [String] The HTID # @param new_namespace_allowed [Boolean] Whether or not to error if the namespace DNE # @raise [NamespaceDoesNotExist] if namespace DNE and new namespace not allowed # @return [Pairtree::Obj] the underlying pairtree object def create(htid, new_namespace_allowed: false) - if !namespace_exists?(htid) - if new_namespace_allowed - create_namespace_dir(htid) - else - raise NamespaceDoesNotExist.new("Namespace #{namespace(htid)} does not exist") - end + unless namespace_exists?(htid) || new_namespace_allowed + raise NamespaceDoesNotExist.new("Namespace #{namespace(htid)} does not exist") end - pairtree_for(htid).mk(htid) - end - - def namespace_exists?(htid) - namespace_dir(htid).exists? + pairtree_for(htid, create: new_namespace_allowed).mk(htid) end + private - def create_namespace_dir(htid) - ndir = namespace_dir(htid) - return self if Dir.exists?(ndir) - Dir.mkdir(ndir) - File.open(ndir + "pairtree_prefix", 'w:utf-8') {|f| f.print namespace(htid)} - File.open(ndir + "pairtree_version0_1", 'w:utf-8') {|f| } - Dir.mkdir(ndir + "pairtree_root") - return self + def namespace_exists?(htid) + namespace_dir(htid).exist? end - def namespace_dir(htid) @root + namespace(htid) end def namespace(htid) - htid.split('.', 2).first + htid.split(".", 2).first end - def pairtree_root(htid) - ::Pairtree.at(namespace_dir(htid)) + def pairtree_root(htid, create: false) + ::Pairtree.at(namespace_dir(htid), prefix: pairtree_prefix(htid), create: create) end + def pairtree_prefix(htid) + namespace(htid) + "." + end end end diff --git a/lib/ht/pairtree/version.rb b/lib/ht/pairtree/version.rb index 7a84045..8ea11bd 100644 --- a/lib/ht/pairtree/version.rb +++ b/lib/ht/pairtree/version.rb @@ -1,5 +1,5 @@ module HathiTrust - class Pairtree + class Pairtree VERSION = "0.1.0" end end diff --git a/spec/ht/pairtree_spec.rb b/spec/ht/pairtree_spec.rb index 4910b4f..3b1edaf 100644 --- a/spec/ht/pairtree_spec.rb +++ b/spec/ht/pairtree_spec.rb @@ -1,58 +1,69 @@ -require 'pairtree' -require 'fileutils' -require 'tmpdir' - +require "pairtree" +require "fileutils" +require "tmpdir" RSpec.describe HathiTrust::Pairtree do - TMPDIR = Pathname.new(Dir.mktmpdir) - puts TMPDIR - - idmap = { - 'uc1.c3292592' => 'sdr1/obj/uc1/pairtree_root/c3/29/25/92/c3292592', - 'loc.ark:/13960/t9w09k00s' => 'sdr1/obj/loc/pairtree_root/ar/k+/=1/39/60/=t/9w/09/k0/0s/ark+=13960=t9w09k00s', - 'ia.ark:/13960/t9w10cs7x' => 'sdr1/obj/ia/pairtree_root/ar/k+/=1/39/60/=t/9w/10/cs/7x/ark+=13960=t9w10cs7x' - } - + let(:idmap) do + { + "uc1.c3292592" => "sdr1/obj/uc1/pairtree_root/c3/29/25/92/c3292592", + "loc.ark:/13960/t9w09k00s" => "sdr1/obj/loc/pairtree_root/ar/k+/=1/39/60/=t/9w/09/k0/0s/ark+=13960=t9w09k00s", + "ia.ark:/13960/t9w10cs7x" => "sdr1/obj/ia/pairtree_root/ar/k+/=1/39/60/=t/9w/10/cs/7x/ark+=13960=t9w10cs7x" + } + end - before(:context) do + def make_paths(idmap) idmap.values.each do |subdir| - dir = TMPDIR + subdir - puts "Trying to make #{dir}" + dir = @tmpdir + subdir FileUtils.mkdir_p(dir) - root = TMPDIR + Pathname.new(subdir.split('/')[0..2].join('/')) - prefix = subdir.split('/')[2] - File.open(root + 'pairtree_prefix', 'w:utf-8') do |pp| - pp.print(prefix + '.') + root = @tmpdir + Pathname.new(subdir.split("/")[0..2].join("/")) + prefix = subdir.split("/")[2] + File.open(root + "pairtree_prefix", "w:utf-8") do |pp| + pp.print(prefix + ".") end - File.open(root + 'pairtree_version0_1', 'w:utf-8') {} + File.open(root + "pairtree_version0_1", "w:utf-8") {} end end - after(:context) do - FileUtils.rm_f(TMPDIR) + around(:each) do |example| + Dir.mktmpdir do |d| + @tmpdir = Pathname.new(d) + FileUtils.mkdir_p(@tmpdir + "sdr1/obj") + example.run + end end - it "has a version number" do expect(HathiTrust::Pairtree::VERSION).not_to be nil end - - describe "translates names into directories" do - pt = HathiTrust::Pairtree.new(root: TMPDIR + 'sdr1/obj') + it "translates names into directories" do + make_paths(idmap) + pt = HathiTrust::Pairtree.new(root: @tmpdir + "sdr1/obj") idmap.each_pair do |id, dir| - it id do - expect(pt.path_for(id).to_s).to eq((TMPDIR + dir).to_s) - end + expect(pt.path_for(id).to_s).to eq((@tmpdir + dir).to_s) end end - it "Can create new object directories" do - pt = HathiTrust::Pairtree.new(root: TMPDIR + 'sdr1/obj') - expect {pt.create('bill.dueber', create_new_namespace: false)}.to raise_error(Pairtree::PathError) - expect(pt.create('bill.dueber', create_new_namespace: true).exists?('.')).to be true - end + describe "#create" do + let(:pt) { HathiTrust::Pairtree.new(root: @tmpdir + "sdr1/obj") } + + it "can create new object directories in the expected location" do + expect { pt.create("test.something", new_namespace_allowed: false) }.to raise_error(HathiTrust::Pairtree::NamespaceDoesNotExist) + pt_obj = pt.create("test.something", new_namespace_allowed: true) + expect(pt_obj.exist?(".")).to be true + expect(pt_obj.to_path).to eq((@tmpdir + "sdr1/obj/test/pairtree_root/so/me/th/in/g/something").to_s) + end + it "create is idempotent" do + pt.create("test.something", new_namespace_allowed: true) + expect(pt.create("test.something").exist?(".")).to be true + end + + it "new_namespace_allowed is idempotent" do + pt.create("test.something", new_namespace_allowed: true) + expect(pt.create("test.somethingelse", new_namespace_allowed: true).exist?(".")).to be true + end + end end