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,78 @@
1
+ require 'sidewalk/errors'
2
+
3
+ module Sidewalk
4
+ # Base class for HTTP-level redirections.
5
+ #
6
+ # This, and its' subclasses are expected to be +raise+d.
7
+ class Redirect < HttpError
8
+ # Where to redirect to.
9
+ attr_reader :url
10
+
11
+ # Initialize a Redirect.
12
+ #
13
+ # You probably don't want to use this directly - use
14
+ # {PermanentRedirect} or {SeeOtherRedirect} instead.
15
+ #
16
+ # @param [String, URI] url is where to redirect to
17
+ # @param [Fixnum] status is a numeric HTTP status code
18
+ # @param [String] description is a short description of the status
19
+ # code, such as 'Moved Permanently'
20
+ def initialize url, status, description
21
+ @url = url.to_s
22
+ super status, description
23
+ end
24
+ end
25
+
26
+ # Use if a resource has permanently moved.
27
+ #
28
+ # Does not change POST into GET.
29
+ #
30
+ # Uses a 301 Moved Permanently response.
31
+ class PermanentRedirect < Redirect
32
+ def initialize url
33
+ super url, 301, 'Moved Permanently'
34
+ end
35
+ end
36
+
37
+ # Use if the resource hasn't moved, but you want to redirect anyway.
38
+ #
39
+ # The new request will be a GET request.
40
+ #
41
+ # Standard usage is to redirect after a POST, to make the browser's
42
+ # Refresh and back/forward buttons work as expected.
43
+ #
44
+ # Defaults to a '303 See Other', but if you need to support pre-HTTP/1.1
45
+ # browsers, you might want to change this.
46
+ #
47
+ # @example Supporting Legacy Browsers
48
+ class SeeOtherRedirect < Redirect
49
+ def initialize url
50
+ super url, nil, nil
51
+ end
52
+
53
+ # Return an appropriate status code.
54
+ #
55
+ # @return +303+ for HTTP/1.1 clients
56
+ # @return +302+ for HTTP/1.0 clients
57
+ def status request
58
+ if request.http_version == '1.1'
59
+ 303
60
+ else
61
+ 302
62
+ end
63
+ end
64
+
65
+ # Return an appropriate description.
66
+ #
67
+ # @return 'See Other' for HTTP/1.1 clients
68
+ # @return 'Found' for HTTP/1.0 clients
69
+ def description request
70
+ case status(request)
71
+ when 303
72
+ 'See Other'
73
+ when 302
74
+ 'Found'
75
+ end
76
+ end
77
+ end
78
+ end
@@ -11,13 +11,40 @@ module Sidewalk
11
11
  end
12
12
  end
13
13
 
14
- USING_ONIGURUMA = RUBY_VERSION.start_with?('1.8.')
15
- if USING_ONIGURUMA
14
+ REGEXP_USING_ONIGURUMA = RUBY_VERSION.start_with?('1.8.')
15
+ if REGEXP_USING_ONIGURUMA
16
16
  require 'oniguruma'
17
- Regexp = ::Oniguruma::ORegexp
18
-
17
+ REGEXP_BASE = ::Oniguruma::ORegexp
19
18
  ::MatchData.send(:include, Sidewalk::Oniguruma::MatchData)
20
19
  else
21
- Regexp = ::Regexp
20
+ REGEXP_BASE = ::Regexp
21
+ end
22
+
23
+ # A Regexp class that supports named captures.
24
+ #
25
+ # This class exists just to give a portable class name to refer to.
26
+ #
27
+ # * On Ruby 1.9, this inherits +::Regexp+.
28
+ # * On Ruby 1.8, this inherits +Oniguruma::ORegexp+ (which is basically
29
+ # the same library that Ruby 1.9 uses for +::Regexp+)
30
+ #
31
+ # @example Using named captures
32
+ # regexp = Sidewalk::Regexp.new('(?<foo>bar)')
33
+ # match = regexp.match(regexp)
34
+ # match['foo'].should == 'bar'
35
+ class Regexp < REGEXP_BASE
36
+ # Whether we're using +Onigirumua::ORegexp+.
37
+ #
38
+ # Inverse of {#native?}.
39
+ def self.oniguruma?
40
+ Sidewalk::REGEXP_USING_ONIGURUMA
41
+ end
42
+
43
+ # Whether we're using +::Regexp+
44
+ #
45
+ # Inverse of {#native?}.
46
+ def self.native?
47
+ !self.oniguruma?
48
+ end
22
49
  end
