sidewalk 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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