webservice 0.6.1 → 0.7.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: 4edd2b67e6b95a73140481707e95618cc95e1fff
4
- data.tar.gz: c6c27bc0393659f8282e46fe3ade997b6e8312e0
3
+ metadata.gz: 8ad7a2790f5e6914233060b188e91202c2d02bca
4
+ data.tar.gz: e790a006ecbaa61295205a44936b7bf7802849b1
5
5
  SHA512:
6
- metadata.gz: 217c555d56853dd4343a57a729cf5b32abc96798d84b01716bd4a7195bf793a674c243f7d0fdc839d30bc6d72b88a9392ddf8a8ce8410116ba0aeaffe85b7d28
7
- data.tar.gz: a3364296545ad1016a188303fa8f99a1f7963bf4fc8b6bbd53e6ccbad638f58312444716d1db980d4e3a5a51b91fd968d2fcec3708a4590aeaa3025968071f15
6
+ metadata.gz: 03ea246f0daf6d8602774d681886afb70f3a4ec7e2626747c4d6f5311a0629146a1407335896b10d18bc83c39a05f9a60514bf4cdf8aa47300bc5feffbaf4997
7
+ data.tar.gz: 5c5ef7e78019811e996f65b9daf1cbb5462f9016fd341a58976fa794777c3b520a00d898766c3b80961f1f66ec3f39819d018b8cb99f772ce76ae2859d772d04
@@ -4,12 +4,14 @@ README.md
4
4
  Rakefile
5
5
  assets/webservice-32x32.png
6
6
  lib/webservice.rb
7
- lib/webservice/base.rb
8
- lib/webservice/response_handler.rb
7
+ lib/webservice/base/base.rb
8
+ lib/webservice/base/response_handler.rb
9
+ lib/webservice/metal.rb
9
10
  lib/webservice/version.rb
10
11
  test/helper.rb
11
12
  test/service/app.rb
12
- test/test_app.rb
13
+ test/service/debug.rb
14
+ test/test_metal.rb
13
15
  test/test_mustermann.rb
14
- test/test_samples.rb
15
- test/test_samples_debug.rb
16
+ test/test_service_app.rb
17
+ test/test_service_debug.rb
data/README.md CHANGED
@@ -134,7 +134,7 @@ get '/beer/(r|rnd|rand|random)' do # special keys for random beer
134
134
  end
135
135
 
136
136
  get '/beer/:key'
137
- Beer.find_by! key: params[:key]
137
+ Beer.find_by! key: params['key']
138
138
  end
139
139
 
140
140
  get '/brewery/(r|rnd|rand|random)' do # special keys for random brewery
@@ -142,13 +142,33 @@ get '/brewery/(r|rnd|rand|random)' do # special keys for random brewery
142
142
  end
143
143
 
144
144
  get '/brewery/:key'
145
- Brewery.find_by! key: params[:key]
145
+ Brewery.find_by! key: params['key']
146
146
  end
