angelo 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bfff4ac5299ee639afca3b56bc130f71b36940fb
4
- data.tar.gz: bae829642aefa5762ff1f1b612d5bf7cfd707d99
3
+ metadata.gz: 71eca9d2230c44d5b635e6e46080b5fab3848dab
4
+ data.tar.gz: 2a29741be7955f76e270e08b7fd05ad5a2ee0394
5
5
  SHA512:
6
- metadata.gz: 100f313a038eb3dd61f1a87542f9339158c7e0e7b8e4f179c01f7ef83b08a7860f1c49db9ce01a7ba14d1765540d556f98c6f4994cb44a6535a3567a0f5e5da9
7
- data.tar.gz: 29708a62189bb0ee987e84d34d2b31c19a56f710ab7147c304f7575a5a59d1a9b20ce4edb0b3d8b0ac96818a5886ab2de29fd041a3d567777127eb536e2fd243
6
+ metadata.gz: e274a5f18cc333463ccaee271cd7ba08dd1b550552f045cfe82ba18b08e73caa09df6712b2e296dc41e37c2b05be079be205337e055a3fdee18e3f822e3e9ab5
7
+ data.tar.gz: b7034f60500807c47f0177acab56c5de0c4e15bf647ec3a61c995ac5635f2d0653b9217f530077be7a8b95b81e3a2256e30d110c2988bfd9e85d9ac0ec131a95
data/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
1
  angelo-*.gem
2
2
  tmp
3
3
  Gemfile.lock
4
+ test/coverage
@@ -1,11 +1,8 @@
1
1
  sudo: false
2
2
  language: ruby
3
3
  rvm:
4
- - "1.9.3"
5
- - "2.0.0"
6
- - "2.1.2"
7
- - "2.1.3"
8
4
  - "2.1.5"
5
+ - "2.2.0"
9
6
  - "rbx-2"
10
7
  script: rake
11
8
  matrix:
@@ -1,6 +1,28 @@
1
1
  changelog
2
2
  =========
3
3
 
