blix-rest 0.1.30
Sign up to get free protection for your applications and to get access to all the features.
- 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,50 @@
|
|
1
|
+
module Blix::Rest
|
2
|
+
|
3
|
+
module ResourceCache
|
4
|
+
|
5
|
+
private
|
6
|
+
|
7
|
+
def _cache_hash
|
8
|
+
@_cache ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def _cache_get(*args,&block)
|
12
|
+
field = args[0].to_s
|
13
|
+
if block && args.length == 1
|
14
|
+
if _cache?(field)
|
15
|
+
_cache_hash[field]
|
16
|
+
else
|
17
|
+
_cache_hash[field]= block.call
|
18
|
+
end
|
19
|
+
elsif args.length == 1
|
20
|
+
_cache_hash[field]
|
21
|
+
elsif args.length == 2
|
22
|
+
if _cache?(field)
|
23
|
+
_cache_hash[field]
|
24
|
+
else
|
25
|
+
_cache_hash[field]= args[1]
|
26
|
+
end
|
27
|
+
else
|
28
|
+
raise "wrong number of arguments:#{args.length} for 1 or 2"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def _cache_set(field,val)
|
33
|
+
_cache_hash[field.to_s] = val
|
34
|
+
end
|
35
|
+
|
36
|
+
def _cache_reset(field)
|
37
|
+
_cache_hash.delete field.to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def _cache(field)
|
41
|
+
_cache_hash[field.to_s]
|
42
|
+
end
|
43
|
+
|
44
|
+
def _cache?(field)
|
45
|
+
_cache_hash.key?(field.to_s)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# pass a response object to the controller to set
|
2
|
+
# header status and content.
|
3
|
+
|
4
|
+
module Blix::Rest
|
5
|
+
|
6
|
+
|
7
|
+
class Response
|
8
|
+
|
9
|
+
attr_accessor :status
|
10
|
+
attr_reader :headers
|
11
|
+
attr_accessor :content
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@status = 200
|
15
|
+
@headers = {}
|
16
|
+
@content = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def set(status,content=nil,headers=nil)
|
20
|
+
@status = status if status
|
21
|
+
@content = String.new(content) if content
|
22
|
+
@headers.merge!(headers) if headers
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Blix::Rest
|
4
|
+
class Server
|
5
|
+
|
6
|
+
def initialize(opts = {})
|
7
|
+
@_parsers = {}
|
8
|
+
@_mime_types = {}
|
9
|
+
|
10
|
+
# register the default parsers and any passed in as options.
|
11
|
+
|
12
|
+
register_parser('html', HtmlFormatParser.new)
|
13
|
+
register_parser('json', JsonFormatParser.new)
|
14
|
+
register_parser('xml', XmlFormatParser.new)
|
15
|
+
register_parser('raw', RawFormatParser.new)
|
16
|
+
extract_parsers_from_options(opts)
|
17
|
+
@_options = opts
|
18
|
+
end
|
19
|
+
|
20
|
+
def _cache
|
21
|
+
@_cache ||= {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def extract_parsers_from_options(opts)
|
25
|
+
opts.each do |k, v|
|
26
|
+
next unless k =~ /^(\w*)_parser&/
|
27
|
+
|
28
|
+
format = Regexp.last_match(1)
|
29
|
+
parser = v
|
30
|
+
register_parser(format, parser)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def set_custom_headers(format, headers)
|
35
|
+
parser = get_parser(format)
|
36
|
+
raise "parser not found for custom headers format=>#{format}" unless parser
|
37
|
+
|
38
|
+
parser.__custom_headers = headers
|
39
|
+
end
|
40
|
+
|
41
|
+
def get_parser(format)
|
42
|
+
@_parsers[format.to_s]
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_parser_from_type(type)
|
46
|
+
@_mime_types[type.downcase]
|
47
|
+
end
|
48
|
+
|
49
|
+
def register_parser(format, parser)
|
50
|
+
raise "#{k} must be an object with parent class Blix::Rest::FormatParser" unless parser.is_a?(FormatParser)
|
51
|
+
|
52
|
+
parser._format = format
|
53
|
+
@_parsers[format.to_s.downcase] = parser
|
54
|
+
parser._types.each { |t| @_mime_types[t.downcase] = parser } # register each of the mime types
|
55
|
+
end
|
56
|
+
|
57
|
+
def retrieve_params(env)
|
58
|
+
post_params = {}
|
59
|
+
body = ''
|
60
|
+
params = env['params'] || {}
|
61
|
+
params.merge!(::Rack::Utils.parse_nested_query(env['QUERY_STRING']))
|
62
|
+
|
63
|
+
if env['rack.input']
|
64
|
+
post_params = ::Rack::Utils::Multipart.parse_multipart(env)
|
65
|
+
unless post_params
|
66
|
+
body = env['rack.input'].read
|
67
|
+
env['rack.input'].rewind
|
68
|
+
|
69
|
+
if body.empty?
|
70
|
+
post_params = {}
|
71
|
+
else
|
72
|
+
begin
|
73
|
+
post_params = case (env['CONTENT_TYPE'])
|
74
|
+
when URL_ENCODED
|
75
|
+
::Rack::Utils.parse_nested_query(body)
|
76
|
+
when JSON_ENCODED then
|
77
|
+
json = MultiJson.load(body)
|
78
|
+
if json.is_a?(Hash)
|
79
|
+
json
|
80
|
+
else
|
81
|
+
{ '_json' => json }
|
82
|
+
end
|
83
|
+
else
|
84
|
+
{}
|
85
|
+
end
|
86
|
+
rescue StandardError => e
|
87
|
+
raise BadRequestError, "Invalid parameters: #{e.class}"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
[params, post_params, body]
|
93
|
+
end
|
94
|
+
|
95
|
+
# accept header can have multiple entries. match on regexp
|
96
|
+
# can look like this
|
97
|
+
# text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 !!!!!
|
98
|
+
|
99
|
+
def get_format(env)
|
100
|
+
case env['HTTP_ACCEPT']
|
101
|
+
when JSON_ENCODED then :json
|
102
|
+
when HTML_ENCODED then :html
|
103
|
+
when XML_ENCODED then :xml
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def get_format_from_mime(mime)
|
108
|
+
case mime
|
109
|
+
when 'application/json' then :json
|
110
|
+
when 'text/html' then :html
|
111
|
+
when 'application/xml' then :xml
|
112
|
+
when 'application/xhtml+xml' then :xhtml
|
113
|
+
when '*/*' then :*
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# attempt to handle mjltiple accept formats here..
|
118
|
+
# mime can include '.../*' and '*/*'
|
119
|
+
# FIXME
|
120
|
+
def get_format_new(env, options)
|
121
|
+
accept = options && options[:accept] || :json
|
122
|
+
accept = [accept].flatten
|
123
|
+
|
124
|
+
requested = env['HTTP_ACCEPT'].to_s.split(',')
|
125
|
+
requested.each do |request|
|
126
|
+
parts = request.split(';') # the quality part is after a ;
|
127
|
+
mime = parts[0].strip # the mime type
|
128
|
+
try = get_format_from_mime(mime)
|
129
|
+
next unless try
|
130
|
+
return accept[0] || :json if try == :*
|
131
|
+
return try if accept.include?(try)
|
132
|
+
end
|
133
|
+
nil # no match found
|
134
|
+
end
|
135
|
+
|
136
|
+
# convert the response to the appropriate format
|
137
|
+
def format_error(_message, _format)
|
138
|
+
parser
|
139
|
+
end
|
140
|
+
|
141
|
+
def call(env)
|
142
|
+
req = Rack::Request.new(env)
|
143
|
+
|
144
|
+
verb = env['REQUEST_METHOD']
|
145
|
+
path = req.path
|
146
|
+
|
147
|
+
blk, path_params, options = RequestMapper.match(verb, path)
|
148
|
+
|
149
|
+
blk, path_params, options = RequestMapper.match('ALL', path) unless blk
|
150
|
+
|
151
|
+
default_format = options && options[:default] && options[:default].to_sym
|
152
|
+
force_format = options && options[:force] && options[:force].to_sym
|
153
|
+
do_cache = options && options[:cache]
|
154
|
+
clear_cache = options && options[:cache_reset]
|
155
|
+
|
156
|
+
query_format = options && options[:query] && req.GET['format'] && req.GET['format'].to_sym
|
157
|
+
|
158
|
+
format = query_format || path_params[:format] || get_format_new(env, options) || default_format || :json
|
159
|
+
|
160
|
+
parser = get_parser(force_format || format)
|
161
|
+
|
162
|
+
return [406, {}, ["Invalid Format: #{format}"]] unless parser
|
163
|
+
|
164
|
+
parser._options = options
|
165
|
+
|
166
|
+
# check for cached response end return with cached response if found.
|
167
|
+
#
|
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]]
|
171
|
+
end
|
172
|
+
|
173
|
+
response = Response.new
|
174
|
+
|
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
|
+
if blk
|
182
|
+
|
183
|
+
begin
|
184
|
+
params = env['params']
|
185
|
+
value = blk.call(path_params, params, req, format, response, @_options)
|
186
|
+
rescue ServiceError => e
|
187
|
+
response.set(e.status, parser.format_error(e.message), e.headers)
|
188
|
+
rescue AuthorizationError => e
|
189
|
+
response.set(401, parser.format_error(e.message), AUTH_HEADER => "#{e.type} realm=\"#{e.realm}\", charset=\"UTF-8\"")
|
190
|
+
rescue Exception => e
|
191
|
+
response.set(500, parser.format_error('internal error'))
|
192
|
+
::Blix::Rest.logger << "----------------------------\n#{$!}\n----------------------------"
|
193
|
+
::Blix::Rest.logger << "----------------------------\n#{$@}\n----------------------------"
|
194
|
+
else # no error
|
195
|
+
parser.format_response(value, response)
|
196
|
+
# cache response if requested
|
197
|
+
_cache.clear if clear_cache
|
198
|
+
_cache["#{verb}|#{format}|#{path}"] = response if do_cache
|
199
|
+
end
|
200
|
+
|
201
|
+
else
|
202
|
+
response.set(404, parser.format_error('Invalid Url'))
|
203
|
+
end
|
204
|
+
[response.status, response.headers, [response.content]]
|
205
|
+
end
|
206
|
+
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
unless Hash.method_defined? :transform_keys
|
4
|
+
class Hash
|
5
|
+
# Returns a new hash with all keys converted using the block operation.
|
6
|
+
#
|
7
|
+
# hash = { name: 'Rob', age: '28' }
|
8
|
+
#
|
9
|
+
# hash.transform_keys{ |key| key.to_s.upcase }
|
10
|
+
# # => {"NAME"=>"Rob", "AGE"=>"28"}
|
11
|
+
def transform_keys
|
12
|
+
result = {}
|
13
|
+
each_key do |key|
|
14
|
+
result[yield(key)] = self[key]
|
15
|
+
end
|
16
|
+
result
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Blix::Rest
|
22
|
+
# indifferent hash for symbols or string keys.
|
23
|
+
# stores keys as a string
|
24
|
+
|
25
|
+
class StringHash < Hash
|
26
|
+
|
27
|
+
alias_method :parent_merge!, :merge!
|
28
|
+
|
29
|
+
# initialize without conversion. params must be in
|
30
|
+
# string key format.
|
31
|
+
def initialize(*params)
|
32
|
+
super()
|
33
|
+
parent_merge!(*params) unless params.empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
# create with conversion
|
37
|
+
def self.create(params)
|
38
|
+
h = new
|
39
|
+
h.merge(params)
|
40
|
+
h
|
41
|
+
end
|
42
|
+
|
43
|
+
def [](k)
|
44
|
+
super(k.to_s)
|
45
|
+
end
|
46
|
+
|
47
|
+
def get(k, default = nil)
|
48
|
+
if key?(k.to_s)
|
49
|
+
self[k]
|
50
|
+
else
|
51
|
+
default
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def merge(*params)
|
56
|
+
super(* params.map { |h| h.transform_keys(&:to_s) })
|
57
|
+
end
|
58
|
+
|
59
|
+
def merge!(*params)
|
60
|
+
super(* params.map { |h| h.transform_keys(&:to_s) })
|
61
|
+
end
|
62
|
+
|
63
|
+
def replace(h)
|
64
|
+
super(h.transform_keys(&:to_s))
|
65
|
+
end
|
66
|
+
|
67
|
+
def has_key?(key)
|
68
|
+
super(key.to_s)
|
69
|
+
end
|
70
|
+
|
71
|
+
def member?(key)
|
72
|
+
super(key.to_s)
|
73
|
+
end
|
74
|
+
|
75
|
+
def store(key, value)
|
76
|
+
super(key.to_s, value)
|
77
|
+
end
|
78
|
+
|
79
|
+
def key(key)
|
80
|
+
super(key.to_s)
|
81
|
+
end
|
82
|
+
|
83
|
+
def key?(key)
|
84
|
+
super(key.to_s)
|
85
|
+
end
|
86
|
+
|
87
|
+
def []=(k, v)
|
88
|
+
super(k.to_s, v)
|
89
|
+
end
|
90
|
+
|
91
|
+
def include?(k)
|
92
|
+
super(k.to_s)
|
93
|
+
end
|
94
|
+
|
95
|
+
def delete(k)
|
96
|
+
super(k.to_s)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
data/lib/blix/utils.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module Blix
|
4
|
+
|
5
|
+
def self.require_dir(path)
|
6
|
+
raise "invalid dir path:#{path}" unless File.directory?(path)
|
7
|
+
Dir.glob("#{path}/*.rb").each {|file| require File.expand_path(file)[0..-4] }
|
8
|
+
end
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
# filter the hash using the supplied filter
|
13
|
+
#
|
14
|
+
# the filter is an array of keys that are permitted
|
15
|
+
# returns a hash containing only the permitted keys and values
|
16
|
+
def self.filter_hash(filter,hash)
|
17
|
+
hash = hash || {}
|
18
|
+
hash.select {|key, value| filter.include?(key.to_sym) || filter.include?(key.to_s)}
|
19
|
+
end
|
20
|
+
|
21
|
+
# used to raise errors on
|
22
|
+
module DatamapperExceptions
|
23
|
+
def save(*args)
|
24
|
+
raise ServiceError, errors.full_messages.join(',') unless super
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def update(*args)
|
29
|
+
raise ServiceError, errors.full_messages.join(',') unless super
|
30
|
+
self
|
31
|
+
end
|
32
|
+
|
33
|
+
def destroy(*args)
|
34
|
+
raise ServiceError, errors.full_messages.join(',') unless super
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
module ClassMethods
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.included(mod)
|
43
|
+
mod.extend DatamapperExceptions::ClassMethods
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
class String
|
51
|
+
|
52
|
+
# try to convert utf special characters to normal characters.
|
53
|
+
def to_ascii
|
54
|
+
unicode_normalize(:nfd).gsub(/[\u0300-\u036f]/, "")
|
55
|
+
end
|
56
|
+
|
57
|
+
# standardize utf strings
|
58
|
+
def normalize
|
59
|
+
unicode_normalize(:nfc)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|