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.
@@ -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