haveapi 0.3.0
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/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
|