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 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 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 230
|
require 'forwardable'
require 'flipper/ui/error'
require 'flipper/ui/eruby'
require 'json'
module Flipper
module UI
class Action
extend Forwardable
VALID_REQUEST_METHOD_NAMES = Set.new([
'get'.freeze,
'post'.freeze,
'put'.freeze,
'delete'.freeze,
]).freeze
# Public: Call this in subclasses so the action knows its route.
#
# regex - The Regexp that this action should run for.
#
# Returns nothing.
def self.route(regex)
@regex = regex
end
# Internal: Initializes and runs an action for a given request.
#
# flipper - The Flipper::DSL instance.
# request - The Rack::Request that was sent.
#
# Returns result of Action#run.
def self.run(flipper, request)
new(flipper, request).run
end
# Internal: The regex that matches which routes this action will work for.
def self.regex
@regex || raise("#{name}.route is not set")
end
# Private: The path to the views folder.
def self.views_path
@views_path ||= Flipper::UI.root.join('views')
end
# Private: The path to the public folder.
def self.public_path
@public_path ||= Flipper::UI.root.join('public')
end
# Public: The instance of the Flipper::DSL the middleware was
# initialized with.
attr_reader :flipper
# Public: The Rack::Request to provide a response for.
attr_reader :request
# Public: The params for the request.
def_delegator :@request, :params
def initialize(flipper, request)
@flipper = flipper
@request = request
@code = 200
@headers = { 'Content-Type' => 'text/plain' }
@breadcrumbs =
if Flipper::UI.application_breadcrumb_href
[Breadcrumb.new('App', Flipper::UI.application_breadcrumb_href)]
else
[]
end
end
# Public: Runs the request method for the provided request.
#
# Returns whatever the request method returns in the action.
def run
if valid_request_method? && respond_to?(request_method_name)
catch(:halt) { send(request_method_name) }
else
raise UI::RequestMethodNotSupported,
"#{self.class} does not support request method #{request_method_name.inspect}"
end
end
# Public: Runs another action from within the request method of a
# different action.
#
# action_class - The class of the other action to run.
#
# Examples
#
# run_other_action Home
# # => result of running Home action
#
# Returns result of other action.
def run_other_action(action_class)
action_class.new(flipper, request).run
end
# Public: Call this with a response to immediately stop the current action
# and respond however you want.
#
# response - The response you would like to return.
def halt(response)
throw :halt, response
end
# Public: Compiles a view and returns rack response with that as the body.
#
# name - The Symbol name of the view.
#
# Returns a response.
def view_response(name)
header 'Content-Type', 'text/html'
body = view_with_layout { view_without_layout name }
halt [@code, @headers, [body]]
end
# Public: Dumps an object as json and returns rack response with that as
# the body. Automatically sets Content-Type to "application/json".
#
# object - The Object that should be dumped as json.
#
# Returns a response.
def json_response(object)
header 'Content-Type', 'application/json'
body = JSON.dump(object)
halt [@code, @headers, [body]]
end
# Public: Redirect to a new location.
#
# location - The String location to set the Location header to.
def redirect_to(location)
status 302
header 'Location', "#{script_name}#{location}"
halt [@code, @headers, ['']]
end
# Public: Set the status code for the response.
#
# code - The Integer code you would like the response to return.
def status(code)
@code = code.to_i
end
# Public: Set a header.
#
# name - The String name of the header.
# value - The value of the header.
def header(name, value)
@headers[name] = value
end
class Breadcrumb
attr_reader :text, :href
def initialize(text, href = nil)
@text = text
@href = href
end
def active?
@href.nil?
end
end
# Public: Add a breadcrumb to the trail.
#
# text - The String text for the breadcrumb.
# href - The String href for the anchor tag (optional). If nil, breadcrumb
# is assumed to be the end of the trail.
def breadcrumb(text, href = nil)
breadcrumb_href = href.nil? ? href : "#{script_name}#{href}"
@breadcrumbs << Breadcrumb.new(text, breadcrumb_href)
end
# Private
def view_with_layout(&block)
view :layout, &block
end
# Private
def view_without_layout(name)
view name
end
# Private
def view(name)
path = views_path.join("#{name}.erb")
raise "Template does not exist: #{path}" unless path.exist?
contents = path.read
compiled = Eruby.new(contents)
compiled.result proc {}.binding
end
# Internal: The path the app is mounted at.
def script_name
request.env['SCRIPT_NAME']
end
# Private
def views_path
self.class.views_path
end
# Private
def public_path
self.class.public_path
end
# Private: Returns the request method converted to an action method.
def request_method_name
@request_method_name ||= @request.request_method.downcase
end
def csrf_input_tag
%(<input type="hidden" name="authenticity_token" value="#{@request.session[:csrf]}">)
end
def valid_request_method?
VALID_REQUEST_METHOD_NAMES.include?(request_method_name)
end
end
end
end
|