blix-rest 0.1.30 → 0.7.2

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.
@@ -4,7 +4,7 @@
4
4
  class RestWorld
5
5
  # the entry point to the rack application to be tested
6
6
  def self.app
7
- @_app ||= Rack::Builder.parse_file('config.ru').first
7
+ @_app ||= Rack::Builder.parse_file('config.ru')
8
8
  end
9
9
 
10
10
  # a dummy request to sent to the server
@@ -16,15 +16,16 @@ class RestWorld
16
16
  class Response
17
17
  def initialize(resp)
18
18
  @resp = resp
19
- if @resp.header['Content-Type'] == 'application/json'
19
+ content_type = @resp.headers['Content-Type'] || @resp.headers['content-type']
20
+ if content_type == 'application/json'
20
21
  begin
21
- @h = MultiJson.load(@resp.body) || {}
22
+ @h = MultiJson.load(body) || {}
22
23
  rescue Exception => e
23
- puts 'INVALID RESPONSE BODY=>' + @resp.body
24
+ log 'INVALID RESPONSE BODY=>' + body
24
25
  raise
25
26
  end
26
27
  else
27
- @h = { 'html' => @resp.body }
28
+ @h = { 'html' => body }
28
29
  end
29
30
 
30
31
  # get_ids_from_hash
@@ -35,7 +36,7 @@ class RestWorld
35
36
  end
36
37
 
37
38
  def body
38
- @resp.body
39
+ [@resp.body].flatten.join('')
39
40
  end
40
41
 
41
42
  def data
@@ -51,7 +52,7 @@ class RestWorld
51
52
  end
52
53
 
53
54
  def header
54
- @resp.header || {}
55
+ @resp.headers || {}
55
56
  end
56
57
 
57
58
  def content_type
@@ -93,10 +94,10 @@ class RestWorld
93
94
  end
94
95
 
95
96
  def explain
96
- puts "request ==> #{@_verb} #{@_request}"
97
- puts "cookies ==> #{cookies.join('; ')}" if cookies.length > 0
98
- puts "body ==> #{@_body}" if @_body
99
- puts "response ==> #{@_response.inspect}"
97
+ log "request ==> #{@_verb} #{@_request}"
98
+ log "cookies ==> #{cookies.join('; ')}" if cookies.length > 0
99
+ log "body ==> #{@_body}" if @_body
100
+ log "response ==> #{@_response.inspect}"
100
101
  end
101
102
 
102
103
  def before_parse_path(path); end
@@ -217,7 +218,7 @@ class RestWorld
217
218
  @_response = Response.new(raw_response)
218
219
  # add cookies to the cookie jar.
219
220
  #unless @_current_user=="guest"
220
- if cookie = @_response.header["Set-Cookie"]
221
+ if cookie = @_response.header["Set-Cookie"] || @_response.header["set-cookie"]
221
222
  parts = cookie.split(';')
222
223
  cookies << parts[0].strip
223
224
  end
@@ -72,16 +72,16 @@ module Blix::Rest
72
72
  def format_response(value, response)
73
73
  if value.is_a?(RawJsonString)
74
74
  response.content = if _options[:nodata]
75
- value.to_s
75
+ [value.to_s]
76
76
  else
77
- "{\"data\":#{value}}"
77
+ ["{\"data\":#{value}}"]
78
78
  end
79
79
  else
80
80
  begin
81
81
  response.content = if _options[:nodata]
82
- MultiJson.dump(value)
82
+ [MultiJson.dump(value)]
83
83
  else
84
- MultiJson.dump('data' => value)
84
+ [MultiJson.dump('data' => value)]
85
85
  end
86
86
  rescue Exception => e
87
87
  ::Blix::Rest.logger << e.to_s
@@ -107,7 +107,8 @@ module Blix::Rest
107
107
  end
108
108
 
109
109
  def format_response(value, response)
110
- response.content = value.to_s
110
+ value = [value.to_s] unless value.respond_to?(:each) || value.respond_to?(:call)
111
+ response.content = value
111
112
  end
112
113
 
113
114
  end
@@ -121,8 +122,8 @@ module Blix::Rest
121
122
 
