wouter 0.0.5 → 0.0.6

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
  SHA256:
3
- metadata.gz: 4731ee9735261f806ded54813c9655fcae91349f6c3b9a5061f0664ac2618b72
4
- data.tar.gz: 0bb29c4009bcc7846e07dfad0dae5c8c0ba4af3ceaa5bf1607ca4f89590e3e59
3
+ metadata.gz: 328983ffb5c818d8e95bfd645fa6a4390bc97efee9d3b195d28cd86421b69315
4
+ data.tar.gz: '01191a528dab4215b98d03545b863c4cd8c3f0b5be7c0f9f91e97a0bbe300e72'
5
5
  SHA512:
6
- metadata.gz: 81a9adf9928ce8af300ede523ed9fad8ac9c657ccee665fae108b4f13d54c3fa77a11c61cefdd3d4bceae992d4f68e8cf2e65e6f103dafd0b2287ca2e310fb1a
7
- data.tar.gz: ad519803a201b62cbb9e9b1d93365329a74d0517859dd60fff1ca8b10088d86788baad5d9d5903aa04df8dbdd423bcf26f88433e3efd1f9e334ece9037ee59c0
6
+ metadata.gz: 5e1b52bc209def3b17235c036102d145c1e43ed31deb318288437956307489636573c037342dd064d541c8f59d6343b9d42f363c4e3638e4d36d83e9d2c2c9ea
7
+ data.tar.gz: a41ead4e722ebccfd50af05e13571cd880f3f580eec097d89cc992f09376bc6352ab96c1560d85589cb9cd20d1c08d4352c12d98f48146199d65ad14d8a11ae4
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ gemspec
data/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # Wouter
2
+
3
+ Wouter is a modular web framework built on top of Rack.
4
+
5
+ Wouter design goals: explicit, modular, readable.
6
+
7
+ ## Basic Usage
8
+
9
+ Wouter allows you to define routes that connect to endpoints.
10
+
11
+ Here is an example `rackup` compatible file:
12
+
13
+ ```ruby
14
+ require 'wouty'
15
+
16
+ class HelloWorld < Wouty::Endpoint
17
+ def respond
18
+ 'Hello, world!'
19
+ end
20
+ end
21
+
22
+ class Routes < Wouty
23
+ get '/', HelloWorld
24
+ end
25
+
26
+ run Routes.build
27
+ ```
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler'
4
+ Bundler.setup(:default, :test)
5
+
6
+ desc 'Run tests'
7
+ task :test do
8
+ require_relative './test/helper'
9
+
10
+ Dir['test/**/*_test.rb'].each do |file|
11
+ load file
12
+ end
13
+ end
data/examples/hello.rb ADDED
@@ -0,0 +1,15 @@
1
+ # Usage: rackup hello.rb
2
+
3
+ require 'wouter'
4
+
5
+ class HelloWorld < Wouty::Endpoint
6
+ def respond
7
+ 'Hello, world!'
8
+ end
9
+ end
10
+
11
+ class Routes < Wouty
12
+ get '/', HelloWorld
13
+ end
14
+
15
+ run Routes.build
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ # `Wouter::Endpoint` is a class to help define Rack compatible endpoints that
4
+ # can be assigned to a route path. Each `Wouter::Endpoint` instance is a Rack
5
+ # application of it's own. It provides convience methods for creating endpoints.
6
+ #
7
+ class Wouter::Endpoint
8
+ class NotImplementedError < StandardError; end
9
+
10
+ attr_reader :req, :res
11
+
12
+ def initialize(req, res)
13
+ @req = req
14
+ @res = res
15
+ end
16
+
17
+ def self.call(env)
18
+ endpoint = new(Rack::Request.new(env), Rack::Response.new)
19
+ response = endpoint.respond
20
+
21
+ return response.finish if response.is_a?(Rack::Response)
22
+
23
+ endpoint.res.write response
24
+ endpoint.res.finish
25
+ end
26
+
27
+ def respond
28
+ raise NotImplementedError, 'you must implemented #respond in your Wouter::Endpoint class'
29
+ end
30
+
31
+ def params
32
+ @req.params
33
+ end
34
+
35
+ def headers(hash = nil)
36
+ @res.headers.merge! hash if hash
37
+ @res.headers
38
+ end
39
+
40
+ def status(code)
41
+ @res.status = code
42
+ @res.status
43
+ end
44
+
45
+ def not_found
46
+ Wouter::Util.not_found
47
+ end
48
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # `Wouter::Route` represents a route definition. Example:
4
+ #
5
+ # get '/users/:id/comments/:comment_id', RouteKlass
6
+ #
7
+ # It contains utility methods for matching a Rack request to a route.
8
+ #
9
+ class Wouter::Route
10
+ REGEX = '([A-Za-z0-9\-\_]+)'
11
+
12
+ attr_reader :request_method, :path, :klass
13
+
14
+ def initialize(request_method:, path:, klass:)
15
+ @request_method = request_method
16
+ @path = path
17
+ @klass = klass
18
+ end
19
+
20
+ # Expects `request` to be a `Rack::Request`
21
+ # Check `request.request_method` and `request.path`
22
+ def match?(request)
23
+ return false if request.request_method.to_sym != @request_method
24
+ return true if request.path == path
25
+
26
+ path_regex =~ request.path
27
+ end
28
+
29
+ # Expects `request` to be a `Rack::Request`
30
+ # Parse the path parameters out of `request.path`
31
+ def params(request)
32
+ request.path.match(named_path_regex)&.named_captures
33
+ end
34
+
35
+ private
36
+
37
+ # /user/:id/comments/:comment_id
38
+ # /\/user\/(?<id>\w+)\/comments\/(?<comment_id>\w+)/
39
+ def named_path_regex
40
+ @named_path_regex ||= begin
41
+ string_regex = path
42
+ path.scan(/(:\w+)/).flatten.each do |param|
43
+ string_regex = string_regex.gsub(param, '(?<' + param[1..-1] +'>' + REGEX + ')')
44
+ end
45
+ Regexp.new(string_regex)
46
+ end
47
+ end
48
+
49
+ # /user/:id/comments/:comment_id
50
+ # /\/user\/(\w+)\/comments\/(\w+)/
51
+ def path_regex
52
+ @path_regex ||= begin
53
+ string_regex = path
54
+ path.scan(/(:\w+)/).flatten.each do |param|
55
+ string_regex = string_regex.gsub(param, REGEX)
56
+ end
57
+ Regexp.new(string_regex)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # `Wouter::Util` provides some common untility methods.
4
+ #
5
+ class Wouter::Util
6
+ # HTTP 404 Not Found
7
+ def self.not_found
8
+ resp = Rack::Response.new
9
+ resp.status = 404
10
+ resp.write 'Not Found'
11
+ resp
12
+ end
13
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ # `Wouter::Wrapper` is a class that provides a top level Rack application containing
4
+ # all the routing endpoints. HTTP requests come through `Wouter::Wrapper` and it dispatches
5
+ # to the appropriate route class that was defined by the user.
6
+ #
7
+ class Wouter::Wrapper
8
+ def initialize(routes:)
9
+ @routes = routes
10
+ end
11
+
12
+ def call(env)
13
+ request = Rack::Request.new(env)
14
+ route = find_route(request)
15
+
16
+ if route
17
+ route.params(request).each do |name, value|
18
+ request.update_param(name, value)
19
+ end
20
+ route.klass.call(request.env)
21
+ else
22
+ Wouter::Util.not_found
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def find_route(request)
29
+ @routes.find do |route|
30
+ route.match?(request)
31
+ end
32
+ end
33
+ end
data/lib/wouter.rb CHANGED
@@ -4,180 +4,54 @@ require 'bundler'
4
4
  Bundler.setup(:default)
