lib/build/files/path.rb



# Copyright, 2014, by Samuel G. D. Williams. <http://www.codeotaku.com>
# 
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

module Build
	module Files
		# Represents a file path with an absolute root and a relative offset:
		class Path
			def self.split(path)
				# Effectively dirname and basename:
				dirname, separator, filename = path.rpartition(File::SEPARATOR)
				filename, dot, extension = filename.rpartition('.')
				
				return dirname + separator, filename, dot + extension
			end
			
			# Returns the length of the prefix which is shared by two strings.
			def self.prefix_length(a, b)
				[a.size, b.size].min.times{|i| return i if a[i] != b[i]}
			end
			
			# Returns a list of components for a path, either represented as a Path instance or a String.
			def self.components(path)
				if Path === path
					path.components
				else
					path.split(File::SEPARATOR)
				end
			end
			
			# Return the shortest relative path to get to path from root. Root should be a directory with which you are computing the relative path.
			def self.shortest_path(path, root)
				path_components = Path.components(path)
				root_components = Path.components(root)
				
				# Find the common prefix:
				i = prefix_length(path_components, root_components) || 0
				
				# The difference between the root path and the required path, taking into account the common prefix:
				up = root_components.size - i
				
				return File.join([".."] * up + path_components[i..-1])
			end
			
			def self.relative_path(root, full_path)
				relative_offset = root.length
				
				# Deal with the case where the root may or may not end with the path separator:
				relative_offset += 1 unless root.end_with?(File::SEPARATOR)
				
				return full_path.slice(relative_offset..-1)
			end
			
			def self.[] path
				self === path ? path : self.new(path.to_s)
			end
			
			# Both paths must be full absolute paths, and path must have root as an prefix.
			def initialize(full_path, root = nil, relative_path = nil)
				# This is the object identity:
				@full_path = full_path
				
				if root
					@root = root
					@relative_path = relative_path
				else
					# Effectively dirname and basename:
					@root, _, @relative_path = full_path.rpartition(File::SEPARATOR)
				end
				
				# This improves the cost of hash/eql? slightly but the root cannot be deconstructed if it was an instance of Path.
				# @root = @root.to_s
			end
			
			attr :root
			attr :full_path
			
			def length
				@full_path.length
			end
			
			def components
				@components ||= @full_path.split(File::SEPARATOR).freeze
			end
			
			def basename
				self.parts.last
			end
			
			def start_with?(*args)
				@full_path.start_with?(*args)
			end
			
			alias parts components
			
			def relative_path
				@relative_path ||= Path.relative_path(@root.to_s, @full_path).freeze
			end
			
			def relative_parts
				dirname, _, basename = self.relative_path.rpartition(File::SEPARATOR)
				
				return dirname, basename
			end
			
			def append(extension)
				self.class.new(@full_path + extension, @root)
			end
			
			# Add a path component to the current path.
			# @param path [String, nil] (Optionally) the path to append.
			def +(path)
				if path
					self.class.new(File.join(@full_path, path), @root)
				else
					self
				end
			end
			
			# Use the current path to define a new root, with an optional sub-path.
			# @param path [String, nil] (Optionally) the path to append.
			def /(path)
				if path
					self.class.new(File.join(self, path), self)
				else
					self.class.new(self, self)
				end
			end
			
			def rebase(root)
				self.class.new(File.join(root, relative_path), root)
			end
			
			def with(root: @root, extension: nil, basename: false)
				relative_path = self.relative_path
				
				if basename
					dirname, filename, _ = self.class.split(relative_path)
					
					# Replace the filename if the basename is supplied:
					filename = basename if basename.is_a? String
					
					relative_path = dirname + filename
				end
				
				if extension
					relative_path = relative_path + extension
				end
				
				self.class.new(File.join(root, relative_path), root, relative_path)
			end
			
			def self.join(root, relative_path)
				self.new(File.join(root, relative_path), root)
			end
			
			# Expand a subpath within a given root, similar to `File.expand_path`
			def self.expand(subpath, root = Dir.getwd)
				if subpath.start_with? File::SEPARATOR
					self.new(subpath)
				else
					self.join(root, subpath)
				end
			end
			
			def shortest_path(root)
				self.class.shortest_path(self, root)
			end
			
			def to_str
				@full_path
			end
			
			def to_path
				@full_path
			end
			
			def to_s
				@full_path
			end
			
			def inspect
				"#{@root.inspect}/#{relative_path.inspect}"
			end
			
			def hash
				[@root, @full_path].hash
			end
			
			def eql?(other)
				self.class.eql?(other.class) and @root.eql?(other.root) and @full_path.eql?(other.full_path)
			end
			
			include Comparable
			
			def <=>(other)
				self.to_s <=> other.to_s
			end
			
			# Match a path with a given pattern, using `File#fnmatch`.
			def match(pattern, flags = 0)
				path = pattern.start_with?('/') ? full_path : relative_path
				
				return File.fnmatch(pattern, path, flags)
			end
			
			def for_reading
				[@full_path, File::RDONLY]
			end
			
			def for_writing
				[@full_path, File::CREAT|File::TRUNC|File::WRONLY]
			end
			
			def for_appending
				[@full_path, File::CREAT|File::APPEND|File::WRONLY]
			end
		end
	end
end