angelo 0.3.3 → 0.4.0

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