5
5
  require 'rack'
6
6
 
7
- # Wouter main class to define routes from
8
- class Wouter
9
- # ========== Endpoint class to make life easier
10
- class Endpoint
11
- attr_accessor :req, :res
7
+ # Define `Wouter` so we can load stuff
8
+ class Wouter; end
12
9
 
13
- # @req : Request
14
- # @res : Response
15
- def initialize(req, res)
16
- @req = req
17
- @res = res
18
- end
10
+ require_relative 'wouter/util'
11
+ require_relative 'wouter/route'
12
+ require_relative 'wouter/endpoint'
13
+ require_relative 'wouter/wrapper'
19
14
 
20
- def self.call(env)
21
- endpoint = new(Rack::Request.new(env), Rack::Response.new)
22
- final_resp = endpoint.respond
23
- # Return a `Rack::Response` or write to the body of the response
24
- if final_resp.is_a?(Rack::Response)
25
- final_resp
26
- else
27
- endpoint.res.write final_resp
28
- endpoint.res
29
- end
30
- end
31
-
32
- # Users of `Wouter::Endpoint` should implement their own `#response`
33
- def respond
34
- @res
35
- end
36
-
37
- ## ~~~~~ Convience methods
38
-
39
- def params
40
- @req.params
15
+ # Main route definition DSL
16
+ class Wouter
17
+ class <<self
18
+ def get(route, klass)
19
+ define_route(:GET, route, klass)
41
20
  end
