• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ruby-rdf / rdf / 5194667710

07 Jun 2023 12:30AM UTC coverage: 91.808% (+0.1%) from 91.682%
5194667710

push

github

gkellogg
More ruby warning fixes.

4886 of 5322 relevant lines covered (91.81%)

16981.21 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.66
/lib/rdf/model/uri.rb
1
# coding: utf-8
2
# frozen_string_literal: true
3
require 'cgi'
2✔
4

5
module RDF
2✔
6
  ##
7
  # A Uniform Resource Identifier (URI).
8
  # Also compatible with International Resource Identifier (IRI)
9
  #
10
  # @example Creating a URI reference (1)
11
  #   uri = RDF::URI.new("https://rubygems.org/gems/rdf")
12
  #
13
  # @example Creating a URI reference (2)
14
  #   uri = RDF::URI.new(scheme: 'http', host: 'rubygems.org', path: '/gems/rdf')
15
  #     #=> RDF::URI.new("https://rubygems.org/gems/rdf")
16
  #
17
  # @example Creating an interned URI reference
18
  #   uri = RDF::URI.intern("https://rubygems.org/gems/rdf")
19
  #
20
  # @example Getting the string representation of a URI
21
  #   uri.to_s #=> "https://rubygems.org/gems/rdf"
22
  #
23
  # @see https://en.wikipedia.org/wiki/Internationalized_Resource_Identifier
24
  # @see https://en.wikipedia.org/wiki/Uniform_Resource_Identifier
25
  # @see https://www.ietf.org/rfc/rfc3986.txt
26
  # @see https://www.ietf.org/rfc/rfc3987.txt
27
  # @see https://rubydoc.info/gems/addressable
28
  class URI
2✔
29
    include RDF::Resource
2✔
30

31
    # IRI components
32
    UCSCHAR = Regexp.compile(<<-EOS.gsub(/\s+/, ''))
2✔
33
      [\\u00A0-\\uD7FF]|[\\uF900-\\uFDCF]|[\\uFDF0-\\uFFEF]|
34
      [\\u{10000}-\\u{1FFFD}]|[\\u{20000}-\\u{2FFFD}]|[\\u{30000}-\\u{3FFFD}]|
35
      [\\u{40000}-\\u{4FFFD}]|[\\u{50000}-\\u{5FFFD}]|[\\u{60000}-\\u{6FFFD}]|
36
      [\\u{70000}-\\u{7FFFD}]|[\\u{80000}-\\u{8FFFD}]|[\\u{90000}-\\u{9FFFD}]|
37
      [\\u{A0000}-\\u{AFFFD}]|[\\u{B0000}-\\u{BFFFD}]|[\\u{C0000}-\\u{CFFFD}]|
38
      [\\u{D0000}-\\u{DFFFD}]|[\\u{E1000}-\\u{EFFFD}]
39
    EOS
40
    IPRIVATE = Regexp.compile("[\\uE000-\\uF8FF]|[\\u{F0000}-\\u{FFFFD}]|[\\u100000-\\u10FFFD]").freeze
2✔
41
    SCHEME = Regexp.compile("[A-Za-z](?:[A-Za-z0-9+-\.])*").freeze
2✔
42
    PORT = Regexp.compile("[0-9]*").freeze
2✔
43
    IP_literal = Regexp.compile("\\[[0-9A-Fa-f:\\.]*\\]").freeze  # Simplified, no IPvFuture
2✔
44
    PCT_ENCODED = Regexp.compile("%[0-9A-Fa-f][0-9A-Fa-f]").freeze
2✔
45
    GEN_DELIMS = Regexp.compile("[:/\\?\\#\\[\\]@]").freeze
2✔
46
    SUB_DELIMS = Regexp.compile("[!\\$&'\\(\\)\\*\\+,;=]").freeze
2✔
47
    RESERVED = Regexp.compile("(?:#{GEN_DELIMS}|#{SUB_DELIMS})").freeze
2✔
48
    UNRESERVED = Regexp.compile("[A-Za-z0-9\._~-]").freeze
2✔
49

50
    IUNRESERVED = Regexp.compile("[A-Za-z0-9\._~-]|#{UCSCHAR}").freeze
2✔
51

52
    IPCHAR = Regexp.compile("(?:#{IUNRESERVED}|#{PCT_ENCODED}|#{SUB_DELIMS}|:|@)").freeze
2✔
53

54
    IQUERY = Regexp.compile("(?:#{IPCHAR}|#{IPRIVATE}|/|\\?)*").freeze
2✔
55

56
    IFRAGMENT = Regexp.compile("(?:#{IPCHAR}|/|\\?)*").freeze
2✔
57

58
    ISEGMENT = Regexp.compile("(?:#{IPCHAR})*").freeze
2✔
59
    ISEGMENT_NZ = Regexp.compile("(?:#{IPCHAR})+").freeze
2✔
60
    ISEGMENT_NZ_NC = Regexp.compile("(?:(?:#{IUNRESERVED})|(?:#{PCT_ENCODED})|(?:#{SUB_DELIMS})|@)+").freeze
2✔
61

62
    IPATH_ABEMPTY = Regexp.compile("(?:/#{ISEGMENT})*").freeze
2✔
63
    IPATH_ABSOLUTE = Regexp.compile("/(?:(?:#{ISEGMENT_NZ})(/#{ISEGMENT})*)?").freeze
2✔
64
    IPATH_NOSCHEME = Regexp.compile("(?:#{ISEGMENT_NZ_NC})(?:/#{ISEGMENT})*").freeze
2✔
65
    IPATH_ROOTLESS = Regexp.compile("(?:#{ISEGMENT_NZ})(?:/#{ISEGMENT})*").freeze
2✔
66
    IPATH_EMPTY = Regexp.compile("").freeze
2✔
67

68
    IREG_NAME   = Regexp.compile("(?:(?:#{IUNRESERVED})|(?:#{PCT_ENCODED})|(?:#{SUB_DELIMS}))*").freeze
2✔
69
    IHOST = Regexp.compile("(?:#{IP_literal})|(?:#{IREG_NAME})").freeze
2✔
70
    IUSERINFO = Regexp.compile("(?:(?:#{IUNRESERVED})|(?:#{PCT_ENCODED})|(?:#{SUB_DELIMS})|:)*").freeze
2✔
71
    IAUTHORITY = Regexp.compile("(?:#{IUSERINFO}@)?#{IHOST}(?::#{PORT})?").freeze
2✔
72
    
73
    IRELATIVE_PART = Regexp.compile("(?:(?://#{IAUTHORITY}(?:#{IPATH_ABEMPTY}))|(?:#{IPATH_ABSOLUTE})|(?:#{IPATH_NOSCHEME})|(?:#{IPATH_EMPTY}))").freeze
2✔
74
    IRELATIVE_REF = Regexp.compile("^#{IRELATIVE_PART}(?:\\?#{IQUERY})?(?:\\##{IFRAGMENT})?$").freeze
2✔
75

76
    IHIER_PART = Regexp.compile("(?:(?://#{IAUTHORITY}#{IPATH_ABEMPTY})|(?:#{IPATH_ABSOLUTE})|(?:#{IPATH_ROOTLESS})|(?:#{IPATH_EMPTY}))").freeze
2✔
77
    IRI = Regexp.compile("^#{SCHEME}:(?:#{IHIER_PART})(?:\\?#{IQUERY})?(?:\\##{IFRAGMENT})?$").freeze
2✔
78

79
    # Split an IRI into it's component parts
80
    # scheme, authority, path, query, fragment