23
50
  end
@@ -0,0 +1,38 @@
1
+ require 'sidewalk/controller'
2
+ require 'sidewalk/rooted_uri'
3
+ require 'sidewalk/request'
4
+
5
+ module Sidewalk
6
+ # URI relative to the current request.
7
+ #
8
+ # For example, if your app lives at 'http://www.example.com/foo',
9
+ # the current request is to '/foo/bar', +RelativeUri.new('/baz').to_s+
10
+ # will return 'http://www.example.com/foo/bar/baz', whereas {AppUri}
11
+ # would give you 'http://www.example.com/foo/baz'.
12
+ #
13
+ # Existing query data is discarded.
14
+ #
15
+ # Not a real class as the +URI+ hierarchy doesn't lend itself to
16
+ # subclassing.
17
+ module RelativeUri
18
+ # Create a URI relative to the current request.
19
+ #
20
+ # If this is called, it _must_ have {Controller#call} in the call stack
21
+ # so that {Controller#current} works - otherwise it does not have
22
+ # enough information to construct the URI.
23
+ #
24
+ # Query string data is discarded.
25
+ #
26
+ # @param [String] path is the path relative to the current request.
27
+ # @param [Hash] query is a +Hash+ of key-value query data.
28
+ def self.new path, query = {}
29
+ context = Sidewalk::Controller.current
30
+ unless context
31
+ raise ScriptError.new("Only valid when called by a controller")
32
+ end
33
+ uri = context.request.uri
34
+
35
+ Sidewalk::RootedUri.new(uri, path, query)
36
+ end
37
+ end
38
+ end
@@ -4,38 +4,135 @@ require 'rack/request'
4
4
  require 'uri'
5
5
 
6
6
  module Sidewalk
7
+ # An object representing an HTTP request.
8
+ #
9
+ # This is meant to provide convenient, high-level functions.
10
+ # You do have access to #{rack_environment} and #{rack_request} for
11
+ # lower-level information, but it's best avoided.
12
+ #
13
+ #
14
+ # Instances of this class are created by the {Application}.
7
15
  class Request
8
- attr_reader :root_uri, :request_uri
16
+ # The environment array provided by Rack.
17
+ #
18
+ # Please don't use this directly unless you're sure you need to.
19
+ attr_reader :rack_environment
20
+ # An instance of +Rack::Request+.
21
+ #
22
+ # This is a lower-level wrapper around {#rack_environment}
23
+ #
24
+ # Please don't use this directly unless you're sure you need to.
25
+ attr_reader :rack_request
26
+
27
+ # Create a new instance from a Rack environment array
28
+ #
29
+ # @param [Array] env is the environment data provided by Rack.
9
30
  def initialize env
10
- @env = env
31
+ @rack_environment = env
11
32
  @rack_version = env['rack.version']
12
33
 
13
34
  sanity_check!
14
- @rack_request = Rack::Request.new(@env)
35
+ @rack_request = Rack::Request.new(rack_environment)
15
36
 
16
37
  initialize_uris
17
38
  end
18
39
 
