Class: Utopia::Path

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

Overview

Represents an application path as a traversal through a tree.

Each component names a node and / represents the edge between adjacent nodes. A leading empty component anchors the traversal at the root, while a trailing empty component preserves an explicit final edge and denotes a directory:

  • ["foo", "bar"] represents foo/bar.
  • ["", "foo", "bar"] represents /foo/bar.
  • ["", "foo", "bar", ""] represents /foo/bar/.

The structural root is represented by [""] and contains no traversed edge. It is intentionally distinct from parsing /, which preserves the explicit edge as ["", ""]. Both serialize as /, but they retain different structural representations. In particular, the structural root maps to an empty local path so it can be resolved relative to an application root.

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.



25
26
27
# File 'lib/utopia/path.rb', line 25

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

Instance Attribute Details

#componentsObject

Returns the value of attribute components.



29
30
31
# File 'lib/utopia/path.rb', line 29

def components
  @components
end

Class Method Details

.[](path) ⇒ Object

Coerce the given value into a path.



97
98
99
# File 'lib/utopia/path.rb', line 97

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

.create(path) ⇒ Object

Coerce a value into a path.



141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/utopia/path.rb', line 141

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.



134
135
136
# File 'lib/utopia/path.rb', line 134

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.



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

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

.load(value) ⇒ Object

Load a path from its serialized form.



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

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.



57
58
59
# File 'lib/utopia/path.rb', line 57

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

.rootObject

Construct the structural root path without an explicit trailing separator.



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

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

.shortest_path(path, root) ⇒ Object

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



65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/utopia/path.rb', line 65

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.



104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/utopia/path.rb', line 104

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.



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

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.



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

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



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

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.



453
454
455
# File 'lib/utopia/path.rb', line 453

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

#==(other) ⇒ Object

Compare this object with another object.



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

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.



497
498
499
# File 'lib/utopia/path.rb', line 497

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 #[].



505
506
507
# File 'lib/utopia/path.rb', line 505

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

#absolute?Boolean

Check whether this path is absolute.

Returns:

  • (Boolean)


200
201
202
# File 'lib/utopia/path.rb', line 200

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

#ascend(&block) ⇒ Object

Enumerate paths from this path up to its first component.



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

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



370
371
372
373
374
# File 'lib/utopia/path.rb', line 370

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.



512
513
514
# File 'lib/utopia/path.rb', line 512

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

#descend(&block) ⇒ Object

Enumerate paths from the first component down to this path.



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

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)


172
173
174
# File 'lib/utopia/path.rb', line 172

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

#dirname(count = 1) ⇒ Object

Remove trailing path components.



386
387
388
389
390
# File 'lib/utopia/path.rb', line 386

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.



446
447
448
# File 'lib/utopia/path.rb', line 446

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

#empty?Boolean

Check whether this path has no components.

Returns:

  • (Boolean)


43
44
45
# File 'lib/utopia/path.rb', line 43

def empty?
	@components.empty?
end

#eql?(other) ⇒ Boolean

Check whether this object is equivalent to another object.

Returns:

  • (Boolean)


460
461
462
# File 'lib/utopia/path.rb', line 460

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.



260
261
262
# File 'lib/utopia/path.rb', line 260

def expand(root)
	root + self
end

#extensionObject



377
378
379
380
381
# File 'lib/utopia/path.rb', line 377

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)


178
179
180
# File 'lib/utopia/path.rb', line 178

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

#firstObject

Return the first path component, excluding the root marker.



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

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

#freezeObject

Freeze this object and its internal state.



33
34
35
36
37
38
39
# File 'lib/utopia/path.rb', line 33

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

#hashObject

Compute the hash value for this object.



466
467
468
# File 'lib/utopia/path.rb', line 466

def hash
	@components.hash
end

#include?(*arguments) ⇒ Boolean

Check whether this collection includes the given value.

Returns:

  • (Boolean)


166
167
168
# File 'lib/utopia/path.rb', line 166

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

#join(other) ⇒ Object



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

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.



352
353
354
355
356
# File 'lib/utopia/path.rb', line 352

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

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

Format this path using a local filesystem separator.



395
396
397
# File 'lib/utopia/path.rb', line 395

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.



362
363
364
365
366
367
# File 'lib/utopia/path.rb', line 362

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)


194
195
196
# File 'lib/utopia/path.rb', line 194

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

#replace(other_path) ⇒ Object

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



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

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.



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

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

#simplifyObject

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



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
337
338
# File 'lib/utopia/path.rb', line 309

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.



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

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)


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

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.



242
243
244
# File 'lib/utopia/path.rb', line 242

def to_a
	@components
end

#to_absoluteObject

Convert this path to an absolute path.



206
207
208
209
210
211
212
# File 'lib/utopia/path.rb', line 206

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

#to_directoryObject

Convert this path to a directory path.



184
185
186
187
188
189
190
# File 'lib/utopia/path.rb', line 184

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

#to_strObject Also known as: to_s

Convert this object to a string.



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

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

#to_url_pathObject

Encode this application path as a URL path.



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

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.



286
287
288
# File 'lib/utopia/path.rb', line 286

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