81
    IRI_PARTS = /^(?:([^:\/?#]+):)?(?:\/\/([^\/?#]*))?([^?#]*)(\?[^#]*)?(#.*)?$/.freeze
2✔
82

83
    # Remove dot expressions regular expressions
84
    RDS_2A = /^\.?\.\/(.*)$/.freeze
2✔
85
    RDS_2B1 = /^\/\.$/.freeze
2✔
86
    RDS_2B2 = /^(?:\/\.\/)(.*)$/.freeze
2✔
87
    RDS_2C1 = /^\/\.\.$/.freeze
2✔
88
    RDS_2C2 = /^(?:\/\.\.\/)(.*)$/.freeze
2✔
89
    RDS_2D  = /^\.\.?$/.freeze
2✔
90
    RDS_2E = /^(\/?[^\/]*)(\/?.*)?$/.freeze
2✔
91

92
    # Remove port, if it is standard for the scheme when normalizing
93
    PORT_MAPPING = {
2✔
94
      "http"     => 80,
95
      "https"    => 443,
96
      "ftp"      => 21,
97
      "tftp"     => 69,
98
      "sftp"     => 22,
99
      "ssh"      => 22,
100
      "svn+ssh"  => 22,
101
      "telnet"   => 23,
102
      "nntp"     => 119,
103
      "gopher"   => 70,
104
      "wais"     => 210,
105
      "ldap"     => 389,
106
      "prospero" => 1525
107
    }
108

109
    # List of schemes known not to be hierarchical
110
    NON_HIER_SCHEMES = %w(
2✔
111
      about acct bitcoin callto cid data fax geo gtalk h323 iax icon im jabber
112
      jms magnet mailto maps news pres proxy session sip sips skype sms spotify stun stuns
113
      tag tel turn turns tv urn javascript
114
    ).freeze
115

116
    # Characters in a PName which must be escaped
117
    # Note: not all reserved characters need to be escaped in SPARQL/Turtle, but they must be unescaped when encountered
118
    PN_ESCAPE_CHARS      = /[~\.!\$&'\(\)\*\+,;=\/\?\#@%]/.freeze
2✔
119
    PN_ESCAPES           = /\\#{Regexp.union(PN_ESCAPE_CHARS, /[\-_]/)}/.freeze
2✔
120

121
    # For URI encoding
122
    ENCODE_USER = Regexp.compile("[^#{IUNRESERVED}#{SUB_DELIMS}]").freeze
2✔
123
    ENCODE_PASSWORD = Regexp.compile("[^#{IUNRESERVED}#{SUB_DELIMS}]").freeze
2✔
124
    ENCODE_ISEGMENT = Regexp.compile("[^#{IPCHAR}]").freeze
2✔
125
    ENCODE_ISEGMENT_NC = Regexp.compile("[^#{IUNRESERVED}|#{PCT_ENCODED}|[#{SUB_DELIMS}]|@]").freeze
2✔
126
    ENCODE_IQUERY = Regexp.compile("[^#{IQUERY}]").freeze
2✔
127
    ENCODE_IFRAGMENT = Regexp.compile("[^#{IFRAGMENT}]").freeze
2✔
128
    ENCODE_PORT = Regexp.compile('[^\d]').freeze
2✔
129
    ENCODE_IHOST = Regexp.compile("(?:#{IP_literal})|(?:#{IREG_NAME})").freeze
2✔
130

131
    ##
132
    # Cache size may be set through {RDF.config} using `uri_cache_size`.
133
    #
134
    # @return [RDF::Util::Cache]
135
    # @private
136
    def self.cache
2✔
137
      require 'rdf/util/cache' unless defined?(::RDF::Util::Cache)
160,708✔
138
      @cache ||= RDF::Util::Cache.new(RDF.config.uri_cache_size)
160,708✔
139
    end
140

141
    ##
142
    # Returns an interned `RDF::URI` instance based on the given `uri`
143
    # string.
144
    #
145
    # The maximum number of cached interned URI references is given by the
146
    # `CACHE_SIZE` constant. This value is unlimited by default, in which
147
    # case an interned URI object will be purged only when the last strong
148
    # reference to it is garbage collected (i.e., when its finalizer runs).
149
    #
150
    # Excepting special memory-limited circumstances, it should always be
151
    # safe and preferred to construct new URI references using
152
    # `RDF::URI.intern` instead of `RDF::URI.new`, since if an interned
153
    # object can't be returned for some reason, this method will fall back
154
    # to returning a freshly-allocated one.
155
    #
156
    # (see #initialize)
157
    # @return [RDF::URI] an immutable, frozen URI object
158
    def self.intern(str, *args, **options)
2✔
159
      (cache[(str = str.to_s).to_sym] ||= self.new(str, *args, **options)).freeze
74,880✔
160
    end
161

162
    ##
163
    # Creates a new `RDF::URI` instance based on the given `uri` string.
164
    #
165
    # This is just an alias for {RDF::URI#initialize} for compatibity
166
    # with `Addressable::URI.parse`. Actual parsing is defered
167
    # until {#object} is accessed.
168
    #
169
    # @param  [String, #to_s] str
170
    # @return [RDF::URI]
171
    def self.parse(str)
2✔
172
      self.new(str)
146✔
173
    end
174

175
    ##
176
    # Resolve paths to their simplest form.
177
    #
178
    # @todo This process is correct, but overly iterative. It could be better done with a single regexp which handled most of the segment collapses all at once. Parent segments would still require iteration.
179
    #
180
    # @param [String] path
181
    # @return [String] normalized path
182
    # @see http://tools.ietf.org/html/rfc3986#section-5.2.4
183
    def self.normalize_path(path)
2✔
184
      output, input = String.new, path.to_s
400✔
185
      if input.encoding != Encoding::ASCII_8BIT
400✔
186
        input = input.dup.force_encoding(Encoding::ASCII_8BIT)
400✔
187
      end
188
      until input.empty?
400✔
189
        if input.match(RDS_2A)
856✔
190
          # If the input buffer begins with a prefix of "../" or "./", then remove that prefix from the input buffer; otherwise,
191
          input = $1
×
192
        elsif input.match(RDS_2B1) || input.match(RDS_2B2)
856✔
193
          # if the input buffer begins with a prefix of "/./" or "/.", where "." is a complete path segment, then replace that prefix with "/" in the input buffer; otherwise,
194
          input = "/#{$1}"
18✔
195
        elsif input.match(RDS_2C1) || input.match(RDS_2C2)
838✔
196
          # if the input buffer begins with a prefix of "/../" or "/..", where ".." is a complete path segment, then replace that prefix with "/" in the input buffer
197
          input = "/#{$1}"
88✔
198

199
          #  and remove the last segment and its preceding "/" (if any) from the output buffer; otherwise,
200
          output.sub!(/\/?[^\/]*$/, '')
88✔
201
        elsif input.match(RDS_2D)
750✔
202
          # if the input buffer consists only of "." or "..", then remove that from the input buffer; otherwise,
203
          input = ""
×
204
        elsif input.match(RDS_2E)
750✔
205
          # move the first path segment in the input buffer to the end of the output buffer, including the initial "/" character (if any) and any subsequent characters up to, but not including, the next "/" character or the end of the input buffer.end
206
          seg, input = $1, $2
750✔
207
          output << seg
750✔
208
        end
209
      end
210

211
      output.force_encoding(Encoding::UTF_8)
400✔
212
    end
213

214
    ##
215
    # @overload initialize(uri, **options)
216
    #   @param  [URI, String, #to_s]    uri
217
    #
218
    # @overload initialize(**options)
219
    #   @param  [Hash{Symbol => Object}] options
220
    #   @option [String, #to_s] :scheme The scheme component.
221
    #   @option [String, #to_s] :user The user component.
222
    #   @option [String, #to_s] :password The password component.
223
    #   @option [String, #to_s] :userinfo
224
    #     The userinfo component. If this is supplied, the user and password
225
    #     components must be omitted.
226
    #   @option [String, #to_s] :host The host component.
227
    #   @option [String, #to_s] :port The port component.
228
    #   @option [String, #to_s] :authority
229
    #     The authority component. If this is supplied, the user, password,
230
    #     userinfo, host, and port components must be omitted.
231
    #   @option [String, #to_s] :path The path component.
232
    #   @option [String, #to_s] :query The query component.
233
    #   @option [String, #to_s] :fragment The fragment component.
234
    #
235
    #   @param [Boolean] validate (false)
236
    #   @param [Boolean] canonicalize (false)
237
    def initialize(*args, validate: false, canonicalize: false, **options)
2✔
238
      @value = @object = @hash = nil
255,108✔
239
      @mutex = Mutex.new
255,108✔
240
      uri = args.first
255,108✔
241
      if uri
255,108✔
242
        @value = uri.to_s.dup
254,918✔
243
        @value.dup.force_encoding(Encoding::UTF_8) if @value.encoding != Encoding::UTF_8
254,918✔
244
        @value.freeze
254,918✔
245
      else
246
        %i(
190✔
247
          scheme
248
          user password userinfo
249
          host port authority
250
          path query fragment
251
        ).each do |meth|
252
          if options.key?(meth)
1,900✔
253
            self.send("#{meth}=".to_sym, options[meth])
906✔
254
          else
255
            self.send(meth)
994✔
256
          end
257
        end
258
      end
259

260
      validate!     if validate
255,108✔
261
      canonicalize! if canonicalize
255,038✔
262
    end
263

264
    ##
265
    # Returns `true`.
266
    #
267
    # @return [Boolean] `true` or `false`
268
    # @see    http://en.wikipedia.org/wiki/Uniform_Resource_Identifier
269
    def uri?
2✔
270
      true
11,334✔
271
    end
272

273
    ##
274
    # Returns `true` if this URI is a URN.
275
    #
276
    # @example
277
    #   RDF::URI('http://example.org/').urn?                    #=> false
278
    #
279
    # @return [Boolean] `true` or `false`
280
    # @see    http://en.wikipedia.org/wiki/Uniform_Resource_Name
281
    # @since  0.2.0
282
    def urn?
2✔
283
      @object ? @object[:scheme] == 'urn' : start_with?('urn:')
154✔
284
    end
285

286
    ##
287
    # Returns `true` if the URI scheme is hierarchical.
288
    #
289
    # @example
290
    #   RDF::URI('http://example.org/').hier?                    #=> true
291
    #   RDF::URI('urn:isbn:125235111').hier?                     #=> false
292
    #
293
    # @return [Boolean] `true` or `false`
294
    # @see    http://en.wikipedia.org/wiki/URI_scheme
295
    # @see    NON_HIER_SCHEMES
296
    # @since  1.0.10
297
    def hier?
2✔
298
      !NON_HIER_SCHEMES.include?(scheme)
274✔
299
    end
300

301
    ##
302
    # Returns `true` if this URI is a URL.
303
    #
304
    # @example
305
    #   RDF::URI('http://example.org/').url?                    #=> true
306
    #
307
    # @return [Boolean] `true` or `false`
308
    # @see    http://en.wikipedia.org/wiki/Uniform_Resource_Locator
309
    # @since  0.2.0
310
    def url?
2✔
311
      !urn?
24✔
312
    end
313

314
    ##
315
    # A URI is absolute when it has a scheme
316
    # @return [Boolean] `true` or `false`
317
    def absolute?; !scheme.nil?; end
2,792✔
318

319
    ##
320
    # A URI is relative when it does not have a scheme
321
    # @return [Boolean] `true` or `false`
322
    def relative?; !absolute?; end
1,954✔
323
    
324
    # Attempt to make this URI relative to the provided `base_uri`. If successful, returns a relative URI, otherwise the original URI
325
    # @param [#to_s] base_uri
326
    # @return [RDF::URI]
327
    def relativize(base_uri)
2✔
328
      if self.to_s.start_with?(base_uri.to_s) && %w(# ?).include?(self.to_s[base_uri.to_s.length, 1]) ||
30✔
329
         base_uri.to_s.end_with?("/", "#") &&
330
         self.to_s.start_with?(base_uri.to_s)
331
        return RDF::URI(self.to_s[base_uri.to_s.length..-1])
8✔
332
      else
333
        # Create a list of parents, for which this IRI may be relative.
334
        u = RDF::URI(base_uri)
22✔
335
        iri_set = u.to_s.end_with?('/') ? [u.to_s] : []
22✔
336
        iri_set << u.to_s while (u = u.parent)
22✔
337
        iri_set.each_with_index do |bb, index|
22✔
338
          next unless self.to_s.start_with?(bb)
66✔
339
          rel = "../" * index + self.to_s[bb.length..-1]
20✔
340
          return rel.empty? ? "./" : rel
20✔
341
        end
342
      end
343
      self
2✔
344
    end
345

346
    ##
347
    # Returns the string length of this URI.
348
    #
349
    # @example
350
    #   RDF::URI('http://example.org/').length                  #=> 19
351
    #
352
    # @return [Integer]
353
    # @since  0.3.0
354
    def length
2✔
355
      to_s.length
44,502✔
356
    end
357
    alias_method :size, :length
2✔
358

359
    ##
360
    # Determine if the URI is a valid according to RFC3987
361
    #
362
    # Note that RDF URIs syntactically can contain Unicode escapes, which are unencoded in the internal representation. To validate, %-encode specifically excluded characters from IRIREF
363
    #
364
    # @return [Boolean] `true` or `false`
365
    # @since 0.3.9
366
    def valid?
2✔
367
      RDF::URI::IRI.match?(to_s) || false
75,142✔
368
    end
369

370
    ##
371
    # Validates this URI, raising an error if it is invalid.
372
    #
373
    # @return [RDF::URI] `self`
374
    # @raise  [ArgumentError] if the URI is invalid
375
    # @since  0.3.0
376
    def validate!
2✔
377
      raise ArgumentError, "#{to_base.inspect} is not a valid IRI" if invalid?
3,368✔
378
      self
3,256✔
379
    end
380

381
    ##
382
    # Returns a copy of this URI converted into its canonical lexical
383
    # representation.
384
    #
385
    # @return [RDF::URI]
386
    # @since  0.3.0
387
    def canonicalize
2✔
388
      self.dup.canonicalize!
146✔
389
    end
390
    alias_method :normalize, :canonicalize
2✔
391

392
    ##
393
    # Converts this URI into its canonical lexical representation.
394
    #
395
    # @return [RDF::URI] `self`
396
    # @since  0.3.0
397
    def canonicalize!
2✔
398
      @object = {
399
        scheme: normalized_scheme,
342✔
400
        authority: normalized_authority,
401
        path: normalized_path.squeeze('/'),
402
        query: normalized_query,
403
        fragment: normalized_fragment
404
      }
405
      @value = nil
342✔
406
      @hash = nil
342✔
407
      self
342✔
408
    end
409
    alias_method :normalize!, :canonicalize!
2✔
410

411
    ##
412
    # Joins several URIs together.
413
    #
414
    # This method conforms to join normalization semantics as per RFC3986,
415
    # section 5.2.  This method normalizes URIs, removes some duplicate path
416
    # information, such as double slashes, and other behavior specified in the
417
    # RFC.
418
    #
419
    # Other URI building methods are `#/` and `#+`.
420
    #
421
    # For an up-to-date list of edge case behavior, see the shared examples for
422
    # RDF::URI in the rdf-spec project.
423
    #
424
    # @example Joining two URIs
425
    #     RDF::URI.new('http://example.org/foo/bar').join('/foo')
426
    #     #=> RDF::URI('http://example.org/foo')
427
    # @see <https://github.com/ruby-rdf/rdf-spec/blob/master/lib/rdf/spec/uri.rb>
428
    # @see <http://tools.ietf.org/html/rfc3986#section-5.2>
429
    # @see RDF::URI#/
430
    # @see RDF::URI#+
431
    # @param  [Array<String, RDF::URI, #to_s>] uris absolute or relative URIs.
432
    # @return [RDF::URI]
433
    # @see http://tools.ietf.org/html/rfc3986#section-5.2.2
434
    # @see http://tools.ietf.org/html/rfc3986#section-5.2.3
435
    def join(*uris)
2✔
436
      joined_parts = object.dup.delete_if {|k, v| %i(user password host port).include?(k)}
902✔
437

438
      uris.each do |uri|
82✔
439
        uri = RDF::URI.new(uri) unless uri.is_a?(RDF::URI)
82✔
440
        next if uri.to_s.empty? # Don't mess with base URI
82✔
441

442
        case
443
        when uri.scheme
70✔
444
          joined_parts = uri.object.merge(path: self.class.normalize_path(uri.path))
6✔
445
        when uri.authority
446
          joined_parts[:authority] = uri.authority
×
447
          joined_parts[:path] = self.class.normalize_path(uri.path)
×
448
          joined_parts[:query] = uri.query
×
449
        when uri.path.to_s.empty?
450
          joined_parts[:query] = uri.query if uri.query
16✔
451
        when uri.path[0,1] == '/'
452
          joined_parts[:path] = self.class.normalize_path(uri.path)
14✔
453
          joined_parts[:query] = uri.query
14✔
454
        else
455
          # Merge path segments from section 5.2.3
456
          # Note that if the path includes no segments, the entire path is removed
457
          #  > return a string consisting of the reference's path component appended to all but the last segment of the base URI's path (i.e., excluding any characters after the right-most "/" in the base URI path, or excluding the entire base URI path if it does not contain any "/" characters).
458
          base_path = path.to_s.include?('/') ? path.to_s.sub(/\/[^\/]*$/, '/') : ''
34✔
459
          joined_parts[:path] = self.class.normalize_path(base_path + uri.path)
34✔
460
          joined_parts[:query] = uri.query
34✔
461
        end
462
        joined_parts[:fragment] = uri.fragment
70✔
463
      end
464

465
      # Return joined URI
466
      RDF::URI.new(**joined_parts)
82✔
467
    end
468

469
    ##
470
    # 'Smart separator' URI builder
471
    #
472
    # This method attempts to use some understanding of the most common use
473
    # cases for URLs and URNs to create a simple method for building new URIs
474
    # from fragments.  This means that it will always insert a separator of
475
    # some sort, will remove duplicate seperators, will always assume that a
476
    # fragment argument represents a relative and not absolute path, and throws
477
    # an exception when an absolute URI is received for a fragment argument.
478
    #
479
    # This is separate from the semantics for `#join`, which are well-defined by
480
    # RFC3986 section 5.2 as part of the merging and normalization process;
481
    # this method does not perform any normalization, removal of spurious
482
    # paths, or removal of parent directory references `(/../)`.
483
    #
484
    # When `fragment` is a path segment containing a colon, best practice is to prepend a `./` and use {#join}, which resolves dot-segments.
485
    #
486
    # See also `#+`, which concatenates the string forms of two URIs without
487
    # any sort of checking or processing.
488
    #
489
    # For an up-to-date list of edge case behavior, see the shared examples for
490
    # RDF::URI in the rdf-spec project.
491
    #
492
    # @param [Any] fragment A URI fragment to be appended to this URI
493
    # @return [RDF::URI]
494
    # @raise  [ArgumentError] if the URI is invalid
495
    # @see RDF::URI#+
496
    # @see RDF::URI#join
497
    # @see <http://tools.ietf.org/html/rfc3986#section-5.2>
498
    # @see <https://github.com/ruby-rdf/rdf-spec/blob/master/lib/rdf/spec/uri.rb>
499
    # @example Building a HTTP URL
500
    #     RDF::URI.new('http://example.org') / 'jhacker' / 'foaf.ttl'
501
    #     #=> RDF::URI('http://example.org/jhacker/foaf.ttl')
502
    # @example Building a HTTP URL (absolute path components)
503
    #     RDF::URI.new('http://example.org/') / '/jhacker/' / '/foaf.ttl'
504
    #     #=> RDF::URI('http://example.org/jhacker/foaf.ttl')
505
    # @example Using an anchored base URI
506
    #     RDF::URI.new('http://example.org/users#') / 'jhacker'
507
    #     #=> RDF::URI('http://example.org/users#jhacker')
508
    # @example Building a URN
509
    #     RDF::URI.new('urn:isbn') / 125235111
510
    #     #=> RDF::URI('urn:isbn:125235111')
511
    def /(fragment)
2✔
512
      frag = fragment.respond_to?(:to_uri) ? fragment.to_uri : RDF::URI(fragment.to_s)
108✔
513
      raise ArgumentError, "Non-absolute URI or string required, got #{frag}" unless frag.relative?
108✔
514
      if urn?
106✔
515
        RDF::URI.intern(to_s.sub(/:+$/,'') + ':' + fragment.to_s.sub(/^:+/,''))
14✔
516
      else # !urn?
517
        res = self.dup
92✔
518
        if res.fragment
92✔
519
          case fragment.to_s[0,1]
34✔
520
          when '/'
521
            # Base with a fragment, fragment beginning with '/'. The fragment wins, we use '/'.
522
            path, frag = fragment.to_s.split('#', 2)
8✔
523
            res.path = "#{res.path}/#{path.sub(/^\/*/,'')}"
8✔
524
            res.fragment = frag
8✔
525
          else
526
            # Replace fragment
527
            res.fragment = fragment.to_s.sub(/^#+/,'')
26✔
528
          end
529
        else
530
          # Not a fragment. includes '/'. Results from bases ending in '/' are the same as if there were no trailing slash.
531
          case fragment.to_s[0,1]
58✔
532
          when '#'
533
            # Base ending with '/', fragment beginning with '#'. The fragment wins, we use '#'.
534
            res.path = res.path.to_s.sub(/\/*$/, '')
16✔
535
            # Add fragment
536
            res.fragment = fragment.to_s.sub(/^#+/,'')
16✔
537
          else
538
            # Add fragment as path component
539
            path, frag = fragment.to_s.split('#', 2)
42✔
540
            res.path = res.path.to_s.sub(/\/*$/,'/') + path.sub(/^\/*/,'')
42✔
541
            res.fragment = frag
42✔
542
          end
543
        end
544
        RDF::URI.intern(res.to_s)
92✔
545
      end
546
    end
547

548
    ##
549
    # Simple concatenation operator.  Returns a URI formed from concatenating
550
    # the string form of two elements.
551
    #
552
    # For building URIs from fragments, you may want to use the smart
553
    # separator, `#/`.  `#join` implements another set of URI building
554
    # semantics.
555
    #
556
    # @example Concatenating a string to a URI
557
    #     RDF::URI.new('http://example.org/test') + 'test'
558
    #     #=> RDF::URI('http://example.org/testtest')
559
    # @example Concatenating two URIs
560
    #     RDF::URI.new('http://example.org/test') + RDF::URI.new('test')
561
    #     #=> RDF::URI('http://example.org/testtest')
562
    # @see RDF::URI#/
563
    # @see RDF::URI#join
564
    # @param [Any] other
565
    # @return [RDF::URI]
566
    def +(other)
2✔
567
      RDF::URI.intern(self.to_s + other.to_s)
26✔
568
    end
569

570
    ##
571
    # Returns `true` if this URI's scheme is not hierarchical,
572
    # or its path component is equal to `/`.
573
    # Protocols not using hierarchical components are always considered
574
    # to be at the root.
575
    #
576
    # @example
577
    #   RDF::URI('http://example.org/').root?                   #=> true
578
    #   RDF::URI('http://example.org/path/').root?              #=> false
579
    #   RDF::URI('urn:isbn').root?                              #=> true
580
    #
581
    # @return [Boolean] `true` or `false`
582
    def root?
2✔
583
      !self.hier?  || self.path == '/' || self.path.to_s.empty?
264✔
584
    end
585

586
    ##
587
    # Returns a copy of this URI with the path component set to `/`.
588
    #
589
    # @example
590
    #   RDF::URI('http://example.org/').root                    #=> RDF::URI('http://example.org/')
591
    #   RDF::URI('http://example.org/path/').root               #=> RDF::URI('http://example.org/')
592
    #
593
    # @return [RDF::URI]
594
    def root
2✔
595
      if root?
24✔
596
        self
14✔
597
      else
598
        RDF::URI.new(
10✔
599
          **object.merge(path: '/').
600
          keep_if {|k, v| %i(scheme authority path).include?(k)})
100✔
601
      end
602
    end
603

604
    ##
605
    # Returns `true` if this URI is hierarchical and it's path component isn't equal to `/`.
606
    #
607
    # @example
608
    #   RDF::URI('http://example.org/').parent?             #=> false
609
    #   RDF::URI('http://example.org/path/').parent?        #=> true
610
    #
611
    # @return [Boolean] `true` or `false`
612
    def parent?
2✔
613
      !root?
4✔
614
    end
615
    alias_method :has_parent?, :parent?
2✔
616

617
    ##
618
    # Returns a copy of this URI with the path component ascended to the
619
    # parent directory, if any.
620
    #
621
    # @example
622
    #   RDF::URI('http://example.org/').parent                  #=> nil
623
    #   RDF::URI('http://example.org/path/').parent             #=> RDF::URI('http://example.org/')
624
    #
625
    # @return [RDF::URI]
626
    def parent
2✔
627
      case
628
        when root? then nil
156✔
629
        else
630
          require 'pathname' unless defined?(Pathname)
96✔
631
          if path = Pathname.new(self.path).parent
96✔
632
            uri = self.dup
96✔
633
            uri.path = path.to_s
96✔
634
            uri.path << '/' unless uri.root?
96✔
635
            uri
96✔
636
          end
637
      end
638
    end
639

640
    ##
641
    # Returns a qualified name (QName) as a tuple of `[prefix, suffix]` for this URI based on available vocabularies, if possible.
642
    #
643
    # @example
644
    #   RDF::URI('http://www.w3.org/2000/01/rdf-schema#').qname       #=> [:rdfs, nil]
645
    #   RDF::URI('http://www.w3.org/2000/01/rdf-schema#label').qname  #=> [:rdfs, :label]
646
    #   RDF::RDFS.label.qname                                         #=> [:rdfs, :label]
647
    #   RDF::Vocab::DC.title.qname(
648
    #     prefixes: {dcterms: 'http://purl.org/dc/terms/'})           #=> [:dcterms, :title]
649
    #
650
    # @note within this software, the term QName is used to describe the tuple of prefix and suffix for a given IRI, where the prefix identifies some defined vocabulary. This somewhat contrasts with the notion of a [Qualified Name](https://www.w3.org/TR/2006/REC-xml-names11-20060816/#ns-qualnames) from XML, which are a subset of Prefixed Names.
651
    #
652
    # @param [Hash{Symbol => String}] prefixes
653
    #   Explicit set of prefixes to look for matches, defaults to loaded vocabularies.
654
    # @return [Array(Symbol, Symbol)] or `nil` if no QName found. The suffix component will not have [reserved characters](https://www.w3.org/TR/turtle/#reserved) escaped.
655
    def qname(prefixes: nil)
2✔
656
      if prefixes
70✔
657
        prefixes.each do |prefix, uri|
50✔
658
          return [prefix, self.to_s[uri.length..-1].to_sym] if self.start_with?(uri)
38✔
659
        end
660
      elsif self.to_s =~ %r([:/#]([^:/#]*)$)
20✔
661
        local_name = $1
20✔
662
        vocab_uri  = local_name.empty? ? self.to_s : self.to_s[0...-(local_name.length)]
20✔
663
        Vocabulary.each do |vocab|
20✔
664
          if vocab.to_uri == vocab_uri
290✔
665
            prefix = vocab.equal?(RDF) ? :rdf : vocab.__prefix__
20✔
666
            return [prefix, local_name.empty? ? nil : local_name.to_sym]
20✔
667
          end
668
        end
669
      else
670
        Vocabulary.each do |vocab|
×
671
          vocab_uri = vocab.to_uri
×
672
          if self.start_with?(vocab_uri)
×
673
            prefix = vocab.equal?(RDF) ? :rdf : vocab.__prefix__
×
674
            local_name = self.to_s[vocab_uri.length..-1]
×
675
            return [prefix, local_name.empty? ? nil : local_name.to_sym]
×
676
          end
677
        end
678
      end
679
      return nil # no QName found
680
    end
681

682
    ##
683
    # Returns a Prefixed Name (PName) or the full IRI with any [reserved characters](https://www.w3.org/TR/turtle/#reserved) in the suffix escaped.
684
    #
685
    # @example Using a custom prefix for creating a PNname.
686
    #   RDF::URI('http://purl.org/dc/terms/creator').
687
    #     pname(prefixes: {dcterms: 'http://purl.org/dc/terms/'})
688
    #     #=> "dcterms:creator"
689
    #
690
    # @param [Hash{Symbol => String}] prefixes
691
    #   Explicit set of prefixes to look for matches, defaults to loaded vocabularies.
692
    # @return [String] or `nil`
693
    # @see #qname
694
    # @see https://www.w3.org/TR/rdf-sparql-query/#prefNames
695
    def pname(prefixes: nil)
2✔
696
      q = self.qname(prefixes: prefixes)
52✔
697
      return self.to_s unless q
52✔
698
      prefix, suffix = q
46✔
699
      suffix = suffix.to_s.gsub(PN_ESCAPE_CHARS) {|c| "\\#{c}"} if
32✔
700
        suffix.to_s.match?(PN_ESCAPE_CHARS)
46✔
701
      [prefix, suffix].join(":")
46✔
702
    end
703

704
    ##
705
    # Returns a duplicate copy of `self`.
706
    #
707
    # @return [RDF::URI]
708
    def dup
2✔
709
      self.class.new(@value, **(@object || {}))
528✔
710
    end
711

712
    ##
713
    # @private
714
    def freeze
2✔
715
      unless frozen?
120,794✔
716
        @mutex.synchronize do
43,076✔
717
          # Create derived components
718
          authority; userinfo; user; password; host; port
43,076✔
719
          @value  = value.freeze
43,076✔
720
          @object = object.freeze
43,076✔
721
          @hash = hash.freeze
43,076✔
722
          super
43,076✔
723
        end
724
      end
725
      self
120,794✔
726
    end
727

728
    ##
729
    # Returns `true` if this URI ends with the given `string`.
730
    #
731
    # @example
732
    #   RDF::URI('http://example.org/').end_with?('/')          #=> true
733
    #   RDF::URI('http://example.org/').end_with?('#')          #=> false
734
    #
735
    # @param  [String, #to_s] string
736
    # @return [Boolean] `true` or `false`
737
    # @see    String#end_with?
738
    # @since  0.3.0
739
    def end_with?(string)
2✔
740
      to_s.end_with?(string.to_s)
4✔
741
    end
742
    alias_method :ends_with?, :end_with?
2✔
743

744
    ##
745
    # Checks whether this URI the same term as `other`.
746
    #
747
    # @example
748
    #   RDF::URI('http://t.co/').eql?(RDF::URI('http://t.co/'))    #=> true
749
    #   RDF::URI('http://t.co/').eql?('http://t.co/')              #=> false
750
    #   RDF::URI('http://www.w3.org/2000/01/rdf-schema#').eql?(RDF::RDFS) #=> false
751
    #
752
    # @param  [RDF::URI] other
753
    # @return [Boolean] `true` or `false`
754
    def eql?(other)
2✔
755
      other.is_a?(URI) && self.hash == other.hash && self == other
1,827,854✔
756
    end
757

758
    ##
759
    # Checks whether this URI is equal to `other` (type checking).
760
    #
761
    # Per SPARQL data-r2/expr-equal/eq-2-2, numeric can't be compared with other types
762
    #
763
    # @example
764
    #   RDF::URI('http://t.co/') == RDF::URI('http://t.co/')    #=> true
765
    #   RDF::URI('http://t.co/') == 'http://t.co/'              #=> true
766
    #   RDF::URI('http://www.w3.org/2000/01/rdf-schema#') == RDF::RDFS        #=> true
767
    #
768
    # @param  [Object] other
769
    # @return [Boolean] `true` or `false`
770
    # @see http://www.w3.org/TR/rdf-sparql-query/#func-RDFterm-equal
771
    def ==(other)
2✔
772
      case other
2,425,826✔
773
      when Literal
774
        # If other is a Literal, reverse test to consolodate complex type checking logic
775
        other == self
40,198✔
776
      when String then to_s == other
568✔
777
      when URI then hash == other.hash && to_s == other.to_s
2,374,858✔
778
      else other.respond_to?(:to_uri) && to_s == other.to_uri.to_s
10,202✔
779
      end
780
    end
781

782
    ##
783
    # Checks for case equality to the given `other` object.
784
    #
785
    # @example
786
    #   RDF::URI('http://example.org/') === /example/           #=> true
787
    #   RDF::URI('http://example.org/') === /foobar/            #=> false
788
    #   RDF::URI('http://t.co/') === RDF::URI('http://t.co/')   #=> true
789
    #   RDF::URI('http://t.co/') === 'http://t.co/'             #=> true
790
    #   RDF::URI('http://www.w3.org/2000/01/rdf-schema#') === RDF::RDFS       #=> true
791
    #
792
    # @param  [Object] other
793
    # @return [Boolean] `true` or `false`
794
    # @since  0.3.0
795
    def ===(other)
2✔
796
      case other
1,634✔
797
        when Regexp then other === to_s
4✔
798
        else self == other
1,630✔
799
      end
800
    end
801

802
    ##
803
    # Performs a pattern match using the given regular expression.
804
    #
805
    # @example
806
    #   RDF::URI('http://example.org/') =~ /example/            #=> 7
807
    #   RDF::URI('http://example.org/') =~ /foobar/             #=> nil
808
    #
809
    # @param  [Regexp] pattern
810
    # @return [Integer] the position the match starts
811
    # @see    String#=~
812
    # @since  0.3.0
813
    def =~(pattern)
2✔
814
      case pattern
×
815
        when Regexp then to_s =~ pattern
×
816
        else super # `Object#=~` returns `false`
×
817
      end
818
    end
819

820
    ##
821
    # Returns `self`.
822
    #
823
    # @return [RDF::URI] `self`
824
    def to_uri
2✔
825
      self
37,424✔
826
    end
827

828
    ##
829
    # Returns the string representation of this URI.
830
    #
831
    # @example
832
    #   RDF::URI('http://example.org/').to_str                  #=> 'http://example.org/'
833
    #
834
    # @return [String]
835
    def to_str; value; end
4,054,506✔
836
    alias_method :to_s, :to_str
2✔
837

838
    ##
839
    # Returns a <code>String</code> representation of the URI object's state.
840
    #
841
    # @return [String] The URI object's state, as a <code>String</code>.
842
    def inspect
2✔
843
      sprintf("#<%s:%#0x URI:%s>", URI.to_s, self.object_id, self.to_s)
×
844
    end
845

846
    ##
847
    # lexical representation of URI, either absolute or relative
848
    # @return [String] 
849
    def value
2✔
850
      return @value if @value
4,317,690✔
851
      @value = [
852
        ("#{scheme}:" if absolute?),
762✔
853
        ("//#{authority}" if authority),
762✔
854
        path,
855
        ("?#{query}" if query),
762✔
856
        ("##{fragment}" if fragment)
762✔
857
      ].compact.join("").freeze
858
    end
859

860
    ##
861
    # Returns a hash code for this URI.
862
    #
863
    # @return [Integer]
864
    def hash
2✔
865
      @hash || @hash = (value.hash * -1)
14,521,206✔
866
    end
867

868
    ##
869
    # Returns object representation of this URI, broken into components
870
    #
871
    # @return [Hash{Symbol => String}]
872
    def object
2✔
873
      @object || @object = parse(@value)
319,124✔
874
    end
875
    alias_method :to_h, :object
2✔
876

877
    ##{
878
    # Parse a URI into it's components
879
    #
880
    # @param [String, to_s] value
881
    # @return [Object{Symbol => String}]
882
    def parse(value)
2✔
883
      value = value.to_s.dup.force_encoding(Encoding::ASCII_8BIT)
46,326✔
884
      parts = {}
46,326✔
885
      if matchdata = IRI_PARTS.match(value)
46,326✔
886
        scheme, authority, path, query, fragment = matchdata[1..-1]
46,326✔
887

888
        if Gem.win_platform? && scheme && !authority && scheme.match?(/^[a-zA-Z]$/)
46,326✔
889
          # A drive letter, not a scheme
890
          scheme, path = nil, "#{scheme}:#{path}"
×
891
        end
892

893
        userinfo, hostport = authority.to_s.split('@', 2)
46,326✔
894
        hostport, userinfo = userinfo, nil unless hostport
46,326✔
895
        user, password = userinfo.to_s.split(':', 2)
46,326✔
896
        host, port = hostport.to_s.split(':', 2)
46,326✔
897

898
        parts[:scheme] = (scheme.dup.force_encoding(Encoding::UTF_8) if scheme)
46,326✔
899
        parts[:authority] = (authority.dup.force_encoding(Encoding::UTF_8) if authority)
46,326✔
900
        parts[:userinfo] = (userinfo.dup.force_encoding(Encoding::UTF_8) if userinfo)
46,326✔
901
        parts[:user] = (user.dup.force_encoding(Encoding::UTF_8) if user)
46,326✔
902
        parts[:password] = (password.dup.force_encoding(Encoding::UTF_8) if password)
46,326✔
903
        parts[:host] = (host.dup.force_encoding(Encoding::UTF_8) if host)
46,326✔
904
        parts[:port] = (CGI.unescape(port).to_i if port)
46,326✔
905
        parts[:path] = (path.to_s.dup.force_encoding(Encoding::UTF_8) unless path.empty?)
46,326✔
906
        parts[:query] = (query[1..-1].dup.force_encoding(Encoding::UTF_8) if query)
46,326✔
907
        parts[:fragment] = (fragment[1..-1].dup.force_encoding(Encoding::UTF_8) if fragment)
46,326✔
908
      end
909
      
910
      parts
46,326✔
911
    end
912

913
    ##
914
    # @return [String]
915
    def scheme
2✔
916
      object.fetch(:scheme) do
4,590✔
917
        nil
×
918
      end
919
    end
920

921
    ##
922
    # @param [String, #to_s] value
923
    # @return [RDF::URI] self
924
    def scheme=(value)
2✔
925
      object[:scheme] = (value.to_s.dup.force_encoding(Encoding::UTF_8) if value)
142✔
926
      @value = nil
142✔
927
      self
142✔
928
    end
929

930
    ##
931
    # Return normalized version of scheme, if any
932
    # @return [String]
933
    def normalized_scheme
2✔
934
      scheme.strip.downcase if scheme
356✔
935
    end
936

937
    ##
938
    # @return [String]
939
    def user
2✔
940
      object.fetch(:user) do
43,412✔
941
        @object[:user] = (userinfo.split(':', 2)[0] if userinfo)
176✔
942
      end
943
    end
944

945
    ##
946
    # @param [String, #to_s] value
947
    # @return [RDF::URI] self
948
    def user=(value)
2✔
949
      object[:user] = (value.to_s.dup.force_encoding(Encoding::UTF_8) if value)
48✔
950
      @object[:userinfo] = format_userinfo("")
48✔
951
      @object[:authority] = format_authority
48✔
952
      @value = nil
48✔
953
      self
48✔
954
    end
955
    
956
    ##
957
    # Normalized version of user
958
    # @return [String]
959
    def normalized_user
2✔
960
      URI.encode(CGI.unescape(user), ENCODE_USER).force_encoding(Encoding::UTF_8) if user
8✔
961
    end
962

963
    ##
964
    # @return [String]
965
    def password
2✔
966
      object.fetch(:password) do
43,416✔
967
        @object[:password] = (userinfo.split(':', 2)[1] if userinfo)
176✔
968
      end
969
    end
970

971
    ##
972
    # @param [String, #to_s] value
973
    # @return [RDF::URI] self
974
    def password=(value)
2✔
975
      object[:password] = (value.to_s.dup.force_encoding(Encoding::UTF_8) if value)
48✔
976
      @object[:userinfo] = format_userinfo("")
48✔
977
      @object[:authority] = format_authority
48✔
978
      @value = nil
48✔
979
      self
48✔
980
    end
981
    
982
    ##
983
    # Normalized version of password
984
    # @return [String]
985
    def normalized_password
2✔
986
      URI.encode(CGI.unescape(password), ENCODE_PASSWORD).force_encoding(Encoding::UTF_8) if password
8✔
987
    end
988

989
    HOST_FROM_AUTHORITY_RE = /(?:[^@]+@)?([^:]+)(?::.*)?$/.freeze
2✔
990

991
    ##
992
    # @return [String]
993
    def host
2✔
994
      object.fetch(:host) do
44,302✔
995
        @object[:host] = ($1 if @object[:authority] && HOST_FROM_AUTHORITY_RE.match(@object[:authority]))
162✔
996
      end
997
    end
998

999
    ##
1000
    # @param [String, #to_s] value
1001
    # @return [RDF::URI] self
1002
    def host=(value)
2✔
1003
      object[:host] = (value.to_s.dup.force_encoding(Encoding::UTF_8) if value)
54✔
1004
      @object[:authority] = format_authority
54✔
1005
      @value = nil
54✔
1006
      self
54✔
1007
    end
1008
    
1009
    ##
1010
    # Normalized version of host
1011
    # @return [String]
1012
    def normalized_host
2✔
1013
      # Remove trailing '.' characters
1014
      host.sub(/\.*$/, '').downcase if host
300✔
1015
    end
1016

1017
    PORT_FROM_AUTHORITY_RE = /:(\d+)$/.freeze
2✔
1018

1019
    ##
1020
    # @return [String]
1021
    def port
2✔
1022
      object.fetch(:port) do
43,736✔
1023
        @object[:port] = ($1 if @object[:authority] && PORT_FROM_AUTHORITY_RE.match(@object[:authority]))
162✔
1024
      end
1025
    end
1026

1027
    ##
1028
    # @param [String, #to_s] value
1029
    # @return [RDF::URI] self
1030
    def port=(value)
2✔
1031
      object[:port] = (value.to_s.to_i if value)
48✔
1032
      @object[:authority] = format_authority
48✔
1033
      @value = nil
48✔
1034
      self
48✔
1035
    end
1036
    
1037
    ##
1038
    # Normalized version of port
1039
    # @return [String]
1040
    def normalized_port
2✔
1041
      if port
302✔
1042
        np = port.to_i
10✔
1043
        PORT_MAPPING[normalized_scheme] != np ? np : nil
10✔
1044
      end
1045
    end
1046

1047
    ##
1048
    # @return [String]
1049
    def path
2✔
1050
      object.fetch(:path) do
2,496✔
1051
        nil
×
1052
      end
1053
    end
1054

1055
    ##
1056
    # @param [String, #to_s] value
1057
    # @return [RDF::URI] self
1058
    def path=(value)
2✔
1059
      if value
312✔
1060
        # Always lead with a slash
1061
        value = "/#{value}" if host && value.to_s.match?(/^[^\/]/)
304✔
1062
        object[:path] = value.to_s.dup.force_encoding(Encoding::UTF_8)
304✔
1063
      else
1064
        object[:path] = nil
8✔
1065
      end
1066
      @value = nil
312✔
1067
      self
312✔
1068
    end
1069
    
1070
    ##
1071
    # Normalized version of path
1072
    # @return [String]
1073
    def normalized_path
2✔
1074
      segments = path.to_s.split('/', -1) # preserve null segments
346✔
1075

1076
      norm_segs = case
1077
      when authority
346✔
1078
        # ipath-abempty
1079
        segments.map {|s| normalize_segment(s, ENCODE_ISEGMENT)}
1,184✔
1080
      when segments[0].nil?
1081
        # ipath-absolute
1082
        res = [nil]
×
1083
        res << normalize_segment(segments[1], ENCODE_ISEGMENT) if segments.length > 1
×
1084
        res += segments[2..-1].map {|s| normalize_segment(s, ENCODE_ISEGMENT)} if segments.length > 2
×
1085
        res
×
1086
      when segments[0].to_s.index(':')
1087
        # ipath-noscheme
1088
        res = []
24✔
1089
        res << normalize_segment(segments[0], ENCODE_ISEGMENT_NC)
24✔
1090
        res += segments[1..-1].map {|s| normalize_segment(s, ENCODE_ISEGMENT)} if segments.length > 1
40✔
1091
        res
24✔
1092
      when segments[0]
1093
        # ipath-rootless
1094
        # ipath-noscheme
1095
        res = []
26✔
1096
        res << normalize_segment(segments[0], ENCODE_ISEGMENT)
26✔
1097
        res += segments[1..-1].map {|s| normalize_segment(s, ENCODE_ISEGMENT)} if segments.length > 1
108✔
1098
        res
26✔
1099
      else
1100
        # Should be empty
1101
        segments
×
1102
      end
1103

1104
      res = self.class.normalize_path(norm_segs.join("/"))
346✔
1105
      # Special rules for specific protocols having empty paths
1106
      res = (res.empty? && %w(http https ftp tftp).include?(normalized_scheme)) ? '/' : res
346✔
1107
    end
1108

1109
    ##
1110
    # @return [String]
1111
    def query
2✔
1112
      object.fetch(:query) do
1,420✔
1113
        nil
×
1114
      end
1115
    end
1116

1117
    ##
1118
    # @param [String, #to_s] value
1119
    # @return [RDF::URI] self
1120
    def query=(value)
2✔
1121
      object[:query] = (value.to_s.dup.force_encoding(Encoding::UTF_8) if value)
278✔
1122
      @value = nil
278✔
1123
      self
278✔
1124
    end
1125

1126
    ##
1127
    # Normalized version of query
1128
    # @return [String]
1129
    def normalized_query
2✔
1130
      normalize_segment(query, ENCODE_IQUERY) if query
346✔
1131
    end
1132

1133
    ##
1134
    # @return [String]
1135
    def fragment
2✔
1136
      object.fetch(:fragment) do
1,526✔
1137
        nil
×
1138
      end
1139
    end
1140

1141
    ##
1142
    # @param [String, #to_s] value
1143
    # @return [RDF::URI] self
1144
    def fragment=(value)
2✔
1145
      object[:fragment] = (value.to_s.dup.force_encoding(Encoding::UTF_8) if value)
352✔
1146
      @value = nil
352✔
1147
      self
352✔
1148
    end
1149

1150
    ##
1151
    # Normalized version of fragment
1152
    # @return [String]
1153
    def normalized_fragment
2✔
1154
      normalize_segment(fragment, ENCODE_IFRAGMENT) if fragment
346✔
1155
    end
1156

1157
    ##
1158
    # Authority is a combination of user, password, host and port
1159
    def authority
2✔
1160
      object.fetch(:authority) {
45,342✔
1161
        @object[:authority] = (format_authority if @object[:host])
82✔
1162
      }
1163
    end
1164

1165
    ##
1166
    # @param [String, #to_s] value
1167
    # @return [RDF::URI] self
1168
    def authority=(value)
2✔
1169
      object.delete_if {|k, v| %i(user password host port userinfo).include?(k)}
1,034✔
1170
      object[:authority] = (value.to_s.dup.force_encoding(Encoding::UTF_8) if value)
94✔
1171
      user; password; userinfo; host; port
94✔
1172
      @value = nil
94✔
1173
      self
94✔
1174
    end
1175

1176
    ##
1177
    # Return normalized version of authority, if any
1178
    # @return [String]
1179
    def normalized_authority
2✔
1180
      if authority
346✔
1181
        (userinfo ? normalized_userinfo.to_s + "@" : "") +
296✔
1182
        normalized_host.to_s +
1183
        (normalized_port ? ":" + normalized_port.to_s : "")
296✔
1184
      end
1185
    end
1186

1187
    ##
1188
    # Userinfo is a combination of user and password
1189
    def userinfo
2✔
1190
      object.fetch(:userinfo) {
43,936✔
1191
        @object[:userinfo] = (format_userinfo("") if @object[:user])
162✔
1192
      }
1193
    end
1194

1195
    ##
1196
    # @param [String, #to_s] value
1197
    # @return [RDF::URI] self
1198
    def userinfo=(value)
2✔
1199
      object.delete_if {|k, v| %i(user password authority).include?(k)}
902✔
1200
      object[:userinfo] = (value.to_s.dup.force_encoding(Encoding::UTF_8) if value)
82✔
1201
      user; password; authority
82✔
1202
      @value = nil
82✔
1203
      self
82✔
1204
    end
1205
    
1206
    ##
1207
    # Normalized version of userinfo
1208
    # @return [String]
1209
    def normalized_userinfo
2✔
1210
      normalized_user + (password ? ":#{normalized_password}" : "") if userinfo
6✔
1211
    end
1212

1213
    ##
1214
    # Converts the query component to a Hash value.
1215
    #
1216
    # @example
1217
    #   RDF::URI.new("?one=1&two=2&three=3").query_values
1218
    #   #=> {"one" => "1", "two" => "2", "three" => "3"}
1219
    #   RDF::URI.new("?one=two&one=three").query_values(Array)
1220
    #   #=> [["one", "two"], ["one", "three"]]
1221
    #   RDF::URI.new("?one=two&one=three").query_values(Hash)
1222
    #   #=> {"one" => ["two", "three"]}
1223
    #
1224
    # @param [Class] return_type (Hash)
1225
    #   The return type desired. Value must be either #   `Hash` or `Array`.
1226
    # @return [Hash, Array] The query string parsed as a Hash or Array object.
1227
    def query_values(return_type=Hash)
2✔
1228
      raise ArgumentError, "Invalid return type. Must be Hash or Array." unless [Hash, Array].include?(return_type)
38✔
1229
      return nil if query.nil?
38✔
1230
      query.to_s.split('&').
36✔
1231
        inject(return_type == Hash ? {} : []) do |memo,kv|
1232
          k,v = kv.to_s.split('=', 2)
60✔
1233
          next if k.to_s.empty?
60✔
1234
          k = CGI.unescape(k)
60✔
1235
          v = CGI.unescape(v) if v
60✔
1236
          if return_type == Hash
60✔
1237
            case memo[k]
38✔
1238
            when nil then memo[k] = v
30✔
1239
            when Array then memo[k] << v
2✔
1240
            else memo[k] = [memo[k], v]
6✔
1241
            end
1242
          else
1243
            memo << [k, v].compact
22✔
1244
          end
1245
          memo
60✔
1246
        end
1247
    end
1248

1249
    ##
1250
    # Sets the query component for this URI from a Hash object.
1251
    # An empty Hash or Array will result in an empty query string.
1252
    #
1253
    # @example Hash with single and array values
1254
    #   uri.query_values = {a: "a", b: ["c", "d", "e"]}
1255
    #   uri.query
1256
    #   # => "a=a&b=c&b=d&b=e"
1257
    #
1258
    # @example Array with Array values including repeated variables
1259
    #   uri.query_values = [['a', 'a'], ['b', 'c'], ['b', 'd'], ['b', 'e']]
1260
    #   uri.query
1261
    #   # => "a=a&b=c&b=d&b=e"
1262
    #
1263
    # @example Array with Array values including multiple elements
1264
    #   uri.query_values = [['a', 'a'], ['b', ['c', 'd', 'e']]]
1265
    #   uri.query
1266
    #   # => "a=a&b=c&b=d&b=e"
1267
    #
1268
    # @example Array with Array values having only one entry
1269
    #   uri.query_values = [['flag'], ['key', 'value']]
1270
    #   uri.query
1271
    #   # => "flag&key=value"
1272
    #
1273
    # @param [Hash, #to_hash, Array] value The new query values.
1274
    def query_values=(value)
2✔
1275
      if value.nil?
18✔
1276
        self.query = nil
2✔
1277
        return
2✔
1278
      end
1279

1280
      value = value.to_hash if value.respond_to?(:to_hash)
16✔
1281
      self.query = case value
16✔
1282
      when Array, Hash
1283
        value.map do |(k,v)|
16✔
1284
          k = normalize_segment(k.to_s, /[^A-Za-z0-9\._~-]/)
28✔
1285
          if v.nil?
28✔
1286
            k
4✔
1287
          else
1288
            Array(v).map do |vv|
24✔
1289
              if vv === TrueClass
36✔
1290
                k
×
1291
              else
1292
                "#{k}=#{normalize_segment(vv.to_s, /[^A-Za-z0-9\._~-]/)}"
36✔
1293
              end
1294
            end.join("&")
1295
          end
1296
        end
1297
      else
1298
        raise TypeError,
×
1299
          "Can't convert #{value.class} into Hash."
1300
      end.join("&")
1301
    end
1302

1303
    ##
1304
    # The HTTP request URI for this URI.  This is the path and the
1305
    # query string.
1306
    #
1307
    # @return [String] The request URI required for an HTTP request.
1308
    def request_uri
2✔
1309
      return nil if absolute? && scheme !~ /^https?$/
68✔
1310
      res = path.to_s.empty? ? "/" : path
62✔
1311
      res += "?#{self.query}" if self.query
62✔
1312
      return res
62✔
1313
    end
1314

1315
    ##
1316
    # Dump of data needed to reconsitute this object using Marshal.load
1317
    # This override is needed to avoid serializing @mutex.
1318
    #
1319
    # @param [Integer] level The maximum depth of objects to dump.
1320
    # @return [String] The dump of data needed to reconsitute this object.
1321
    def _dump(level)
2✔
1322
      value
2✔
1323
    end
1324

1325
    ##
1326
    # Load dumped data to reconsitute marshaled object
1327
    # This override is needed to avoid serializing @mutex.
1328
    #
1329
    # @param [String] data The dump of data needed to reconsitute this object.
1330
    # @return [RDF::URI] The reconsituted object.
1331
    def self._load(data)
2✔
1332
      new(data)
2✔
1333
    end
1334

1335
  private
2✔
1336

1337
    ##
1338
    # Normalize a segment using a character range
1339
    #
1340
    # @param [String] value
1341
    # @param [Regexp] expr matches characters to be encoded
1342
    # @param [Boolean] downcase
1343
    # @return [String]
1344
    def normalize_segment(value, expr, downcase = false)
2✔
1345
      if value
1,164✔
1346
        value = value.dup.force_encoding(Encoding::UTF_8)
1,164✔
1347
        decoded = CGI.unescape(value)
1,164✔
1348
        decoded.downcase! if downcase
1,164✔
1349
        URI.encode(decoded, expr).force_encoding(Encoding::UTF_8)
1,164✔
1350
      end
1351
    end
1352

1353
    def format_userinfo(append = "")
2✔
1354
      if @object[:user]
194✔
1355
        @object[:user] + (@object[:password] ? ":#{@object[:password]}" : "") + append
168✔
1356
      else
1357
        ""
26✔
1358
      end
1359
    end
1360

1361
    def format_authority
2✔
1362
      if @object[:host]
198✔
1363
        format_userinfo("@") + @object[:host] + (object[:port] ? ":#{object[:port]}" : "")
98✔
1364
      else
1365
        ""
100✔
1366
      end
1367
    end
1368

1369
    # URI encode matching characters in value
1370
    # From URI gem, as this is now generally deprecated
1371
    def self.encode(str, expr)
2✔
1372
      str.gsub(expr) do
1,176✔
1373
        us = $&
56✔
1374
        tmp = String.new
56✔
1375
        us.each_byte do |uc|
56✔
1376
          tmp << sprintf('%%%02X', uc)
56✔
1377
        end
1378
        tmp
56✔
1379
      end.force_encoding(Encoding::US_ASCII)
1380
    end
1381

1382
    # URI decode escape sequences in value
1383
    # From URI gem, as this is now generally deprecated
1384
    def self.decode(str)
2✔
1385
      enc = str.encoding
×
1386
      enc = Encoding::UTF_8 if enc == Encoding::US_ASCII
×
1387
      str.gsub(PCT_ENCODED) { [$&[1, 2]].pack('H2').force_encoding(enc) }
×
1388
    end
1389
  end
1390

1391
  # RDF::IRI is a synonym for RDF::URI
1392
  IRI = URI
2✔
1393
end # RDF
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc