Class: Protocol::URL::Reference

Inherits:
Relative
  • Object
show all
Includes:
Comparable
Defined in:
lib/protocol/url/reference.rb

Overview

Represents a "Hypertext Reference", which may include a path, query string, fragment, and user parameters.

This class is designed to be easy to manipulate and combine URL references, following the rules specified in RFC2396, while supporting standard URL encoded parameters.

Use Reference.parse for external/untrusted data, and new for constructing references from known good values.

Instance Attribute Summary collapse

Attributes inherited from Relative

#The fragment identifier., #The path component of the URL., #The query string component., #fragment, #path, #query

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Relative

#<=>, #==, #===, #as_json, #equal?, #hash, #inspect, #normalize!, #to_json, #to_local_path, #to_s

Constructor Details

#initialize(path = "/", query = nil, fragment = nil, parameters = nil) ⇒ Reference

Initialize the reference with raw, unescaped values.

Examples:

Create a reference with parameters.

reference = Reference.new("/search", nil, nil, {"query" => "ruby", "limit" => "10"})
reference.to_s  # => "/search?query=ruby&limit=10"


101
102
103
104
# File 'lib/protocol/url/reference.rb', line 101

def initialize(path = "/", query = nil, fragment = nil, parameters = nil)
	super(path, query, fragment)
	@parameters = parameters
end

Instance Attribute Details

#parametersObject (readonly)

Returns the value of attribute parameters.



107
108
109
# File 'lib/protocol/url/reference.rb', line 107

def parameters
  @parameters
end

Class Method Details

.[](value, parameters = nil) ⇒ Object

Coerce a value into a Protocol::URL::Reference instance.

This method provides flexible conversion from various types into a Protocol::URL::Reference. When given a String, it parses the URL-encoded path, query, and fragment components and unescapes them for internal storage. When given a Protocol::URL::Relative, it converts the encoded values to unescaped form suitable for Protocol::URL::Reference instances.

Examples:

Coerce a string with path, query, and fragment.

reference = Reference["/search?q=ruby#results"]
reference.path      # => "/search"
reference.query     # => "q=ruby"
reference.fragment  # => "results"

Coerce with additional parameters.

reference = Reference["/search", {"limit" => "10"}]
reference.to_s  # => "/search?limit=10"

Coerce a Relative instance.

relative = Relative.new("/path%20with%20spaces", nil, "top")
reference = Reference[relative]
reference.path  # => "/path with spaces"


49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/protocol/url/reference.rb', line 49

def self.[](value, parameters = nil)
	case value
	when String
		if match = value.match(PATTERN)
			path = match[:path]
			query = match[:query]
			fragment = match[:fragment]
			
			# Unescape path and fragment for user-friendly internal storage:
			# Query strings are kept as-is since they contain = and & syntax
			path = Encoding.unescape(path) if path && !path.empty?
			fragment = Encoding.unescape(fragment) if fragment
			
			self.new(path, query, fragment, parameters)
		else
			raise ArgumentError, "Invalid URL (contains whitespace or control characters): #{value.inspect}"
		end
	when Relative
		# Relative stores encoded values, so we need to unescape them for Reference:
		path = value.path
		fragment = value.fragment
		
		path = Encoding.unescape(path) if path && !path.empty?
		fragment = Encoding.unescape(fragment) if fragment
		
		self.new(path, value.query, fragment, parameters)
	when nil
		nil
	else
		raise ArgumentError, "Cannot coerce #{value.inspect} to Reference!"
	end
end

.parse(value = "/", parameters = nil) ⇒ Object

Examples:

Parse a path with query and fragment.

reference = Reference.parse("/search?query=ruby#results")
reference.path      # => "/search"
reference.query     # => "query=ruby"
reference.fragment  # => "results"


87
88
89
# File 'lib/protocol/url/reference.rb', line 87

def self.parse(value = "/", parameters = nil)
	self.[](value, parameters)
end

Instance Method Details

#+(other) ⇒ Object

Merges two references as specified by RFC2396, similar to URI.join.



184
185
186
187
188
189
190
191
192
193
# File 'lib/protocol/url/reference.rb', line 184

def + other
	other = self.class[other]
	
	self.class.new(
		Path.expand(self.path, other.path, true),
		other.query,
		other.fragment,
		other.parameters,
	)