147
+
148
+ ...
147
149
  ```
148
150
 
151
+
149
152
  [**`worlddb / world.db.service`**](https://github.com/worlddb/world.db.service) -
150
153
  world.db HTTP JSON API (web service) scripts
151
154
 
155
+ ```ruby
156
+ get '/countries(.:format)?' do
157
+ Country.by_key.all # sort/order by key
158
+ end
159
+
160
+ get '/cities(.:format)?' do
161
+ City.by_key.all # sort/order by key
162
+ end
163
+
164
+ get '/tag/:slug(.:format)?' do # e.g. /tag/north_america.csv
165
+ Tag.find_by!( slug: params['slug'] ).countries
166
+ end
167
+
168
+ ...
169
+ ```
170
+
171
+
152
172
  [**`sportdb / sport.db.service`**](https://github.com/sportdb/sport.db.service) -
153
173
  sport.db (football.db) HTTP JSON API (web service) scripts
154
174
 
@@ -22,9 +22,10 @@ require 'rack'
22
22
 
23
23
  # our own code
24
24
  require 'webservice/version' # note: let version always go first
25
- require 'webservice/base'
25
+ require 'webservice/metal'
26
26
 
27
- require 'webservice/response_handler' ## default (built-in) response handler (magic)
27
+ require 'webservice/base/base'
28
+ require 'webservice/base/response_handler' ## default (built-in) response handler (magic)
28
29
 
29
30
 
30
31
 
@@ -0,0 +1,117 @@
1
+ # encoding: utf-8
2
+
3
+
4
+ module Webservice
5
+
6
+ class Base < Metal
7
+
8
+
9
+ ## note: before (filter) for now is just a method (NOT a chain for blocks, etc.);
10
+ ## override method to change before (filter)
11
+ def before
12
+ ### move cors headers to responseHandler to initialize!!!! - why? why not??
13
+ ## (auto-)add (merge in) cors headers
14
+ ## todo: move into a before filter ?? lets you overwrite headers - needed - why? why not??
15
+ headers 'Access-Control-Allow-Origin' => '*',
16
+ 'Access-Control-Allow-Headers' => 'Authorization,Accepts,Content-Type,X-CSRF-Token,X-Requested-With',
17
+ 'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,OPTIONS'
18
+ end
19
+
20
+
21
+ # note: for now use "plugable" response handler
22
+ ## rename to respond_with or something? why? why not??
23
+ ## make it a "stateless" function e.g. just retrun tripled [status, headers, body] - why? why not??
24
+ def handle_response( obj, opts={} )
25
+ handler = ResponseHandler.new( self ) ## for now "hard-coded"; make it a setting later - why? why not?
26
+ handler.handle_response( obj ) ## prepare response
27
+ end
28
+
29
+
30
+ ##################################
31
+ ## add some fallback (builtin) routes
32
+
33
+ get '/favicon.ico' do
34
+ ## use 302 to redirect
35
+ ## note: use strg+F5 to refresh page (clear cache for favicon.ico)
36
+ redirect '/webservice-32x32.png'
37
+ end
38
+
39
+ get '/webservice-32x32.png' do
40
+ send_file "#{Webservice.root}/assets/webservice-32x32.png"
41
+ end
42
+
43
+ get '/routes' do
44
+ msg =<<TXT
45
+ #{dump_routes}
46
+
47
+ #{dump_version}
48
+ TXT
49
+ end
50
+
51
+ ## catch all (404 not found)
52
+ get '/*' do
53
+ pp env
54
+ pp self.class.routes ## note: dump routes of derived class
55
+
56
+ msg =<<TXT
57
+ 404 Not Found
58
+
59
+ No route matched >#{request.request_method} #{request.path_info}<:
60
+
61
+ REQUEST_METHOD: >#{request.request_method}<
62
+ PATH_INFO: >#{request.path_info}<
63
+ QUERY_STRING: >#{request.query_string}<
64
+
65
+ SCRIPT_NAME: >#{request.script_name}<
66
+ REQUEST_URI: >#{request.url}<
67
+
68
+
69
+ #{dump_routes}
70
+
71
+ #{dump_version}
72
+ TXT
73
+
74
+ halt 404, msg
75
+ end
76
+
77
+
78
+ ############################
79
+ ## fallback helpers
80
+
81
+ def dump_routes ## todo/check - rename to build_routes/show_routes/etc. - why? why not?
82
+ buf = ""
83
+ walk_routes_for( buf, self.class )
84
+ buf
85
+ end
86
+
87
+ def walk_routes_for( buf, base=self.class )
88
+
89
+ buf << " Routes >#{base.name}<:\n\n"
90
+
91
+ base.routes.each do |method,routes|
92
+ buf << " #{method}:\n"
93
+ routes.each do |pattern,block|
94
+ buf << " #{pattern.to_s}\n"
95
+ end
96
+ end
97
+
98
+ if base.superclass.respond_to? :routes
99
+ buf << "\n\n"
100
+ walk_routes_for( buf, base.superclass )
101
+ end
102
+ end
103
+
104
+
105
+ def dump_version
106
+ ## single line version string
107
+ buf = " " # note: start with two leading spaces (indent)
108
+ buf << "webservice/#{VERSION} "
109
+ buf << "(#{self.class.environment}), "
110
+ buf << "rack/#{Rack::RELEASE} (#{Rack::VERSION.join('.')}) - "
111
+ buf << "ruby/#{RUBY_VERSION} (#{RUBY_RELEASE_DATE}/#{RUBY_PLATFORM})"
112
+ buf
113
+ end
114
+
115
+ end # class Base
116
+
117
+ end # module Webservice
@@ -1,381 +1,297 @@
1
- # encoding: utf-8
2
-
3
-
4
- module Webservice
5
-
6
- ## use (an reuse from Rack) some freezed string constants
7
- ## HTTP verbs
8
- GET = Rack::GET
9
- POST = Rack::POST
10
- PATCH = Rack::PATCH
11
- PUT = Rack::PUT
12
- DELETE = Rack::DELETE
13
- HEAD = Rack::HEAD
14
- OPTIONS = Rack::OPTIONS
15
-
16
- ## HTTP headers
17
- CONTENT_LENGTH = Rack::CONTENT_LENGTH
18
- CONTENT_TYPE = Rack::CONTENT_TYPE
19
- # -- more HTTP headers - not available from Rack
20
- LOCATION = 'Location'.freeze
21
- LAST_MODIFIED = 'Last-Modified'.freeze
22
-
23
-
24
- module Helpers
25
- ## add some more helpers
26
- ## "inspired" by sinatra (mostly) - for staying compatible
27
- ## see https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb
28
-
29
- ## todo -- add status -- why? why not??
30
-
31
- # Halt processing and return the error status provided.
32
- def error( code, body=nil )
33
- response.body = body unless body.nil?
34
- halt code
35
- end
36
-
37
- # Halt processing and return a 404 Not Found.
38
- def not_found( body=nil )
39
- error 404, body
40
- end
41
-
42
-
43
- def redirect_to( uri, status=302 ) ## Note: 302 == Found, 301 == Moved Permanently
44
- halt status, { LOCATION => uri }
45
- end
46
- alias_method :redirect, :redirect_to
47
-
48
-
49
- # Set multiple response headers with Hash.
50
- def headers( hash=nil )
51
- response.headers.merge! hash if hash
52
- response.headers
53
- end
54
-
55
-
56
-
57
- ## (simple) content_type helper - all "hard-coded" for now; always uses utf-8 too
58
- def content_type( type=nil )
59
- return response[ CONTENT_TYPE ] unless type
60
-
61
- if type.to_sym == :json
62
- response[ CONTENT_TYPE ] = 'application/json; charset=utf-8'
63
- elsif type.to_sym == :js || type.to_sym == :javascript
64
- response[ CONTENT_TYPE ] = 'application/javascript; charset=utf-8'
65
- ## use 'text/javascript; charset=utf-8' -- why? why not??
66
- ## note: ietf recommends application/javascript
67
- elsif type.to_sym == :csv || type.to_sym == :text || type.to_sym == :txt
68
- response[ CONTENT_TYPE ] = 'text/plain; charset=utf-8'
69
- elsif type.to_sym == :html || type.to_sym == :htm
70
- response[ CONTENT_TYPE ] = 'text/html; charset=utf-8'
71
- else
72
- ### unknown type; do nothing - sorry; issue warning - why? why not??
73
- end
74
- end ## method content_type
75
-
76
-
77
- ## simple send file (e.g. for images/binary blobs, etc.) helper
78
- def send_file( path )
79
- ## puts "send_file path=>#{path}<"
80
-
81
- ## puts "HTTP_IF_MODIFIED_SINCE:"
82
- ## puts request.get_header('HTTP_IF_MODIFIED_SINCE')
83
-
84
- last_modified = File.mtime(path).httpdate
85
- ## puts "last_modified:"
86
- ## puts last_modified
87
-
88
- ## HTTP 304 => Not Modified
89
- halt 304 if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified
90
-
91
- headers[ LAST_MODIFIED ] = last_modified
92
-
93
- bytes = File.open( path, 'rb' ) { |f| f.read }
94
-
95
- ## puts "encoding:"
96
- ## puts bytes.encoding
97
-
98
- ## puts "size:"
99
- ## puts bytes.size
100
-
101
- extname = File.extname( path )
102
- ## puts "extname:"
103
- ## puts extname
104
-
105
- ## puts "headers (before):"
106
- ## pp headers
107
-
108
- if extname == '.png'
109
- headers[ CONTENT_TYPE ] = 'image/png'
110
- else
111
- ## fallback to application/octet-stream
112
- headers[ CONTENT_TYPE ] = 'application/octet-stream'
113
- end
114
-
115
- headers[ CONTENT_LENGTH ] = bytes.size.to_s ## note: do NOT forget to use to_s (requires string!)
116
-
117
- ## puts "headers (after):"
118
- ## pp headers
119
-
120
- halt 200, bytes
121
- end # method send_file
122
-
123
-
124
-
125
- end ## module Helpers
126
-
127
-
128
- class Base
129
- include Helpers
130
-
131
-
132
- class << self
133
-
134
- def call( env ) ## note self.call(env) lets you use => run Base instead of run Base.new
135
- ## puts "calling #{self.name}.call"
136
- prototype.call( env )
137
- end
138
-
139
- def prototype
140
- ## puts "calling #{self.name}.prototype"
141
- @prototype ||= self.new
142
- ## pp @prototype
143
- ## @prototype
144
- end
145
-
146
-
147
- ## todo/check: all verbs needed! (supported) - why, why not??
148
- ## e.g. add LINK, UNLINK ??
149
-
150
- # Note: for now defining a `GET` handler also automatically defines
151
- # a `HEAD` handler (follows sinatra convention)
152
- def get( pattern, &block )
153
- route( GET, pattern, &block )
154
- route( HEAD, pattern, &block )
155
- end
156
-
157
- def post( pattern, &block) route( POST, pattern, &block ); end
158
- def patch( pattern, &block) route( PATCH, pattern, &block ); end
159
- def put( pattern, &block) route( PUT, pattern, &block ); end
160
- def delete( pattern, &block) route( DELETE, pattern, &block ); end
161
- def head( pattern, &block) route( HEAD, pattern, &block ); end
162
- def options( pattern, &block) route( OPTIONS, pattern, &block ); end
163
-
164
- def route( method, pattern, &block )
165
- puts "[debug] Webservice::Base.#{method.downcase} - add route #{method} '#{pattern}' to #<#{self.name}:#{self.object_id}> : #{self.class.name}"
166
-
167
- ## note: for now use the sintatra-style patterns (with mustermann)
168
- routes[method] << [Mustermann::Sinatra.new(pattern), block]
169
- end
170
-
171
-
172
- def routes
173
- @routes ||= Hash.new { |hash, key| hash[key]=[] }
174
- end
175
-
176
-
177
- ##########################
178
- ## support for "builtin" fallback routes
179
- ##
180
- ## e.g. use like
181
- ## fallback_route GET, '/' do
182
- ## "Hello, World!"
183
- ## end
184
- def fallback_route( method, pattern, &block )
185
- puts "[debug] Webservice::Base.#{method.downcase} - add (fallback) route #{method} '#{pattern}' to #<#{self.name}:#{self.object_id}> : #{self.class.name}"
186
-
187
- ## note: for now use the sintatra-style patterns (with mustermann)
188
- fallback_routes[method] << [Mustermann::Sinatra.new(pattern), block]
189
- end
190
-
191
- def fallback_routes
192
- ## note: !!! use @@ NOT just @ e.g.
193
- ## routes get shared/used by all classes/subclasses
194
- @@fallback_routes ||= Hash.new { |hash, key| hash[key]=[] }
195
- end
196
-
197
-
198
- def environment
199
- ## include APP_ENV why? why not?
200
- ## todo -- cache value? why why not? (see/follow sinatara set machinery ??)
201
- (ENV['APP_ENV'] || ENV['RACK_ENV'] || :development).to_sym
202
- end
203
-
204
- def development?() environment == :development; end
205
- def production?() environment == :production; end
206
- def test?() environment == :test; end
207
-
208
-
209
- ## convenience method
210
- def run!
211
- puts "[debug] Webservice::Base.run! - self = #<#{self.name}:#{self.object_id}> : #{self.class.name}" # note: assumes self is class
212
- app = self ## note: use self; will be derived class (e.g. App and not Base)
213
- port = 4567
214
- Rack::Handler::WEBrick.run( app, Port:port ) do |server|
215
- ## todo: add traps here - why, why not??
216
- end
217
- end
218
-
219
- end ## class << self
220
-
221
-
222
- attr_reader :request
223
- attr_reader :response
224
- attr_reader :params
225
- attr_reader :env
226
-
227
- attr_reader :handler # default response_handler/respond_with handler magic
228
-
229
-
230
- def call( env )
231
- dup.call!( env )
232
- end
233
-
234
- def call!( env )
235
- env['PATH_INFO'] = '/' if env['PATH_INFO'].empty?
236
-
237
- @request = Rack::Request.new( env )
238
- @response = Rack::Response.new
239
- @params = request.params
240
- @env = env
241
-
242
- @handler = ResponseHandler.new( self ) ## for now "hard-coded"; make it a setting later - why? why not?
243
-
244
- ### move cors headers to responseHandler to initialize!!!! - why? why not??
245
- ## (auto-)add (merge in) cors headers
246
- ## todo: move into a before filter ?? lets you overwrite headers - needed - why? why not??
247
- headers 'Access-Control-Allow-Origin' => '*',
248
- 'Access-Control-Allow-Headers' => 'Authorization,Accepts,Content-Type,X-CSRF-Token,X-Requested-With',
249
- 'Access-Control-Allow-Methods' => 'GET,POST,PUT,DELETE,OPTIONS'
250
-
251
- route_eval
252
-
253
- @response.finish
254
- end
255
-
256
-
257
- def halt( *args )
258
- response.status = args.detect{ |arg| arg.is_a?(Fixnum) } || 200
259
- response.header.merge!( args.detect{ |arg| arg.is_a?(Hash) } || {} )
260
- response.body = [args.detect{ |arg| arg.is_a?(String) } || '']
261
- throw :halt, response
262
- end
263
-
264
- private
265
-
266
- def route_eval
267
- puts " [#{self.class.name}] try matching route >#{request.request_method} #{request.path_info}<..."
268
-
269
- catch(:halt) do
270
- ## pass 1
271
- routes = self.class.routes[ request.request_method ]
272
- routes.each do |pattern, block|
273
- ## puts "trying matching route >#{request.path_info}<..."
274
- url_params = pattern.params( request.path_info )
275
- if url_params ## note: params returns nil if no match
276
- ## puts " BINGO! url_params: #{url_params.inspect}"
277
- if !url_params.empty? ## url_params hash NOT empty (e.g. {}) merge with req params
278
- ## todo/fix: check merge order - params overwrites url_params - why? why not??
279
- @params = url_params.merge( @params )
280
- end
281
- handler.handle_response( instance_eval( &block ))
282
- return
283
- end
284
- end
285
-
286
- ## pass 2 - (builtin) fallbacks
287
- routes = self.class.fallback_routes[ request.request_method ]
288
- routes.each do |pattern, block|
289
- url_params = pattern.params( request.path_info )
290
- if url_params ## note: params returns nil if no match
291
- if !url_params.empty? ## url_params hash NOT empty (e.g. {}) merge with req params
292
- @params = url_params.merge( @params )
293
- end
294
- handler.handle_response( instance_eval( &block ))
295
- return
296
- end
297
- end
298
-
299
- # no match found for route/request
300
- halt 404
301
- end
302
- end
303
-
304
-
305
-
306
- ##################################
307
- ### add some fallback (builtin) routes
308
-
309
- fallback_route GET, '/favicon.ico' do
310
- ## use 302 to redirect
311
- ## note: use strg+F5 to refresh page (clear cache for favicon.ico)
312
- redirect_to '/webservice-32x32.png'
313
- end
314
-
315
- fallback_route GET, '/webservice-32x32.png' do
316
- send_file "#{Webservice.root}/assets/webservice-32x32.png"
317
- end
318
-
319
- fallback_route GET, '/routes' do
320
- msg =<<TXT
321
- #{dump_routes}
322
-
323
- #{dump_version}
324
- TXT
325
- end
326
-
327
- ## catch all (404 not found)
328
- fallback_route GET, '/*' do
329
- pp env
330
- pp self.class.routes
331
-
332
- msg =<<TXT
333
- 404 Not Found
334
-
335
- No route matched >#{request.request_method} #{request.path_info}<:
336
-
337
- REQUEST_METHOD: >#{request.request_method}<
338
- PATH_INFO: >#{request.path_info}<
339
- QUERY_STRING: >#{request.query_string}<
340
-
341
- SCRIPT_NAME: >#{request.script_name}<
342
- REQUEST_URI: >#{env['REQUEST_URI']}<
343
-
344
-
345
- #{dump_routes}
346
-
347
- #{dump_version}
348
- TXT
349
-
350
- halt 404, msg
351
- end
352
-
353
- ############################
354
- ## fallback helpers
355
-
356
- def dump_routes ## todo/check - rename to build_routes/show_routes/etc. - why? why not?
357
- buf = ""
358
- buf << " Routes >#{self.class.name}<:\n\n"
359
-
360
- self.class.routes.each do |method,routes|
361
- buf << " #{method}:\n"
362
- routes.each do |pattern,block|
363
- buf << " #{pattern.to_s}\n"
364
- end
365
- end
366
- buf
367
- end
368
-
369
- def dump_version
370
- ## single line version string
371
- buf = " " # note: start with two leading spaces (indent)
372
- buf << "webservice/#{VERSION} "
373
- buf << "(#{self.class.environment}), "
374
- buf << "rack/#{Rack::RELEASE} (#{Rack::VERSION.join('.')}) - "
375
- buf << "ruby/#{RUBY_VERSION} (#{RUBY_RELEASE_DATE}/#{RUBY_PLATFORM})"
376
- buf
377
- end
378
-
379
- end # class Base
380
-
381
- end # module Webservice
1
+ # encoding: utf-8
2
+
3
+
4
+ module Webservice
5
+
6
+ ## use (an reuse from Rack) some freezed string constants
7
+ ## HTTP verbs
8
+ GET = Rack::GET
9
+ POST = Rack::POST
10
+ PATCH = Rack::PATCH
11
+ PUT = Rack::PUT
12
+ DELETE = Rack::DELETE
13
+ HEAD = Rack::HEAD
14
+ OPTIONS = Rack::OPTIONS
15
+
16
+ ## HTTP headers
17
+ CONTENT_LENGTH = Rack::CONTENT_LENGTH
18
+ CONTENT_TYPE = Rack::CONTENT_TYPE
19
+ # -- more HTTP headers - not available from Rack
20
+ LOCATION = 'Location'.freeze
21
+ LAST_MODIFIED = 'Last-Modified'.freeze
22
+
23
+
24
+ module Helpers
25
+ ## add some more helpers
26
+ ## "inspired" by sinatra (mostly) - for staying compatible
27
+ ## see https://github.com/sinatra/sinatra/blob/master/lib/sinatra/base.rb
28
+
29
+ ## todo -- add status -- why? why not??
30
+
31
+ # Halt processing and return the error status provided.
32
+ def error( code, body=nil )
33
+ response.body = body unless body.nil?
34
+ halt code
35
+ end
36
+
37
+ # Halt processing and return a 404 Not Found.
38
+ def not_found( body=nil )
39
+ error 404, body
40
+ end
41
+
42
+
43
+ def redirect( uri, status=302 ) ## Note: 302 == Found, 301 == Moved Permanently
44
+
45
+ ##
46
+ ## todo/fix: add/prepepand SCRIPT_NAME if NOT empty - why? why not??
47
+ ## without SCRIPT_NAME redirect will not work with (non-root) mounted apps
48
+
49
+ halt status, { LOCATION => uri }
50
+ end
51
+ alias_method :redirect_to, :redirect
52
+
53
+
54
+ # Set multiple response headers with Hash.
55
+ def headers( hash=nil )
56
+ response.headers.merge! hash if hash
57
+ response.headers
58
+ end
59
+
60
+
61
+
62
+ ## (simple) content_type helper - all "hard-coded" for now; always uses utf-8 too
63
+ def content_type( type=nil )
64
+ return response[ CONTENT_TYPE ] unless type
65
+
66
+ if type.to_sym == :json
67
+ response[ CONTENT_TYPE ] = 'application/json; charset=utf-8'
68
+ elsif type.to_sym == :js || type.to_sym == :javascript
69
+ response[ CONTENT_TYPE ] = 'application/javascript; charset=utf-8'
70
+ ## use 'text/javascript; charset=utf-8' -- why? why not??
71
+ ## note: ietf recommends application/javascript
72
+ elsif type.to_sym == :csv || type.to_sym == :text || type.to_sym == :txt
73
+ response[ CONTENT_TYPE ] = 'text/plain; charset=utf-8'
74
+ elsif type.to_sym == :html || type.to_sym == :htm
75
+ response[ CONTENT_TYPE ] = 'text/html; charset=utf-8'
76
+ else
77
+ ### unknown type; do nothing - sorry; issue warning - why? why not??
78
+ end
79
+ end ## method content_type
80
+
81
+
82
+ ## simple send file (e.g. for images/binary blobs, etc.) helper
83
+ def send_file( path )
84
+ ## puts "send_file path=>#{path}<"
85
+
86
+ ## puts "HTTP_IF_MODIFIED_SINCE:"
87
+ ## puts request.get_header('HTTP_IF_MODIFIED_SINCE')
88
+
89
+ last_modified = File.mtime(path).httpdate
90
+ ## puts "last_modified:"
91
+ ## puts last_modified
92
+
93
+ ## HTTP 304 => Not Modified
94
+ halt 304 if request.get_header('HTTP_IF_MODIFIED_SINCE') == last_modified
95
+
96
+ headers[ LAST_MODIFIED ] = last_modified
97
+
98
+ bytes = File.open( path, 'rb' ) { |f| f.read }
99
+
100
+ ## puts "encoding:"
101
+ ## puts bytes.encoding
102
+
103
+ ## puts "size:"
104
+ ## puts bytes.size
105
+
106
+ extname = File.extname( path )
107
+ ## puts "extname:"
108
+ ## puts extname
109
+
110
+ ## puts "headers (before):"
111
+ ## pp headers
112
+
113
+ if extname == '.png'
114
+ headers[ CONTENT_TYPE ] = 'image/png'
115
+ else
116
+ ## fallback to application/octet-stream
117
+ headers[ CONTENT_TYPE ] = 'application/octet-stream'
118
+ end
119
+
120
+ headers[ CONTENT_LENGTH ] = bytes.size.to_s ## note: do NOT forget to use to_s (requires string!)
121
+
122
+ ## puts "headers (after):"
123
+ ## pp headers
124
+
125
+ halt 200, bytes
126
+ end # method send_file
127
+
128
+
129
+
130
+ end ## module Helpers
131
+
132
+
133
+ class Metal ## bare bones core (use base for more built-in functionality)
134
+ include Helpers
135
+
136
+
137
+ class << self
138
+
139
+ def call( env ) ## note self.call(env) lets you use => run Base instead of run Base.new
140
+ ## puts "calling #{self.name}.call"
141
+ prototype.call( env )
142
+ end
143
+
144
+ def prototype
145
+ ## puts "calling #{self.name}.prototype"
146
+ @prototype ||= self.new
147
+ ## pp @prototype
148
+ ## @prototype
149
+ end
150
+
151
+
152
+ ## todo/check: all verbs needed! (supported) - why, why not??
153
+ ## e.g. add LINK, UNLINK ??
154
+
155
+ # Note: for now defining a `GET` handler also automatically defines
156
+ # a `HEAD` handler (follows sinatra convention)
157
+ def get( pattern, &block )
158
+ route( GET, pattern, &block )
159
+ route( HEAD, pattern, &block )
160
+ end
161
+
162
+ def post( pattern, &block) route( POST, pattern, &block ); end
163
+ def patch( pattern, &block) route( PATCH, pattern, &block ); end
164
+ def put( pattern, &block) route( PUT, pattern, &block ); end
165
+ def delete( pattern, &block) route( DELETE, pattern, &block ); end
166
+ def head( pattern, &block) route( HEAD, pattern, &block ); end
167
+ def options( pattern, &block) route( OPTIONS, pattern, &block ); end
168
+
169
+ def route( method, pattern, &block )
170
+ puts "[debug] Webservice::Metal.#{method.downcase} - add route #{method} '#{pattern}' to #<#{self.name}:#{self.object_id}> : #{self.class.name}"
171
+
172
+ ## note: for now use (default to) the sintatra-style patterns (with mustermann)
173
+ routes[method] << [Mustermann::Sinatra.new(pattern), block]
174
+ end
175
+
176
+ def routes
177
+ @routes ||= Hash.new { |hash, key| hash[key]=[] }
178
+ end
179
+
180
+ def environment
181
+ ## include APP_ENV why? why not?
182
+ ## todo -- cache value? why why not? (see/follow sinatara set machinery ??)
183
+ (ENV['APP_ENV'] || ENV['RACK_ENV'] || :development).to_sym
184
+ end
185
+
186
+ def development?() environment == :development; end
187
+ def production?() environment == :production; end
188
+ def test?() environment == :test; end
189
+
190
+ ## convenience method
191
+ def run!
192
+ puts "[debug] Webservice::Metal.run! - self = #<#{self.name}:#{self.object_id}> : #{self.class.name}" # note: assumes self is class
193
+ app = self ## note: use self; will be derived class (e.g. App and not Base)
194
+ port = 4567
195
+ Rack::Handler::WEBrick.run( app, Port:port ) do |server|
196
+ ## todo: add traps here - why, why not??
197
+ end
198
+ end
199
+ end ## class << self
200
+
201
+
202
+ attr_reader :request
203
+ attr_reader :response
204
+ attr_reader :params
205
+ attr_reader :env
206
+
207
+
208
+ def call( env )
209
+ dup.call!( env )
210
+ end
211
+
212
+ def call!( env )
213
+ env['PATH_INFO'] = '/' if env['PATH_INFO'].empty?
214
+
215
+ @request = Rack::Request.new( env )
216
+ @response = Rack::Response.new
217
+ @params = request.params
218
+ @env = env
219
+
220
+ catch(:halt) do
221
+ ## call before if defined in derived (sub)classes
222
+ before if respond_to? :before
223
+
224
+ route!
225
+ end
226
+
227
+ @response.finish
228
+ end
229
+
230
+
231
+ def halt( *args )
232
+ response.status = args.detect{ |arg| arg.is_a?(Fixnum) } || 200
233
+ response.header.merge!( args.detect{ |arg| arg.is_a?(Hash) } || {} )
234
+ response.body = [args.detect{ |arg| arg.is_a?(String) } || '']
235
+ throw :halt, response ## todo/check response arg used - what for??
236
+ end
237
+
238
+
239
+ private
240
+
241
+ ## run a route block and throw :halt
242
+ def route_eval( &block )
243
+ obj = instance_eval( &block ) ## return result - for now assumes a single object
244
+
245
+ if respond_to? :handle_response
246
+ handle_response( obj ) ## prepare response
247
+ else
248
+ ## default response; string expected
249
+ ## if string pass it along
250
+ ## if NOT string for debugging / dump to string with inspect
251
+ response.status = 200
252
+ response.body = [obj.is_a?(String) ? obj.to_s : obj.inspect]
253
+ end
254
+
255
+ throw :halt
256
+ end
257
+
258
+
259
+ def route!( base=self.class )
260
+
261
+ puts " [#{base.name}] try matching route >#{request.request_method} #{request.path_info}<..."
262
+
263
+ routes = base.routes[ request.request_method ]
264
+ routes.each do |pattern, block|
265
+ ## puts "trying matching route >#{request.path_info}<..."
266
+ url_params = pattern.params( request.path_info )
267
+ if url_params ## note: params returns nil if no match
268
+ ## puts " BINGO! url_params: #{url_params.inspect}"
269
+ if !url_params.empty? ## url_params hash NOT empty (e.g. {}) merge with req params
270
+ ## todo/fix: check merge order - params overwrites url_params - why? why not??
271
+
272
+ ## todo/fix: check params - params works with string keys only - check for indiffent keys - why? why not?
273
+ ## check rack params - works with indifferent keys by default??
274
+ @params = url_params.merge( @params )
275
+ end
276
+ route_eval( &block )
277
+ ## todo/check: keep return - why? why not? - note: route_eval will always throw :halt
278
+ ## handler.handle_response( instance_eval( &block ))
279
+ ## return
280
+ end
281
+ end
282
+
283
+ ## check recursive - all super(parent)classes too (e.g. App > Base > Metal etc.)
284
+ ## note: superclass is the parent class (returns nil if no more parent class)
285
+ if base.superclass.respond_to? :routes
286
+ route!( base.superclass )
287
+ return
288
+ end
289
+
290
+ # no match found for route/request
291
+ halt 404
292
+ end
293
+
294
+
295
+ end # class Metal
296
+
297
+ end # module Webservice
@@ -4,8 +4,8 @@ module Webservice
4
4
 
5
5
  module Version
6
6
  MAJOR = 0 ## todo: namespace inside version or something - why? why not??
7
- MINOR = 6
8
- PATCH = 1 ## note: if not put in module will overwrite PATCH (HTTP Verb Constant)!!!
7
+ MINOR = 7
8
+ PATCH = 0 ## note: if not put in module will overwrite PATCH (HTTP Verb Constant)!!!
9
9
  end
10
10
 
11
11
  VERSION = [Version::MAJOR,
@@ -2,11 +2,6 @@
2
2
  ## minitest setup
3
3
  require 'minitest/autorun'
4
4
 
5
- ## deps
6
-
7
- ### require 'worlddb'
8
- ## require 'sportdb/models' # note: will include worlddb
9
-
10
5
 
11
6
  ## our own code
12
7
  require 'webservice' ## includes rack
@@ -14,4 +9,3 @@ require 'webservice' ## includes rack
14
9
 
15
10
  ## more libs
16
11
  require 'rack/test'
17
-
@@ -0,0 +1,14 @@
1
+ # encoding: utf-8
2
+
3
+
4
+ ## gets evaluated in class context (self is class) -- uses class_eval
5
+ puts "[debug] eval (top) self = #<#{self.name}:#{self.object_id}> : #{self.class.name}"
6
+
7
+ get '/hello' do
8
+
9
+ ## gets evaluated in object context (self is object) -- uses instance_eval
10
+ puts "[debug] eval (get /hello) self = #<#{self.class.name}:#{self.class.object_id}> : #{self.class.class.name}"
11
+
12
+ data = { text: 'hello' }
13
+ data
14
+ end
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+
3
+ ###
4
+ # to run use
5
+ # ruby -I ./lib -I ./test test/test_metal.rb
6
+
7
+
8
+ require 'helper'
9
+
10
+
11
+ class MetalApp < Webservice::Metal
12
+
13
+ get '/hello' do
14
+ 'Hello, World!'
15
+ end
16
+
17
+ end # class MetalApp
18
+
19
+
20
+
21
+ class TestMetal < MiniTest::Test
22
+
23
+ include Rack::Test::Methods
24
+
25
+ def app
26
+ ## return (rack-ready) app object
27
+ @@app ||= begin
28
+ app_class = MetalApp
29
+ pp app_class.routes
30
+ app_class
31
+ end
32
+ end
33
+
34
+
35
+ def test_get
36
+ get '/hello'
37
+ assert last_response.ok?
38
+
39
+ get '/'
40
+ assert_equal 404, last_response.status
41
+ end
42
+
43
+ end # class TestMetal
@@ -12,12 +12,16 @@ class TestMustermann < MiniTest::Test
12
12
 
13
13
  def test_hello
14
14
 
15
- pp Rack::VERSION
15
+ pp Rack::VERSION # e.g. [1,2]
16
+ pp Rack::VERSION.join('.') # e.g. 1.2
17
+ pp Rack::RELEASE
16
18
  pp Mustermann::VERSION
17
19
 
20
+
18
21
  pattern = Mustermann::Sinatra.new( '/:name' )
19
22
  pp pattern
20
23
  pp pattern.names
24
+ pp pattern.to_s # prints passed in pattern (as "plain" string as is)
21
25
 
22
26
  m = pattern.match( "/test" )
23
27
  pp m
@@ -2,13 +2,13 @@
2
2
 
3
3
  ###
4
4
  # to run use
5
- # ruby -I ./lib -I ./test test/test_app.rb
5
+ # ruby -I ./lib -I ./test test/test_service_app.rb
6
6
 
7
7
 
8
8
  require 'helper'
9
9
 
10
10
 
11
- class TestApp < MiniTest::Test
11
+ class TestServiceApp < MiniTest::Test
12
12
 
13
13
  include Rack::Test::Methods
14
14
 
@@ -17,7 +17,7 @@ class TestApp < MiniTest::Test
17
17
  @@app ||= begin
18
18
  app_class = Webservice.load_file( "#{Webservice.root}/test/service/app.rb" )
19
19
  pp app_class.routes
20
- app_class.new
20
+ app_class
21
21
  end
22
22
  end
23
23
 
@@ -113,4 +113,4 @@ HTML
113
113
  assert_equal "Error fatal", last_response.body
114
114
  end
115
115
 
116
- end # class TestApp
116
+ end # class TestServiceApp
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+
3
+ ###
4
+ # to run use
5
+ # ruby -I ./lib -I ./test test/test_service_debug.rb
6
+
7
+
8
+ require 'helper'
9
+
10
+
11
+ class TestServiceDebug < MiniTest::Test
12
+
13
+ include Rack::Test::Methods
14
+
15
+ def app
16
+ ## return (rack-ready) app object
17
+ @@app ||= begin
18
+ app_class = Webservice.load_file( "#{Webservice.root}/test/service/debug.rb" )
19
+ pp app_class.routes
20
+ app_class
21
+ end
22
+ end
23
+
24
+ def test_get
25
+ get '/hello'
26
+ assert last_response.ok?
27
+
28
+ get '/'
29
+ assert_equal 404, last_response.status
30
+ end
31
+
32
+ end # class TestServiceDebug
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: webservice
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gerald Bauer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-08-30 00:00:00.000000000 Z
11
+ date: 2017-09-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logutils
@@ -95,15 +95,17 @@ files:
95
95
  - Rakefile
96
96
  - assets/webservice-32x32.png
97
97
  - lib/webservice.rb
98
- - lib/webservice/base.rb
99
- - lib/webservice/response_handler.rb
98
+ - lib/webservice/base/base.rb
99
+ - lib/webservice/base/response_handler.rb
100
+ - lib/webservice/metal.rb
100
101
  - lib/webservice/version.rb
101
102
  - test/helper.rb
102
103
  - test/service/app.rb
103
- - test/test_app.rb
104
+ - test/service/debug.rb
105
+ - test/test_metal.rb
104
106
  - test/test_mustermann.rb
105
- - test/test_samples.rb
106
- - test/test_samples_debug.rb
107
+ - test/test_service_app.rb
108
+ - test/test_service_debug.rb
107
109
  homepage: https://github.com/rubylibs/webservice
108
110
  licenses:
109
111
  - Public Domain
@@ -1,34 +0,0 @@
1
- # encoding: utf-8
2
-
3
- ###
4
- # to run use
5
- # ruby -I ./lib -I ./test test/test_samples.rb
6
-
7
-
8
- require 'helper'
9
-
10
-
11
- class TestSamples < MiniTest::Test
12
-
13
- def test_beer
14
- app_class = Webservice.load_file( "#{Webservice.root}/samples/beer.rb" )
15
- pp app_class.routes
16
-
17
- assert true # if we get here - test success
18
- end
19
-
20
- def test_football
21
- app_class = Webservice.load_file( "#{Webservice.root}/samples/football.rb" )
22
- pp app_class.routes
23
-
24
- assert true # if we get here - test success
25
- end
26
-
27
- def test_world
28
- app_class = Webservice.load_file( "#{Webservice.root}/samples/world.rb" )
29
- pp app_class.routes
30
-
31
- assert true # if we get here - test success
32
- end
33
-
34
- end # class TestSamples
@@ -1,20 +0,0 @@
1
- # encoding: utf-8
2
-
3
- ###
4
- # to run use
5
- # ruby -I ./lib -I ./test test/test_samples_debug.rb
6
-
7
-
8
- require 'helper'
9
-
10
-
11
- class TestSamplesDebug < MiniTest::Test
12
-
13
- def test_debug
14
- app_class = Webservice.load_file( "#{Webservice.root}/samples/debug.rb" )
15
- pp app_class.routes
16
-
17
- assert true # if we get here - test success
18
- end
19
-
20
- end # class TestSamplesDebug