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.
@@ -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