Mange / roadie
1
# frozen_string_literal: true
2

3 5
require 'set'
4

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

15
    # Create a new instance with the given URL options.
16
    #
17
    # Initializing without a host setting raises an error, as do unknown keys.
18
    #
19
    # @param [Hash] url_options
20
    # @option url_options [String] :host (required)
21
    # @option url_options [String, Integer] :port
22
    # @option url_options [String] :path root path
23
    # @option url_options [String] :scheme URL scheme ("http" is default)
24
    # @option url_options [String] :protocol alias for :scheme
25 5
    def initialize(url_options)
26 5
      raise ArgumentError, "No URL options were specified" unless url_options
27 5
      raise ArgumentError, "No :host was specified; options are: #{url_options.inspect}" unless url_options[:host]
28 5
      validate_options url_options
29

30 5
      @url_options = url_options
31 5
      @scheme = normalize_scheme(url_options[:scheme] || url_options[:protocol])
32 5
      @root_uri = build_root_uri
33
    end
34

35
    # Generate an absolute URL from a relative URL.
36
    #
37
    # If the passed path is already an absolute URL or just an anchor
38
    # reference, it will be returned as-is.
39
    # If passed a blank path, the "root URL" will be returned. The root URL is
40
    # the URL that the {#url_options} would generate by themselves.
41
    #
42
    # An optional base can be specified. The base is another relative path from
43
    # the root that specifies an "offset" from which the path was found in. A
44
    # common use-case is to convert a relative path found in a stylesheet which
45
    # resides in a subdirectory.
46
    #
47
    # @example Normal conversions
48
    #   generator = Roadie::UrlGenerator.new host: "foo.com", scheme: "https"
49
    #   generator.generate_url("bar.html") # => "https://foo.com/bar.html"
50
    #   generator.generate_url("/bar.html") # => "https://foo.com/bar.html"
51
    #   generator.generate_url("") # => "https://foo.com"
52
    #
53
    # @example Conversions with a base
54
    #   generator = Roadie::UrlGenerator.new host: "foo.com", scheme: "https"
55
    #   generator.generate_url("../images/logo.png", "/css") # => "https://foo.com/images/logo.png"
56
    #   generator.generate_url("../images/logo.png", "/assets/css") # => "https://foo.com/assets/images/logo.png"
57
    #
58
    # @param [String] base The base which the relative path comes from
59
    # @return [String] an absolute URL
60 5
    def generate_url(path, base = "/")
61 5
      return root_uri.to_s if path.nil? or path.empty?
62 5
      return path if path_is_anchor?(path)
63 5
      return add_scheme(path) if path_is_schemeless?(path)
64 5
      return path if Utils.path_is_absolute?(path)
65

66 5
      combine_segments(root_uri, base, path).to_s
67
    end
68

69 5
    protected
70 5
    attr_reader :root_uri, :scheme
71

72 5
    private
73 5
    def build_root_uri
74 5
      path = make_absolute url_options[:path]
75 5
      port = parse_port url_options[:port]
76 5
      URI::Generic.build(scheme: scheme, host: url_options[:host], port: port, path: path)
77
    end
78

79 5
    def add_scheme(path)
80 5
      [scheme, path].join(":")
81
    end
82

83 5
    def combine_segments(root, base, path)
84 5
      new_path = apply_base(base, path)
85 5
      if root.path
86 5
        new_path = File.join(root.path, new_path)
87
      end
88 5
      root.merge(new_path)
89
    end
90

91 5
    def apply_base(base, path)
92 5
      if path[0] == "/"
93 5
        path
94
      else
95 5
        File.join(base, path)
96
      end
97
    end
98

99
    # Strip :// from any scheme, if present
100 5
    def normalize_scheme(scheme)
101 5
      return 'http' unless scheme
102 5
      scheme.to_s[/^\w+/]
103
    end
104

105 5
    def parse_port(port)
106 5
      (port ? port.to_i : port)
107
    end
108

109 5
    def make_absolute(path)
110 5
      if path.nil? || path[0] == "/"
111 5
        path
112
      else
113 5
        "/#{path}"
114
      end
115
    end
116

117 5
    def path_is_schemeless?(path)
118 5
      path =~ %r{^//\w}
119
    end
120

121 5
    def path_is_anchor?(path)
122 5
      path.start_with? '#'
123
    end
124

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

127 5
    def validate_options(options)
128 5
      keys = Set.new(options.keys)
129 5
      unless keys.subset? VALID_OPTIONS
130 5
        raise ArgumentError, "Passed invalid options: #{(keys - VALID_OPTIONS).to_a}, valid options are: #{VALID_OPTIONS.to_a}"
131
      end
132
    end
133
  end
134
end

Read our documentation on viewing source code .

Loading