40
+ # The HTTP headers.
41
+ #
42
+ # This does not include Rack/CGI variables - just real HTTP headers.
43
+ def headers
44
+ @headers ||= rack_environment.select{|k,v| k.start_with? 'HTTP_'}
45
+ end
46
+
47
+ # Cookies provided by the client.
48
+ #
49
+ # @returns [Hash]
50
+ def cookies
51
+ rack_request.cookies
52
+ end
53
+
54
+ # What version of HTTP the client is using.
55
+ #
56
+ # @return '1.1'
57
+ # @return '1.0'
58
+ # @return nil
59
+ def http_version
60
+ @http_version ||= [
61
+ 'HTTP_VERSION',
62
+ 'SERVER_PROTOCOL',
63
+ ].map{ |x| rack_environment[x] }.find.first.to_s.split('/').last
64
+ end
65
+
66
+ # The root URI of the application.
67
+ #
68
+ # If you're looking at this to construct an URI to another page, take a
69
+ # look at {Sidewalk::AppUri}.
70
+ #
71
+ # If you want to find out where the current request was to, see {#uri}.
72
+ #
73
+ # @return [URI::Common]
74
+ def root_uri
75
+ @root_uri.dup
76
+ end
77
+
78
+ # The URI of the current request.
79
+ #
80
+ # If you're looking at this to construct a relative URI, take a look at
81
+ # {Sidewalk::RelativeUri}.
82
+ #
83
+ # If you want to find out what the URI for the application is, see
84
+ # {#root_uri}.
85
+ #
86
+ # @return [URI::Common]
87
+ def uri
88
+ @request_uri.dup
89
+ end
90
+
91
+ # Whether or not this request came via HTTPS.
92
+ #
93
+ # @return [true, false]
19
94
  def secure?
20
95
  @secure
21
96
  end
22
97
 
98
+ # Parameters provided via the query string.
99
+ #
100
+ # Consider using {#params} instead.
101
+ #
102
+ # @return [Hash]
23
103
  def get_params
24
104
  @get_params ||= @rack_request.GET
25
105
  end
26
106
 
107
+ # The {UriMatch} created by {UriMapper}.
27
108
  def uri_match
28
- @uri_match ||= @env['sidewalk.urimatch']
109
+ @uri_match ||= rack_environment['sidewalk.urimatch']
29
110
  end
30
111
 
112
+ # Paramters provided via POST.
113
+ #
114
+ # Consider using {#params} instead.
115
+ #
116
+ # @return [Hash]
31
117
  def post_params
32
118
  @post_params ||= @rack_request.POST
33
119
  end
34
120
 
121
+ # Paramters provided via named captures in the URL.
122
+ #
123
+ # Consider using {#params} instead.
124
+ #
125
+ # @return [Hash]
35
126
  def uri_params
36
127
  uri_match.parameters
37
128
  end
38
129
 
130
+ # Parameters provided by any means.
131
+ #
132
+ # Precedence:
133
+ # * URL captures first
134
+ # * Then query string parameters
135
+ # * Then POST parameters
39
136
  def params
40
137
  # URI parameters take precendence
41
138
  @params ||= post_params.merge(get_params.merge(uri_params))
@@ -44,7 +141,7 @@ module Sidewalk
44
141
  private
45
142
 
46
143
  def initialize_uris
47
- case @env['rack.url_scheme']
144
+ case rack_environment['rack.url_scheme']
48
145
  when 'http'
49
146
  @secure = false
50
147
  uri_class = URI::HTTP
@@ -52,36 +149,42 @@ module Sidewalk
52
149
  @secure = true
53
150
  uri_class = URI::HTTPS
54
151
  else
55
- raise "Unknown URL scheme: #{@env['rack.url_scheme'].inspect}"
152
+ raise ArgumentError.new(
153
+ "Unknown URL scheme: #{rack_environment['rack.url_scheme'].inspect}"
154
+ )
56
155
  end
57
156
 
58
- root = @env['SCRIPT_NAME']
157
+ root = rack_environment['SCRIPT_NAME']
59
158
  unless root.start_with? '/'
60
159
  root[0,0] = '/' # no prepend on 1.8
61
160
  end
62
161
 