122
123
  def set_default_headers(headers)
123
124
  headers[CACHE_CONTROL] = CACHE_NO_STORE
124
- headers[PRAGMA] = NO_CACHE
125
- headers[CONTENT_TYPE] = CONTENT_TYPE_XML
125
+ headers[PRAGMA] = NO_CACHE
126
+ headers[CONTENT_TYPE] = CONTENT_TYPE_XML
126
127
  end
127
128
 
128
129
  def format_error(message)
@@ -130,7 +131,7 @@ module Blix::Rest
130
131
  end
131
132
 
132
133
  def format_response(value, response)
133
- response.content = value.to_s # FIXME
134
+ response.content = [value.to_s]
134
135
  end
135
136
 
136
137
  end
@@ -144,8 +145,14 @@ module Blix::Rest
144
145
 
145
146
  def set_default_headers(headers)
146
147
  headers[CACHE_CONTROL] = CACHE_NO_STORE
147
- headers[PRAGMA] = NO_CACHE
148
- headers[CONTENT_TYPE] = CONTENT_TYPE_HTML
148
+ headers[PRAGMA] = NO_CACHE
149
+ headers[CONTENT_TYPE] = CONTENT_TYPE_HTML
150
+ # headers['X-Frame-Options'] = 'SAMEORIGIN'
151
+ # headers['X-XSS-Protection'] = '1; mode=block'
152
+ # headers['X-Content-Type-Options'] = 'nosniff'
153
+ # headers['X-Download-Options'] = 'noopen'
154
+ # headers['X-Permitted-Cross-Domain-Policies'] = 'none'
155
+ # headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
149
156
  end
150
157
 
151
158
  def format_error(message)
@@ -160,7 +167,7 @@ module Blix::Rest
160
167
  end
161
168
 
162
169
  def format_response(value, response)
163
- response.content = value.to_s
170
+ response.content = [value.to_s]
164
171
  end
165
172
 
166
173
  end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis'
4
+ require_relative 'cache'
5
+ require_relative 'string_hash'
6
+ #
7
+ # options:
8
+ # :expire_secs - how long store should save data.
9
+ # :reset_expire_on_get - start the expire timer again on read.
10
+
11
+ module Blix
12
+ module Rest
13
+ class RedisCache < Cache
14
+
15
+ STORE_PREFIX = 'blixcache'
16
+
17
+ #---------------------------------------------------------------------------
18
+
19
+ # clear all data from the cache
20
+ def clear
21
+ reset
22
+ end
23
+
24
+ # retrieve data from the cache
25
+ def get(id)
26
+ key = _key(id)
27
+ str = redis.get(key)
28
+ data = str && begin
29
+ _decode(str)
30
+ rescue StandardError
31
+ redis.del( key)
32
+ nil
33
+ end
34
+ redis.expire(key, _opts[:expire_secs]) if data && _opts[:reset_expire_on_get] && _opts.key?(:expire_secs)
35
+ data
36
+ end
37
+
38
+ # set data in the cache
39
+ def set(id, data)
40
+ params = {}
41
+ params[:ex] = _opts[:expire_secs] if _opts.key?(:expire_secs)
42
+ redis.set(_key(id), _encode(data), **params)
43
+ data
44
+ end
45
+
46
+ # is key present in the cache
47
+ def key?(id)
48
+ redis.get(_key(id)) != nil
49
+ end
50
+
51
+ def delete(id)
52
+ redis.del(_key(id)) > 0
53
+ end
54
+
55
+ #---------------------------------------------------------------------------
56
+
57
+ def _key(name)
58
+ _prefix + name
59
+ end
60
+
61
+ def _opts
62
+ @_opts ||= begin
63
+ o = ::Blix::Rest::StringHash.new
64
+ o[:prefix] = STORE_PREFIX
65
+ o.merge!(params)
66
+ o
67
+ end
68
+ end
69
+
70
+ def _prefix
71
+ @_prefix ||= _opts[:prefix]
72
+ end
73
+
74
+ # remove all sessions from the store
75
+ def reset(name = nil)
76
+ keys = _all_keys(name)
77
+ redis.del(*keys) unless keys.empty?
78
+ end
79
+
80
+ def _all_keys(name = nil)
81
+ redis.keys("#{_prefix}#{name}*") || []
82
+ end
83
+
84
+ # the redis session store
85
+ def redis
86
+ @redis ||= begin
87
+ r = Redis.new
88
+ begin
89
+ r.ping
90
+ rescue Exception => e
91
+ Blix::Rest.logger.error "cannot reach redis server:#{e}"
92
+ raise
93
+ end
94
+ r
95
+ end
96
+ end
97
+
98
+ # the number of sessions in the store
99
+ def length
100
+ _all_keys.length
101
+ end
102
+
103
+ # delete expired sessions from the store. this should be handled
104
+ # automatically by redis if the ttl is set on save correctly
105
+ def cleanup(opts = nil); end
106
+
107
+ def _encode(data)
108
+ Marshal.dump(data)
109
+ end
110
+
111
+ def _decode(msg)
112
+ Marshal.load(msg)
113
+ end
114
+
115
+ def get_expiry_time
116
+ if expire = _opts[:expire_secs] || _opts['expire_secs']
117
+ Time.now - expire
118
+ end
119
+ end
120
+
121
+ def get_expiry_secs
122
+ _opts[:expire_secs] || _opts['expire_secs']
123
+ end
124
+
125
+ end
126
+ end
127
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'thread'
2
3
 