4
+ ### 0.4.0 5 feb 2015
5
+
6
+ thanks: @tommay, @kyledrake, @katjaeinsfeld
7
+
8
+ * refactor UnboundMethod -> instance_exec (#38)
9
+ * add angelo/main for sinatra-like top-level DSL (#37)
10
+ * make SymHash.new recurse array values
11
+ * remove 'layouts_' from view_dir/layouts/*
12
+ * return anything that respond_to? :to_json when content_type :json
13
+ * remove event restriction to SSE stash contexts
14
+ * add reload_templates! DSL method
15
+ * refactor testing helpers (#35)
16
+ * refactor ParamsParser#parse_post_body (#34)
17
+ * Responder.symhash refactored to Angelo::SymHash (#33)
18
+ * define_app now accepts a subclass of Angelo::Base (#32)
19
+ * testing auxiliary classes moved to angelo/minitest/helpers (#32)
20
+ * SimpleCov testing output support on MRI (#28)
21
+ * Mustermann wholly integrated
22
+ * include Angelo::Tilt::ERB by default
23
+ * add Tilt ~> 2.0 and Mustermann ~> 0.4 to gemspec
24
+ * require ruby >= 2.1 (also a Mustermann requirement)
25
+
4
26
  ### 0.3.3 16 jan 2015
5
27
 
6
28
  thanks: @mighe, @tarcieri, @jc00ke, @gunnarmarten, @tommay
data/Gemfile CHANGED
@@ -4,10 +4,7 @@ gem 'reel', '~>0.5'
4
4
  gem 'tilt', '~>2.0'
5
5
  gem 'mime-types', '~>2.4'
6
6
  gem 'websocket-driver', '~>0.3'
7
-
8
- platform :ruby_21, :ruby_22 do
9
- gem 'mustermann', '~>0.4'
10
- end
7
+ gem 'mustermann', '~>0.4'
11
8
 
12
9
  group :development do
13
10
  gem 'pry', '~>0.10'
@@ -23,4 +20,8 @@ end
23
20
  group :test do
24
21
  gem 'httpclient', '~>2.5'
25
22
  gem 'minitest', '~>5.4'
23
+
24
+ platform :mri do
25
+ gem 'simplecov', '~>0.9.1'
26
+ end
26
27
  end
data/README.md CHANGED
@@ -3,7 +3,7 @@ Angelo
3
3
 
4
4
  [![Build Status](https://travis-ci.org/kenichi/angelo.png?branch=master)](https://travis-ci.org/kenichi/angelo)
5
5
 
6
- A [Sinatra](https://github.com/sinatra/sinatra)-esque DSL for [Reel](https://github.com/celluloid/reel).
6
+ A [Sinatra](https://github.com/sinatra/sinatra)-like DSL for [Reel](https://github.com/celluloid/reel).
7
7
 
8
8
  ### tl;dr
9
9
 
@@ -12,34 +12,35 @@ A [Sinatra](https://github.com/sinatra/sinatra)-esque DSL for [Reel](https://git
12
12
  * contextual websocket/sse stashing via `websockets` and `sses` helpers
13
13
  * `task` handling via `async` and `future` helpers
14
14
  * no rack
15
- * optional tilt/erb support
16
- * optional mustermann support
15
+ * tilt/erb support
16
+ * mustermann support
17
17
 
18
18
  ### What is Angelo?
19
19
 
20
20
  Just like Sinatra, Angelo gives you an expressive DSL for creating web applications. There are some
21
- notable differences, but the basics remain the same. It mostly follows the subclass style of Sinatra:
22
- you must define a subclass of `Angelo::Base` but also `.run` on that class for the service to start.
21
+ notable differences, but the basics remain the same: you can either create a "classic" application
22
+ by requiring 'angelo/main' and using the DSL at the top level of your script, or a "modular"
23
+ application by requiring 'angelo', subclassing `Angelo::Base`, and calling `.run!` on that class for the
24
+ service to start.
23
25
  In addition, and perhaps more importantly, **Angelo is built upon Reel, which is, in turn, built upon
24
26
  Celluloid::IO and gives you a reactor with evented IO in Ruby!**
25
27
 
26
- Note: There currently is no "standalone" capability where one can define route handlers at the top level.
28
+ Things will feel very familiar to anyone experienced with Sinatra. You can define
29
+ route handlers denoted by HTTP verb and path with parameters set from path matching (using
30
+ [Mustermann](#mustermann)), the query string, and post body.
31
+ A route block may return:
27
32
 
28
- Things will feel very familiar to anyone experienced with Sinatra. Inside the subclass, you can define
29
- route handlers denoted by HTTP verb and path. One acceptable return value from a route block is the body
30
- of the response in full as a `String`. Another is a `Hash` if the content type is set to `:json`. Finally,
31
- you may return any object that responds to `#each(&block)` if the transfer encoding is set to `:chunked`.
33
+ * The body of the response in full as a `String`.
34
+ * A `Hash` (or anything that `respond_to? :to_json`) if the content type is set to `:json`.
35
+ * Any object that responds to `#each(&block)` if the transfer encoding is set to `:chunked`.
32
36
  There is also a `chunked_response` helper that will take a block, set the transfer encoding, and return
33
- an appropriate object for you.
37
+ an appropriate object.
34
38
 
35
- There is also [Mustermann](#mustermann) support for full-on, Sinatra-like path
36
- matching and params.
37
-
38
- Angelo also features `before` and `after` blocks, just like Sinatra. Filters are ordered as defined,
39
+ Angelo also features `before` and `after` filter blocks, just like Sinatra. Filters are ordered as defined,
39
40
  and called in that order. When defined without a path, they run for all matched requests. With a path,
40
- the path must match exactly for the block to be called. If `Angelo::Mustermann` is included, the paths
41
- are interpreted as a Mustermann pattern and params are merged. For more info on the difference in how
42
- after blocks are handled, see the Errors section below for more info.
41
+ the path is interpreted as a Mustermann pattern and params are merged. `before` filters can set instance
42
+ variables which can be used in the route block and the `after` filter.
43
+ For more info on the difference in how after blocks are handled, see the Errors section below for more info.
43
44
 
44
45
  ### Websockets!
45
46
 
@@ -52,9 +53,8 @@ for you to build real-time web applications.
52
53
  The `websocket` route builder accepts a path and a block, and passes the actual websocket to the block
53
54
  as the only argument. This socket is an instance of Reel's
54
55
  [WebSocket](https://github.com/celluloid/reel/blob/master/lib/reel/websocket.rb) class, and, as such,
55
- responds to methods like `on_message` and `on_close`. A service-wide `on_pong` handler (defined at the
56
-
57
- a connected websocket client.
56
+ responds to methods like `on_message` and `on_close`. A service-wide `on_pong` handler may be defined
57
+ to customize the behavior when a pong frame comes back from a connected websocket client.
58
58
 
59
59
  ##### `websockets` helper
60
60
 
@@ -96,15 +96,15 @@ end
96
96
  Foo.run!
97
97
  ```
98
98
 
99
- In this case, any clients that connected to a websocket at the path '/' would be stashed in the
100
- default websockets array; clients that connected to '/bar' would be stashed into the `:bar` section.
99
+ In this case, any clients that connect to a websocket at the path '/' will be stashed in the
100
+ default websockets array; clients that connect to '/bar' will be stashed in the `:bar` section.
101
101
 
102
- Each "section" can accessed with a familiar, `Hash`-like syntax, and can be iterated over with
102
+ Each "section" is accessed with a familiar, `Hash`-like syntax, and can be iterated over with
103
103
  a `.each` block.
104
104
 
105
- When a `POST /` with a 'foo' param is received, any value is messaged out to any '/' connected
105
+ When a `POST /` with a 'foo' param is received, any value is messaged out to all '/' connected
106
106
  websockets. When a `POST /bar` with a 'bar' param is received, any value is messaged out to all
107
- websockets that connected to '/bar'.
107
+ websockets connected to '/bar'.
108
108
 
109
109
  ### SSE - Server-Sent Events
110
110
 
@@ -176,12 +176,12 @@ end
176
176
  ##### `sses` helper
177
177
 
178
178
  Angelo also includes a "stash" helper for SSE connections. One can `<<` a socket into `sses` from
179
- inside an `eventsource` handler block. These can also be "later" be iterated over so one can do things
179
+ inside an `eventsource` handler block. These can "later" be iterated over so one can do things
180
180
  like emit a message on every SSE connection when the service receives a POST request.
181
181
 
182
- The `sses` helper includes the same a context ability as the `websockets` helper. Also, by default,
182
+ The `sses` helper includes the same context ability as the `websockets` helper. Also, by default,
183
183
  the helper will `reject!` any closed sockets before returning, just like `websockets`. You may
184
- optionally pass `false` to the helper to skip this step.In addition, the `sses` stash includes
184
+ optionally pass `false` to the helper to skip this step. In addition, the `sses` stash includes
185
185
  methods for easily sending events or messages to all stashed connections. **Note that the
186
186
  `Stash::SSE#event` method only works on non-default contexts and uses the context name as the event
187
187
  name.**
@@ -227,7 +227,7 @@ do not support EventSource yet, since this will respond with a non-"text/event-s
227
227
 
228
228
  ##### `EventSource#on_close` helper
229
229
 
230
- When inside an eventsource block, you can use may want to do something specific when a client closes the
230
+ When inside an eventsource block, you may want to do something specific when a client closes the
231
231
  connection. For this case, there are `on_close` and `on_close=` methods on the object passed to the block
232
232
  that will get called if the client closes the socket. The assignment method takes a proc object and the
233
233
  other one takes a block:
@@ -252,13 +252,13 @@ eventsource '/sse' do |es|
252
252
  end
253
253
  ```
254
254
 
255
- Note the use of the optional parameter the stashes here; by default, stash accessors (`websockets` and
255
+ Note the use of the optional parameter of the stashes here; by default, stash accessors (`websockets` and
256
256
  `sses`) will `reject!` any closed sockets before letting you in. If you pass `false` to the stash
257
257
  accessors, they will skip the `reject!` step.
258
258
 
259
259
  ### Tasks + Async / Future
260
260
 
261
- Angelo is built on Reel and Celluloid::IO, giving your web application class the ability to define
261
+ Angelo is built on Reel and Celluloid::IO, giving your web application the ability to define
262
262
  "tasks" and call them from route handler blocks in an `async` or `future` style.
263
263
 
264
264
  ##### `task` builder
@@ -268,7 +268,7 @@ block. The block can take arguments that you can pass later, with `async` or `fu
268
268
 
269
269
  ```ruby
270
270
  # defining a task on the reactor called `:in_sec` which will sleep for
271
- # given number of seconds, then return the given message.
271
+ # the given number of seconds, then return the given message.
272
272
  #
273
273
  task :in_sec do |sec, msg|
274
274
  sleep sec.to_i
@@ -300,9 +300,9 @@ end
300
300
  ##### `future` helper
301
301
 
302
302
  Just like `async`, this comes from Celluloid as well. It behaves exactly like `async`, with the
303
- notable exception of returing a "future" object that you can call `#value` on later to retreive
304
- the return value of the task. Once `#value` is called, things will "block" until the task is
305
- finished.
303
+ notable exception of returning a "future" object that you can call `#value` on later to retrieve
304
+ the return value of the task. Calling `#value` will "block" until the task is finished, while the
305
+ reactor continues to process requests.
306
306
 
307
307
  ```ruby
308
308
  get '/' do
@@ -324,8 +324,8 @@ Angelo gives you two ordained methods of stopping route processing:
324
324
  * raise an instance of `RequestError`
325
325
  * `halt` with a status code and message
326
326
 
327
- The main difference is that `halt` will still run an `after` block, and raising `RequestError`
328
- will bypass the `after` block.
327
+ The main difference is that `halt` will still run `after` blocks, and raising `RequestError`
328
+ will bypass `after` blocks.
329
329
 
330
330
  Any other exceptions or errors raised by your route handler will be handled with a 500 status
331
331
  code and the message will be the body of the response.
@@ -334,7 +334,7 @@ code and the message will be the body of the response.
334
334
 
335
335
  Raising an instance of `Angelo::RequestError` causes a 400 status code response, and the message
336
336
  in the instance is the body of the the response. If the route or class was set to respond with
337
- JSON, the body is converted to a JSON object with one key, `error`, that has a value of the message.
337
+ JSON, the body is converted to a JSON object with one key, `error`, that has the value of the message.
338
338
  If the message is a `Hash`, the hash is converted to a JSON object, or to a string for other content
339
339
  types.
340
340
 
@@ -414,15 +414,8 @@ everything's fine
414
414
 
415
415
  ### [Tilt](https://github.com/rtomayko/tilt) / ERB
416
416
 
417
- To make `erb` available in route blocks
418
-
419
- 1. add `tilt` to your `Gemfile`: `gem 'tilt'`
420
- 2. require `angelo/tilt/erb`
421
- 3. include `Angelo::Tilt::ERB` in your app
422
-
423
417
  ```ruby
424
418
  class Foo < Angelo::Base
425
- include Angelo::Tilt::ERB
426
419
 
427
420
  views_dir 'some/other/path' # defaults to './views'
428
421
 
@@ -448,17 +441,8 @@ See [views](https://github.com/kenichi/angelo/tree/master/test/test_app_root/vie
448
441
 
449
442
  ### [Mustermann](https://github.com/rkh/mustermann)
450
443
 
451
- To make routes blocks match path with Mustermann patterns
452
-
453
- 1. be using ruby &gt;=2.0.0
454
- 2. add 'mustermann' to to your `Gemfile`: `platform(:ruby_20){ gem 'mustermann' }`
455
- 3. require `angelo/mustermann`
456
- 4. include `Angelo::Mustermann` in your app
457
-
458
444
  ```ruby
459
445
  class Foo < Angelo::Base
460
- include Angelo::Tilt::ERB
461
- include Angelo::Mustermann
462
446
 
463
447
  get '/:foo/things/:bar' do
464
448
 
@@ -486,6 +470,68 @@ class Foo < Angelo::Base
486
470
  end
487
471
  ```
488
472
 
473
+ ### Classic vs. modular apps
474
+
475
+ Like Sinatra, Angelo apps can be written in either "classic" style or
476
+ so-called "modular" style. Which style you use is more of a personal
477
+ preference than anything else.
478
+
479
+ A classic-style app requires "angelo/main" and defines the app
480
+ directly at the top level using the DSL. In addition, classic apps:
481
+
482
+ * Can use a `helpers` block to define methods that can be called from
483
+ filters and route handlers. The `helpers` method can also include methods
484
+ from one or more modules passed as arguments instead of or in addition
485
+ to taking a block.
486
+ * Parse optional command-line options "-o addr" and "-p port" to set
487
+ the bind address and listen port, respectively.
488
+ * Are run automatically.
489
+
490
+ *Note: unlike Sinatra, define a classic app by requiring "angelo/main"
491
+ and a modular app by requiring "angelo". Sinatra uses "sinatra" and
492
+ "sinatra/base" to do the same things.*
493
+
494
+ Here's a classic app:
495
+
496
+ ```ruby
497
+ require 'angelo/main'
498
+
499
+ helpers do
500
+ def say_hello
501
+ "Hello"
502
+ end
503
+ end
504
+
505
+ get "/hello" do
506
+ "#{say_hello} to you, too."
507
+ end
508
+ ```
509
+
510
+ And the same app in modular style:
511
+
512
+ ```ruby
513
+ require 'angelo'
514
+
515
+ class HelloApp < Angelo::Base
516
+ def say_hello
517
+ "Hello"
518
+ end
519
+
520
+ get "/hello" do
521
+ "#{say_hello} to you, too."
522
+ end
523
+ end
524
+
525
+ HelloApp.run!
526
+ ```
527
+
528
+ ### Documentation
529
+
530
+ **I'm bad at documentation and I feel bad.**
531
+
532
+ Others have helped, and there is a YaRD plugin for Angelo [here](https://github.com/artcom/yard-angelo)
533
+ if you would like to document your apps built with Angelo. (thanks: @katjaeinsfeld, @artcom)
534
+
489
535
  ### WORK LEFT TO DO
490
536
 
491
537
  Lots of work left to do!
@@ -494,10 +540,8 @@ Lots of work left to do!
494
540
 
495
541
  ```ruby
496
542
  require 'angelo'
497
- require 'angelo/mustermann'
498
543
 
499
544
  class Foo < Angelo::Base
500
- include Angelo::Mustermann
501
545
 
502
546
  # just some constants to use in routes later...
503
547
  #
@@ -2,16 +2,19 @@
2
2
  require File.expand_path('../lib/angelo/version', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
- gem.authors = ["Kenichi Nakamura"]
6
- gem.email = ["kenichi.nakamura@gmail.com"]
7
- gem.description = gem.summary = "A Sinatra-esque DSL for Reel"
8
- gem.homepage = "https://github.com/kenichi/angelo"
9
- gem.files = `git ls-files | grep -Ev '^example'`.split("\n")
10
- gem.test_files = `git ls-files -- spec/*`.split("\n")
11
- gem.name = "angelo"
12
- gem.require_paths = ["lib"]
13
- gem.version = Angelo::VERSION
14
- gem.license = 'apache'
5
+ gem.authors = ["Kenichi Nakamura"]
6
+ gem.email = ["kenichi.nakamura@gmail.com"]
7
+ gem.description = gem.summary = "A Sinatra-like DSL for Reel"
8
+ gem.homepage = "https://github.com/kenichi/angelo"
9
+ gem.files = `git ls-files | grep -Ev '^example'`.split("\n")
10
+ gem.test_files = `git ls-files -- spec/*`.split("\n")
11
+ gem.name = "angelo"
12
+ gem.require_paths = ["lib"]
13
+ gem.version = Angelo::VERSION
14
+ gem.license = 'apache'
15
+ gem.required_ruby_version = '>= 2.1.0'
15
16
  gem.add_runtime_dependency 'reel', '~>0.5'
17
+ gem.add_runtime_dependency 'tilt', '~>2.0'
18
+ gem.add_runtime_dependency 'mustermann', '~>0.4'
16
19
  gem.add_runtime_dependency 'mime-types', '~>2.4'
17
20
  end
@@ -1,5 +1,7 @@
1
1
  require 'reel'
2
2
  require 'json'
3
+ require 'tilt'
4
+ require 'mustermann'
3
5
 
4
6
  # require 'ruby-prof'
5
7
  #
@@ -128,11 +130,12 @@ end
128
130
  require 'angelo/version'
129
131
  require 'angelo/params_parser'
130
132
  require 'angelo/server'
131
- require 'angelo/base'
132
- require 'angelo/stash'
133
133
  require 'angelo/responder'
134
134
  require 'angelo/responder/eventsource'
135
135
  require 'angelo/responder/websocket'
136
+ require 'angelo/tilt/erb'
137
+ require 'angelo/base'
138
+ require 'angelo/stash'
136
139
 
137
140
  # trap "INT" do
138
141
  # result = RubyProf.stop
@@ -1,11 +1,13 @@
1
1
  module Angelo
2
2
 
3
3
  class Base
4
+ extend Forwardable
4
5
  include ParamsParser
5
6
  include Celluloid::Logger
7
+ include Tilt::ERB
8
+ include Mustermann
6
9
 
7
- extend Forwardable
8
- def_delegators :@responder, :content_type, :headers, :redirect, :request, :transfer_encoding
10
+ def_delegators :@responder, :content_type, :headers, :mustermann, :redirect, :request, :transfer_encoding
9
11
  def_delegators :@klass, :public_dir, :report_errors?, :sse_event, :sse_message, :sses, :websockets
10
12
 
11
13
  attr_accessor :responder
@@ -16,9 +18,15 @@ module Angelo
16
18
 
17
19
  def inherited subclass
18
20
 
19
- # set app_file from caller stack
21
+ # Set app_file by groveling up the caller stack until we find
22
+ # the first caller from a directory different from __FILE__.
23
+ # This allows base.rb to be required from an arbitrarily deep
24
+ # nesting of require "angelo/<whatever>" and still set
25
+ # app_file correctly.
20
26
  #
21
- subclass.app_file = caller(1).map {|l| l.split(/:(?=|in )/, 3)[0,1]}.flatten[0]
27
+ subclass.app_file = caller_locations.map(&:absolute_path).find do |f|
28
+ !f.start_with?(File.dirname(__FILE__) + File::SEPARATOR)
29
+ end
22
30
 
23
31
  # bring RequestError into this namespace
24
32
  #
@@ -30,6 +38,12 @@ module Angelo
30
38
  subclass.ping_time DEFAULT_PING_TIME
31
39
  subclass.log_level DEFAULT_LOG_LEVEL
32
40
 
41
+ # Parse command line options if angelo/main has been required.
42
+ # They could also be parsed in run, but this makes them
43
+ # available to and overridable by the DSL.
44
+ #
45
+ subclass.parse_options(ARGV.dup) if @angelo_main
46
+
33
47
  class << subclass
34
48
 
35
49
  def root
@@ -39,7 +53,12 @@ module Angelo
39
53
  end
40
54
 
41
55
  end
56
+ end
42
57
 
58
+ # Methods defined in module DSL will be available in the DSL both
59
+ # in Angelo::Base subclasses and in the top level DSL.
60
+
61
+ module DSL
43
62
  def addr a = nil
44
63
  @addr = a if a
45
64
  @addr
@@ -76,42 +95,25 @@ module Angelo
76
95
  @report_errors = true
77
96
  end
78
97
 
79
- def report_errors?
80
- !!@report_errors
81
- end
82
-
83
- def compile! name, &block
84
- define_method name, &block
85
- method = instance_method name
86
- remove_method name
87
- method
88
- end
89
-
90
- def routes
91
- @routes ||= Hash.new{|h,k| h[k] = {}}
98
+ HTTPABLE.each do |m|
99
+ define_method m do |path, opts = {}, &block|
100
+ path = ::Mustermann.new path, opts
101
+ routes[m][path] = Responder.new &block
102
+ end
92
103
  end
93
104
 
94
- def filters
95
- @filters ||= {before: {default: []}, after: {default: []}}
105
+ def websocket path, &block
106
+ path = ::Mustermann.new path
107
+ routes[:websocket][path] = Responder::Websocket.new &block
96
108
  end
97
109
 
98
- def filter which, opts = {}, &block
99
- f = compile! :filter, &block
100
- case opts
101
- when String
102
- filter_by which, opts, f
103
- when Hash
104
- if opts[:path]
105
- filter_by which, opts[:path], f
106
- else
107
- filters[which][:default] << f
108
- end
109
- end
110
+ def eventsource path, headers = nil, &block
111
+ path = ::Mustermann.new path
112
+ routes[:get][path] = Responder::Eventsource.new headers, &block
110
113
  end
111
114
 
112
- def filter_by which, path, meth
113
- filters[which][path] ||= []
114
- filters[which][path] << meth
115
+ def task name, &block
116
+ Angelo::Server.define_task name, &block
115
117
  end
116
118
 
117
119
  def before opts = {}, &block
@@ -122,26 +124,47 @@ module Angelo
122
124
  filter :after, opts, &block
123
125
  end
124
126
 
125
- HTTPABLE.each do |m|
126
- define_method m do |path, &block|
127
- routes[m][path] = Responder.new &block
128
- end
127
+ def on_pong &block
128
+ Responder::Websocket.on_pong = block
129
129
  end
130
130
 
131
- def websocket path, &block
132
- routes[:websocket][path] = Responder::Websocket.new &block
131
+ end
132
+
133
+ # Make the DSL methods available to subclass-level code.
134
+ # main.rb makes them available to the top level.
135
+
136
+ extend DSL
137
+
138
+ class << self
139
+ def report_errors?
140
+ !!@report_errors
133
141
  end
134
142
 
135
- def eventsource path, headers = nil, &block
136
- routes[:get][path] = Responder::Eventsource.new headers, &block
143
+ def routes
144
+ @routes ||= Hash.new{|h,k| h[k] = RouteMap.new}
137
145
  end
138
146
 
139
- def on_pong &block
140
- Responder::Websocket.on_pong = block
147
+ def filters
148
+ @filters ||= {before: {default: []}, after: {default: []}}
141
149
  end
142
150
 
143
- def task name, &block
144
- Angelo::Server.define_task name, &block
151
+ def filter which, opts = {}, &block
152
+ case opts
153
+ when String
154
+ filter_by which, opts, block
155
+ when Hash
156
+ if opts[:path]
157
+ filter_by which, opts[:path], block
158
+ else
159
+ filters[which][:default] << block
160
+ end
161
+ end
162
+ end
163
+
164
+ def filter_by which, path, block
165
+ pattern = ::Mustermann.new path
166
+ filters[which][pattern] ||= []
167
+ filters[which][pattern] << block
145
168
  end
146
169
 
147
170
  def websockets reject = true
@@ -215,8 +238,8 @@ module Angelo
215
238
  when GET, DELETE, OPTIONS
216
239
  parse_query_string
217
240
  when POST, PUT
218
- parse_post_body
219
- end
241
+ parse_query_string_and_post_body
242
+ end.merge mustermann.params(request.path)
220
243
  end
221
244
 
222
245
  def request_headers
@@ -319,17 +342,76 @@ module Angelo
319
342
  Celluloid.sleep time
320
343
  end
321
344
 
322
- def filter which
323
- fs = self.class.filters[which][:default]
324
- fs += self.class.filters[which][request.path] if self.class.filters[which][request.path]
325
- fs.each {|f| f.bind(self).call}
326
- end
327
-
328
345
  def chunked_response &block
329
346
  transfer_encoding :chunked
330
347
  ChunkedResponse.new &block
331
348
  end
332
349
 
350
+ def filter which
351
+ self.class.filters[which].each do |pattern, filters|
352
+ case pattern
353
+ when :default
354
+ filters.each {|filter| instance_eval &filter}
355
+ when ::Mustermann
356
+ if pattern.match request.path
357
+ @pre_filter_params = params
358
+ @params = @pre_filter_params.merge pattern.params(request.path)
359
+ filters.each {|filter| instance_eval &filter}
360
+ @params = @pre_filter_params
361
+ end
362
+ end
363
+ end
364
+ end
365
+
366
+ # It seems more sensible to put this in main.rb since it's used
367
+ # only if angelo/main is required, but it's here so it can be
368
+ # tested, since requiring angelo/main doesn't play well with the
369
+ # test code.
370
+
371
+ def self.parse_options(argv)
372
+ require "optparse"
373
+
374
+ optparse = OptionParser.new do |op|
375
+ op.banner = "Usage: #{$0} [options]"
376
+
377
+ op.on('-p port', OptionParser::DecimalInteger, "set the port (default is #{port})") {|val| port val}
378
+ op.on('-o addr', "set the host (default is #{addr})") {|val| addr val}
379
+ op.on('-h', '--help', "Show this help") do
380
+ puts op
381
+ exit
382
+ end
383
+ end
384
+
385
+ begin
386
+ optparse.parse(argv)
387
+ rescue OptionParser::ParseError => ex
388
+ $stderr.puts ex
389
+ $stderr.puts optparse
390
+ exit 1
391
+ end
392
+ end
393
+
394
+ class RouteMap
395
+
396
+ def initialize
397
+ @hash = Hash.new
398
+ end
399
+
400
+ def []= route, responder
401
+ @hash[route] = responder
402
+ end
403
+
404
+ def [] route
405
+ responder = nil
406
+ if mustermann = @hash.keys.select {|k| k.match(route)}.first
407
+ responder = @hash.fetch mustermann
408
+ responder.mustermann = mustermann
409
+ end
410
+ responder
411
+ end
412
+
413
+ end
414
+
333
415
  class EventSource
334
416
  extend Forwardable
335
417