63
162
  @root_uri = uri_class.build(
64
- :host => @env['SERVER_NAME'],
65
- :port => @env['SERVER_PORT'].to_i,
163
+ :host => rack_environment['SERVER_NAME'],
164
+ :port => rack_environment['SERVER_PORT'].to_i,
66
165
  :path => root
67
166
  ).freeze
68
- path_info = @env['PATH_INFO']
167
+ path_info = rack_environment['PATH_INFO']
69
168
  if root.end_with? '/' and path_info.start_with? '/'
70
169
  path_info = path_info[1..-1]
71
170
  end
72
171
  @request_uri = @root_uri.dup
73
172
  @request_uri.path += path_info
74
- @request_uri.query = @env['QUERY_STRING']
173
+ @request_uri.query = rack_environment['QUERY_STRING']
75
174
  @request_uri.freeze
76
175
  end
77
176
 
78
177
  def sanity_check!
79
178
  # Sanity checks
80
179
  unless @rack_version
81
- raise ArgumentError.new "env doesn't specify a Rack version"
180
+ raise ArgumentError.new(
181
+ "env doesn't specify a Rack version"
182
+ )
82
183
  end
83
184
  if @rack_version != [1, 1]
84
- raise "Expected Rack version [1, 1], got #{@rack_version}"
185
+ raise ArgumentError.new(
186
+ "Expected Rack version [1, 1], got #{@rack_version}"
187
+ )
85
188
  end
86
189
  end
87
190
  end