3
4
  module Blix::Rest
4
5
  class RequestMapperError < StandardError; end
@@ -7,6 +8,8 @@ module Blix::Rest
7
8
  # these routes and return an associated block and parameters.
8
9
  class RequestMapper
9
10
 
11
+ @@mutex = Mutex.new
12
+
10
13
  WILD_PLACEHOLDER = '/'
11
14
  PATH_SEP = '/'
12
15
  STAR_PLACEHOLDER = '*'
@@ -52,6 +55,7 @@ module Blix::Rest
52
55
 
53
56
  class << self
54
57
 
58
+ # the root always starts with '/' and finishes with '/'
55
59
  def set_path_root(root)
56
60
  root = root.to_s
57
61
  root = '/' + root if root[0, 1] != '/'
@@ -64,19 +68,30 @@ module Blix::Rest
64
68
  @path_root || '/'
65
69
  end
66
70
 
67
- attr_reader :path_root_length
71
+ def path_root_length
72
+ @path_root_length.to_i
73
+ end
68
74
 
69
75
  def full_path(path)
70
76
  path = path[1..-1] if path[0, 1] == '/'
71
77
  path_root + path
72
78
  end
73
79
 
80
+ # ensure that absolute path is the full path
81
+ def ensure_full_path(path)
82
+ if path[0, 1] == '/' && (path_root_length>0) && path[0, path_root_length] != path_root[0, path_root_length]
83
+ path = path_root + path[1..-1]
84
+ end
85
+ path
86
+ end
87
+
74
88
  def locations
75
89
  @locations ||= Hash.new { |h, k| h[k] = [] }
76
90
  end
77
91
 
78
92
  def table
79
- @table ||= compile
93
+ # compile the table in one thread only.
94
+ @table ||= @@mutex.synchronize{@table ||= compile}
80
95
  end
81
96
 
82
97
  def dump
@@ -147,6 +162,7 @@ module Blix::Rest
147
162
  path = path[1..-1] if path[0, 1] == PATH_SEP
148
163
  RequestMapper.locations[verb] << [verb, path, opts, blk]
149
164
  @table = nil # force recompile
165
+ true
150
166
  end
151
167
 
152
168
  # match a given path to declared route.
@@ -212,12 +228,12 @@ module Blix::Rest
212
228
  # .. if there is a wildpath foloowing then fine ..
213
229
  # .. otherwise an error !
214
230
 
215
- if idx == limit # the last section of request
231
+ if idx == limit # the last section of path
216
232
  if current.blk
217
233
  return [current.blk, parameters, current.opts]
218
234
  elsif (havewild = current[STAR_PLACEHOLDER])
219
235
  parameters[havewild.parameter.to_s] = '/'
