watts 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rake/gempackagetask'
2
+ require 'rake/rdoctask'
3
+
4
+ $: << "#{File.dirname(__FILE__)}/lib"
5
+
6
+ spec = Gem::Specification.new { |s|
7
+ s.platform = Gem::Platform::RUBY
8
+
9
+ s.author = "Pete Elmore"
10
+ s.email = "pete@debu.gs"
11
+ s.files = Dir["{lib,doc,bin,ext}/**/*"].delete_if {|f|
12
+ /\/rdoc(\/|$)/i.match f
13
+ } + %w(Rakefile)
14
+ s.require_path = 'lib'
15
+ s.has_rdoc = true
16
+ s.extra_rdoc_files = Dir['doc/*'].select(&File.method(:file?))
17
+ s.extensions << 'ext/extconf.rb' if File.exist? 'ext/extconf.rb'
18
+ Dir['bin/*'].map(&File.method(:basename)).map(&s.executables.method(:<<))
19
+
20
+ s.name = 'watts'
21
+ s.summary =
22
+ "Another Rack-based web framework. Yes, another one. Sorry, guys."
23
+ s.homepage = "http://debu.gs/#{s.name}"
24
+ %w(metaid).each &s.method(:add_dependency)
25
+ s.version = '0.0.1'
26
+ }
27
+
28
+ Rake::RDocTask.new(:doc) { |t|
29
+ t.main = 'doc/README'
30
+ t.rdoc_files.include 'lib/**/*.rb', 'doc/*', 'bin/*', 'ext/**/*.c',
31
+ 'ext/**/*.rb'
32
+ t.options << '-S' << '-N'
33
+ t.rdoc_dir = 'doc/rdoc'
34
+ }
35
+
36
+ Rake::GemPackageTask.new(spec) { |pkg|
37
+ pkg.need_tar_bz2 = true
38
+ }
39
+ desc "Cleans out the packaged files."
40
+ task(:clean) {
41
+ FileUtils.rm_rf 'pkg'
42
+ }
43
+
44
+ desc "Builds and installs the gem for #{spec.name}"
45
+ task(:install => :package) {
46
+ g = "pkg/#{spec.name}-#{spec.version}.gem"
47
+ system "sudo gem install -l #{g}"
48
+ }
49
+
50
+ desc "Runs IRB, automatically require()ing #{spec.name}."
51
+ task(:irb) {
52
+ exec "irb -Ilib -r#{spec.name}"
53
+ }
data/doc/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010 Peter Elmore (pete at debu.gs)
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a
4
+ copy of this software and associated documentation files (the "Software"),
5
+ to deal in the Software without restriction, including without limitation
6
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
7
+ and/or sell copies of the Software, and to permit persons to whom the
8
+ Software is furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19
+ DEALINGS IN THE SOFTWARE.
data/doc/TODO ADDED
File without changes
@@ -0,0 +1,10 @@
1
+ All of the examples are standalone Watts applications, which you can run
2
+ directly. They could all be pretty easily turned into rackup files.
3
+
4
+ hello_world.rb is the most basic demonstration.
5
+ matching.rb shows how Watts's pattern-matching works.
6
+ hoshi.rb is a demonstration of the view-wrapping resource.
7
+ environment.rb shows what you have to work with in your request.
8
+
9
+ Although they're all single-file applications, there's nothing that
10
+ prevents you from splitting things up arbitrarily. It's all just Ruby.
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env ruby
2
+ # This example gives you a feel for the environment in which Watts::Resources
3
+ # run. By "environment", of course, I really just mean that the 'env' value
4
+ # Rack gives you on requests is accessible from inside your resources. You can
5
+ # request /, /foo, or whatever. If you want to have a look at how query string
6
+ # parsing works, try having a look at /query?asdf=jkl%3B . This example just
7
+ # uses the CGI library that comes with Ruby for parsing queries.
8
+
9
+ require 'watts'
10
+ require 'pp'
11
+ require 'cgi'
12
+
13
+ class WattsEnvironment < Watts::App
14
+ class EnvPrinter < Watts::Resource
15
+ get { |*a|
16
+ s = ''
17
+ PP.pp env, s
18
+ s
19
+ }
20
+ end
21
+
22
+ class Queries < Watts::Resource
23
+ get {
24
+ CGI.parse(env['QUERY_STRING']).inspect rescue 'Couldn\'t parse.'
25
+ }
26
+ end
27
+
28
+ resource('/', EnvPrinter) {
29
+ resource('foo', EnvPrinter)
30
+ resource([:yeah], EnvPrinter)
31
+ resource('query', Queries)
32
+ }
33
+ end
34
+
35
+ app = WattsEnvironment.new
36
+ builder = Rack::Builder.new { run app }
37
+
38
+ Rack::Handler::Mongrel.run builder, :Port => 8080
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+ # This is, I think, the simplest possible Watts application. It starts up Rack
3
+ # on port 8080 and responds only to GET /.
4
+
5
+ require 'watts'
6
+
7
+ class Simple < Watts::App
8
+ class EZResource < Watts::Resource
9
+ get { "Hello, World!\n" }
10
+ end
11
+
12
+ resource('/', EZResource)
13
+ end
14
+
15
+ app = Simple.new
16
+ builder = Rack::Builder.new { run app }
17
+
18
+ Rack::Handler::Mongrel.run builder, :Port => 8080
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+ # An example illustrating how to use the for_html_view method, with help from
3
+ # Hoshi. Try running this and opening http://localhost:8080/ in a browser.
4
+
5
+ require 'watts'
6
+ require 'hoshi'
7
+
8
+ class HelloHTML < Watts::App
9
+ # First, a simple, traditional greeting, done in Hoshi:
10
+ class View < Hoshi::View :html4
11
+ def hello
12
+ doc {
13
+ head { title 'Hello, World!' }
14
+ body {
15
+ h1 'Here is your greeting:'
16
+ p 'Hello, World!'
17
+ }
18
+ }
19
+ render
20
+ end
21
+ end
22
+
23
+ Res = Watts::Resource.for_html_view(View, :hello)
24
+
25
+ resource('/', Res)
26
+ end
27
+
28
+ app = HelloHTML.new
29
+ builder = Rack::Builder.new { run app }
30
+
31
+ Rack::Handler::Mongrel.run builder, :Port => 8080
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env ruby
2
+ # An illustration of the pattern-matching capabilities of Watts. Some URLs to
3
+ # try if you start this one up:
4
+ # http://localhost:8080/strlen/foo (Which should tell you '3'.)
5
+ # http://localhost:8080/fib/15 (Which should give you 987.)
6
+ # http://localhost:8080/fib/foo (Which is a 404. 'foo' isn't a number!)
7
+ # http://localhost:8080/fib/f (Which should give you 0x3db.)
8
+ # http://localhost:8080/fib/0x15 (Which should give you 0x452f.)
9
+
10
+ require 'watts'
11
+
12
+ class MatchingDemo < Watts::App
13
+ class Strlen < Watts::Resource
14
+ # Takes an argument, and just returns the length of the argument.
15
+ get { |str| str.length.to_s + "\n" }
16
+ end
17
+
18
+ class Fibonacci < Watts::Resource
19
+ # This resource takes an argument for GET. It is filled in by Watts
20
+ # according to the argument pattern passed into resource below.
21
+ get { |n| fib(n.to_i).to_s + "\n" }
22
+
23
+ # A naive, recursive, slow, text-book implementation of Fibonacci.
24
+ def fib(n)
25
+ if n < 2
26
+ 1
27
+ else
28
+ fib(n - 1) + fib(n - 2)
29
+ end
30
+ end
31
+ end
32
+
33
+ # As above, but with a base-16 number.
34
+ class HexFibonacci < Fibonacci
35
+ get { |n| "0x" + fib(n.to_i(16)).to_s(16) + "\n" }
36
+ end
37
+
38
+ resource('/') {
39
+ # A symbol can be used to indicate an 'argument' component of a path,
40
+ # which is in turn passed to the resource's method as paths. It will
41
+ # match anything, making it almost equivalent to just using an empty
42
+ # regex (see below), except that it can serve as documentation.
43
+ resource(['strlen', :str], Strlen)
44
+
45
+ resource('fib') {
46
+ # You can match arguments based on a regex. The path component for
47
+ # the regex is passed to the resource's method as part of the
48
+ # argument list.
49
+ resource([/^[0-9]+$/], Fibonacci)
50
+
51
+ # As above, but here we use hexadecimal. If the pattern for
52
+ # Fibonacci doesn't match, then we'll end up hitting this one.
53
+ resource([/^(0x)?[0-9a-f]+$/i], HexFibonacci)
54
+ }
55
+ }
56
+ end
57
+
58
+ app = MatchingDemo.new
59
+ builder = Rack::Builder.new { run app }
60
+
61
+ Rack::Handler::Mongrel.run builder, :Port => 8080
data/lib/watts.rb ADDED
@@ -0,0 +1,270 @@
1
+ %w(
2
+ forwardable
3
+ metaid
4
+ rack
5
+ watts/monkey_patching
6
+ ).each &method(:require)
7
+
8
+ # Here's the main module, Watts.
9
+ module Watts
10
+ # You are unlikely to need to interact with this. It's mainly for covering
11
+ # up the path-matching logic for Resources.
12
+ class Path
13
+ extend Forwardable
14
+ include Enumerable
15
+
16
+ attr_accessor :resource
17
+ attr_new Hash, :sub_paths
18
+
19
+ def match path, args
20
+ if path.empty?
21
+ [resource, args]
22
+ elsif(sub = self[path[0]])
23
+ sub.match(path[1..-1], args)
24
+ else
25
+ each { |k,sub|
26
+ if k.kind_of?(Regexp) && k.match(path[0])
27
+ return sub.match(path[1..-1], args + [path[0]])
28
+ end
29
+ }
30
+ each { |k,sub|
31
+ if k.kind_of?(Symbol)
32
+ return sub.match(path[1..-1], args + [path[0]])
33
+ end
34
+ }
35
+ nil
36
+ end
37
+ end
38
+
39
+ def_delegators :sub_paths, :'[]', :'[]=', :each
40
+ end
41
+
42
+ # In order to have a Watts app, you'll want to subclass Watts::App. For a
43
+ # good time, you'll also probably want to provide some resources to that
44
+ # class using the resource method, which maps paths to resources.
45
+ class App
46
+ Errors = {
47
+ 400 =>
48
+ [400, {'Content-Type' => 'text/plain'}, "400 Bad Request.\n"],
49
+ 404 =>
50
+ [404, {'Content-Type' => 'text/plain'}, "404 Not Found\n"],
51
+ }
52
+
53
+ class << self
54
+ attr_new Hash, :http_methods
55
+ attr_new Watts::Path, :path_map
56
+ attr_new Array, :path_stack
57
+ attr_writer :path_stack
58
+ end
59
+
60
+ def self.decypher_path p
61
+ return p if p.kind_of?(Array)
62
+ return [] if ['/', ''].include?(p)
63
+ p = p.split('/')
64
+ p.select { |sub| sub != '' }
65
+ end
66
+
67
+ to_instance :path_map, :decypher_path
68
+
69
+ # If you want your Watts application to do anything at all, you're very
70
+ # likely to want to call this method at least once. The basic purpose
71
+ # of the method is to tell your app how to match a resource to a path.
72
+ # For example, if you create a resource (see Watts::Resource) Foo, and
73
+ # you want requests against '/foo' to match it, you could do this:
74
+ # resource('foo', Foo)
75
+ #
76
+ # The first argument is the path, and the second is the resource that
77
+ # path is to match. (Please see the README for more detailed
78
+ # documentation of path-matching.) You may also pass it a block, in
79
+ # which resources that are defined are 'namespaced'. For example, if
80
+ # you also had a resource called Bar and wanted its path to be a
81
+ # sub-path of the Foo resource's (e.g., '/foo/bar'), then typing these
82
+ # lines is a pretty good plan:
83
+ # resource('foo', Foo) {
84
+ # resource('bar', Bar)
85
+ # }
86
+ #
87
+ # Lastly, the resource argument itself is optional, for when you want a
88
+ # set of resources to be namespaced under a given path, but don't
89
+ # have a resource in mind. For example, if you suddenly needed your
90
+ # entire application to reside under '/api', you could do this:
91
+ # resource('api') {
92
+ # resource('foo', Foo) {
93
+ # resource('bar', Bar)
94
+ # resource('baz', Baz)
95
+ # }
96
+ # }
97
+ #
98
+ # This is probably the most important method in Watts. Have a look at
99
+ # the README and the example applications under doc/examples if you
100
+ # want to understand the pattern-matching, arguments to resources, etc.
101
+ def self.resource(path, res = nil, &b)
102
+ path = decypher_path(path)
103
+
104
+ last = (path_stack + path).inject(path_map) { |m,p|
105
+ m[p] ||= Path.new
106
+ }
107
+ last.resource = res
108
+
109
+ if b
110
+ old_stack = path_stack
111
+ self.path_stack = old_stack + path
112
+ b.call
113
+ self.path_stack = old_stack
114
+ end
115
+ res
116
+ end
117
+
118
+ # Given a path, returns the matching resource, if any.
119
+ def match req_path
120
+ req_path = decypher_path req_path
121
+ path_map.match req_path, []
122
+ end
123
+
124
+ # Our interaction with Rack.
125
+ def call env, req_path = nil
126
+ rm = env['REQUEST_METHOD'].downcase.to_sym
127
+ return(Errors[400]) unless Resource::HTTPMethods.include?(rm)
128
+
129
+ req_path ||= decypher_path env['REQUEST_PATH']
130
+ resource_class, args = match req_path
131
+
132
+ if resource_class
133
+ res = resource_class.new(env)
134
+ res.send(rm, *args)
135
+ else
136
+ Errors[404]
137
+ end
138
+ end
139
+ end
140
+
141
+ # HTTP is all about resources, and this class represents them. You'll want
142
+ # to subclass it and then define some HTTP methods on it, then use
143
+ # your application's resource method to tell it where to find these
144
+ # resources. (See Watts::App.resource().) If you want your resource to
145
+ # respond to GET with a cheery, text/plain greeting, for example:
146
+ # class Foo < Watts::Resource
147
+ # get { || "Hello, world!" }
148
+ # end
149
+ #
150
+ # Or you could do something odd like this:
151
+ # class RTime < Watts::Resource
152
+ # class << self; attr_accessor :last_post_time; end
153
+ #
154
+ # get { || "The last POST was #{last_post_time}." }
155
+ # post { ||
156
+ # self.class.last_post_time = Time.now.strftime('%F %R')
157
+ # [204, {}, []]
158
+ # }
159
+ #
160
+ # def last_post_time
161
+ # self.class.last_post_time || "...never"
162
+ # end
163
+ # end
164
+ #
165
+ # It is also possible to define methods in the usual way (e.g., 'def get
166
+ # ...'), although you'll need to add them to the list of allowed methods
167
+ # (for OPTIONS) manually. Have a look at the README and doc/examples.
168
+ class Resource
169
+ HTTPMethods =
170
+ [:get, :post, :put, :delete, :head, :options, :trace, :connect]
171
+
172
+ class << self
173
+ attr_new Array, :http_methods
174
+ end
175
+
176
+ # For each method allowed by HTTP, we define a "Method not allowed"
177
+ # response, and a method for generating a method. You may also just
178
+ # def methods, as seen below for the options method.
179
+ HTTPMethods.each { |http_method|
180
+ meta_def(http_method) { |&b|
181
+ http_methods << http_method.to_s.upcase
182
+ bmname = "__#{http_method}".to_sym
183
+ define_method(bmname, &b)
184
+ define_method(http_method) { |*args|
185
+ begin
186
+ resp = send bmname, *args
187
+ rescue ArgumentError => e
188
+ # TODO: Arity/path args mismatch handler here.
189
+ raise e
190
+ end
191
+
192
+ # TODO: Problems.
193
+ case resp
194
+ when nil
195
+ [response.status, response.headers, response.body]
196
+ when Array
197
+ resp
198
+ else
199
+ [200, {'Content-Type' => 'text/plain'}, resp.to_s]
200
+ end
201
+ }
202
+ }
203
+ define_method(http_method) { |*args| default_http_method(*args) }
204
+ }
205
+
206
+ # This method is for creating Resources that simply wrap first-class
207
+ # HTML views. It was created with Hoshi in mind, although you can use
208
+ # any class that can be instantiated and render some HTML when the
209
+ # specified method is called. It takes two arguments: the view class,
210
+ # and the method to call to render the HTML.
211
+ def self.for_html_view klass, method
212
+ c = Class.new HTMLViewResource
213
+ c.view_class = klass
214
+ c.view_method = method
215
+ c
216
+ end
217
+
218
+ to_instance :http_methods
219
+ attr_new Rack::Response, :response
220
+ attr_accessor :env, :response
221
+
222
+ # Every resource, on being instantiated, is given the Rack env.
223
+ def initialize(env)
224
+ self.env = env
225
+ self.response = Rack::Response.new
226
+ end
227
+
228
+ # The default options method, to comply with RFC 2616, returns a list
229
+ # of allowed methods in the Allow header. These are filled in when the
230
+ # method-defining methods (i.e., get() et al) are called.
231
+ def options(*args)
232
+ [
233
+ 200,
234
+ {
235
+ 'Content-Length' => '0', # cf. RFC 2616
236
+ 'Allow' => http_methods.join(', ')
237
+ },
238
+ []
239
+ ]
240
+ end
241
+
242
+ # By default, we return "405 Method Not Allowed" and set the Allow:
243
+ # header appropriately.
244
+ def default_http_method(*args)
245
+ [405, { 'Allow' => http_methods.join(', ') }, 'Method not allowed.']
246
+ end
247
+ end
248
+
249
+ # See the documentation for Watts::Resource.for_html_view().
250
+ class HTMLViewResource < Resource
251
+ class << self
252
+ attr_writer :view_class, :view_method
253
+ end
254
+
255
+ def self.view_class
256
+ @view_class ||= (superclass.view_class rescue nil)
257
+ end
258
+
259
+ def self.view_method
260
+ @view_method ||= (superclass.view_method rescue nil)
261
+ end
262
+
263
+ to_instance :view_class, :view_method
264
+
265
+ def get *args
266
+ [200, {'Content-Type' => 'text/html'},
267
+ view_class.new.send(view_method, *args)]
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,26 @@
1
+ # This is the place to stuff all of the monkey-patches.
2
+
3
+ require 'metaid'
4
+
5
+ class Class
6
+ # Has instances delegate methods to the class.
7
+ def to_instance *ms
8
+ ms.each { |m|
9
+ define_method(m) { |*a|
10
+ self.class.send(m, *a)
11
+ }
12
+ }
13
+ end
14
+
15
+ # A replacement for def x; @x ||= Y.new; end
16
+ def attr_new klass, *attrs
17
+ attrs.each { |attr|
18
+ ivname = "@#{attr}"
19
+ define_method(attr) {
20
+ ivval = instance_variable_get(ivname)
21
+ return ivval if ivval
22
+ instance_variable_set(ivname, klass.new)
23
+ }
24
+ }
25
+ end
26
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: watts
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Pete Elmore
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-06-01 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: metaid
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :runtime
31
+ version_requirements: *id001
32
+ description:
33
+ email: pete@debu.gs
34
+ executables: []
35
+
36
+ extensions: []
37
+
38
+ extra_rdoc_files:
39
+ - doc/TODO
40
+ - doc/LICENSE
41
+ files:
42
+ - lib/watts.rb
43
+ - lib/watts/monkey_patching.rb
44
+ - doc/TODO
45
+ - doc/examples/hello_world.rb
46
+ - doc/examples/environment.rb
47
+ - doc/examples/README
48
+ - doc/examples/matching.rb
49
+ - doc/examples/hoshi.rb
50
+ - doc/LICENSE
51
+ - Rakefile
52
+ has_rdoc: true
53
+ homepage: http://debu.gs/watts
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options: []
58
+
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ segments:
66
+ - 0
67
+ version: "0"
68
+ required_rubygems_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ requirements: []
76
+
77
+ rubyforge_project:
78
+ rubygems_version: 1.3.6
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: Another Rack-based web framework. Yes, another one. Sorry, guys.
82
+ test_files: []
83
+