Module: Furi

Defined in:
lib/furi.rb,
lib/furi/uri.rb,
lib/furi/utils.rb,
lib/furi/version.rb,
lib/furi/query_token.rb

Defined Under Namespace

Classes: Error, FormattingError, ParseError, QueryParseError, QueryToken, Uri, Utils

Constant Summary collapse

ESSENTIAL_PARTS =
[
  :anchor, :protocol, :query_tokens,
  :path, :host, :port, :username, :password,
  :filename,
]
COMBINED_PARTS =
[
  :hostinfo, :userinfo, :authority, :ssl, :domain, :domainname,
  :domainzone, :request, :location, :endpoint, :query,
  :directory, :extension, :file
]
PARTS =
ESSENTIAL_PARTS + COMBINED_PARTS
ALIASES =
{
  protocol: [:schema, :scheme],
  anchor: [:fragment],
  host: [:hostname],
  username: [:user],
  request: [:request_uri]
}
DELEGATES =
[:port!, :host!, :path!, :home_page?]
PROTOCOLS =
{
  "http" => {port: 80, ssl: false},
  "https" => {port: 443, ssl: true},
  "ftp" => {port: 21},
  "tftp" => {port: 69},
  "sftp" => {port: 22},
  "ssh" => {port: 22, ssl: true},
  "svn" => {port: 3690},
  "svn+ssh" => {port: 22, ssl: true},
  "telnet" => {port: 23},
  "nntp" => {port: 119},
  "gopher" => {port: 70},
  "wais" => {port: 210},
  "ldap" => {port: 389},
  "prospero" => {port: 1525},
  "file" => {port: nil},
  "postgres" => {port: 5432},
  "mysql" => {port: 3306},
  "mailto" => {port: nil}
}
SSL_MAPPING =
{
  'http' => 'https',
  'ftp' => 'sftp',
  'svn' => 'svn+ssh',
}
WEB_PROTOCOL =
['http', 'https']
ROOT =
'/'
VERSION =
"0.2.8"

Class Method Summary collapse

Class Method Details

.build(argument) ⇒ Object

Builds an URL from given parts

Furi.build(path: "/dashboard", host: 'example.com', protocol: "https")
# => "https://example.com/dashboard"


76
77
78
# File 'lib/furi.rb', line 76

def self.build(argument)
  Uri.new(argument).to_s
end

.defaults(string, parts) ⇒ Object

Puts the default values for given URL that are not defined

Furi.defaults("gusiev.com/hello.html", protocol: 'http', path: '/index.html')
# => "http://gusiev.com/hello.html"


103
104
105
# File 'lib/furi.rb', line 103

def self.defaults(string, parts)
  parse(string).defaults(parts).to_s
end

.join(*uris) ⇒ Object



175
176
177
178
179
180
181
# File 'lib/furi.rb', line 175

def self.join(*uris)
  uris.map do |uri|
    Furi.parse(uri)
  end.reduce do |memo, uri|
    memo.send(:join, uri)
  end
end

.parse(argument, parts = nil) ⇒ Object

Parses a given string and return an URL object Optionally accepts parts to update the parsed URL object



68
69
70
# File 'lib/furi.rb', line 68

def self.parse(argument, parts = nil)
  Uri.new(argument).update(parts)
end

.parse_query(query) ⇒ Object

Parses a query into nested paramters hash using a rack convension with square brackets.

Furi.parse_query("a[]=1&a[]=2")       # => {a: [1,2]}
Furi.parse_query("p[email]=a&a[two]=2") # => {a: {one: 1, two: 2}}
Furi.parse_query("p[one]=1&a[two]=2") # => {a: {one: 1, two: 2}}
Furi.serialize({p: {name: 'Bogdan Gusiev', email: '[email protected]', data: {one: 1, two: 2}}})
# => "p%5Bname%5D=Bogdan&p%5Bemail%5D=bogdan%40example.com&p%5Bdata%5D%5Bone%5D=1&p%5Bdata%5D%5Btwo%5D=2"


127
128
129
130
131
132
133
134
135
136
# File 'lib/furi.rb', line 127

def self.parse_query(query)
  return Furi::Utils.stringify_keys(query) if query.is_a?(Hash)

  params = {}
  query_tokens(query).each do |token|
    parse_query_token(params, token.name, token.value)
  end

  return params
end

.parse_query_token(params, name, value) ⇒ Object



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
261
262
263
264
265
266
267
268
# File 'lib/furi.rb', line 231