220
- return [havewild.blk, parameters, havewild.opts]
236
+ return [havewild.blk, parameters, havewild.opts, true]
221
237
  else
222
238
  return [nil, {}, nil]
223
239
  end
@@ -250,7 +266,7 @@ module Blix::Rest
250
266
  return [current.blk, parameters, current.opts]
251
267
  elsif (havewild = current[STAR_PLACEHOLDER])
252
268
  parameters[havewild.parameter.to_s] = '/'
253
- return [havewild.blk, parameters, havewild.opts]
269
+ return [havewild.blk, parameters, havewild.opts, true]
254
270
  else
255
271
  return [nil, {}, nil]
256
272
  end
@@ -268,7 +284,7 @@ module Blix::Rest
268
284
  parameters['format'] = wildformat[1..-1].to_sym
269
285
  end
270
286
  parameters[current.parameter.to_s] = wildpath
271
- return [current.blk, parameters, current.opts]
287
+ return [current.blk, parameters, current.opts, true]
272
288
  else
273
289
  return [nil, {}, nil]
274
290
  end
@@ -12,13 +12,13 @@ module Blix::Rest
12
12
 
13
13
  def initialize
14
14
  @status = 200
15
- @headers = {}
15
+ @headers = Rack::Headers.new
16
16
  @content = nil
17
17
  end
18
18
 
19
19
  def set(status,content=nil,headers=nil)
20
20
  @status = status if status
21
- @content = String.new(content) if content
21
+ @content = [String.new(content)] if content
22
22
  @headers.merge!(headers) if headers
23
23
  end
24
24
 
@@ -17,8 +17,23 @@ module Blix::Rest
17
17
  @_options = opts
18
18
  end
19
19
 
20
+ # options passed to the server
21
+ def _options
22
+ @_options
23
+ end
24
+
25
+
26
+ # the object serving as a cache
20
27
  def _cache
21
- @_cache ||= {}
28
+ @_cache ||= begin
29
+ obj = _options[:cache] || _options['cache']
30
+ if obj
31
+ raise "cache must be a subclass of Blix::Rest::Cache" unless obj.is_a?(Cache)
32
+ obj
33
+ else
34
+ MemoryCache.new
35
+ end
36
+ end
22
37
  end
23
38
 
24
39
  def extract_parsers_from_options(opts)
@@ -34,7 +49,6 @@ module Blix::Rest
34
49
  def set_custom_headers(format, headers)
35
50
  parser = get_parser(format)
36
51
  raise "parser not found for custom headers format=>#{format}" unless parser
37
-
38
52
  parser.__custom_headers = headers
39
53
  end
40
54
 
@@ -54,6 +68,7 @@ module Blix::Rest
54
68
  parser._types.each { |t| @_mime_types[t.downcase] = parser } # register each of the mime types
55
69
  end
56
70
 
71
+ # retrieve parameters from the http request
57
72
  def retrieve_params(env)
58
73
  post_params = {}
59
74
  body = ''
@@ -104,6 +119,7 @@ module Blix::Rest
104
119
  end
105
120
  end
106
121
 
122
+ # determine standard format from http mime type
107
123
  def get_format_from_mime(mime)
108
124
  case mime
109
125
  when 'application/json' then :json
@@ -138,20 +154,24 @@ module Blix::Rest
138
154
  parser
139
155
  end
140
156
 
157
+ # the call function is the interface with the rack framework
141
158
  def call(env)
142
159
  req = Rack::Request.new(env)
143
160
 
144
- verb = env['REQUEST_METHOD']
145
- path = req.path
161
+ verb = env['REQUEST_METHOD']
162
+ path = req.path
163
+ path = CGI.unescape(path).gsub('+',' ') unless _options[:unescape] == false
146
164
 
147
- blk, path_params, options = RequestMapper.match(verb, path)
165
+ blk, path_params, options, is_wild = RequestMapper.match(verb, path)
166
+
167
+ match_all = RequestMapper.match('ALL', path) unless blk && !is_wild
168
+ blk, path_params, options = match_all if match_all && match_all[0] # override
148
169
 
149
- blk, path_params, options = RequestMapper.match('ALL', path) unless blk
150
170
 