@@ -0,0 +1,32 @@
1
+ require 'rack/utils'
2
+
3
+ module Sidewalk
4
+ # A URI relative to a specified root URI.
5
+ #
6
+ # You probably don't want to use this directly - see {AppUri} and
7
+ # {RelativeUri} for examples.
8
+ module RootedUri
9
+ # Create a new URI, relative to the specified root.
10
+ #
11
+ # @param [URI::Common] root is the uri that you want to use as the base
12
+ # @param [String] path is the sub-path to add
13
+ # @param [Hash] query is a +Hash+ of key-values to add to the query
14
+ # string.
15
+ def self.new root, path, query = {}
16
+ uri = root.dup
17
+ root_path = uri.path
18
+ root_path = root_path[0..-2] if root_path.end_with? '/'
19
+ uri.path = root_path + path
20
+
21
+ query_string = query.map do |k,v|
22
+ '%s=%s' % [
23
+ Rack::Utils.escape(k.to_s),
24
+ Rack::Utils.escape(v.to_s),
25
+ ]
26
+ end.join('&')
27
+ uri.query = query_string unless query.empty?
28
+
29
+ uri
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,30 @@
1
+ module Sidewalk
2
+ module TemplateHandlers
3
+ # Base class for TemplateHandlers.
4
+ #
5
+ # This currently just defines an interface - see {ErbHandler} for an
6
+ # example.
7
+ #
8
+ # Instances of this class may be re-used between requests. Anything
9
+ # request-specific needs to go in the {#render} method.
10
+ class Base
11
+ # Actually render a template.
12
+ #
13
+ # @param {Controller} controller is the controller that wants the
14
+ # template. Instance variables should be available to the template,
15
+ # and the handler should not change the scope. See {BaseDelegate}
16
+ # and {ErbHandler::Delegate} for an approach to this.
17
+ #
18
+ # @return [String]
19
+ def render controller
20
+ raise NotImplementedError.new
21
+ end
22
+
23
+ # A new instance of the controller.
24
+ #
25
+ # @param [String] path a full path to a template source file.
26
+ def initialize path
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,39 @@
1
+ module Sidewalk
2
+ module TemplateHandlers
3
+ # A delegate scope to run templates in.
4
+ #
5
+ # Used to provide templates access to the controller's instance
6
+ # variables, and extra helper functions, without polluting the
7
+ # controller with them. For example, {ErbHandler::Delegate} will also
8
+ # include the standard +ERB::Util+ helper functions like +#h+ (aka
9
+ # +#html_escape+)
10
+ class BaseDelegate
11
+ # A new instance of delegate.
12
+ #
13
+ # Copies all instance variables from controller to object.
14
+ #
15
+ # @param [Controller] controller
16
+ def initialize controller
17
+ controller.instance_variables.each do |name|
18
+ self.instance_variable_set(
19
+ name,
20
+ controller.instance_variable_get(name)
21
+ )
22
+ end
23
+ end
24
+
25
+ # Render the template in the scope.
26
+ #
27
+ # The default implementation raises a NotImplementedError.
28
+ #
29
+ # @example {ErbDelegate#render}
30
+ # def render
31
+ # # @template is set by {ErbDelegate}'s constructor.
32
+ # @template.result binding
33
+ # end
34
+ def render
35
+ raise NotImplementedError.new
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+ require 'sidewalk/template_handlers/base'
2
+ require 'sidewalk/template_handlers/base_delegate'
3
+
4
+ require 'erb'
5
+
6
+ module Sidewalk
7
+ module TemplateHandlers
8
+ # Handler for ERB Templates.
9
+ class ErbHandler < Base
10
+ def initialize path
11
+ super path
12
+ @template = ERB::new(File.read(path))
13
+ @template.filename = path
14
+ end
15
+
16
+ def render controller
17
+ Delegate.new(@template, controller).render
18
+ end
19
+
20
+ # Class representing the controller to ERB.
21
+ #
22
+ # Using a delegate so we can add extra methods to the binding
23
+ # without polluting the class - for example, most people expect an
24
+ # ERB template to have access to ERB::Util#h
25
+ class Delegate < BaseDelegate
26
+ # Pull in '#html_escape' aka '#h
27
+ include ERB::Util
28
+
29
+ def initialize template, controller
30
+ @template = template
31
+ super controller
32
+ end
33
+
34
+ def render
35
+ @template.result binding
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,19 @@
1
+ require 'sidewalk/template_handlers/base'
2
+ require 'sidewalk/template_handlers/base_delegate'
3
+
4
+ require 'haml'
5
+
6
+ module Sidewalk
7
+ module TemplateHandlers
8
+ class HamlHandler < Base
9
+ def initialize path
10
+ super path
11
+ @engine = Haml::Engine.new(File.read(path))
12
+ end
13
+
14
+ def render controller
15
+ @engine.render(BaseDelegate.new(controller))
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ require 'sidewalk/template_handlers/base'
2
+ require 'sidewalk/template_handlers/base_delegate'
3
+
4
+ require 'rxhp'
5
+ require 'rxhp/html'
6
+
7
+ module Sidewalk
8
+ module TemplateHandlers
9
+ class RxhpHandler < Base
10
+ def initialize path
11
+ @path = path
12
+ @template = File.read(path)
13
+ end
14
+
15
+ def render controller
16
+ Delegate.new(@path, @template, controller).render
17
+ end
18
+
19
+ class Delegate < BaseDelegate
20
+ include Rxhp::Html
21
+ def initialize path, template, controller
22
+ @path, @template, @controller = path, template, controller
23
+ super controller
24
+ end
25
+
26
+ def render
27
+ eval(@template, binding, @path).render
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,2 @@
1
+ require 'sidewalk/app_uri'
2
+ require 'sidewalk/relative_uri'
@@ -1,16 +1,83 @@
1
1
  require 'sidewalk/regexp'
2
2
  require 'sidewalk/uri_match'
3
3
 
4
+ require 'active_support/inflector'
5
+
4
6
  module Sidewalk
7
+ autoload :Application, 'sidewalk/application'
8
+
9
+ # Maps URIs to Controllers.
10
+ #
11
+ # Used to decide how to respond to a given HTTP request.
5
12
  class UriMapper
