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,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