File: url_generator.rb

package info (click to toggle)
ruby-roadie 5.2.1-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 528 kB
  • sloc: ruby: 3,418; makefile: 5
file content (150 lines) | stat: -rw-r--r-- 4,557 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# frozen_string_literal: true

require "set"

module Roadie
  # @api private
  # Class that handles URL generation
  #
  # URL generation is all about converting relative URLs into absolute URLS
  # according to the given options. It is written such as absolute URLs will
  # get passed right through, so all URLs could be passed through here.
  class UrlGenerator
    attr_reader :url_options

    # Create a new instance with the given URL options.
    #
    # Initializing without a host setting raises an error, as do unknown keys.
    #
    # @param [Hash] url_options
    # @option url_options [String] :host (required)
    # @option url_options [String, Integer] :port
    # @option url_options [String] :path root path
    # @option url_options [String] :scheme URL scheme ("http" is default)
    # @option url_options [String] :protocol alias for :scheme
    def initialize(url_options)
      unless url_options
        raise ArgumentError, "No URL options were specified"
      end

      unless url_options[:host]
        raise ArgumentError,
          "No :host was specified; options were: #{url_options.inspect}"
      end

      validate_options url_options

      @url_options = url_options
      @scheme = normalize_scheme(url_options[:scheme] || url_options[:protocol])
      @root_uri = build_root_uri
    end

    # Generate an absolute URL from a relative URL.
    #
    # If the passed path is already an absolute URL or just an anchor
    # reference, it will be returned as-is.
    # If passed a blank path, the "root URL" will be returned. The root URL is
    # the URL that the {#url_options} would generate by themselves.
    #
    # An optional base can be specified. The base is another relative path from
    # the root that specifies an "offset" from which the path was found in. A
    # common use-case is to convert a relative path found in a stylesheet which
    # resides in a subdirectory.
    #
    # @example Normal conversions
    #   generator = Roadie::UrlGenerator.new host: "foo.com", scheme: "https"
    #   generator.generate_url("bar.html") # => "https://foo.com/bar.html"
    #   generator.generate_url("/bar.html") # => "https://foo.com/bar.html"
    #   generator.generate_url("") # => "https://foo.com"
    #
    # @example Conversions with a base
    #   generator = Roadie::UrlGenerator.new host: "foo.com", scheme: "https"
    #   generator.generate_url("../images/logo.png", "/css") # => "https://foo.com/images/logo.png"
    #   generator.generate_url("../images/logo.png", "/assets/css") # => "https://foo.com/assets/images/logo.png"
    #
    # @param [String] base The base which the relative path comes from
    # @return [String] an absolute URL
    def generate_url(path, base = "/")
      return root_uri.to_s if path.nil? || path.empty?
      return path if path_is_anchor?(path)
      return add_scheme(path) if path_is_schemeless?(path)
      return path if Utils.path_is_absolute?(path)

      combine_segments(root_uri, base, path).to_s
    end

    protected

    attr_reader :root_uri, :scheme

    private

    def build_root_uri
      path = make_absolute url_options[:path]
      port = parse_port url_options[:port]
      URI::Generic.build(
        scheme: scheme,
        host: url_options[:host],
        port: port,
        path: path
      )
    end

    def add_scheme(path)
      [scheme, path].join(":")
    end

    def combine_segments(root, base, path)
      new_path = apply_base(base, path)
      if root.path
        new_path = File.join(root.path, new_path)
      end
      root.merge(new_path)
    end

    def apply_base(base, path)
      if path[0] == "/"
        path
      else
        File.join(base, path)
      end
    end

    # Strip :// from any scheme, if present
    def normalize_scheme(scheme)
      return "http" unless scheme
      scheme.to_s[/^\w+/]
    end

    def parse_port(port)
      (port ? port.to_i : port)
    end

    def make_absolute(path)
      if path.nil? || path[0] == "/"
        path
      else
        "/#{path}"
      end
    end

    def path_is_schemeless?(path)
      path =~ %r{^//\w}
    end

    def path_is_anchor?(path)
      path.start_with? "#"
    end

    VALID_OPTIONS = Set[:host, :port, :path, :protocol, :scheme].freeze

    def validate_options(options)
      keys = Set.new(options.keys)
      unless keys.subset? VALID_OPTIONS
        raise ArgumentError,
          "Passed invalid options: #{(keys - VALID_OPTIONS).to_a}, " \
            "valid options are: #{VALID_OPTIONS.to_a}"
      end
    end
  end
end