Class: Utopia::Path

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/utopia/path.rb,
lib/utopia/path/matcher.rb

Overview

Represents a path as an array of path components. Useful for efficient URL manipulation.

Defined Under Namespace

Classes: Matcher

Constant Summary collapse

SEPARATOR =
"/"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(components = []) ⇒ Path

Initialize a path from its individual components.



17
18
19
# File 'lib/utopia/path.rb', line 17

def initialize(components = [])
	@components = components
end

Instance Attribute Details

#componentsObject

Returns the value of attribute components.



21
22
23
# File 'lib/utopia/path.rb', line 21

def components
  @components
end

Class Method Details

.[](path) ⇒ Object

Coerce the given value into a path.



89
90
91
# File 'lib/utopia/path.rb', line 89

def self.[] path
	self.create(path)
end

.create(path) ⇒ Object

Coerce a value into a path.



133
134
135
136
137
138
139
140
141
142
143
144
145
146
# File 'lib/utopia/path.rb', line 133

def self.create(path)
	case path
	when Path
		return path
	when Array
		return self.new(path)
	when String
		return self.new(unescape(path).split(SEPARATOR, -1))
	when nil
		return nil
	else
		return self.new([path])
	end
end

.dump(instance) ⇒ Object

Serialize a path.



126
127
128
# File 'lib/utopia/path.rb', line 126

def self.dump(instance)
	instance.to_s if instance
end

.from_string(string) ⇒ Object

Construct a path from URL-encoded text. This is an optimized direct entry point used by controller invocations.



112
113
114
# File 'lib/utopia/path.rb', line 112

def self.from_string(string)
	self.new(unescape(string).split(SEPARATOR, -1))
end

.load(value) ⇒ Object

Load a path from its serialized form.



119
120
121
# File 'lib/utopia/path.rb', line 119

def self.load(value)
	from_string(value) if value
end

.prefix_length(a, b) ⇒ Object

Compute the number of leading components shared by two sequences.



49
50
51
# File 'lib/utopia/path.rb', line 49

def self.prefix_length(a, b)
	[a.size, b.size].min.times{|i| return i if a[i] != b[i]}
end

.rootObject

Construct the root path.



41
42
43
# File 'lib/utopia/path.rb', line 41

def self.root
	self.new([""])
end

.shortest_path(path, root) ⇒ Object

Compute the shortest relative path from the containing directory of root to path.



57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/utopia/path.rb', line 57

def self.shortest_path(path, root)
	path = self.create(path)
	root = self.create(root).dirname
	
	# 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 self.create([".."] * up + path.components[i..-1])
end

.split(path) ⇒ Object

Convert a path value into an array of components.



96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/utopia/path.rb', line 96

def self.split(path)
	case path
	when Path
		return path.to_a
	when Array
		return path
	when String
		create(path).to_a
	else
		[path]
	end
end

.unescape(string) ⇒ Object

Decode URL-encoded path content, converting + to whitespace and percent-encoded bytes to their corresponding characters.



80
81
82
83
84
# File 'lib/utopia/path.rb', line 80

def self.unescape(string)
	string.tr("+", " ").gsub(/((?:%[0-9a-fA-F]{2})+)/n) do
		[$1.delete("%")].pack("H*")
	end
end

Instance Method Details

#+(other) ⇒ Object

Append path components and return the resulting path.



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/utopia/path.rb', line 265

def +(other)
	if other.kind_of? Path
		if other.absolute?
			return other
		else
			return join(other.components)
		end
	elsif other.kind_of? Array
		return join(other)
	elsif other.kind_of? String
		return join(other.split(SEPARATOR, -1))
	else
		return join([other.to_s])
	end
end

#-(other) ⇒ Object

Computes the difference of the path. /a/b/c - /a/b -> c a/b/c - a/b -> c



293
294
295
296
297
298
299
300
301
302
303
# File 'lib/utopia/path.rb', line 293

def -(other)
	i = 0
	
	while i < other.components.size
		break if @components[i] != other.components[i]
		
		i += 1
	end
	
	return self.class.new(@components[i,@components.size])
end

#<=>(other) ⇒ Object

Compare this object with another object.



451
452
453
# File 'lib/utopia/path.rb', line 451

def <=> other
	@components <=> other.components
end

#==(other) ⇒ Object

Compare this object with another object.



471
472
473
474
475
476
477
478
479
# File 'lib/utopia/path.rb', line 471

def == other
	return false unless other
	
	case other
	when String then self.to_s == other
	when Array then self.to_a == other
	else other.is_a?(self.class) && @components == other.components
	end