151
171
  default_format = options && options[:default] && options[:default].to_sym
152
172
  force_format = options && options[:force] && options[:force].to_sym
153
- do_cache = options && options[:cache]
154
- clear_cache = options && options[:cache_reset]
173
+ do_cache = options && options[:cache] && !Blix::Rest.cache_disabled
174
+ clear_cache = options && options[:cache_reset]
155
175
 
156
176
  query_format = options && options[:query] && req.GET['format'] && req.GET['format'].to_sym
157
177
 
@@ -159,39 +179,49 @@ module Blix::Rest
159
179
 
160
180
  parser = get_parser(force_format || format)
161
181
 
162
- return [406, {}, ["Invalid Format: #{format}"]] unless parser
182
+ unless parser
183
+ if blk
184
+ return [406, {}, ["Invalid Format: #{format}"]]
185
+ else
186
+ return [404, {}, ["Invalid Url"]]
187
+ end
188
+ end
163
189
 
164
190
  parser._options = options
165
191
 
166
192
  # check for cached response end return with cached response if found.
167
193
  #
168
- if do_cache && _cache["#{verb}|#{format}|#{path}"]
169
- response = _cache["#{verb}|#{format}|#{path}"]
170
- return [response.status, response.headers.merge('X-Blix-Cache' => 'cached'), [response.content]]
194
+ if do_cache && ( response = _cache["#{verb}|#{format}|#{path}"] )
195
+ return [response.status, response.headers.merge('x-blix-cache' => 'cached'), response.content]
171
196
  end
172
197
 
173
198
  response = Response.new
174
199
 
175
- if parser.__custom_headers
176
- response.headers.merge! parser.__custom_headers
177
- else
178
- parser.set_default_headers(response.headers)
179
- end
180
-
181
200
  if blk
182
201
 
183
202
  begin
184
203
  params = env['params']
185
- value = blk.call(path_params, params, req, format, response, @_options)
204
+ context = Context.new(path_params, params, req, format, response, verb, self)
205
+ value = blk.call(context)
186
206
  rescue ServiceError => e
207
+ set_default_headers(parser,response)
187
208
  response.set(e.status, parser.format_error(e.message), e.headers)
209
+ rescue RawResponse => e
210
+ value = e.content
211
+ value = [value.to_s] unless value.respond_to?(:each) || value.respond_to?(:call)
212
+ response.status = e.status if e.status
213
+ response.content = value
214
+ response.headers.merge!(e.headers) if e.headers
188
215
  rescue AuthorizationError => e
216
+ set_default_headers(parser,response)
189
217
  response.set(401, parser.format_error(e.message), AUTH_HEADER => "#{e.type} realm=\"#{e.realm}\", charset=\"UTF-8\"")
190
218
  rescue Exception => e
219
+ set_default_headers(parser,response)
191
220
  response.set(500, parser.format_error('internal error'))
192
221
  ::Blix::Rest.logger << "----------------------------\n#{$!}\n----------------------------"
193
222
  ::Blix::Rest.logger << "----------------------------\n#{$@}\n----------------------------"
194
223
  else # no error
224
+ set_default_headers(parser,response)
195
225
  parser.format_response(value, response)
196
226
  # cache response if requested
197
227
  _cache.clear if clear_cache
@@ -199,9 +229,19 @@ module Blix::Rest
199
229
  end
200
230
 
201
231
  else
232
+ set_default_headers(parser,response)
202
233
  response.set(404, parser.format_error('Invalid Url'))
203
234
  end
204
- [response.status, response.headers, [response.content]]
235
+
236
+ [response.status.to_i, response.headers, response.content]
237
+ end
238
+
239
+ def set_default_headers(parser,response)
240
+ if parser.__custom_headers
241
+ response.headers.merge! parser.__custom_headers
242
+ else
243
+ parser.set_default_headers(response.headers)
244
+ end
205
245
  end
206
246
 
207
247
  end
@@ -1,5 +1,5 @@
1
1
  module Blix
2
2
  module Rest
3
- VERSION = "0.1.30"
3
+ VERSION = "0.7.2"
4
4
  end
5
5
  end
