Class: Protocol::URL::Path
- Inherits:
-
Object
- Object
- Protocol::URL::Path
- Includes:
- Comparable
- Defined in:
- lib/protocol/url/path.rb
Overview
Represents a URL path without losing its encoded segment boundaries.
String input is interpreted as an encoded URL path. A literal / is structural,
while %2F remains encoded data within a single segment. Decoding is explicit and
controlled by the encoding object passed to #components.
Constant Summary collapse
- SEPARATOR =
The path separator.
"/"
Class Method Summary collapse
-
.[](path) ⇒ Object
Coerce an encoded string or encoded segment array into a path.
-
.for(components, encoding: Encoding) ⇒ Object
Construct a path from decoded components.
-
.relative(target, from) ⇒ Object
Calculate the relative path from one absolute path to another.
Instance Method Summary collapse
-
#<=>(other) ⇒ Object
Paths compare by their exact encoded representation.
- #==(other) ⇒ Object
- #absolute? ⇒ Boolean
-
#basename(extension: true) ⇒ Object
The final decoded component.
-
#components(encoding = Encoding) ⇒ Object
Decode the path segments using the given encoding.
- #directory? ⇒ Boolean
- #empty? ⇒ Boolean
- #encoded ⇒ Object (also: #to_s, #to_str)
-
#eql?(other) ⇒ Boolean
Compare this path with another path using exact encoded string identity.
-
#freeze ⇒ Object
Freeze the path and materialize both lossless representations.
- #hash ⇒ Object
-
#initialize(encoded, segments = nil) ⇒ Path
constructor
Initialize a path from either its complete encoded representation or encoded segments.
-
#join(other, pop: true, simplify: true) ⇒ Object
Resolve another path relative to this path.
-
#local_path(root) ⇒ Object
Resolve a URL path beneath a local filesystem root.
-
#parent(level = 1) ⇒ Object
Return a path with its final component removed.
-
#relative(from) ⇒ Object
Calculate this path relative to another path.
- #relative? ⇒ Boolean
- #segments ⇒ Object
-
#simplify ⇒ Object
Return a canonical path by resolving literal or percent-encoded dot segments and repeated separators.
-
#simplify! ⇒ Object
Simplify this path in place by resolving literal or percent-encoded dot segments and repeated separators.
Constructor Details
#initialize(encoded, segments = nil) ⇒ Path
Initialize a path from either its complete encoded representation or encoded segments.
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/protocol/url/path.rb', line 87 def initialize(encoded, segments = nil) if encoded @encoded = -encoded end if encoded.nil? && segments.nil? segments = EMPTY_SEGMENTS elsif segments segments.each do |segment| unless segment.is_a?(String) && !segment.include?(SEPARATOR) raise ArgumentError, "Path contains an invalid encoded segment!" end end segments = segments.map(&:-@).freeze end @segments = segments end |
Class Method Details
.[](path) ⇒ Object
Coerce an encoded string or encoded segment array into a path.
29 30 31 32 33 34 35 36 37 |
# File 'lib/protocol/url/path.rb', line 29 def self.[](path) if path.is_a?(self) return path elsif path.is_a?(Array) return self.new(nil, path) else return self.new(path.to_s) end end |
.for(components, encoding: Encoding) ⇒ Object
Construct a path from decoded components.
Each component is escaped independently, so decoded / characters remain data
inside one encoded segment rather than becoming structural separators.
48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/protocol/url/path.rb', line 48 def self.for(components, encoding: Encoding) segments = components.map do |component| segment = encoding.escape(component) unless segment.is_a?(String) && !segment.include?(SEPARATOR) raise ArgumentError, "Path encoding produced an invalid segment!" end segment end return self.new(nil, segments) end |
.relative(target, from) ⇒ Object
Calculate the relative path from one absolute path to another.
This is useful for generating relative URLs from one location to another, such as creating page-specific import maps or relative links.
78 79 80 |
# File 'lib/protocol/url/path.rb', line 78 def self.relative(target, from) return Path[target].relative(from).to_s end |
Instance Method Details
#<=>(other) ⇒ Object
Paths compare by their exact encoded representation.
203 204 205 206 207 |
# File 'lib/protocol/url/path.rb', line 203 def <=>(other) return nil unless other.is_a?(Path) encoded <=> other.encoded end |
#==(other) ⇒ Object
211 212 213 |
# File 'lib/protocol/url/path.rb', line 211 def ==(other) eql?(other) end |
#absolute? ⇒ Boolean
119 120 121 |
# File 'lib/protocol/url/path.rb', line 119 def absolute? encoded.start_with?(SEPARATOR) end |
#basename(extension: true) ⇒ Object
The final decoded component. A path with a trailing separator has an empty basename.
137 138 139 140 141 142 143 144 145 146 147 |
# File 'lib/protocol/url/path.rb', line 137 def basename(extension: true) component = self.components.last return component if extension || component.nil? if index = component.rindex(".") basename = component[0...index] return basename if basename.b.match?(/[^.]/n) end return component end |
#components(encoding = Encoding) ⇒ Object
Decode the path segments using the given encoding.
The result is not cached because different encoding objects can produce different
component values. In particular, a decoded component may contain / without
changing its boundary in the returned array.
188 189 190 |
# File 'lib/protocol/url/path.rb', line 188 def components(encoding = Encoding) segments.map{|segment| encoding.unescape(segment)} end |
#directory? ⇒ Boolean
129 130 131 |
# File 'lib/protocol/url/path.rb', line 129 def directory? encoded.end_with?(SEPARATOR) end |
#empty? ⇒ Boolean
198 199 200 |
# File 'lib/protocol/url/path.rb', line 198 def empty? encoded.empty? end |
#encoded ⇒ Object Also known as: to_s, to_str
193 194 195 |
# File 'lib/protocol/url/path.rb', line 193 def encoded @encoded ||= @segments.join(SEPARATOR).freeze end |
#eql?(other) ⇒ Boolean
Compare this path with another path using exact encoded string identity.
218 219 220 |
# File 'lib/protocol/url/path.rb', line 218 def eql?(other) other.is_a?(Path) && encoded.eql?(other.encoded) end |
#freeze ⇒ Object
Freeze the path and materialize both lossless representations.
109 110 111 112 113 114 115 116 |
# File 'lib/protocol/url/path.rb', line 109 def freeze return self if frozen? self.segments self.encoded return super end |
#hash ⇒ Object
223 224 225 |
# File 'lib/protocol/url/path.rb', line 223 def hash encoded.hash end |
#join(other, pop: true, simplify: true) ⇒ Object
Resolve another path relative to this path.
287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 |
# File 'lib/protocol/url/path.rb', line 287 def join(other, pop: true, simplify: true) other = Path[other] return self if other.empty? if other.absolute? return simplify ? other.simplify : other end segments = self.segments.dup # RFC2396 Section 5.2: # 6) a) All but the last segment of the base URI's path component is # copied to the buffer. In other words, any characters after the # last (right-most) slash character, if any, are excluded. if pop and dot_segment(segments.last) != ".." segments.pop elsif segments.last == "" segments.pop end segments.concat(other.segments) if simplify simplify_segments!(segments) end return Path.new(nil, segments) end |
#local_path(root) ⇒ Object
Resolve a URL path beneath a local filesystem root.
Each decoded URL component must map to exactly one local path component. Components
containing NUL or a platform path separator cannot be represented and are rejected.
Absolute URL paths are interpreted relative to root, not the filesystem root.
This establishes lexical containment only. It does not resolve symbolic links or prevent filesystem races while a returned path is subsequently opened.
239 240 241 242 243 244 245 246 247 248 249 250 |
# File 'lib/protocol/url/path.rb', line 239 def local_path(root) root = File.(root) root_prefix = root.end_with?(File::SEPARATOR) ? root : root + File::SEPARATOR components = self.components(Encoding::System) components.shift if components.first == "" path = File.(File.join(root, *components)) return path if path == root || path.start_with?(root_prefix) raise ArgumentError, "Path escapes the specified root!" end |
#parent(level = 1) ⇒ Object
Return a path with its final component removed.
The empty path and absolute root are their own parents. For a directory path, this removes the trailing empty component which represents its separator.
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/protocol/url/path.rb', line 157 def parent(level = 1) unless level.is_a?(Integer) && level >= 0 raise ArgumentError, "Path parent level must be a non-negative integer!" end segments = self.segments return self if level == 0 || segments.empty? || segments == ROOT_SEGMENTS remaining = segments.size - level if absolute? segments = remaining <= 1 ? ROOT_SEGMENTS : segments.first(remaining) else segments = remaining <= 0 ? EMPTY_SEGMENTS : segments.first(remaining) end return self.class.new(nil, segments) end |
#relative(from) ⇒ Object
Calculate this path relative to another path.
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 |
# File 'lib/protocol/url/path.rb', line 320 def relative(from) target_segments = self.segments from_segments = Path[from].segments # Remove the last component from 'from' to get the directory from_segments = from_segments[0...-1] if from_segments.size > 0 # Find the common prefix common_length = 0 [target_segments.size, from_segments.size].min.times do |i| break if target_segments[i] != from_segments[i] common_length = i + 1 end # Calculate how many levels to go up up_levels = from_segments.size - common_length # Build the relative path segments relative_segments = [".."] * up_levels + target_segments[common_length..-1] return Path.new(nil, relative_segments) end |
#relative? ⇒ Boolean
124 125 126 |
# File 'lib/protocol/url/path.rb', line 124 def relative? !absolute? end |
#segments ⇒ Object
176 177 178 |
# File 'lib/protocol/url/path.rb', line 176 def segments @segments ||= @encoded.split(SEPARATOR, -1).map!(&:-@).freeze end |
#simplify ⇒ Object
Return a canonical path by resolving literal or percent-encoded dot segments and repeated separators.
Absolute paths do not retain parent components above the root. Relative paths retain leading parent components which cannot be resolved locally.
274 275 276 277 278 279 |
# File 'lib/protocol/url/path.rb', line 274 def simplify segments = simplify_segments return self unless segments return self.class.new(nil, segments) end |
#simplify! ⇒ Object
Simplify this path in place by resolving literal or percent-encoded dot segments and repeated separators.
258 259 260 261 262 263 264 265 266 |
# File 'lib/protocol/url/path.rb', line 258 def simplify! simplified = simplify return nil if simplified.equal?(self) @encoded = simplified.encoded @segments = simplified.segments return self end |