end

#[](index) ⇒ Object

Fetch one or more path components, excluding root and directory markers from indexing.



495
496
497
# File 'lib/utopia/path.rb', line 495

def [] index
	return @components[component_offset(index)]
end

#[]=(index, value) ⇒ Object

Replace one or more path components using the same root- and directory-marker-aware indexing as #[].



503
504
505
# File 'lib/utopia/path.rb', line 503

def []= index, value
	return @components[component_offset(index)] = value
end

#absolute?Boolean

Check whether this path is absolute.

Returns:

  • (Boolean)


192
193
194
# File 'lib/utopia/path.rb', line 192

def absolute?
	@components.first == ""
end

#ascend(&block) ⇒ Object

Enumerate paths from this path up to its first component.



415
416
417
418
419
420
421
422
423
424
425
# File 'lib/utopia/path.rb', line 415

def ascend(&block)
	return to_enum(:ascend) unless block_given?
	
	components = self.components.dup
	
	while components.any?
		yield self.class.new(components.dup)
		
		components.pop
	end
end

#basenameObject



368
369
370
371
372
# File 'lib/utopia/path.rb', line 368

def basename
	basename, _ = @components.last.split(".", 2)
	
	return basename || ""
end

#delete_at(index) ⇒ Object

Delete a path component, excluding root and directory markers from indexing.



510
511
512
# File 'lib/utopia/path.rb', line 510

def delete_at(index)
	@components.delete_at(component_offset(index))
end

#descend(&block) ⇒ Object

Enumerate paths from the first component down to this path.



400
401
402
403
404
405
406
407
408
409
410
# File 'lib/utopia/path.rb', line 400

def descend(&block)
	return to_enum(:descend) unless block_given?
	
	components = []
	
	@components.each do |component|
		components << component
		
		yield self.class.new(components.dup)
	end
end

#directory?Boolean

Check whether this path denotes a directory.

Returns:

  • (Boolean)


164
165
166
# File 'lib/utopia/path.rb', line 164

def directory?
	return @components.last == ""
end

#dirname(count = 1) ⇒ Object

Remove trailing path components.



384
385
386
387
388
# File 'lib/utopia/path.rb', line 384

def dirname(count = 1)
	path = self.class.new(@components[0...-count])
	
	return absolute? ? path.to_absolute : path
end

#dupObject

Copy this path and its component array.



444
445
446
# File 'lib/utopia/path.rb', line 444

def dup
	return Path.new(components.dup)
end

#empty?Boolean

Check whether this path has no components.

Returns:

  • (Boolean)


35
36
37
# File 'lib/utopia/path.rb', line 35

def empty?
	@components.empty?
end

#eql?(other) ⇒ Boolean

Check whether this object is equivalent to another object.

Returns:

  • (Boolean)


458
459
460
# File 'lib/utopia/path.rb', line 458

def eql? other
	self.class.eql?(other.class) and @components.eql?(other.components)
end

#expand(root) ⇒ Object

Resolve this path relative to a root path.



258
259
260
# File 'lib/utopia/path.rb', line 258

def expand(root)
	root + self
end

#extensionObject



375
376
377
378
379
# File 'lib/utopia/path.rb', line 375

def extension
	_, extension = @components.last.split(".", 2)
	
	return extension
end

#file?Boolean Also known as: last?

Check whether this path denotes a file.

Returns:

  • (Boolean)


170
171
172
# File 'lib/utopia/path.rb', line 170

def file?
	return @components.last != ""
end

#firstObject

Return the first path component, excluding the root marker.



340
341
342
343
344
345
346
# File 'lib/utopia/path.rb', line 340

def first
	if absolute?
		@components[1]
	else
		@components[0]
	end
end

#freezeObject

Freeze this object and its internal state.



25
26
27
28
29
30
31
# File 'lib/utopia/path.rb', line 25

def freeze
	return self if frozen?
	
	@components.freeze
	
	super
end

#hashObject

Compute the hash value for this object.



464
465
466
# File 'lib/utopia/path.rb', line 464

def hash
	@components.hash
end

#include?(*arguments) ⇒ Boolean

Check whether this collection includes the given value.

Returns:

  • (Boolean)


158
159
160
# File 'lib/utopia/path.rb', line 158

def include?(*arguments)
	@components.include?(*arguments)
end

#join(other) ⇒ Object



246
247
248
249
250
251
252
253
# File 'lib/utopia/path.rb', line 246

