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 query string component., #fragment, #path, #query

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Relative

#<=>, #==, #===, #The path component of the URL.=, #as_json, #equal?, #hash, #inspect, #local_path, #normalize!, #to_json, #to_s

Constructor Details

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

Initialize the reference from an encoded path and reference 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"


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

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

Instance Attribute Details

#parametersObject

Returns the value of attribute parameters.



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

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 preserves the path as a Path. When given a Protocol::URL::Relative, it preserves the existing path and its component boundaries.

Examples:

Coerce a string with path, query, and fragment.

reference = Reference["/search?q=ruby#results"]
reference.path.to_s # => "/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.components  # => ["", "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
# 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]
			
			# Paths retain their encoded structure while exposing decoded components.
			path = Path[path]
			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 an encoded path; preserve its component boundaries.
		path = value.path
		fragment = value.fragment
		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

Generate a reference from a path and user parameters. The path may contain a #fragment or ?query=parameters.

Examples:

Parse a path with query and fragment.

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


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

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.



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

def + other
	other = self.class[other]
	
	self.class.new(
		@path.join(other.path),
		other.query,
		other.fragment,
		other.parameters,
	)
end

#append(buffer = String.new) ⇒ Object

Append the reference to the given buffer. Encodes the fragment; the path already retains its encoded structure. Query strings are passed through as-is (they contain = and & which are valid syntax).



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

def append(buffer = String.new)
	buffer << @path.encoded
	
	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.



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

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

#fragment?Boolean

Returns:

  • (Boolean)


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

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

#freezeObject

Freeze the reference.



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

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

#parameters?Boolean

Returns:

  • (Boolean)


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

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.



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

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)


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

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

#to_aryObject

Implicit conversion to an array.



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

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

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



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

attr_accessor :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"


217
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
258
259
260
# File 'lib/protocol/url/reference.rb', line 217

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
	
	if path.nil?
		path = @path
	else
		path = @path.join(path, pop: pop)
	end
	
	self.class.new(path, query, fragment, parameters)
end