6
- attr_reader :uri_map
13
+ # Initialize a UriMapper.
14
+ #
15
+ # This converts a convenient-to-write map into a fast-to-lookup map.
16
+ #
17
+ # The input hash takes +String+ regular expression patterns as keys,
18
+ # and values can either be another map, a {Controller} class (not an
19
+ # instance), a +Symbol+ or +String+ that is the name of a {Controller}
20
+ # class, or a +Proc+.
21
+ #
22
+ # If the value is a +String+, it will:
23
+ # * see if a +Class+ with the same name exists; if so, use that.
24
+ # * try to +require+ +foo_bar_controller.rb+ for 'FooBarController'
25
+ # * see if a +Class+ now exists with the same name
26
+ # * if so, done; if not, bail out.
27
+ #
28
+ # It looks for controller files in
29
+ # {Application#local_root}+/controllers+
30
+ #
31
+ # You usually won't interact with this class directly - instead you'll
32
+ # usually pass the input hash to {Sidewalk::Application}.
33
+ #
34
+ # Keys are required to be +String+s instead of +Regexp+s because they
35
+ # are converted to a +Sidewalk::+{Regexp} behind the scenes; this is
36
+ # is the same thing as a +::Regexp+ on Ruby 1.9, but on Ruby 1.8 it
37
+ # adds several additional features such as named captures.
38
+ #
39
+ # @example A Simple Map
40
+ # urimap = {
41
+ # '$' => :IndexController,
42
+ # 'hello$' => :HelloController,
43
+ # }
44
+ # run Sidewalk::Application.new(urimap)
45
+ #
46
+ # @example A Nested Map
47
+ # urimap = {
48
+ # '$' => :IndexController,
49
+ # 'foo/' => {
50
+ # '$' => :FooController,
51
+ # 'bar$' => :FooBarController,
52
+ # },
53
+ # }
54
+ # run Sidewalk::Application.new(urimap)
55
+ #
56
+ # @example A Map With Named Captures
57
+ # urimap = {
58
+ # '$' => :IndexController,
59
+ # 'foo/$' => :FooController,
60
+ # '~(?<username>[^/]+)/$' => :UserController
61
+ # }
62
+ # run Sidewalk::Application.new(urimap)
63
+ #
64
+ # @param [Hash] uri_map
7
65
  def initialize uri_map = {}
8
66
  unless uri_map.is_a? Hash
9
67
  raise ArgumentError.new('URI map must be a Hash')
10
68
  end
69
+ $LOAD_PATH.push File.join(
70
+ Sidewalk::Application.local_root,
71
+ 'controllers'
72
+ )
11
73
  @uri_map = Sidewalk::UriMapper.convert_map(uri_map)
74
+ $LOAD_PATH.pop
12
75
  end
13
76
 
77
+ # Given a path, find what should respond to it.
78
+ #
79
+ # @return [UriMatch] if something was found.
80
+ # @return [nil] if there is no match.
14
81
  def map path
15
82
  Sidewalk::UriMapper.map(
16
83
  [], #stack
@@ -19,7 +86,17 @@ module Sidewalk
19
86
  )
20
87
  end
21
88
 
22
- # Replace string keys with Sidewalk::Regexp instances
89
+ # The normalized URI map.
90
+ #
91
+ # @return [Hash<Regexp,(Controller|Proc)>]
92
+ attr_reader :uri_map
93
+
94
+ private
95
+
96
+ # Convert uri_map from easy-to-write to fast-to-use.
97
+ #
98
+ # - Replace string keys with Sidewalk::Regexp instances
99
+ # - Attempt to load classes for symbols
23
100
  def self.convert_map uri_map
24
101
  out = {}
25
102
  uri_map.each do |key, value|
@@ -36,6 +113,16 @@ module Sidewalk
36
113
  key = Sidewalk::Regexp.new(key)
37
114
  if value.is_a? Hash
38
115
  out[key] = convert_map(value)
116
+ elsif value.is_a? Class
117
+ out[key] = value
118
+ elsif value.to_s.end_with? 'Controller'
119
+ # Attempt to load the class
120
+ begin
121
+ out[key] = value.to_s.constantize
122
+ rescue NameError
123
+ require value.to_s.underscore
124
+ retry
125
+ end
39
126
  else
40
127
  out[key] = value
41
128
  end