forked from lsegal/yard
/
base.rb
209 lines (184 loc) · 7.14 KB
/
base.rb
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
# frozen_string_literal: true
require 'fileutils'
module YARD
module Server
module Commands
# This is the base command class used to implement custom commands for
# a server. A command will be routed to by the {Router} class and return
# a Rack-style response.
#
# == Attribute Initializers
# All attributes can be initialized via options passed into the {#initialize}
# method. When creating a custom command, the {Adapter#options} will
# automatically be mapped to attributes by the same name on your class.
#
# class MyCommand < Base
# attr_accessor :myattr
# end
#
# Adapter.new(libs, {:myattr => 'foo'}).start
#
# # when a request comes in, cmd.myattr == 'foo'
#
# == Subclassing Notes
# To implement a custom command, override the {#run} method, not {#call}.
# In your implementation, you should set the body and status for requests.
# See details in the +#run+ method documentation.
#
# Note that if your command deals directly with libraries, you should
# consider subclassing the more specific {LibraryCommand} class instead.
#
# @abstract
# @see #run
class Base
# @group Basic Command and Adapter Options
# @return [Hash] the options passed to the command's constructor
attr_accessor :command_options
# @return [Adapter] the server adapter
attr_accessor :adapter
# @return [Boolean] whether to cache
attr_accessor :caching
# @group Attributes Set Per Request
# @return [Rack::Request] request object
attr_accessor :request
# @return [String] the path after the command base URI
attr_accessor :path
# @return [Hash{String => String}] response headers
attr_accessor :headers
# @return [Numeric] status code. Defaults to 200 per request
attr_accessor :status
# @return [String] the response body. Defaults to empty string.
attr_accessor :body
# @group Instance Method Summary
# Creates a new command object, setting attributes named by keys
# in the options hash. After initialization, the options hash
# is saved in {#command_options} for further inspection.
#
# @example Creating a Command
# cmd = DisplayObjectCommand.new(:caching => true, :library => mylib)
# cmd.library # => mylib
# cmd.command_options # => {:caching => true, :library => mylib}
# @param [Hash] opts the options hash, saved to {#command_options}
# after initialization.
def initialize(opts = {})
opts.each do |key, value|
send("#{key}=", value) if respond_to?("#{key}=")
end
self.command_options = opts
end
# The main method called by a router with a request object.
#
# @note This command should not be overridden by subclasses. Implement
# the callback method {#run} instead.
# @param [Adapter Dependent] request the request object
# @return [Array(Numeric,Hash,Array<String>)] a Rack-style response
# of status, headers, and body wrapped in an array.
def call(request)
self.request = request
self.path ||= request.path_info[1..-1]
self.headers = {'Content-Type' => 'text/html'}
self.body = ''
self.status = 200
add_cache_control
begin
run
rescue FinishRequest
nil # noop
rescue NotFoundError => e
self.body = e.message if e.message != e.class.to_s
not_found
end
# keep this to support commands setting status manually.
not_found if status == 404
[status, headers, body.is_a?(Array) ? body : [body]]
end
# @group Abstract Methods
# Subclass this method to implement a custom command. This method
# should set the {#status} and {#body}, and optionally modify the
# {#headers}. Note that +#status+ defaults to 200.
#
# @example A custom command
# class ErrorCommand < Base
# def run
# self.body = 'ERROR! The System is down!'
# self.status = 500
# self.headers['Conten-Type'] = 'text/plain'
# end
# end
#
# @abstract
# @return [void]
def run
raise NotImplementedError
end
protected
# @group Helper Methods
# Renders a specific object if provided, or a regular template rendering
# if object is not provided.
#
# @todo This method is dependent on +#options+, it should be in {LibraryCommand}.
# @param [CodeObjects::Base, nil] object calls {CodeObjects::Base#format} if
# an object is provided, or {Templates::Engine.render} if object is nil. Both
# receive +#options+ as an argument.
# @return [String] the resulting output to display
def render(object = nil)
case object
when CodeObjects::Base
cache object.format(options)
when nil
cache Templates::Engine.render(options)
else
cache object
end
end
# Override this method to implement custom caching mechanisms for
#
# @example Caching to memory
# $memory_cache = {}
# def cache(data)
# $memory_cache[path] = data
# end
# @param [String] data the data to cache
# @return [String] the same cached data (for chaining)
# @see StaticCaching
def cache(data)
if caching && adapter.document_root
path = File.join(adapter.document_root, request.path_info.sub(/\.html$/, '') + '.html')
path = path.sub(%r{/\.html$}, '.html')
FileUtils.mkdir_p(File.dirname(path))
log.debug "Caching data to #{path}"
File.open(path, 'wb') {|f| f.write(data) }
end
self.body = data
end
# Sets the body and headers for a 404 response. Does not modify the
# body if already set.
#
# @return [void]
def not_found
self.status = 404
return unless body.empty?
self.body = "Not found: #{request.path}"
headers['Content-Type'] = 'text/plain'
headers['X-Cascade'] = 'pass'
headers.delete('Cache-Control')
end
# Sets the headers and status code for a redirection to a given URL
# @param [String] url the URL to redirect to
# @raise [FinishRequest] causes the request to terminate.
def redirect(url)
headers['Location'] = url
self.status = 302
raise FinishRequest
end
private
# Add a conservative cache control policy to reduce load on
# requests served with "?1234567890" style timestamp query strings.
def add_cache_control
return if request.query_string.to_i == 0
headers['Cache-Control'] = 'private, max-age=300'
end
end
end
end
end