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
			# 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:
			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
			
			def components
				@components ||= @full_path.split(File::SEPARATOR)
			end
			
			def basename
				self.components.last
			end
			
			# Ensure the path has an absolute root if it doesn't already:
			def to_absolute(root)
				if @root == "."
					self.rebase(root)
				else
					self
				end
			end
			
			attr :root
			attr :full_path
			
			def length
				@full_path.length
			end
			
			def parts
				@parts ||= @full_path.split(File::SEPARATOR)
			end
			
			def relative_path
				@relative_path ||= Path.relative_path(@root.to_s, @full_path)
			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
			
			def +(path)
				self.class.new(File.join(@full_path, path), @root)
			end
			
			# Define a new root with a sub-path:
			def /(path)
				self.class.new(File.join(self, path), self)
			end
			
			def rebase(root)
				self.class.new(File.join(root, relative_path), root)
			end
			
			def with(root: @root, extension: nil)
				# self.relative_path should be a string so using + to add an extension should be fine.
				relative_path = extension ? self.relative_path + extension : self.relative_path
				
				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
			
			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
			
			def ==(other)
				self.to_s == other.to_s
			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