# frozen_string_literal: true module Stashify # A common abstraction for interacting with directories. All methods # that need to interact with directories are assumed to adhere to # the public methods defined here. Specifically, the methods # {#find}, {#write}, {#delete}, and {#files} are guaranteed to exist # and behave in a way that is consistent across all gems. Unless # called out separately, documentation for those methods here will # hold true of any implementations of this class. class Directory # Provides the files and subdirectories of this directory. In the # base class, this is implemented as an attribute which defaults # to an empty list, but most implementations will override this # with a method. In most implementations, the performance cost of # reading all of thie names in a directory to construct these # objects is high enough that we only want to pay it if it's # actually needed. # # @return [Array] Returns # an Enumerable of Stashify::File and Stashify::Directory # objects. attr_reader :files # The name of the directory. It is everything that follows the # final "/" in the {#path}. This is always guaranteed to be # populated. attr_reader :name # The full path to the directory this represents. Anything after the # final "/" will also be returned from {#name}. This is not # necessarily guaranteed to be populated, but usually will be. attr_reader :path # Basic information associated with a directory that is necessary # to enable memory-based interactions. # # @param name [String not containing a "/"] The name of the file. Either this or path must be defined. # @param path [String] The path of the file, will populate name with everything following the final "/". # @param files An array of Stashify::File and Stashify::Directory # objects representing the contents of this directory. def initialize(name: nil, path: nil, files: []) raise StandardError, "name or path must be defined" unless name || path @path = path @name = name || ::File.basename(path) @files = files end # Look up the item in this directory represented by the provided # name. # # For those looking to implement this method, it's typically more # effective to override {#directory?}, {#directory}, {#exists?} # and {#file}. Unless there are performance concerns with calling # those, the default implementation will work pretty well. # # @param name [String with no "/"] The name of the desired item in # this directory. # # @return Either a Stashify::File or Stashify::Directory object, # depending on what that name represents. def find(name) if directory?(name) directory(name) elsif exists?(name) file(name) end end # Write the provided item into the directory. If the item is a # directory itself, then all of the contents will be copied. # # For those looking to implement this method, it's typically # easier to implement {#write_file} and {#write_directory}. This # helps you avoid having to know what type of object you're # dealing with. # # @param item Either a Stashify::File or Stashify::Directory # object. Note that these can be any implementation of these # base classes, it's not limited to the classes from the same # provider. def write(item) if item.is_a?(Stashify::Directory) write_directory(item) else write_file(item) end end # Writes the provided directory. Typically you will want to # interact with this functionality through {#write} rather than # directly, as it protects you from errors related to accidentally # passing a Stashify::File value in. It is primarily implemented # separately to give a more specific hook for various # implementations. # # The default implementation might work for you, as it iterates # through {#files} and writes them to the new subdirectory. def write_directory(directory) subdir = self.directory(directory.name) directory.files.each { |file| subdir.write(file) } end # Writes the provided file. Typically you will want to interact # with this functionality through {#write} rather than directly, # as it protects you from errors related to accidentally passing a # Stashify::Directory value in. It is primarily implemented # separately to give a more specific hook for various # implementations. def write_file(file) file(file.name).write(file.contents) end # Delete provided name from the directory. If the item is a # directory itself, then all of the contents will be copied. # # For those looking to implement this method, it's typically # easier to implement {#directory?}, {#delete_directory} and # {#delete_file}. The primary reason to override this method would # be for performance reasons. # # @param name [String] Name of the item to be deleted. def delete(name) if directory?(name) delete_directory(name) else delete_file(name) end end # Deletes the provided directory name. Typically you will want to # interact with this functionality through {#delete} rather than # directly, as it protects you from errors related to accidentally # asking to delete a file as a directory. def delete_directory(name) subdir = directory(name) subdir.files.each { |file| subdir.delete(file.name) } end # Deletes the provided file name. Typically you will want to # interact with this functionality through {#delete} rather than # directly, as it protects you from errors related to accidentally # asking to delete a directory as a file. def delete_file(name) file(name).delete end # Two directories are equal if their files are equal. This is # distinct from being the same directory, which is served by the # {#eql?} method. def ==(other) files == other.files end # This answers if the two directories are the same, which is # usually more specific than you want. If you wish to determine if # all of the files are equal, consider {#==} instead.. def eql?(other) self.class == other.class && name == other.name && path == other.path end # @return [Stashify::File] Return an object representing a single # file in this directory. def file(name) Stashify::File.new(path: path_of(name)) end # The full path to the item in this directory provided by the # names. Any number of names can be provided, allowing arbitrarily # deep paths to be constructed below this directory. def path_of(*names) ::File.join(path, *names) end end end