42
21
 
43
- def json(body)
44
- @res.set_header('Content-Type', 'application/json')
45
- @res.write body
46
- @res
22
+ def put(route, klass)
23
+ define_route(:PUT, route, klass)
47
24
  end
48
25
 
49
- def status(code)
50
- @res.status = code
51
- @res
26
+ def post(route, klass)
27
+ define_route(:POST, route, klass)
52
28
  end
53
- end
54
-
55
- ## ========== Internal
56
29
 
57
- def self.app
58
- @app ||= Rack::Builder.new
59
- end
60
-
61
- def self.routes
62
- @routes ||= []
63
- end
64
-
65
- ## ========== DSL
66
-
67
- class <<self
68
- %i[get post put delete].each do |m|
69
- define_method(m) do |path, route_class|
70
- route = {
71
- method: m.to_s.upcase.to_sym,
72
- path: path,
73
- route_class: route_class
74
- }
75
- routes.push(route)
76
- end
30
+ def delete(route, klass)
31
+ define_route(:DELETE, route, klass)
77
32
  end
78
33
 
79
34
  def middleware(klass, *args, &block)
80
35
  app.use(klass, *args, &block)
81
36
  end
82
- end
83
-
84
- ## ========== Build a Rack entry point
85
-
86
- def self.build
87
- app.run(Wrapper.new(routes))
88
- app.to_app
89
- end
90
-
91
- # Wrapper class for creating single `.call` interface for `Rack::Builder`
92
- class Wrapper
93
- attr_reader :routes
94
-
95
- def initialize(routes)
96
- @routes = routes
97
- end
98
-
99
- def call(env)
100
- request = Rack::Request.new(env)
101
-
102
- route = routes.find do |r|
103
- matching_route?(r, request)
104
- end
105
-
106
- return not_found unless route
107
37
 
108
- path_parameters(route, request.path) do |name, value|
109
- request.update_param(name, value)
110
- end
111
-
112
- resp = route[:route_class].call(request.env)
113
- resp.finish
38
+ def build
39
+ app.run(Wrapper.new(routes: routes))
40
+ app.to_app
114
41
  end
115
42
 
116
43
  private
117
44
 
118
- # Does the given route match the request
119
- def matching_route?(route, request)
120
- return false unless route[:method] == request.request_method.to_sym
121
-
122
- if parameterized_path?(route[:path])
123
- path_regex = build_path_regex(route[:path].split('/'))
124
- path_regex.match?(request.path)
125
- else
126
- route[:path] == request.path
127
- end
128
- end
129
-
130
- # Yield the parameters parsed from the path
131
- # Example definition: "/hello/:name"
132
- # Example request: "/hello/robert"
133
- # Example yield: name, robert
134
- def path_parameters(route, path)
135
- return unless parameterized_path?(route[:path])
136
-
137
- split_path = route[:path].split('/')
138
- path_parameter_data = build_path_regex(split_path).match(path)
139
- path_parameter_names = find_parameter_names_in_path(split_path)
140
- path_parameter_names.each_with_index do |parameter_name, i|
141
- yield parameter_name, path_parameter_data[i + 1]
142
- end
143
- end
144
-
145
- # Does the path have a parameter in it? ex: '/user/:id'
146
- def parameterized_path?(path)
147
- path.include?(':')
148
- end
149
-
150
- # Find all the named parameters in the route
151
- # Then drop the ':' so we have the names: ':id' => 'id'
152
- def find_parameter_names_in_path(split_path)
153
- params = split_path.find_all do |s|
154
- if s.size > 1
155
- s[0] == ':'
156
- else
157
- false
158
- end
159
- end
160
- params.map { |s| s[1..-1] }
45
+ def app
46
+ @app ||= Rack::Builder.new
161
47
  end