def self.parse_query_token(params, name, value)
  name =~ %r(\A[\[\]]*([^\[\]]+)\]*)
  namespace = $1 || ''
  after = $' || ''

  return if namespace.empty?

  current = params[namespace]
  if after == ""
    current = value
  elsif after == "[]"
    current ||= []
    unless current.is_a?(Array)
      raise QueryParseError, "expected Array (got #{current.class}) for param `#{namespace}'"
    end
    current << value
  elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$)
    child_key = $1
    current ||= []
    unless current.is_a?(Array)
      raise QueryParseError, "expected Array (got #{current.class}) for param `#{namespace}'"
    end
    if current.last.is_a?(Hash) && !current.last.key?(child_key)
      parse_query_token(current.last, child_key, value)
    else
      current << parse_query_token({}, child_key, value)
    end
  else
    current ||= {}
    unless current.is_a?(Hash)
      raise QueryParseError, "expected Hash (got #{current.class}) for param `#{namespace}'"
    end
    current = parse_query_token(current, after, value)
  end
  params[namespace] = current

  return params
end

.query_tokens(query) ⇒ Object

Parses query key/value pairs from query string and returns them raw without organising them into hashes and without normalising them.

Furi.query_tokens("a=1&b=2").map {|k,v| "#{k} -> #{v}"}  # => ['a -> 1', 'b -> 2']
Furi.query_tokens("a=1&a=1&a=2").map {|k,v| "#{k} -> #{v}"}  # => ['a -> 1', 'a -> 1', 'a -> 2']
Furi.query_tokens("name=Bogdan&email=bogdan%40example.com") # => [name=Bogdan, [email protected]]
Furi.query_tokens("a[one]=1&a[two]=2") # => [a[one]=1, a[two]=2]


145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/furi.rb', line 145

def self.query_tokens(query)
  case query
  when Enumerable, Enumerator
    query.map do |token|
      QueryToken.parse(token)
    end
  when nil, ''
    []
  when String
    query.gsub(/\A\?/, '').split(/[&;] */n, -1).map do |p|
      QueryToken.parse(p)
    end
  else
    raise QueryParseError, "can not parse #{query.inspect} query tokens"
  end
end

.replace(string, parts) ⇒ Object

Replaces a given URL string with given parts. Same as update but works different for URL query parameter: replaces newly specified parameters instead of merging to existing ones

Furi.update("/hello.html?a=1", host: 'gusiev.com', query: {b: 2})
# => "gusiev.com/hello.html?a=1&b=2"


114
115
116
# File 'lib/furi.rb', line 114

def self.replace(string, parts)
  parse(string).replace(parts).to_s
end

.serialize(query, namespace = nil) ⇒ Object

Serializes query parameters into query string. Optionaly accepts a basic name space.

Furi.serialize({a: 1, b: 2}) # => "a=1&b=2"
Furi.serialize({a: [1,2]}) # => "a[]=1&a[]=2"
Furi.serialize({a: {b: 1, c:2}}) # => "a[b]=1&a[c]=2"
Furi.serialize({name: 'Bogdan', email: '[email protected]'}, "person")
# => "person[name]=Bogdan&person[email]=bogdan%40example.com"


171
172
173
# File 'lib/furi.rb', line 171

def self.serialize(query, namespace = nil)
  serialize_tokens(query, namespace).join("&")
end

.serialize_tokens(query, namespace = nil) ⇒ Object



197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/furi.rb', line 197

def self.serialize_tokens(query, namespace = nil)
  case query
  when Hash
    result = query.map do |key, value|
      unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
        serialize_tokens(value, namespace ? "#{namespace}[#{key}]" : key)
      else
        nil
      end
    end
    result.flatten!
    result.compact!
    result
  when Array
    if namespace.nil? || namespace.empty?
      raise FormattingError, "Can not serialize Array without namespace"
    end

    namespace = "#{namespace}[]"
    query.map do |item|
      if item.is_a?(Array)
        raise FormattingError, "Can not serialize #{item.inspect} as element of an Array"
      end
      serialize_tokens(item, namespace)
    end
  else
    if namespace
      QueryToken.new(namespace, query)
    else
      []
    end
  end
end

.update(string, parts) ⇒ Object Also known as: merge

Replaces a given URL string with given parts

Furi.update("http://gusiev.com", protocol: 'https', subdomain: 'www')
# => "https://www.gusiev.com"


92
93
94
# File 'lib/furi.rb', line 92

def self.update(string, parts)
  parse(string).update(parts).to_s
end