sidewalk 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/sidewalk/app_uri.rb +33 -0
- data/lib/sidewalk/application.rb +146 -0
- data/lib/sidewalk/config.rb +160 -0
- data/lib/sidewalk/controller.rb +141 -0
- data/lib/sidewalk/controller_mixins/view_templates.rb +94 -0
- data/lib/sidewalk/errors.rb +71 -0
- data/lib/sidewalk/redirect.rb +78 -0
- data/lib/sidewalk/regexp.rb +32 -5
- data/lib/sidewalk/relative_uri.rb +38 -0
- data/lib/sidewalk/request.rb +116 -13
- data/lib/sidewalk/rooted_uri.rb +32 -0
- data/lib/sidewalk/template_handlers/base.rb +30 -0
- data/lib/sidewalk/template_handlers/base_delegate.rb +39 -0
- data/lib/sidewalk/template_handlers/erb_handler.rb +40 -0
- data/lib/sidewalk/template_handlers/haml_handler.rb +19 -0
- data/lib/sidewalk/template_handlers/rxhp_handler.rb +32 -0
- data/lib/sidewalk/uri.rb +2 -0
- data/lib/sidewalk/uri_mapper.rb +89 -2
- data/lib/sidewalk/uri_match.rb +17 -1
- data/lib/sidewalk.rb +2 -0
- metadata +32 -4
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'sidewalk/controller'
|
2
|
+
require 'sidewalk/rooted_uri'
|
3
|
+
require 'sidewalk/request'
|
4
|
+
|
5
|
+
module Sidewalk
|
6
|
+
# URI relative to the root of the application.
|
7
|
+
#
|
8
|
+
# For example, if your app lives at 'http://www.example.com/foo',
|
9
|
+
# +AppUri.new('/bar').to_s+ will reutrn 'http://www.example.com/foo/bar'.
|
10
|
+
#
|
11
|
+
# Not a real +URI+ subclass, as URI subclasses by protocol, and this
|
12
|
+
# might return a +URI::HTTP+ or +URI::HTTPS+ subclass.
|
13
|
+
module AppUri
|
14
|
+
# Create a URI relative to the application.
|
15
|
+
#
|
16
|
+
# If this is called, it _must_ have {Controller#call} in the call stack
|
17
|
+
# so that {Controller#current} works - otherwise it does not have
|
18
|
+
# enough information to construct the URI.
|
19
|
+
#
|
20
|
+
# @param [String] path is the path relative to the root of the
|
21
|
+
# application.
|
22
|
+
# @param [Hash] query is a +Hash+ of key-value query data.
|
23
|
+
def self.new path, query = {}
|
24
|
+
context = Sidewalk::Controller.current
|
25
|
+
unless context
|
26
|
+
raise ScriptError.new("Only valid when called by a controller")
|
27
|
+
end
|
28
|
+
uri = context.request.root_uri
|
29
|
+
|
30
|
+
Sidewalk::RootedUri.new(uri, path, query)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'sidewalk/request'
|
2
|
+
require 'sidewalk/errors'
|
3
|
+
require 'sidewalk/redirect'
|
4
|
+
require 'sidewalk/uri_mapper'
|
5
|
+
|
6
|
+
autoload :Logger, 'logger'
|
7
|
+
|
8
|
+
module Sidewalk
|
9
|
+
# The main Rack Application.
|
10
|
+
#
|
11
|
+
# This class is responsible for dispatch requests (based on a
|
12
|
+
# {Sidewalk::UriMapper}), and handling some exceptions, such as
|
13
|
+
# {NotFoundError}, {ForbiddenError}, or {SeeOtherRedirect}.
|
14
|
+
#
|
15
|
+
# If you want special error handling, subclass this, and reimplement
|
16
|
+
# {#respond_to_error}.
|
17
|
+
class Application
|
18
|
+
# The {UriMapper} class this application is using.
|
19
|
+
#
|
20
|
+
# This is constructed from the uri_map Hash argument to {#initialize}.
|
21
|
+
attr_reader :mapper
|
22
|
+
|
23
|
+
# The path on the server where the application code is.
|
24
|
+
#
|
25
|
+
# This should be the path containing config.ru. This will be something
|
26
|
+
# like +/var/www+, or +/home/fred/public_html+.
|
27
|
+
def self.local_root
|
28
|
+
@local_root ||= Dir.pwd
|
29
|
+
end
|
30
|
+
|
31
|
+
# Construct an instance, based on a URI-mapping Hash.
|
32
|
+
#
|
33
|
+
# @param [Hash] uri_map is a +String+ => +Class+, +Symbol+, +String+,
|
34
|
+
# or +Proc+ map. See {UriMapper#initialize} for details of acceptable
|
35
|
+
# keys; the short version is that they are +String+s that contain
|
36
|
+
# regular expression patterns. They are not +Regexp+s.
|
37
|
+
def initialize uri_map
|
38
|
+
@mapper = Sidewalk::UriMapper.new(uri_map)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Per-request Rack entry point.
|
42
|
+
#
|
43
|
+
# This is called for every request. It's responsible for:
|
44
|
+
# * Normalizing the environment parameter
|
45
|
+
# * Finding what should respond to the request (via {UriMapper})
|
46
|
+
# * Actually responding
|
47
|
+
# * Error handling - see {#handle_error}
|
48
|
+
#
|
49
|
+
# @param [Hash] env is an environment +Hash+, defined in the Rack
|
50
|
+
# specification.
|
51
|
+
def call env
|
52
|
+
logger = ::Logger.new(env['rack.error'])
|
53
|
+
|
54
|
+
path_info = env['PATH_INFO']
|
55
|
+
if path_info.start_with? '/'
|
56
|
+
path_info[0,1] = ''
|
57
|
+
end
|
58
|
+
|
59
|
+
match = @mapper.map path_info
|
60
|
+
if match
|
61
|
+
env['sidewalk.urimatch'] = match
|
62
|
+
# Normalize reponders to Procs
|
63
|
+
if match.controller.is_a? Class
|
64
|
+
responder = lambda do |request, logger|
|
65
|
+
match.controller.new(request, logger).call
|
66
|
+
end
|
67
|
+
else
|
68
|
+
responder = match.controller
|
69
|
+
end
|
70
|
+
else
|
71
|
+
responder = lambda { |*args| raise NotFoundError.new }
|
72
|
+
end
|
73
|
+
request = Sidewalk::Request.new(env)
|
74
|
+
|
75
|
+
# Try and call - but it can throw special exceptions that we
|
76
|
+
# want to map into HTTP status codes - for example, NotFoundError
|
77
|
+
# is a 404
|
78
|
+
begin
|
79
|
+
responder.call(
|
80
|
+
request,
|
81
|
+
logger
|
82
|
+
)
|
83
|
+
rescue Exception => e
|
84
|
+
response = respond_to_error(e, request, logger)
|
85
|
+
raise if response.nil? || response.empty?
|
86
|
+
response
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Give a response for a given error.
|
91
|
+
#
|
92
|
+
# If you want custom error pages, you will want to subclass
|
93
|
+
# {Application}, reimplementing this method. At a minimum, you will
|
94
|
+
# probably want to include support for:
|
95
|
+
# * {HttpError}
|
96
|
+
# * {Redirect}
|
97
|
+
#
|
98
|
+
# This implementation has hard-coded responses in the format defined in
|
99
|
+
# the Rack specification.
|
100
|
+
#
|
101
|
+
# @example An implementation that uses {Controller} subclasses:
|
102
|
+
# def respond_to_error(error, request, logger)
|
103
|
+
# case error
|
104
|
+
# when Redirect
|
105
|
+
# MyRedirectController.new(error, request, logger).call
|
106
|
+
# when HttpError
|
107
|
+
# # ...
|
108
|
+
# else
|
109
|
+
# super(error, request, logger)
|
110
|
+
# end
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# @param [Exception] error is the error that was +raise+d
|
114
|
+
# @param [Request] request gives information on the request that
|
115
|
+
# led to the error.
|
116
|
+
# @param [Logger] logger is something implementing a similiar API
|
117
|
+
# to Ruby's standard +Logger+ class.
|
118
|
+
#
|
119
|
+
# @return A Rack response +Array+ - i.e.:
|
120
|
+
# +[status, headers, body_parts]+ - see the specification for
|
121
|
+
# details.
|
122
|
+
# @return +nil+ to indicate the error isn't handled - it will get
|
123
|
+
# escalated to Rack and probably lead to a 500.
|
124
|
+
def respond_to_error(error, request, logger)
|
125
|
+
case error
|
126
|
+
when Redirect
|
127
|
+
[
|
128
|
+
error.status(request),
|
129
|
+
{
|
130
|
+
'Content-Type' => 'text/plain',
|
131
|
+
'Location' => error.url,
|
132
|
+
},
|
133
|
+
["#{error.status(request)} #{error.description(request)}"]
|
134
|
+
]
|
135
|
+
when HttpError
|
136
|
+
[
|
137
|
+
error.status(request),
|
138
|
+
{'Content-Type' => 'text/plain'},
|
139
|
+
["#{error.status(request)} #{error.description(request)}"]
|
140
|
+
]
|
141
|
+
else
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
module Sidewalk
|
4
|
+
autoload :Application, 'sidewalk/application'
|
5
|
+
|
6
|
+
# Class for storing application configuration.
|
7
|
+
#
|
8
|
+
# This will:
|
9
|
+
# * load +config/environment.rb+ (arbitrary Ruby)
|
10
|
+
# * read +config/environment.yaml+ into a +Hash+
|
11
|
+
# * the same for +config/#{ENV['RACK_ENV']}.rb+ and +.yaml+
|
12
|
+
#
|
13
|
+
# @example environment.yaml
|
14
|
+
# activerecord:
|
15
|
+
# adapter: sqlite
|
16
|
+
# database: data.sqlite
|
17
|
+
#
|
18
|
+
# @example Reading from {Sidewalk::Config}
|
19
|
+
# ActiveRecord::Base.establish_connection(
|
20
|
+
# Sidewalk::Config['activerecord']
|
21
|
+
# )
|
22
|
+
class Config
|
23
|
+
# Initialize a Config object at the given path.
|
24
|
+
#
|
25
|
+
# You probably want to use the class methods instead.
|
26
|
+
def initialize path
|
27
|
+
@config = Hash.new
|
28
|
+
@root = path
|
29
|
+
[
|
30
|
+
'environment.rb',
|
31
|
+
"#{Config.environment}.rb",
|
32
|
+
].each{|x| load_ruby!(x, :silent => true)}
|
33
|
+
|
34
|
+
yaml_configs = [
|
35
|
+
'environment.yaml',
|
36
|
+
"#{Config.environment}.yaml",
|
37
|
+
].each{|x| load_yaml!(x, :silent => true)}
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return a configuration value from YAML.
|
41
|
+
#
|
42
|
+
# Acts like a +Hash+. Also available as a class method.
|
43
|
+
#
|
44
|
+
# @param [String] key
|
45
|
+
def [] key
|
46
|
+
@config[key]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Store a configuration value, as if it were in the YAML.
|
50
|
+
#
|
51
|
+
# Also available as a class method.
|
52
|
+
#
|
53
|
+
# @param [String] key
|
54
|
+
# @param [Object] value
|
55
|
+
def []= key, value
|
56
|
+
@config[key] = value
|
57
|
+
end
|
58
|
+
|
59
|
+
# Execute a Ruby file.
|
60
|
+
#
|
61
|
+
# Nothing special is done, it's just evaluated.
|
62
|
+
#
|
63
|
+
# Also available as a class method.
|
64
|
+
#
|
65
|
+
# @raise [LoadError] if +file+ does not exist, unless +:silent+ is set
|
66
|
+
# to true in +options+.
|
67
|
+
# @param [String] file the path to the file, relative to the base
|
68
|
+
# configuration path.
|
69
|
+
# @param [Hash] options
|
70
|
+
def load_ruby! file, options = {}
|
71
|
+
path = File.join(@root, file)
|
72
|
+
begin
|
73
|
+
load path
|
74
|
+
rescue LoadError
|
75
|
+
raise unless options[:silent]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Read a YAML file, and merge with the current config.
|
80
|
+
#
|
81
|
+
# This is available via {#[]}
|
82
|
+
#
|
83
|
+
# Also available as a class method.
|
84
|
+
#
|
85
|
+
# @raise [LoadError] if +file+ does not exist, unless +:silent+ is set
|
86
|
+
# to true in +options+.
|
87
|
+
# @param [String] file the path to the file, relative to the base
|
88
|
+
# configuration path.
|
89
|
+
# @param [Hash] options
|
90
|
+
def load_yaml! file, options = {}
|
91
|
+
path = File.join(@root, file)
|
92
|
+
if File.exists? path
|
93
|
+
@config.merge! YAML.load(File.read(path))
|
94
|
+
else
|
95
|
+
unless options[:silent]
|
96
|
+
raise LoadError.new("unable to find #{file}")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
class<<self
|
102
|
+
# The main instance of {Config}.
|
103
|
+
#
|
104
|
+
# You probably don't want to use this - all of the methods defined on
|
105
|
+
# instances are usable as class methods instead, that map onto the
|
106
|
+
# instance.
|
107
|
+
#
|
108
|
+
# @return [Config] an instance of {Config}
|
109
|
+
def instance
|
110
|
+
@instance ||= Config.new(self.path)
|
111
|
+
end
|
112
|
+
alias :init! :instance
|
113
|
+
|
114
|
+
# Where to look for configuration files.
|
115
|
+
#
|
116
|
+
# This defaults to +config/+ underneath the application root.
|
117
|
+
def path
|
118
|
+
@path ||= Application.local_root + '/config'
|
119
|
+
end
|
120
|
+
attr_writer :path
|
121
|
+
|
122
|
+
# What the current Rack environment is.
|
123
|
+
#
|
124
|
+
# This is an arbitrary string, but will usually be:
|
125
|
+
# production:: this is live, probably via Passenger
|
126
|
+
# development:: running on a developer's local machine, probably via
|
127
|
+
# +rackup+ or +shotgun+
|
128
|
+
# testing:: automated tests are running.
|
129
|
+
#
|
130
|
+
# This is copied from +ENV['RACK_ENV']+, but can be overridden (see
|
131
|
+
# {#environment=}).
|
132
|
+
def environment
|
133
|
+
@environment ||= ENV['RACK_ENV'] || raise("Unable to determine environment. Set ENV['RACK_ENV'].")
|
134
|
+
end
|
135
|
+
|
136
|
+
# Override the auto-detected environment.
|
137
|
+
#
|
138
|
+
# Handy for testing - especially as RACK_ENV might not even be set.
|
139
|
+
def environment= foo
|
140
|
+
@environment = foo
|
141
|
+
end
|
142
|
+
|
143
|
+
def production?
|
144
|
+
self.environment == 'production'
|
145
|
+
end
|
146
|
+
|
147
|
+
def development?
|
148
|
+
self.environment == 'development'
|
149
|
+
end
|
150
|
+
|
151
|
+
def testing?
|
152
|
+
self.environment == 'testing'
|
153
|
+
end
|
154
|
+
|
155
|
+
%w{[] []= load_ruby! load_yaml!}.map(&:to_sym).each do |method|
|
156
|
+
define_method(method, lambda{ |*args| instance.send(method, *args)})
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
require 'sidewalk/app_uri'
|
2
|
+
|
3
|
+
require 'rack/utils'
|
4
|
+
|
5
|
+
require 'continuation' unless RUBY_VERSION.start_with? '1.8.'
|
6
|
+
require 'time' # Rack::Utils.set_cookie_header! on Ruby 1.8
|
7
|
+
|
8
|
+
module Sidewalk
|
9
|
+
# Base class for page controllers.
|
10
|
+
#
|
11
|
+
# {UriMapper} maps URIs to classes or Procs. If it maps to a class, that
|
12
|
+
# class needs to implement {#initialize} and {#call} in the same way as
|
13
|
+
# this class. This class provides some added convenience.
|
14
|
+
#
|
15
|
+
# You might want to look at {ControllerMixins} for some additional
|
16
|
+
# optional features.
|
17
|
+
#
|
18
|
+
# To handle an URL, you will usually want to:
|
19
|
+
# * subclass this
|
20
|
+
# * implement {#response}
|
21
|
+
# * add your class to your application URI map.
|
22
|
+
class Controller
|
23
|
+
# Initialize a new controller instance.
|
24
|
+
#
|
25
|
+
# @param [Request] request has information on the HTTP request.
|
26
|
+
# @param [Logger] logger is something implement the same interface as
|
27
|
+
# Ruby's +Logger+ class.
|
28
|
+
def initialize request, logger
|
29
|
+
@request, @logger = request, logger
|
30
|
+
@status = 200
|
31
|
+
@headers = {
|
32
|
+
'Content-Type' => 'text/html'
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
# The response headers.
|
37
|
+
#
|
38
|
+
# For request headers, see {#request} and {Request#headers}.
|
39
|
+
attr_reader :headers
|
40
|
+
|
41
|
+
# The instance of {Request} corresponding to the current HTTP request.
|
42
|
+
attr_reader :request
|
43
|
+
|
44
|
+
# An object implementing an interface that is compatible with +Logger+
|
45
|
+
attr_reader :logger
|
46
|
+
|
47
|
+
# The numeric HTTP status to return.
|
48
|
+
#
|
49
|
+
# In most cases, you won't actually want to change this - you might
|
50
|
+
# want to +raise+ an instance of a subclass of {HttpError} or
|
51
|
+
# {Redirect} instead.
|
52
|
+
attr_accessor :status
|
53
|
+
|
54
|
+
# Set a cookie :)
|
55
|
+
#
|
56
|
+
# Valid options are:
|
57
|
+
# +:expires+:: accepts a +Time+. Default is a session cookie.
|
58
|
+
# +:secure+:: if +true+, only send the cookie via https
|
59
|
+
# +:httponly+:: do not allow Flash, JavaScript etc to access the cookie
|
60
|
+
# +:domain+:: restrict the cookie to only be available on a given
|
61
|
+
# domain, and subdomains. Default is the request domain.
|
62
|
+
# +:path+:: make the cookie accessible to other paths - default is the
|
63
|
+
# request path.
|
64
|
+
#
|
65
|
+
# @param key [String] is the name of the cookie to set
|
66
|
+
# @param value [Object] is the value to set - as long as it responds to
|
67
|
+
# +#to_s+, it's fine.
|
68
|
+
def set_cookie key, value, options = {}
|
69
|
+
rack_value = options.dup
|
70
|
+
rack_value[:value] = value.to_s
|
71
|
+
Rack::Utils.set_cookie_header! self.headers, key.to_s, rack_value
|
72
|
+
end
|
73
|
+
|
74
|
+
# What mime-type to return to the user agent.
|
75
|
+
#
|
76
|
+
# "text/html" is the default.
|
77
|
+
def content_type
|
78
|
+
headers['Content-Type']
|
79
|
+
end
|
80
|
+
|
81
|
+
def content_type= value
|
82
|
+
headers['Content-Type'] = value
|
83
|
+
end
|
84
|
+
|
85
|
+
# Actually respond to the request.
|
86
|
+
#
|
87
|
+
# This calls {#response}, then ties it together with {#status}
|
88
|
+
# and {#content_type}.
|
89
|
+
#
|
90
|
+
# @return a response in Rack's +Array+ format.
|
91
|
+
def call
|
92
|
+
cc = catch(:sidewalk_controller_current) do
|
93
|
+
body = self.response
|
94
|
+
return [status, {'Content-Type' => content_type}, [body]]
|
95
|
+
end
|
96
|
+
cc.call(self) if cc
|
97
|
+
end
|
98
|
+
|
99
|
+
# The body of the HTTP response to set.
|
100
|
+
#
|
101
|
+
# In most cases, this is what you'll want to implement in a subclass.
|
102
|
+
# You can call {#status=}, but you probably don't want to - just raise
|
103
|
+
# an appropriate {HttpError} subclass instead.
|
104
|
+
#
|
105
|
+
# You might be interested in {ControllerMixins::ViewTemplates}.
|
106
|
+
#
|
107
|
+
# @return [String] the body of the HTTP response
|
108
|
+
def response
|
109
|
+
raise NotImplementedError.new
|
110
|
+
end
|
111
|
+
|
112
|
+
# The current Controller.
|
113
|
+
#
|
114
|
+
# @return [Controller] the current Controller if {#call} is in the
|
115
|
+
# stack.
|
116
|
+
# @return +nil+ otherwise.
|
117
|
+
def self.current
|
118
|
+
begin
|
119
|
+
callcc{|cc| throw(:sidewalk_controller_current, cc)}
|
120
|
+
rescue NameError, ArgumentError # 1.8 and 1.9 are different
|
121
|
+
nil
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Optional extension features to the Controller class.
|
127
|
+
#
|
128
|
+
# @example Adding +#render+ and supporting +views/+
|
129
|
+
# class MyController < Sidewalk::Controller
|
130
|
+
# include Sidewalk::ControllerMixins::ViewTemplates
|
131
|
+
# def response
|
132
|
+
# # Look for 'views/my_controller.*' - for example,
|
133
|
+
# # 'views/my_controller.erb' would be rendered with ERB if
|
134
|
+
# # present.
|
135
|
+
# render
|
136
|
+
# end
|
137
|
+
# end
|
138
|
+
module ControllerMixins
|
139
|
+
# see controller_mixins/
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'sidewalk/application'
|
2
|
+
|
3
|
+
require 'active_support/inflector'
|
4
|
+
|
5
|
+
module Sidewalk
|
6
|
+
module ControllerMixins
|
7
|
+
# Mixin for supporting view templates.
|
8
|
+
#
|
9
|
+
# This provides {#render}, which looks in views/ for a suitably named
|
10
|
+
# file, such as +views/hello.erb' for HelloController.
|
11
|
+
#
|
12
|
+
# See {TemplateHandlers::Base} for a list of supported formats.
|
13
|
+
module ViewTemplates
|
14
|
+
# The local path where templates are stored
|
15
|
+
#
|
16
|
+
# This will usually be the views/ subdirectory of the application
|
17
|
+
# root.
|
18
|
+
def self.view_path
|
19
|
+
@templates_path ||= File.join(
|
20
|
+
Sidewalk::Application.local_root,
|
21
|
+
'views'
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
# What handler to use for a given extension.
|
26
|
+
#
|
27
|
+
# @example ERB
|
28
|
+
# erb_handler = Sidewalk::ViewTemplates.handler('erb')
|
29
|
+
# erb_handler.should == Sidewalk::TemplateHandlers::ErbHandler
|
30
|
+
#
|
31
|
+
# @param [String] a filename extension.
|
32
|
+
# @return [Class] a {TemplateHandlers::Base} subclass.
|
33
|
+
def self.handler type
|
34
|
+
name = type.camelize + 'Handler'
|
35
|
+
begin
|
36
|
+
Sidewalk::TemplateHandlers.const_get(name)
|
37
|
+
rescue NameError
|
38
|
+
require ('Sidewalk::TemplateHandlers::' + name).underscore
|
39
|
+
Sidewalk::TemplateHandlers.const_get(name)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Get a {TemplateHandlers::Base} instance for a given path.
|
44
|
+
#
|
45
|
+
# @return [TemplateHandlers::Base]
|
46
|
+
def self.template path
|
47
|
+
self.templates[path.to_s]
|
48
|
+
end
|
49
|
+
|
50
|
+
# A +Hash+ of all available templates.
|
51
|
+
#
|
52
|
+
# @return [Hash] a +path+ +=>+ {TemplateHandlers::Base} map.
|
53
|
+
def self.templates
|
54
|
+
return @templates if @templates
|
55
|
+
|
56
|
+
@templates = {}
|
57
|
+
Dir.glob(
|
58
|
+
self.view_path + '/**/*'
|
59
|
+
).each do |path| # eg '/path/views/foo/bar.erb'
|
60
|
+
|
61
|
+
# eg '.erb'
|
62
|
+
ext = File.extname(path)
|
63
|
+
# eg 'erb'
|
64
|
+
type = ext[1..-1]
|
65
|
+
handler = self.handler(type)
|
66
|
+
|
67
|
+
# eg 'foo/bar.erb'
|
68
|
+
relative = path.sub(self.view_path + '/', '')
|
69
|
+
# convert to 'foo/bar'
|
70
|
+
parts = relative.split('/')
|
71
|
+
parts[-1] = File.basename(path, ext)
|
72
|
+
key = parts.join('/')
|
73
|
+
|
74
|
+
@templates[key] = handler.new(path)
|
75
|
+
end
|
76
|
+
@templates
|
77
|
+
end
|
78
|
+
|
79
|
+
# Return the result of rendering a view.
|
80
|
+
#
|
81
|
+
# @param [nil,String] which view to render. The default is
|
82
|
+
# +foo_controller+ if called from +FooController+.
|
83
|
+
# @return [String] a rendered result.
|
84
|
+
def render view = nil
|
85
|
+
view ||= self.class.name.sub('Controller', '').underscore
|
86
|
+
template = Sidewalk::ControllerMixins::ViewTemplates.template(view)
|
87
|
+
if template.nil?
|
88
|
+
raise ScriptError.new("Unable to find a template for #{view}")
|
89
|
+
end
|
90
|
+
template.render(self)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module Sidewalk
|
2
|
+
# Base class for HTTP errors.
|
3
|
+
#
|
4
|
+
# This is +raise+d by controllers.
|
5
|
+
class HttpError < ::RuntimeError
|
6
|
+
# Initialize an Error exception.
|
7
|
+
#
|
8
|
+
# If the parameters are +nil+, you will need to override {#status} and
|
9
|
+
# {#description} for this class to work as expected.
|
10
|
+
#
|
11
|
+
# @param [Fixnum,nil] status is the numeric status code
|
12
|
+
# @param [String,nil] description is a short description like 'Not
|
13
|
+
# Found'
|
14
|
+
def initialize status, description
|
15
|
+
@status, @description = status, description
|
16
|
+
end
|
17
|
+
|
18
|
+
# The status code to return in response to the request.
|
19
|
+
#
|
20
|
+
# It takes a request to allow different responses to be given to,
|
21
|
+
# for example, HTTP/1.0 clients vs HTTP/1.1 clients.
|
22
|
+
#
|
23
|
+
# @param [Request] request is an object representing the current HTTP
|
24
|
+
# request
|
25
|
+
# @return [Fixnum] a numeric HTTP status code.
|
26
|
+
def status request
|
27
|
+
@status
|
28
|
+
end
|
29
|
+
|
30
|
+
# The description to return in response to the request.
|
31
|
+
#
|
32
|
+
# It takes a request to allow different responses to be given to,
|
33
|
+
# for example, HTTP/1.0 clients vs HTTP/1.1 clients.
|
34
|
+
#
|
35
|
+
# @param [Request] request is an object representing the current HTTP
|
36
|
+
# request
|
37
|
+
# @return [String] a text description of the code, like 'Not Found'
|
38
|
+
def description request
|
39
|
+
@description
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Request that the client provide authentication headers.
|
44
|
+
#
|
45
|
+
# This is a 401 response
|
46
|
+
class NotAuthorizedError < HttpError
|
47
|
+
def initialize
|
48
|
+
super 401, 'Not Authorized'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Forbid the client from accessing a resource.
|
53
|
+
#
|
54
|
+
# Providing authentication headers will not help.
|
55
|
+
#
|
56
|
+
# This is a 403 response.
|
57
|
+
class ForbiddenError < HttpError
|
58
|
+
def initialize
|
59
|
+
super 403, 'Forbidden'
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Tells the client that the resource they requested does not exist.
|
64
|
+
#
|
65
|
+
# This is a 404 response.
|
66
|
+
class NotFoundError < HttpError
|
67
|
+
def initialize
|
68
|
+
super 404, 'Not Found'
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|