162
48
 
163
- # Turn the route into a regex: '/hello/:name' => '\/hello\/(\w*)'
164
- def build_path_regex(split_path)
165
- regex_string = split_path.map do |s|
166
- if s[0] == ':'
167
- '(\\w*)'
168
- else
169
- s
170
- end
171
- end
172
- Regexp.new(regex_string.join('\/'))
49
+ def routes
50
+ @routes ||= []
173
51
  end
174
52
 
175
- # Return HTTP 404
176
- def not_found
177
- resp = Rack::Response.new
178
- resp.status = 404
179
- resp.write 'Not Found'
180
- resp
53
+ def define_route(request_method, path, klass)
54
+ routes << Wouter::Route.new(request_method: request_method, path: path, klass: klass)
181
55
  end
182
56
  end
183
57
  end
data/wouter.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'wouter'
5
+ s.platform = Gem::Platform::RUBY
6
+ s.version = '0.0.6'
7
+ s.summary = 'Rack web router.'
8
+ s.description = 'Wouter is a modular web router built on Rack.'
9
+ s.authors = ['Robert Peterson']
10
+ s.email = 'robertpeterson@gmail.com'
11
+ s.files = Dir['lib/**/*', 'examples/*'] + ['README.md', 'Gemfile', 'Rakefile', 'wouter.gemspec']
12
+ s.test_files = s.files.select { |p| p =~ /^test\/.*_test.rb/ }
13
+ s.license = 'MIT'
14
+ s.homepage = 'https://github.com/rawburt/wouter'
15
+
16
+ s.add_dependency 'rack', '~> 2.0', '>= 2.0.6'
17
+ s.add_development_dependency 'm', '~> 1.5', '>= 1.5.1'
18
+ s.add_development_dependency 'minitest', '~> 5.11', '>= 5.11.3'
19
+ s.add_development_dependency 'minitest-rg', '~> 5.2', '>= 5.2.0'
20
+ s.add_development_dependency 'mocha', '~> 1.7', '>= 1.7.0'
21
+ s.add_development_dependency 'rack-test', '~> 1.1', '>= 1.1.0'
22
+ s.add_development_dependency 'rake', '~> 12.3', '>= 12.3.1'
23
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wouter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Peterson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-11-16 00:00:00.000000000 Z
11
+ date: 2018-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -90,6 +90,26 @@ dependencies:
90
90
  - - ">="
91
91
  - !ruby/object:Gem::Version
92
92
  version: 5.2.0
93
+ - !ruby/object:Gem::Dependency
94
+ name: mocha
95
+ requirement: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '1.7'
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 1.7.0
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '1.7'
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: 1.7.0
93
113
  - !ruby/object:Gem::Dependency
94
114
  name: rack-test
95
115
  requirement: !ruby/object:Gem::Requirement
@@ -136,7 +156,16 @@ executables: []
136
156
  extensions: []
137
157
  extra_rdoc_files: []
138
158
  files:
159
+ - Gemfile
160
+ - README.md
161
+ - Rakefile
162
+ - examples/hello.rb
139
163
  - lib/wouter.rb
164
+ - lib/wouter/endpoint.rb
165
+ - lib/wouter/route.rb
166
+ - lib/wouter/util.rb
167
+ - lib/wouter/wrapper.rb
168
+ - wouter.gemspec
140
169
  homepage: https://github.com/rawburt/wouter
141
170
  licenses:
142
171
  - MIT