~hacktivista/leanweb

ref: 122ee2c848ec24bdd2f6c88172fb2cc1a1203959 leanweb/lib/leanweb/route.rb -rw-r--r-- 5.5 KiB
122ee2c8Felix Freeman Nested Controller#render 4 months ago
                                                                                
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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
# frozen_string_literal: true

# Copyright 2022 Felix Freeman <libsys@hacktivista.org>
#
# This file is part of "LeanWeb" and licensed under the terms of the Hacktivista
# General Public License version 0.1 or (at your option) any later version. You
# should have received a copy of this license along with the software. If not,
# see <https://hacktivista.org/licenses/>.


module LeanWeb
  # Action for {Route#action}.
  Action = Struct.new(:file, :controller, :action, keyword_init: true)

  # A single route which routes with the {#respond} method. It can also {#build}
  # an static file.
  class Route
    attr_reader :path, :method, :action, :static

    # A new instance of Route.
    #
    # @param path [String, Regexp] Path matcher, can be an String or Regexp with
    #   positional or named capture groups, `@action` will receive these as
    #   positional or named arguments.
    # @param method [String, nil] Must be an HTTP verb such as `GET` or `POST`.
    # @param action [Proc, Hash, String, nil] References a Method/Proc to be
    #   triggered. It can be:
    #
    #   - A full hash `{ file: 'val', controller: 'val', action: 'val' }`.
    #   - A hash with `{ 'file' => 'action' }` only (which has a controller
    #     class name `{File}Controller`).
    #   - A simple string (which will consider file `main.rb` and controller
    #     `MainController`). Defaults to "{#path_basename}_{#method}", (ex:
    #     `index_get`).
    #   - It can also be a `Proc`.
    #
    # @param static [Boolean|Array] Defines a route as static. Set to `false` to
    #   say it can only work dynamically. You can also supply an array of arrays
    #   or hashes to generate static files based on that positional or keyword
    #   params.
    def initialize(path:, method: 'GET', action: nil, static: true)
      @path = path
      @method = method
      self.action = action
      @static = static
    end

    # Last identifier on a path, returns `index` for `/`.
    def path_basename
      str_path[-1] == '/' ? 'index' : File.basename(str_path)
    end

    # @param request [Rack::Request]
    # @return [Array] a valid rack response.
    def respond(request)
      return respond_proc(request) if @action.instance_of?(Proc)

      respond_method(request)
    end

    # String path, independent if {#path} is Regexp or String.
    def str_path
      @path.source.gsub(/[\^$]/, '')
    rescue NoMethodError
      @path
    end

    # On Regexp paths, return a string valid for making a request to this route.
    #
    # @param seed [Array, Hash] Seeds to use as replacement on capture groups.
    # @return [String] sown path.
    def seed_path(seed)
      sown_path = str_path
      if seed.instance_of?(Hash)
        seed.each { |key, val| sown_path.sub!(/\(\?<#{key}>[^)]+\)/, val) }
      else
        seed.each { |val| sown_path.sub!(/\([^)]+\)/, val) }
      end
      sown_path
    end

    # Build this route as an static file and place it relative to
    #   {LeanWeb::PUBLIC_PATH}.
    #
    # @param request_path [String] Request path for dynamic (regex) routes.
    def build(request_path = @path)
      response = respond(
        Rack::Request.new(
          { 'PATH_INFO' => request_path, 'REQUEST_METHOD' => 'GET' }
        )
      )
      out_path = output_path(request_path, response[1]['Content-Type'] || nil)
      FileUtils.mkdir_p(File.dirname(out_path))

      File.write(out_path, response[2][0])
    end

    protected

    # Assign value to `@action`.
    def action=(value)
      @action = if value.instance_of?(Proc)
                  value
                else
                  Action.new(**prepare_action_hash(value))
                end
    end

    # @param value [Hash, String, nil] Check {#initialize} action param for
    #   valid input.
    # @return [Hash] valid hash for {Action}.
    def prepare_action_hash(value)
      begin
        value[:file], value[:action] = value.first \
          unless %i[file controller action].include?(value.keys.first)
      rescue NoMethodError
        value = { action: value }
      end
      value[:file] ||= 'main'
      value[:controller] ||= "#{value[:file].capitalize}Controller"
      value[:action] ||= "#{path_basename}_#{@method.downcase}"
      value
    end

    # @param request [Rack::Request]
    # @return [Array] a valid Rack response.
    def respond_method(request)
      params = action_params(request.path)
      require_relative("#{LeanWeb::CONTROLLER_PATH}/#{@action.file}")
      controller = Object.const_get(@action.controller).new(self, request)
      return controller.public_send(@action.action, **params)\
        if params.instance_of?(Hash)

      controller.public_send(@action.action, *params)
    end

    # @param request [Rack::Request]
    # @return [Array] a valid Rack response.
    def respond_proc(request)
      params = action_params(request.path)
      return @action.call(**params) if params.instance_of?(Hash)

      @action.call(*params)
    end

    # @param request_path [String]
    # @return [Array, Hash]
    def action_params(request_path)
      return nil unless @path.instance_of?(Regexp)

      matches = @path.match(request_path)
      return matches.named_captures.transform_keys(&:to_sym)\
        if matches.named_captures != {}

      matches.captures
    end

    # Output path for public file.
    #
    # @param path [String]
    # @param content_type [String]
    # @return [String] absolute route to path + extension based on content_type.
    def output_path(path, content_type)
      path += 'index' if path[-1] == '/'
      "#{LeanWeb::PUBLIC_PATH}#{path}#{LeanWeb::MEDIA_EXTENSIONS[content_type]}"
    end
  end
end