blix-rest 0.1.30
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 +7 -0
- data/LICENSE +25 -0
- data/README.md +457 -0
- data/lib/blix/rest.rb +145 -0
- data/lib/blix/rest/controller.rb +512 -0
- data/lib/blix/rest/cucumber.rb +8 -0
- data/lib/blix/rest/cucumber/hooks.rb +5 -0
- data/lib/blix/rest/cucumber/request_steps.rb +207 -0
- data/lib/blix/rest/cucumber/resource_steps.rb +28 -0
- data/lib/blix/rest/cucumber/world.rb +273 -0
- data/lib/blix/rest/format.rb +15 -0
- data/lib/blix/rest/format_parser.rb +167 -0
- data/lib/blix/rest/handlebars_assets_fix.rb +10 -0
- data/lib/blix/rest/request_mapper.rb +332 -0
- data/lib/blix/rest/resource_cache.rb +50 -0
- data/lib/blix/rest/response.rb +26 -0
- data/lib/blix/rest/server.rb +208 -0
- data/lib/blix/rest/string_hash.rb +100 -0
- data/lib/blix/rest/version.rb +5 -0
- data/lib/blix/utils.rb +2 -0
- data/lib/blix/utils/misc.rb +62 -0
- data/lib/blix/utils/redis_store.rb +173 -0
- data/lib/blix/utils/yaml_config.rb +74 -0
- metadata +126 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
# the format of a request is determined by
|
2
|
+
# - the uri extension eg: .html
|
3
|
+
# - the mime type of the request ( only for html, xml, json )
|
4
|
+
# - in some cases a query parameter.
|
5
|
+
|
6
|
+
# this file is for looking into the idea of mapping
|
7
|
+
# multiple extensions and multiple mime types onto one
|
8
|
+
# format type.
|
9
|
+
|
10
|
+
# for now it does not seem neccessary.
|
11
|
+
|
12
|
+
module Blix::Rest
|
13
|
+
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Blix::Rest
|
4
|
+
# this is the base class for all the format parsers
|
5
|
+
class FormatParser
|
6
|
+
|
7
|
+
attr_accessor :__custom_headers
|
8
|
+
|
9
|
+
def set_default_headers(headers)
|
10
|
+
# headers[CACHE_CONTROL]= CACHE_NO_STORE
|
11
|
+
# headers[PRAGMA] = NO_CACHE
|
12
|
+
# headers[CONTENT_TYPE] = CONTENT_TYPE_JSON
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :_format
|
16
|
+
|
17
|
+
attr_writer :_options
|
18
|
+
|
19
|
+
def _options
|
20
|
+
@_options || {}
|
21
|
+
end
|
22
|
+
|
23
|
+
def _format=(val)
|
24
|
+
@_format = val.to_s.downcase
|
25
|
+
end
|
26
|
+
|
27
|
+
def self._types
|
28
|
+
@_types || []
|
29
|
+
end
|
30
|
+
|
31
|
+
def _types
|
32
|
+
self.class._types
|
33
|
+
end
|
34
|
+
|
35
|
+
# the accept header types that correspont to this parser.
|
36
|
+
def self.accept_types(types)
|
37
|
+
types = [types].flatten
|
38
|
+
@_types = types
|
39
|
+
end
|
40
|
+
|
41
|
+
# construct the body of an error messsage.
|
42
|
+
def format_error(message)
|
43
|
+
message.to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
# set the response content / headers / status
|
47
|
+
# headers are the default headers if not set
|
48
|
+
# status is 200 if not set
|
49
|
+
def format_response(value, response)
|
50
|
+
response.content = value.to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
#-----------------------------------------------------------------------------
|
56
|
+
# the default json format parser
|
57
|
+
#
|
58
|
+
class JsonFormatParser < FormatParser
|
59
|
+
|
60
|
+
accept_types CONTENT_TYPE_JSON
|
61
|
+
|
62
|
+
def set_default_headers(headers)
|
63
|
+
headers[CACHE_CONTROL] = CACHE_NO_STORE
|
64
|
+
headers[PRAGMA] = NO_CACHE
|
65
|
+
headers[CONTENT_TYPE] = CONTENT_TYPE_JSON
|
66
|
+
end
|
67
|
+
|
68
|
+
def format_error(message)
|
69
|
+
MultiJson.dump({"error"=>message.to_s}) rescue "{\"error\":\"Internal Formatting Error\"}"
|
70
|
+
end
|
71
|
+
|
72
|
+
def format_response(value, response)
|
73
|
+
if value.is_a?(RawJsonString)
|
74
|
+
response.content = if _options[:nodata]
|
75
|
+
value.to_s
|
76
|
+
else
|
77
|
+
"{\"data\":#{value}}"
|
78
|
+
end
|
79
|
+
else
|
80
|
+
begin
|
81
|
+
response.content = if _options[:nodata]
|
82
|
+
MultiJson.dump(value)
|
83
|
+
else
|
84
|
+
MultiJson.dump('data' => value)
|
85
|
+
end
|
86
|
+
rescue Exception => e
|
87
|
+
::Blix::Rest.logger << e.to_s
|
88
|
+
response.set(500, format_error('Internal Formatting Error'))
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
#-----------------------------------------------------------------------------
|
96
|
+
# the default raw format parser
|
97
|
+
#
|
98
|
+
class RawFormatParser < FormatParser
|
99
|
+
|
100
|
+
# def set_default_headers(headers)
|
101
|
+
# #headers[CACHE_CONTROL]= CACHE_NO_STORE
|
102
|
+
# #headers[PRAGMA] = NO_CACHE
|
103
|
+
# end
|
104
|
+
|
105
|
+
def format_error(message)
|
106
|
+
message
|
107
|
+
end
|
108
|
+
|
109
|
+
def format_response(value, response)
|
110
|
+
response.content = value.to_s
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
#-----------------------------------------------------------------------------
|
116
|
+
# the default xml format parser
|
117
|
+
#
|
118
|
+
class XmlFormatParser < FormatParser
|
119
|
+
|
120
|
+
accept_types CONTENT_TYPE_XML
|
121
|
+
|
122
|
+
def set_default_headers(headers)
|
123
|
+
headers[CACHE_CONTROL] = CACHE_NO_STORE
|
124
|
+
headers[PRAGMA] = NO_CACHE
|
125
|
+
headers[CONTENT_TYPE] = CONTENT_TYPE_XML
|
126
|
+
end
|
127
|
+
|
128
|
+
def format_error(message)
|
129
|
+
"<error>#{message}</error>"
|
130
|
+
end
|
131
|
+
|
132
|
+
def format_response(value, response)
|
133
|
+
response.content = value.to_s # FIXME
|
134
|
+
end
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
#-----------------------------------------------------------------------------
|
139
|
+
# the default html format parser
|
140
|
+
#
|
141
|
+
class HtmlFormatParser < FormatParser
|
142
|
+
|
143
|
+
accept_types CONTENT_TYPE_HTML
|
144
|
+
|
145
|
+
def set_default_headers(headers)
|
146
|
+
headers[CACHE_CONTROL] = CACHE_NO_STORE
|
147
|
+
headers[PRAGMA] = NO_CACHE
|
148
|
+
headers[CONTENT_TYPE] = CONTENT_TYPE_HTML
|
149
|
+
end
|
150
|
+
|
151
|
+
def format_error(message)
|
152
|
+
%(
|
153
|
+
<html>
|
154
|
+
<head></head>
|
155
|
+
<body>
|
156
|
+
<p>#{message}</p>
|
157
|
+
</body>
|
158
|
+
</html>
|
159
|
+
)
|
160
|
+
end
|
161
|
+
|
162
|
+
def format_response(value, response)
|
163
|
+
response.content = value.to_s
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,332 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Blix::Rest
|
4
|
+
class RequestMapperError < StandardError; end
|
5
|
+
|
6
|
+
# register routes with this class and then we can match paths to
|
7
|
+
# these routes and return an associated block and parameters.
|
8
|
+
class RequestMapper
|
9
|
+
|
10
|
+
WILD_PLACEHOLDER = '/'
|
11
|
+
PATH_SEP = '/'
|
12
|
+
STAR_PLACEHOLDER = '*'
|
13
|
+
|
14
|
+
# the
|
15
|
+
|
16
|
+
class TableNode
|
17
|
+
|
18
|
+
attr_accessor :blk
|
19
|
+
attr_reader :value
|
20
|
+
attr_accessor :opts
|
21
|
+
attr_reader :parameter
|
22
|
+
attr_reader :children
|
23
|
+
attr_accessor :extract_format
|
24
|
+
|
25
|
+
def initialize(name)
|
26
|
+
@children = {}
|
27
|
+
if name[0, 1] == ':'
|
28
|
+
@parameter = name[1..-1].to_sym
|
29
|
+
@value = WILD_PLACEHOLDER
|
30
|
+
elsif name[0, 1] == '*'
|
31
|
+
@parameter = if name[1..-1].empty?
|
32
|
+
:wildpath
|
33
|
+
else
|
34
|
+
name[1..-1].to_sym
|
35
|
+
end
|
36
|
+
@value = STAR_PLACEHOLDER
|
37
|
+
else
|
38
|
+
@value = name
|
39
|
+
end
|
40
|
+
@extract_format = true
|
41
|
+
end
|
42
|
+
|
43
|
+
def [](k)
|
44
|
+
@children[k]
|
45
|
+
end
|
46
|
+
|
47
|
+
def []=(k, v)
|
48
|
+
@children[k] = v
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
class << self
|
54
|
+
|
55
|
+
def set_path_root(root)
|
56
|
+
root = root.to_s
|
57
|
+
root = '/' + root if root[0, 1] != '/'
|
58
|
+
root += '/' if root[-1, 1] != '/'
|
59
|
+
@path_root = root
|
60
|
+
@path_root_length = @path_root.length - 1
|
61
|
+
end
|
62
|
+
|
63
|
+
def path_root
|
64
|
+
@path_root || '/'
|
65
|
+
end
|
66
|
+
|
67
|
+
attr_reader :path_root_length
|
68
|
+
|
69
|
+
def full_path(path)
|
70
|
+
path = path[1..-1] if path[0, 1] == '/'
|
71
|
+
path_root + path
|
72
|
+
end
|
73
|
+
|
74
|
+
def locations
|
75
|
+
@locations ||= Hash.new { |h, k| h[k] = [] }
|
76
|
+
end
|
77
|
+
|
78
|
+
def table
|
79
|
+
@table ||= compile
|
80
|
+
end
|
81
|
+
|
82
|
+
def dump
|
83
|
+
table.each do |k, v|
|
84
|
+
puts k
|
85
|
+
dump_node(v, 1)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def dump_node(item, indent = 0)
|
90
|
+
puts "#{' ' * indent} value=#{item.value.inspect} opts=#{item.opts.inspect} params=#{item.parameter.inspect}"
|
91
|
+
item.children.each_value { |c| dump_node(c, indent + 1) }
|
92
|
+
end
|
93
|
+
|
94
|
+
# used for testing only !!
|
95
|
+
def reset(vals = nil)
|
96
|
+
save = [@table&.dup, @locations&.dup, @path_root&.dup, @path_root_length]
|
97
|
+
if vals
|
98
|
+
@table = vals[0]
|
99
|
+
@locations = vals[1]
|
100
|
+
@path_root = vals[2]
|
101
|
+
@path_root_length = vals[3]
|
102
|
+
else
|
103
|
+
@table = nil
|
104
|
+
@locations = nil
|
105
|
+
@path_root = nil
|
106
|
+
@path_root_length = 0
|
107
|
+
end
|
108
|
+
save
|
109
|
+
end
|
110
|
+
|
111
|
+
# compile routes into a tree structure for easy lookup
|
112
|
+
def compile
|
113
|
+
@table = Hash.new { |h, k| h[k] = TableNode.new('') }
|
114
|
+
locations.each do |verb, routes|
|
115
|
+
routes.each do |info|
|
116
|
+
verb, path, opts, blk = info
|
117
|
+
parts = path.split(PATH_SEP)
|
118
|
+
current = @table[verb]
|
119
|
+
parts.each_with_index do |section, idx|
|
120
|
+
node = TableNode.new(section)
|
121
|
+
# check that a wildstar is the last element.
|
122
|
+
if (section[0] == STAR_PLACEHOLDER) && (idx < (parts.length - 1))
|
123
|
+
raise RequestMapperError, "do not add a path after the * in #{path}"
|
124
|
+
end
|
125
|
+
|
126
|
+
# check that wild card match in name
|
127
|
+
if current[node.value]
|
128
|
+
if (node.value == WILD_PLACEHOLDER) && (node.parameter != current[node.value].parameter)
|
129
|
+
raise RequestMapperError, "parameter mismatch in route=#{path}, expected #{current[node.value].parameter} but got #{node.parameter}"
|
130
|
+
end
|
131
|
+
|
132
|
+
else
|
133
|
+
current[node.value] = node
|
134
|
+
end
|
135
|
+
current = current[node.value]
|
136
|
+
end
|
137
|
+
current.blk = blk
|
138
|
+
current.opts = opts || {}
|
139
|
+
current.extract_format = opts[:extension] if opts.key?(:extension)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
@table
|
143
|
+
end
|
144
|
+
|
145
|
+
# declare a route
|
146
|
+
def add_path(verb, path, opts = {}, &blk)
|
147
|
+
path = path[1..-1] if path[0, 1] == PATH_SEP
|
148
|
+
RequestMapper.locations[verb] << [verb, path, opts, blk]
|
149
|
+
@table = nil # force recompile
|
150
|
+
end
|
151
|
+
|
152
|
+
# match a given path to declared route.
|
153
|
+
def match(verb, path)
|
154
|
+
path = PATH_SEP + path if path[0, 1] != PATH_SEP # ensure a leading slash on path
|
155
|
+
|
156
|
+
path = path[path_root_length..-1] if (path_root_length.to_i > 0) #&& (path[0,path_root_length] == path_root)
|
157
|
+
if path
|
158
|
+
path = path[1..-1] if path[0, 1] == PATH_SEP # remove the leading slash
|
159
|
+
else
|
160
|
+
return [nil, {}, nil]
|
161
|
+
end
|
162
|
+
|
163
|
+
parameters = StringHash.new
|
164
|
+
|
165
|
+
parts = path.split(PATH_SEP)
|
166
|
+
current = table[verb]
|
167
|
+
limit = parts.length - 1
|
168
|
+
|
169
|
+
# handle the root node here
|
170
|
+
if path == ''
|
171
|
+
if current.blk
|
172
|
+
return [current.blk, parameters, current.opts]
|
173
|
+
elsif (havewild = current[STAR_PLACEHOLDER])
|
174
|
+
parameters[havewild.parameter.to_s] = '/'
|
175
|
+
return [havewild.blk, parameters, havewild.opts]
|
176
|
+
else
|
177
|
+
return [nil, {}, nil]
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
parts.each_with_index do |section, idx|
|
182
|
+
# first save the last node that we used
|
183
|
+
# before updating the current node.
|
184
|
+
|
185
|
+
last = current # table nodes
|
186
|
+
|
187
|
+
# check to see if there is a path which includes a format part
|
188
|
+
# only on the last section
|
189
|
+
if idx == limit
|
190
|
+
if last[section]
|
191
|
+
current = last[section]
|
192
|
+
else
|
193
|
+
format = File.extname(section)
|
194
|
+
base = File.basename(section, format)
|
195
|
+
current = last[base]
|
196
|
+
if current
|
197
|
+
parameters['format'] = format[1..-1].to_sym # !format.empty?
|
198
|
+
section = base
|
199
|
+
end
|
200
|
+
end
|
201
|
+
else
|
202
|
+
current = last[section]
|
203
|
+
end
|
204
|
+
|
205
|
+
# if current is set here that means that this section matches a fixed
|
206
|
+
# part of the route.
|
207
|
+
if current
|
208
|
+
|
209
|
+
# if this is the last section then we have to decide here if we
|
210
|
+
# have a valid result..
|
211
|
+
# .. if we have a block then fine ..
|
212
|
+
# .. if there is a wildpath foloowing then fine ..
|
213
|
+
# .. otherwise an error !
|
214
|
+
|
215
|
+
if idx == limit # the last section of request
|
216
|
+
if current.blk
|
217
|
+
return [current.blk, parameters, current.opts]
|
218
|
+
elsif (havewild = current[STAR_PLACEHOLDER])
|
219
|
+
parameters[havewild.parameter.to_s] = '/'
|
220
|
+
return [havewild.blk, parameters, havewild.opts]
|
221
|
+
else
|
222
|
+
return [nil, {}, nil]
|
223
|
+
end
|
224
|
+
end
|
225
|
+
else
|
226
|
+
|
227
|
+
# this section is not part of a static path so
|
228
|
+
# check if we have a path variable first ..
|
229
|
+
|
230
|
+
current = last[WILD_PLACEHOLDER]
|
231
|
+
if current
|
232
|
+
|
233
|
+
# yes this is a path variable section
|
234
|
+
if idx == limit
|
235
|
+
|
236
|
+
# the last section of request -
|
237
|
+
if current.extract_format
|
238
|
+
format = File.extname(section)
|
239
|
+
base = File.basename(section, format)
|
240
|
+
parameters[current.parameter.to_s] = base
|
241
|
+
parameters['format'] = format[1..-1].to_sym unless format.empty?
|
242
|
+
else
|
243
|
+
parameters[current.parameter.to_s] = section
|
244
|
+
end
|
245
|
+
|
246
|
+
# check if we have a valid block otherwise see if
|
247
|
+
# a wild path follows.
|
248
|
+
|
249
|
+
if current.blk
|
250
|
+
return [current.blk, parameters, current.opts]
|
251
|
+
elsif (havewild = current[STAR_PLACEHOLDER])
|
252
|
+
parameters[havewild.parameter.to_s] = '/'
|
253
|
+
return [havewild.blk, parameters, havewild.opts]
|
254
|
+
else
|
255
|
+
return [nil, {}, nil]
|
256
|
+
end
|
257
|
+
|
258
|
+
else
|
259
|
+
parameters[current.parameter.to_s] = section
|
260
|
+
end
|
261
|
+
else
|
262
|
+
current = last[STAR_PLACEHOLDER]
|
263
|
+
if current
|
264
|
+
wildpath = '/' + parts[idx..-1].join('/')
|
265
|
+
wildformat = File.extname(wildpath)
|
266
|
+
unless wildformat.empty? || !current.extract_format
|
267
|
+
wildpath = wildpath[0..-(wildformat.length + 1)]
|
268
|
+
parameters['format'] = wildformat[1..-1].to_sym
|
269
|
+
end
|
270
|
+
parameters[current.parameter.to_s] = wildpath
|
271
|
+
return [current.blk, parameters, current.opts]
|
272
|
+
else
|
273
|
+
return [nil, {}, nil]
|
274
|
+
end
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
[nil, {}, nil]
|
279
|
+
end
|
280
|
+
|
281
|
+
# match a path to a route and call any associated block with the extracted parameters.
|
282
|
+
def process(verb, path)
|
283
|
+
blk, params = match(verb, path)
|
284
|
+
blk&.call(params)
|
285
|
+
end
|
286
|
+
|
287
|
+
def routes
|
288
|
+
hash = {}
|
289
|
+
locations.values.each do |group|
|
290
|
+
group.each do |route|
|
291
|
+
verb = route[0]
|
292
|
+
options = route[2]
|
293
|
+
options_string = String.new
|
294
|
+
options_string = ' ' + options.inspect.to_s unless options.empty?
|
295
|
+
path = '/' + route[1]
|
296
|
+
hash[path] ||= {}
|
297
|
+
hash[path][verb] = options_string
|
298
|
+
end
|
299
|
+
end
|
300
|
+
list = hash.to_a
|
301
|
+
list.sort! { |a, b| a[0] <=> b[0] }
|
302
|
+
str = String.new
|
303
|
+
list.each do |route|
|
304
|
+
#pairs = route[1]
|
305
|
+
(HTTP_VERBS + ['ALL']).each do |verb|
|
306
|
+
if route[1].key? verb
|
307
|
+
str << verb << "\t" << route[0] << route[1][verb] << "\n"
|
308
|
+
end
|
309
|
+
end
|
310
|
+
str << "\n"
|
311
|
+
end
|
312
|
+
str
|
313
|
+
end
|
314
|
+
|
315
|
+
end
|
316
|
+
|
317
|
+
end # RequestMapper
|
318
|
+
|
319
|
+
def self.set_path_root(*args)
|
320
|
+
RequestMapper.set_path_root(*args)
|
321
|
+
end
|
322
|
+
|
323
|
+
def self.path_root
|
324
|
+
RequestMapper.path_root
|
325
|
+
end
|
326
|
+
|
327
|
+
def self.full_path(path)
|
328
|
+
RequestMapper.full_path(path)
|
329
|
+
end
|
330
|
+
|
331
|
+
RequestMapper.set_path_root(ENV['BLIX_REST_ROOT']) if ENV['BLIX_REST_ROOT']
|
332
|
+
end # Rest
|