data/lib/blix/rest.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'base64'
2
2
  require 'logger'
3
3
  require 'time'
4
+ require 'cgi'
5
+ require 'rack'
4
6
 
5
7
  module Blix
6
8
  module Rest
@@ -8,14 +10,14 @@ module Blix
8
10
  # EXPIRED_TOKEN_MESSAGE = 'token expired'
9
11
  # INVALID_TOKEN_MESSAGE = 'invalid token'
10
12
 
11
- CONTENT_TYPE = 'Content-Type'.freeze
13
+ CONTENT_TYPE = 'content-type'.freeze
12
14
  CONTENT_TYPE_JSON = 'application/json'.freeze
13
15
  CONTENT_TYPE_HTML = 'text/html; charset=utf-8'.freeze
14
16
  CONTENT_TYPE_XML = 'application/xml'.freeze
15
- AUTH_HEADER = 'WWW-Authenticate'.freeze
16
- CACHE_CONTROL = 'Cache-Control'.freeze
17
+ AUTH_HEADER = 'www-authenticate'.freeze
18
+ CACHE_CONTROL = 'cache-control'.freeze
17
19
  CACHE_NO_STORE = 'no-store'.freeze
18
- PRAGMA = 'Pragma'.freeze
20
+ PRAGMA = 'pragma'.freeze
19
21
  NO_CACHE = 'no-cache'.freeze
20
22
  URL_ENCODED = %r{^application/x-www-form-urlencoded}.freeze
21
23
  JSON_ENCODED = %r{^application/json}.freeze # NOTE: "text/json" and "text/javascript" are deprecated forms
@@ -43,6 +45,14 @@ module Blix
43
45
  @_logger = val
44
46
  end
45
47
 
48
+ def self.cache_disabled
49
+ @_cache_disabled
50
+ end
51
+
52
+ def self.disable_cache
53
+ @_cache_disabled = true
54
+ end
55
+
46
56
  def self.logger
47
57
  @_logger ||= begin
48
58
  l = Logger.new(STDOUT)
@@ -67,13 +77,10 @@ module Blix
67
77
  end
68
78
 
69
79
  # interpret payload string as json
70
- class RawJsonString
71
- def initialize(str)
72
- @str = str
73
- end
80
+ class RawJsonString < String
74
81
 
75
82
  def as_json(*_a)
76
- @str
83
+ self
77
84
  end
78
85
 
79
86
  def to_json(*a)
@@ -94,6 +101,19 @@ module Blix
94
101
  end
95
102
  end
96
103
 
104
+ class RawResponse < StandardError
105
+ attr_reader :status
106
+ attr_reader :headers
107
+ attr_reader :content
108
+
109
+ def initialize(message, status = nil, headers = nil)
110
+ super(message || "")
111
+ @status = status
112
+ @headers = headers
113
+ @content = message
114
+ end
115
+ end
116
+
97
117
  class AuthorizationError < StandardError
98
118
  attr_reader :realm, :type
99
119
  def initialize(message=nil, realm=nil, type=nil)
@@ -128,6 +148,7 @@ require 'rack'
128
148
  require 'blix/rest/response'
129
149
  require 'blix/rest/format_parser'
130
150
  require 'blix/rest/request_mapper'
151
+ require 'blix/rest/cache'
131
152
  require 'blix/rest/server'
132
153
  # require 'blix/rest/provider'
133
154
  require 'blix/rest/controller'
@@ -36,7 +36,7 @@ module Blix
36
36
  def store_data(id, data)
37
37
  params = {}
38
38
  params[:ex] = _opts[:expire_secs] if _opts.key?(:expire_secs)
39
- redis.set(_key(id), data, params)
39
+ redis.set(_key(id), data, **params)
40
40
  data
41
41
  end
42
42
 
@@ -89,7 +89,7 @@ module Blix
89
89
  params[:ex] = _opts[:expire_secs] if _opts.key?(:expire_secs)
90
90
  hash ||= {}
91
91
  hash['_last_access'] = Time.now
92
- redis.set(_key(id), _encode(hash), params)
92
+ redis.set(_key(id), _encode(hash), **params)
93
93
  hash
94
94
  end
95
95