def join(other)
	# Check whether other is an absolute path:
	if other.first == ""
		self.class.new(other)
	else
		self.class.new(@components + other).simplify
	end
end

#lastObject

Return the last path component, excluding the root marker.



350
351
352
353
354
# File 'lib/utopia/path.rb', line 350

def last
	if @components != [""]
		@components.last
	end
end

#local_path(separator = File::SEPARATOR) ⇒ Object

Format this path using a local filesystem separator.



393
394
395
# File 'lib/utopia/path.rb', line 393

def local_path(separator = File::SEPARATOR)
	@components.join(separator)
end

#popObject

Remove the last path component without converting the root path to a relative path.



360
361
362
363
364
365
# File 'lib/utopia/path.rb', line 360

def pop
	# We don't want to convert an absolute path to a relative path.
	if @components != [""]
		@components.pop
	end
end

#relative?Boolean

Check whether this path is relative.

Returns:

  • (Boolean)


186
187
188
# File 'lib/utopia/path.rb', line 186

def relative?
	@components.first != ""
end

#replace(other_path) ⇒ Object

Replace this path's components with a copy of another path's components.



151
152
153
# File 'lib/utopia/path.rb', line 151

def replace(other_path)
	@components = other_path.components.dup
end

#shortest_path(root) ⇒ Object

Compute the shortest relative path from the containing directory of root to this path.



73
74
75
# File 'lib/utopia/path.rb', line 73

def shortest_path(root)
	self.class.shortest_path(self, root)
end

#simplifyObject

Normalize current-directory, parent-directory, and repeated-separator components.



307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/utopia/path.rb', line 307

def simplify
	components = []
	
	index = 0
	
	if @components[0] == ""
		components << ""
		index += 1
	end
	
	while index < @components.size
		bit = @components[index]
		if bit == "."
			# No-op (ignore current directory)
		elsif bit == "" && index != @components.size - 1
			# No-op (ignore multiple slashes)
		elsif bit == ".." && components.last && components.last != ".."
			if components.last != ""
				# We can go up one level:
				components.pop
			end
		else
			components << bit
		end
		
		index += 1
	end
	
	return self.class.new(components)
end

#split(at) ⇒ Object

Split this path around a component or component index.



430
431
432
433
434
435
436
437
438
439
440
# File 'lib/utopia/path.rb', line 430

def split(at)
	if at.kind_of?(String)
		at = @components.index(at)
	end
	
	if at
		return [self.class.new(@components[0...at]), self.class.new(@components[at+1..-1])]
	else
		return nil
	end
end

#start_with?(other) ⇒ Boolean

Check whether this path starts with the given path.

Returns:

  • (Boolean)


484
485
486
487
488
489
490
# File 'lib/utopia/path.rb', line 484

def start_with? other
	other.components.each_with_index do |part, index|
		return false if @components[index] != part
	end
	
	return true
end

#to_aObject

Convert this path to an array of components.



240
241
242
# File 'lib/utopia/path.rb', line 240

def to_a
	@components
end

#to_absoluteObject

Convert this path to an absolute path.



198
199
200
201
202
203
204
# File 'lib/utopia/path.rb', line 198

def to_absolute
	if absolute?
		return self
	else
		return self.class.new([""] + @components)
	end
end

#to_directoryObject

Convert this path to a directory path.



176
177
178
179
180
181
182
# File 'lib/utopia/path.rb', line 176

def to_directory
	if directory?
		return self
	else
		return self.class.new(@components + [""])
	end
end

#to_relative!Object

Remove the first component when this path is relative.



208
209
210
# File 'lib/utopia/path.rb', line 208

def to_relative!
	@components.shift if relative?
end

#to_strObject Also known as: to_s

Convert this object to a string.



214
215
216
217
218
219
220
# File 'lib/utopia/path.rb', line 214

def to_str
	if @components == [""]
		SEPARATOR
	else
		@components.join(SEPARATOR)
	end
end

#to_url_pathObject

Encode this application path as a URL path.



226
227
228
229
230
231
232
233
234
235
236
# File 'lib/utopia/path.rb', line 226

def to_url_path
	# Preserve Utopia's compact representation of the absolute root:
	if @components == [""]
		return Protocol::URL::Path[SEPARATOR]
	end
	
	return Protocol::URL::Path.for(
		@components,
		encoding: Protocol::URL::Encoding::System,
	)
end

#with_prefix(*arguments) ⇒ Object

Prepend a path to this path.



284
285
286
# File 'lib/utopia/path.rb', line 284

def with_prefix(*arguments)
	self.class.create(*arguments) + self
end