webservice 0.6.1 → 0.7.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: 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