blix-rest 0.1.30

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,10 @@
1
+ module HandlebarsAssets
2
+
3
+ module JSON
4
+
5
+ def self.dump(str)
6
+ str.to_json
7
+ end
8
+ end
9
+
10
+ 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