haveapi 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/haveapi/action.rb +521 -0
- data/lib/haveapi/actions/default.rb +55 -0
- data/lib/haveapi/actions/paginable.rb +12 -0
- data/lib/haveapi/api.rb +66 -0
- data/lib/haveapi/authentication/base.rb +37 -0
- data/lib/haveapi/authentication/basic/provider.rb +37 -0
- data/lib/haveapi/authentication/chain.rb +110 -0
- data/lib/haveapi/authentication/token/provider.rb +166 -0
- data/lib/haveapi/authentication/token/resources.rb +107 -0
- data/lib/haveapi/authorization.rb +108 -0
- data/lib/haveapi/common.rb +38 -0
- data/lib/haveapi/context.rb +78 -0
- data/lib/haveapi/example.rb +36 -0
- data/lib/haveapi/extensions/action_exceptions.rb +25 -0
- data/lib/haveapi/extensions/base.rb +9 -0
- data/lib/haveapi/extensions/resource_prefetch.rb +7 -0
- data/lib/haveapi/hooks.rb +190 -0
- data/lib/haveapi/metadata.rb +56 -0
- data/lib/haveapi/model_adapter.rb +119 -0
- data/lib/haveapi/model_adapters/active_record.rb +352 -0
- data/lib/haveapi/model_adapters/hash.rb +27 -0
- data/lib/haveapi/output_formatter.rb +57 -0
- data/lib/haveapi/output_formatters/base.rb +29 -0
- data/lib/haveapi/output_formatters/json.rb +9 -0
- data/lib/haveapi/params/param.rb +114 -0
- data/lib/haveapi/params/resource.rb +109 -0
- data/lib/haveapi/params.rb +314 -0
- data/lib/haveapi/public/css/bootstrap-theme.min.css +7 -0
- data/lib/haveapi/public/css/bootstrap.min.css +7 -0
- data/lib/haveapi/public/js/bootstrap.min.js +6 -0
- data/lib/haveapi/public/js/jquery-1.11.1.min.js +4 -0
- data/lib/haveapi/resource.rb +120 -0
- data/lib/haveapi/route.rb +22 -0
- data/lib/haveapi/server.rb +440 -0
- data/lib/haveapi/spec/helpers.rb +103 -0
- data/lib/haveapi/types.rb +24 -0
- data/lib/haveapi/version.rb +3 -0
- data/lib/haveapi/views/doc_layout.erb +27 -0
- data/lib/haveapi/views/doc_sidebars/create-client.erb +20 -0
- data/lib/haveapi/views/doc_sidebars/protocol.erb +42 -0
- data/lib/haveapi/views/index.erb +12 -0
- data/lib/haveapi/views/main_layout.erb +50 -0
- data/lib/haveapi/views/version_page.erb +195 -0
- data/lib/haveapi/views/version_sidebar.erb +42 -0
- data/lib/haveapi.rb +22 -0
- metadata +242 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
module HaveAPI::Authentication
|
2
|
+
module Basic
|
3
|
+
# HTTP basic authentication provider.
|
4
|
+
#
|
5
|
+
# Example usage:
|
6
|
+
# class MyBasicAuth < HaveAPI::Authentication::Basic::Provider
|
7
|
+
# protected
|
8
|
+
# def find_user(request, username, password)
|
9
|
+
# ::User.find_by(login: username, password: password)
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# Finally put the provider in the authentication chain:
|
14
|
+
# api = HaveAPI.new(...)
|
15
|
+
# ...
|
16
|
+
# api.auth_chain << MyBasicAuth
|
17
|
+
class Provider < Base
|
18
|
+
def authenticate(request)
|
19
|
+
user = nil
|
20
|
+
|
21
|
+
auth = Rack::Auth::Basic::Request.new(request.env)
|
22
|
+
if auth.provided? && auth.basic? && auth.credentials
|
23
|
+
user = find_user(request, *auth.credentials)
|
24
|
+
end
|
25
|
+
|
26
|
+
user
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
# Reimplement this method. It has to return an authenticated
|
31
|
+
# user or nil.
|
32
|
+
def find_user(request, username, password)
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
module HaveAPI::Authentication
|
2
|
+
# Authentication chain.
|
3
|
+
# At every request, #authenticate is called to authenticate user.
|
4
|
+
class Chain
|
5
|
+
def initialize(server)
|
6
|
+
@server = server
|
7
|
+
@chain = {}
|
8
|
+
@instances = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup(versions)
|
12
|
+
versions.each do |v|
|
13
|
+
@instances[v] ||= []
|
14
|
+
|
15
|
+
@chain[v] && @chain[v].each { |p| register_provider(v, p) }
|
16
|
+
end
|
17
|
+
|
18
|
+
if @chain[:all]
|
19
|
+
@chain[:all].each do |p|
|
20
|
+
@instances.each_key { |v| register_provider(v, p) }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# @chain.each do |p|
|
25
|
+
# @instances << p.new(@server)
|
26
|
+
#
|
27
|
+
# parts = p.to_s.split('::')
|
28
|
+
# mod = Kernel.const_get((parts[0..-2] << 'Resources').join('::'))
|
29
|
+
#
|
30
|
+
# @server.add_module(mod, prefix: parts[-2].tableize) if mod
|
31
|
+
# end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Iterate through authentication providers registered for version +v+
|
35
|
+
# until authentication is successful or the end is reached and user
|
36
|
+
# is not authenticated.
|
37
|
+
# Authentication provider can deny the user access by calling Base#deny.
|
38
|
+
def authenticate(v, *args)
|
39
|
+
catch(:return) do
|
40
|
+
@instances[v].each do |provider|
|
41
|
+
u = provider.authenticate(*args)
|
42
|
+
return u if u
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def describe(context)
|
50
|
+
ret = {}
|
51
|
+
|
52
|
+
@instances[context.version].each do |provider|
|
53
|
+
ret[provider.name] = provider.describe
|
54
|
+
|
55
|
+
if provider.resources
|
56
|
+
ret[provider.name][:resources] = {}
|
57
|
+
|
58
|
+
@server.routes[context.version][:authentication][provider.name][:resources].each do |r, children|
|
59
|
+
ret[provider.name][:resources][r.to_s.demodulize.underscore.to_sym] = r.describe(children, context)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
ret
|
65
|
+
end
|
66
|
+
|
67
|
+
# Return provider list for version +v+.
|
68
|
+
# Used for registering providers to specific version, e.g.
|
69
|
+
# api.auth_chain[1] << MyAuthProvider
|
70
|
+
def [](v)
|
71
|
+
@chain[v] ||= []
|
72
|
+
@chain[v]
|
73
|
+
end
|
74
|
+
|
75
|
+
# Register authentication +provider+ for all available API versions.
|
76
|
+
# +provider+ may also be an Array of providers.
|
77
|
+
def <<(provider)
|
78
|
+
@chain[:all] ||= []
|
79
|
+
|
80
|
+
if provider.is_a?(Array)
|
81
|
+
provider.each { |p| @chain[:all] << p }
|
82
|
+
else
|
83
|
+
@chain[:all] << provider
|
84
|
+
end
|
85
|
+
|
86
|
+
self
|
87
|
+
end
|
88
|
+
|
89
|
+
def empty?
|
90
|
+
@chain.empty?
|
91
|
+
end
|
92
|
+
|
93
|
+
protected
|
94
|
+
def register_provider(v, p)
|
95
|
+
instance = p.new(@server, v)
|
96
|
+
parts = p.superclass.name.split('::')
|
97
|
+
|
98
|
+
instance.name = parts[-2].underscore.to_sym
|
99
|
+
|
100
|
+
@instances[v] << instance
|
101
|
+
|
102
|
+
provider = Kernel.const_get(parts[0..-2].join('::'))
|
103
|
+
|
104
|
+
if provider.const_defined?('Resources')
|
105
|
+
instance.resources = provider.const_get('Resources')
|
106
|
+
@server.add_auth_module(v, instance.name, instance.resources, prefix: parts[-2].underscore)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module HaveAPI::Authentication
|
2
|
+
module Token
|
3
|
+
# Exception that has to be raised when generated token already exists.
|
4
|
+
# Provider will catch it and generate another token.
|
5
|
+
class TokenExists < Exception
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
# Provider for token authentication. This class has to be subclassed
|
10
|
+
# and implemented.
|
11
|
+
#
|
12
|
+
# Token auth contains resource token. User can request a token by calling
|
13
|
+
# action Resources::Token::Request. Returned token is then used for
|
14
|
+
# authenticating the user. Client sends the token with each request
|
15
|
+
# in configured #http_header or #query_parameter.
|
16
|
+
#
|
17
|
+
# Token can be revoked by calling Resources::Token::Revoke.
|
18
|
+
#
|
19
|
+
# === \Example usage:
|
20
|
+
#
|
21
|
+
# \Token model:
|
22
|
+
# class ApiToken < ActiveRecord::Base
|
23
|
+
# belongs_to :user
|
24
|
+
#
|
25
|
+
# validates :user_id, :token, presence: true
|
26
|
+
# validates :token, length: {is: 100}
|
27
|
+
#
|
28
|
+
# enum lifetime: %i(fixed renewable_manual renewable_auto permanent)
|
29
|
+
#
|
30
|
+
# def renew
|
31
|
+
# self.valid_to = Time.now + interval
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# Authentication provider:
|
36
|
+
# class MyTokenAuth < HaveAPI::Authentication::Token::Provider
|
37
|
+
# protected
|
38
|
+
# def save_token(request, user, token, lifetime, interval)
|
39
|
+
# user.tokens << ::Token.new(token: token, lifetime: lifetime,
|
40
|
+
# valid_to: (lifetime != 'permanent' ? Time.now + interval : nil),
|
41
|
+
# interval: interval, label: request.user_agent)
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# def revoke_token(request, user, token)
|
45
|
+
# user.tokens.delete(token: token)
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# def renew_token(request, user, token)
|
49
|
+
# t = ::Token.find_by(user: user, token: token)
|
50
|
+
#
|
51
|
+
# if t.lifetime.start_with('renewable')
|
52
|
+
# t.renew
|
53
|
+
# t.save
|
54
|
+
# t.valid_to
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# def find_user_by_credentials(request, username, password)
|
59
|
+
# ::User.find_by(login: username, password: password)
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# def find_user_by_token(request, token)
|
63
|
+
# t = ::Token.find_by(token: token)
|
64
|
+
#
|
65
|
+
# if t
|
66
|
+
# # Renew the token if needed
|
67
|
+
# if t.lifetime == 'renewable_auto'
|
68
|
+
# t.renew
|
69
|
+
# t.save
|
70
|
+
# end
|
71
|
+
#
|
72
|
+
# t.user # return the user
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
# end
|
76
|
+
#
|
77
|
+
# Finally put the provider in the authentication chain:
|
78
|
+
# api = HaveAPI.new(...)
|
79
|
+
# ...
|
80
|
+
# api.auth_chain << MyTokenAuth
|
81
|
+
class Provider < Base
|
82
|
+
def setup
|
83
|
+
Resources::Token.token_instance ||= {}
|
84
|
+
Resources::Token.token_instance[@version] = self
|
85
|
+
|
86
|
+
@server.allow_header(http_header)
|
87
|
+
end
|
88
|
+
|
89
|
+
def authenticate(request)
|
90
|
+
t = token(request)
|
91
|
+
|
92
|
+
t && find_user_by_token(request, t)
|
93
|
+
end
|
94
|
+
|
95
|
+
def token(request)
|
96
|
+
request[query_parameter] || request.env[header_to_env]
|
97
|
+
end
|
98
|
+
|
99
|
+
def describe
|
100
|
+
{
|
101
|
+
http_header: http_header,
|
102
|
+
query_parameter: query_parameter,
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
protected
|
107
|
+
# HTTP header that is searched for token.
|
108
|
+
def http_header
|
109
|
+
'X-HaveAPI-Auth-Token'
|
110
|
+
end
|
111
|
+
|
112
|
+
# Query parameter searched for token.
|
113
|
+
def query_parameter
|
114
|
+
:_auth_token
|
115
|
+
end
|
116
|
+
|
117
|
+
# Generate token. Implicit implementation returns token of 100 chars.
|
118
|
+
def generate_token
|
119
|
+
SecureRandom.hex(50)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Save generated +token+ for +user+. Token has given +lifetime+
|
123
|
+
# and when not permanent, also a +interval+ of validity.
|
124
|
+
# Returns a date time which is token expiration.
|
125
|
+
# It is up to the implementation of this method to remember
|
126
|
+
# token lifetime and interval.
|
127
|
+
# Must be implemented.
|
128
|
+
def save_token(request, user, token, lifetime, interval)
|
129
|
+
|
130
|
+
end
|
131
|
+
|
132
|
+
# Revoke existing +token+ for +user+.
|
133
|
+
# Must be implemented.
|
134
|
+
def revoke_token(request, user, token)
|
135
|
+
|
136
|
+
end
|
137
|
+
|
138
|
+
# Renew existing +token+ for +user+.
|
139
|
+
# Returns a date time which is token expiration.
|
140
|
+
# Must be implemented.
|
141
|
+
def renew_token(request, user, token)
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
# Used by action Resources::Token::Request when the user is requesting
|
146
|
+
# a token. This method returns user object or nil.
|
147
|
+
# Must be implemented.
|
148
|
+
def find_user_by_credentials(request, username, password)
|
149
|
+
|
150
|
+
end
|
151
|
+
|
152
|
+
# Authenticate user by +token+. Return user object or nil.
|
153
|
+
# If the token was created as auto-renewable, this method
|
154
|
+
# is responsible for its renewal.
|
155
|
+
# Must be implemented.
|
156
|
+
def find_user_by_token(request, token)
|
157
|
+
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
def header_to_env
|
162
|
+
"HTTP_#{http_header.upcase.gsub(/\-/, '_')}"
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module HaveAPI::Authentication::Token
|
2
|
+
module Resources
|
3
|
+
class Token < HaveAPI::Resource
|
4
|
+
auth false
|
5
|
+
version :all
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :token_instance
|
9
|
+
end
|
10
|
+
|
11
|
+
class Request < HaveAPI::Action
|
12
|
+
route ''
|
13
|
+
http_method :post
|
14
|
+
|
15
|
+
input(:hash) do
|
16
|
+
string :login, label: 'Login', required: true
|
17
|
+
string :password, label: 'Password', required: true
|
18
|
+
string :lifetime, label: 'Lifetime', required: true,
|
19
|
+
choices: %i(fixed renewable_manual renewable_auto permanent),
|
20
|
+
desc: <<END
|
21
|
+
fixed - the token has a fixed validity period, it cannot be renewed
|
22
|
+
renewable_manual - the token can be renewed, but it must be done manually via renew action
|
23
|
+
renewable_auto - the token is renewed automatically to now+interval every time it is used
|
24
|
+
permanent - the token will be valid forever, unless deleted
|
25
|
+
END
|
26
|
+
integer :interval, label: 'Interval',
|
27
|
+
desc: 'How long will requested token be valid, in seconds.',
|
28
|
+
default: 60*5, fill: true
|
29
|
+
end
|
30
|
+
|
31
|
+
output(:hash) do
|
32
|
+
string :token
|
33
|
+
datetime :valid_to
|
34
|
+
end
|
35
|
+
|
36
|
+
authorize do
|
37
|
+
allow
|
38
|
+
end
|
39
|
+
|
40
|
+
def exec
|
41
|
+
klass = self.class.resource.token_instance[@version]
|
42
|
+
|
43
|
+
user = klass.send(
|
44
|
+
:find_user_by_credentials,
|
45
|
+
request,
|
46
|
+
input[:login],
|
47
|
+
input[:password]
|
48
|
+
)
|
49
|
+
error('bad login or password') unless user
|
50
|
+
|
51
|
+
token = expiration = nil
|
52
|
+
|
53
|
+
loop do
|
54
|
+
begin
|
55
|
+
token = klass.send(:generate_token)
|
56
|
+
expiration = klass.send(:save_token,
|
57
|
+
@request,
|
58
|
+
user,
|
59
|
+
token,
|
60
|
+
params[:token][:lifetime],
|
61
|
+
params[:token][:interval])
|
62
|
+
break
|
63
|
+
|
64
|
+
rescue TokenExists
|
65
|
+
next
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
{token: token, valid_to: expiration}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Revoke < HaveAPI::Action
|
74
|
+
# route ''
|
75
|
+
http_method :post
|
76
|
+
auth true
|
77
|
+
|
78
|
+
authorize do
|
79
|
+
allow
|
80
|
+
end
|
81
|
+
|
82
|
+
def exec
|
83
|
+
klass = self.class.resource.token_instance[@version]
|
84
|
+
klass.send(:revoke_token, request, current_user, klass.token(request))
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
class Renew < HaveAPI::Action
|
89
|
+
http_method :post
|
90
|
+
auth true
|
91
|
+
|
92
|
+
output(:hash) do
|
93
|
+
datetime :valid_to
|
94
|
+
end
|
95
|
+
|
96
|
+
authorize do
|
97
|
+
allow
|
98
|
+
end
|
99
|
+
|
100
|
+
def exec
|
101
|
+
klass = self.class.resource.token_instance[@version]
|
102
|
+
klass.send(:renew_token, request, current_user, klass.token(request))
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module HaveAPI
|
2
|
+
class Authorization
|
3
|
+
def initialize(&block)
|
4
|
+
@block = block
|
5
|
+
end
|
6
|
+
|
7
|
+
# Returns true if user is authorized.
|
8
|
+
# Block must call allow to authorize user, default rule is deny.
|
9
|
+
def authorized?(user)
|
10
|
+
@restrict = []
|
11
|
+
|
12
|
+
catch(:rule) do
|
13
|
+
instance_exec(user, &@block) if @block
|
14
|
+
deny # will not be called if block throws allow
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Apply restrictions on query which selects objects from database.
|
19
|
+
# Most common usage is restrict user to access only objects he owns.
|
20
|
+
def restrict(*args)
|
21
|
+
@restrict << args.first
|
22
|
+
end
|
23
|
+
|
24
|
+
# Restrict parameters client can set/change.
|
25
|
+
# [whitelist] allow only listed parameters
|
26
|
+
# [blacklist] allow all parameters except listed ones
|
27
|
+
def input(whitelist: nil, blacklist: nil)
|
28
|
+
@input = {
|
29
|
+
whitelist: whitelist,
|
30
|
+
blacklist: blacklist,
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
# Restrict parameters client can retrieve.
|
35
|
+
# [whitelist] allow only listed parameters
|
36
|
+
# [blacklist] allow all parameters except listed ones
|
37
|
+
def output(whitelist: nil, blacklist: nil)
|
38
|
+
@output = {
|
39
|
+
whitelist: whitelist,
|
40
|
+
blacklist: blacklist,
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def allow
|
45
|
+
throw(:rule, true)
|
46
|
+
end
|
47
|
+
|
48
|
+
def deny
|
49
|
+
throw(:rule, false)
|
50
|
+
end
|
51
|
+
|
52
|
+
def restrictions
|
53
|
+
ret = {}
|
54
|
+
|
55
|
+
@restrict.each do |r|
|
56
|
+
ret.update(r)
|
57
|
+
end
|
58
|
+
|
59
|
+
ret
|
60
|
+
end
|
61
|
+
|
62
|
+
def filter_input(input, params)
|
63
|
+
filter_inner(input, @input, params, false)
|
64
|
+
end
|
65
|
+
|
66
|
+
def filter_output(output, params, format = false)
|
67
|
+
filter_inner(output, @output, params, format)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
def filter_inner(allowed_params, direction, params, format)
|
72
|
+
allowed = {}
|
73
|
+
|
74
|
+
allowed_params.each do |p|
|
75
|
+
if params.has_param?(p.name)
|
76
|
+
allowed[p.name] = format ? p.format_output(params[p.name]) : params[p.name]
|
77
|
+
|
78
|
+
elsif params.has_param?(p.name.to_s) # FIXME: remove double checking
|
79
|
+
allowed[p.name] = format ? p.format_output(params[p.name.to_s]) : params[p.name.to_s]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
return allowed unless direction
|
84
|
+
|
85
|
+
if direction[:whitelist]
|
86
|
+
ret = {}
|
87
|
+
|
88
|
+
direction[:whitelist].each do |p|
|
89
|
+
ret[p] = allowed[p] if allowed && allowed[p]
|
90
|
+
end
|
91
|
+
|
92
|
+
ret
|
93
|
+
|
94
|
+
elsif direction[:blacklist]
|
95
|
+
ret = allowed.dup
|
96
|
+
|
97
|
+
direction[:blacklist].each do |p|
|
98
|
+
ret.delete(p)
|
99
|
+
end
|
100
|
+
|
101
|
+
ret
|
102
|
+
|
103
|
+
else
|
104
|
+
allowed
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module HaveAPI
|
2
|
+
class Common
|
3
|
+
class << self
|
4
|
+
attr_accessor :custom_attrs
|
5
|
+
|
6
|
+
def has_attr(name, default=nil)
|
7
|
+
@custom_attrs ||= []
|
8
|
+
@custom_attrs << name
|
9
|
+
|
10
|
+
instance_variable_set("@#{name}", default)
|
11
|
+
|
12
|
+
self.class.send(:define_method, name) do |value=nil|
|
13
|
+
if value.nil?
|
14
|
+
instance_variable_get("@#{name}")
|
15
|
+
else
|
16
|
+
instance_variable_set("@#{name}", value)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Called before subclass defines it's attributes (before has_attr or custom
|
22
|
+
# attr setting), so copy defaults from parent and let it override it.
|
23
|
+
def inherit_attrs(subclass)
|
24
|
+
return unless @custom_attrs
|
25
|
+
|
26
|
+
subclass.custom_attrs = []
|
27
|
+
|
28
|
+
@custom_attrs.each do |attr|
|
29
|
+
# puts "#{subclass}: Inherit #{attr} = #{instance_variable_get("@#{attr}")}"
|
30
|
+
subclass.method(attr).call(instance_variable_get("@#{attr}"))
|
31
|
+
subclass.custom_attrs << attr
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
has_attr :obj_type
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module HaveAPI
|
2
|
+
class Context
|
3
|
+
attr_accessor :server, :version, :resource, :action, :url, :args,
|
4
|
+
:params, :current_user, :authorization, :endpoint,
|
5
|
+
:action_instance, :action_prepare, :layout
|
6
|
+
|
7
|
+
def initialize(server, version: nil, resource: [], action: nil,
|
8
|
+
url: nil, args: nil, params: nil, user: nil,
|
9
|
+
authorization: nil, endpoint: nil)
|
10
|
+
@server = server
|
11
|
+
@version = version
|
12
|
+
@resource = resource
|
13
|
+
@action = action
|
14
|
+
@url = url
|
15
|
+
@args = args
|
16
|
+
@params = params
|
17
|
+
@current_user = user
|
18
|
+
@authorization = authorization
|
19
|
+
@endpoint = endpoint
|
20
|
+
end
|
21
|
+
|
22
|
+
def resolved_url
|
23
|
+
return @url unless @args
|
24
|
+
|
25
|
+
ret = @url.dup
|
26
|
+
|
27
|
+
@args.each do |arg|
|
28
|
+
resolve_arg!(ret, arg)
|
29
|
+
end
|
30
|
+
|
31
|
+
ret
|
32
|
+
end
|
33
|
+
|
34
|
+
def url_for(action, args=nil)
|
35
|
+
top_module = Kernel
|
36
|
+
top_route = @server.routes[@version]
|
37
|
+
|
38
|
+
action.to_s.split('::').each do |name|
|
39
|
+
top_module = top_module.const_get(name)
|
40
|
+
|
41
|
+
begin
|
42
|
+
top_module.obj_type
|
43
|
+
|
44
|
+
rescue NoMethodError
|
45
|
+
next
|
46
|
+
end
|
47
|
+
|
48
|
+
if top_module.obj_type == :resource
|
49
|
+
top_route = top_route[:resources][top_module]
|
50
|
+
else
|
51
|
+
top_route = top_route[:actions][top_module]
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
ret = top_route.dup
|
56
|
+
|
57
|
+
args.each { |arg| resolve_arg!(ret, arg) } if args
|
58
|
+
|
59
|
+
ret
|
60
|
+
end
|
61
|
+
|
62
|
+
def call_url_params(action, obj)
|
63
|
+
ret = params && action.resolve.call(obj)
|
64
|
+
|
65
|
+
return [ret] if ret && !ret.is_a?(Array)
|
66
|
+
ret
|
67
|
+
end
|
68
|
+
|
69
|
+
def url_with_params(action, obj)
|
70
|
+
url_for(action, call_url_params(action, obj))
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
def resolve_arg!(url, arg)
|
75
|
+
url.sub!(/:[a-zA-Z\-_]+/, arg.to_s)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module HaveAPI
|
2
|
+
class Example
|
3
|
+
def initialize(title)
|
4
|
+
@title = title
|
5
|
+
end
|
6
|
+
|
7
|
+
def request(f)
|
8
|
+
@request = f
|
9
|
+
end
|
10
|
+
|
11
|
+
def response(f)
|
12
|
+
@response = f
|
13
|
+
end
|
14
|
+
|
15
|
+
def comment(str)
|
16
|
+
@comment = str
|
17
|
+
end
|
18
|
+
|
19
|
+
def provided?
|
20
|
+
@request || @response || @comment
|
21
|
+
end
|
22
|
+
|
23
|
+
def describe
|
24
|
+
if provided?
|
25
|
+
{
|
26
|
+
title: @title,
|
27
|
+
request: @request,
|
28
|
+
response: @response,
|
29
|
+
comment: @comment
|
30
|
+
}
|
31
|
+
else
|
32
|
+
{}
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module HaveAPI::Extensions
|
2
|
+
class ActionExceptions < Base
|
3
|
+
class << self
|
4
|
+
def enabled
|
5
|
+
HaveAPI::Action.connect_hook(:exec_exception) do |ret, action, e|
|
6
|
+
break(ret) unless @exceptions
|
7
|
+
|
8
|
+
@exceptions.each do |handler|
|
9
|
+
if e.is_a?(handler[:klass])
|
10
|
+
ret = handler[:block].call(ret, e)
|
11
|
+
break
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
ret
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def rescue(klass, &block)
|
20
|
+
@exceptions ||= []
|
21
|
+
@exceptions << {klass: klass, block: block}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|