sidewalk 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|