end

#append(buffer = String.new) ⇒ Object

Append the reference to the given buffer. Encodes the path and fragment which are stored unescaped internally. Query strings are passed through as-is (they contain = and & which are valid syntax).



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/protocol/url/reference.rb', line 166

def append(buffer = String.new)
	buffer << Encoding.escape_path(@path)
	
	if @query and !@query.empty?
		buffer << "?" << @query
		buffer << "&" << Encoding.encode(@parameters) if parameters?
	elsif parameters?
		buffer << "?" << Encoding.encode(@parameters)
	end
	
	if @fragment and !@fragment.empty?
		buffer << "#" << Encoding.escape_fragment(@fragment)
	end
	
	return buffer
end

#baseObject

Just the base path, without any query string, parameters or fragment.



196
197
198
# File 'lib/protocol/url/reference.rb', line 196

def base
	self.class.new(@path, nil, nil, nil)
end

#fragment?Boolean

Returns:

  • (Boolean)


159
160
161
# File 'lib/protocol/url/reference.rb', line 159

def fragment?
	@fragment and !@fragment.empty?
end

#freezeObject

Freeze the reference.



112
113
114
115
116
117
118
# File 'lib/protocol/url/reference.rb', line 112

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

#parameters?Boolean

Returns:

  • (Boolean)


128
129
130
# File 'lib/protocol/url/reference.rb', line 128

def parameters?
	@parameters and !@parameters.empty?
end

#parse_query!(encoding = Encoding) ⇒ Object

Parse the query string into parameters and merge with existing parameters.

Afterwards, the query attribute will be cleared.



137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/protocol/url/reference.rb', line 137

def parse_query!(encoding = Encoding)
	if @query and !@query.empty?
		parsed = encoding.decode(@query)
		
		if @parameters
			@parameters = @parameters.merge(parsed)
		else
			@parameters = parsed
		end
		
		@query = nil
	end
	
	return @parameters
end

#query?Boolean

Returns:

  • (Boolean)


154
155
156
# File 'lib/protocol/url/reference.rb', line 154

def query?
	@query and !@query.empty?
end

#to_aryObject

Implicit conversion to an array.



123
124
125
# File 'lib/protocol/url/reference.rb', line 123

def to_ary
	[@path, @query, @fragment, @parameters]
end

#User supplied parameters that will be appended to the query part.=(suppliedparametersthatwillbeappendedtothequerypart. = (value)) ⇒ Object



107
# File 'lib/protocol/url/reference.rb', line 107

attr :parameters

#with(path: nil, query: false, fragment: @fragment, parameters: false, pop: false, merge: true) ⇒ Object

Update the reference with the given path, query, fragment, and parameters.

Examples:

Merge parameters.

reference = Reference.new("/search", nil, nil, {"query" => "ruby"})
updated = reference.with(parameters: {"limit" => "10"})
updated.to_s  # => "/search?query=ruby&limit=10"

Replace parameters.

reference = Reference.new("/search", nil, nil, {"query" => "ruby"})
updated = reference.with(parameters: {"query" => "python"}, merge: false)
updated.to_s  # => "/search?query=python"


218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/protocol/url/reference.rb', line 218

def with(path: nil, query: false, fragment: @fragment, parameters: false, pop: false, merge: true)
	if merge
		# If merging, we keep existing query unless explicitly overridden:
		if query == false
			query = @query
		end
		
		# Merge mode: combine new parameters with existing, keep query:
		# parameters = (@parameters || {}).merge(parameters || {})
		if @parameters
			if parameters
				parameters = @parameters.merge(parameters)
			else
				parameters = @parameters
			end
		elsif !parameters
			parameters = @parameters
		end
	else
		# Replace mode: use new parameters if provided, clear query when replacing:
		if parameters == false
			# No new parameters provided, keep existing:
			parameters = @parameters
			
			# Also keep query if not explicitly specified:
			if query == false
				query = @query
			end
		else
			# New parameters provided, clear query unless explicitly specified:
			if query == false
				query = nil
			end
		end
	end
	
	path = Path.expand(@path, path, pop)
	
	self.class.new(path, query, fragment, parameters)
end