haveapi-client 0.3.0 → 0.4.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 +4 -4
- data/CHANGELOG +17 -0
- data/README.md +14 -2
- data/haveapi-client.gemspec +5 -5
- data/lib/haveapi/cli.rb +5 -0
- data/lib/haveapi/cli/cli.rb +528 -277
- data/lib/haveapi/cli/command.rb +52 -0
- data/lib/haveapi/cli/output_formatter.rb +221 -0
- data/lib/haveapi/client.rb +5 -0
- data/lib/haveapi/client/action.rb +102 -98
- data/lib/haveapi/client/authentication/token.rb +1 -0
- data/lib/haveapi/client/client.rb +21 -3
- data/lib/haveapi/client/communicator.rb +161 -129
- data/lib/haveapi/client/exceptions.rb +23 -10
- data/lib/haveapi/client/parameters/resource.rb +29 -0
- data/lib/haveapi/client/parameters/typed.rb +72 -0
- data/lib/haveapi/client/params.rb +72 -0
- data/lib/haveapi/client/resource_instance.rb +2 -0
- data/lib/haveapi/client/validator.rb +55 -0
- data/lib/haveapi/client/validators/acceptance.rb +9 -0
- data/lib/haveapi/client/validators/confirmation.rb +18 -0
- data/lib/haveapi/client/validators/custom.rb +9 -0
- data/lib/haveapi/client/validators/exclusion.rb +9 -0
- data/lib/haveapi/client/validators/format.rb +16 -0
- data/lib/haveapi/client/validators/inclusion.rb +14 -0
- data/lib/haveapi/client/validators/length.rb +14 -0
- data/lib/haveapi/client/validators/numericality.rb +24 -0
- data/lib/haveapi/client/validators/presence.rb +11 -0
- data/lib/haveapi/client/version.rb +3 -2
- metadata +28 -25
@@ -8,11 +8,17 @@ class HaveAPI::Client::Client
|
|
8
8
|
# The client by default uses the default version of the API.
|
9
9
|
# API is asked for description only when needed or by calling #setup.
|
10
10
|
# +identity+ is sent in each request to the API in User-Agent header.
|
11
|
-
def initialize(url, v = nil, identity: 'haveapi-client')
|
11
|
+
def initialize(url, v = nil, identity: 'haveapi-client', communicator: nil)
|
12
12
|
@setup = false
|
13
13
|
@version = v
|
14
|
-
|
15
|
-
|
14
|
+
|
15
|
+
if communicator
|
16
|
+
@api = communicator
|
17
|
+
|
18
|
+
else
|
19
|
+
@api = HaveAPI::Client::Communicator.new(url, v)
|
20
|
+
@api.identity = identity
|
21
|
+
end
|
16
22
|
end
|
17
23
|
|
18
24
|
# Get the description from the API now.
|
@@ -36,6 +42,18 @@ class HaveAPI::Client::Client
|
|
36
42
|
@api.authenticate(*args)
|
37
43
|
end
|
38
44
|
|
45
|
+
# Get uthentication provider
|
46
|
+
# @return [HaveAPI::Client::Authentication::Base] if authenticated
|
47
|
+
# @return [nil] if not authenticated
|
48
|
+
def auth
|
49
|
+
@api.auth
|
50
|
+
end
|
51
|
+
|
52
|
+
# @see Communicator#compatible?
|
53
|
+
def compatible?
|
54
|
+
@api.compatible?
|
55
|
+
end
|
56
|
+
|
39
57
|
# Initialize the client if it is not yet initialized and call the resource
|
40
58
|
# if it exists.
|
41
59
|
def method_missing(symbol, *args)
|
@@ -4,182 +4,214 @@ require 'active_support/inflections'
|
|
4
4
|
require 'active_support/inflector'
|
5
5
|
require_rel '../../restclient_ext'
|
6
6
|
|
7
|
-
module HaveAPI
|
8
|
-
|
9
|
-
class
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
@auth_methods[name] = klass
|
16
|
-
end
|
7
|
+
module HaveAPI::Client
|
8
|
+
class Communicator
|
9
|
+
class << self
|
10
|
+
attr_reader :auth_methods
|
11
|
+
|
12
|
+
def register_auth_method(name, klass)
|
13
|
+
@auth_methods ||= {}
|
14
|
+
@auth_methods[name] = klass
|
17
15
|
end
|
16
|
+
end
|
18
17
|
|
19
|
-
|
20
|
-
|
18
|
+
attr_reader :url, :auth
|
19
|
+
attr_accessor :identity
|
21
20
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
21
|
+
def initialize(url, v = nil)
|
22
|
+
@url = url
|
23
|
+
@auth = Authentication::NoAuth.new(self, {}, {})
|
24
|
+
@rest = RestClient::Resource.new(@url)
|
25
|
+
@version = v
|
26
|
+
@identity = 'haveapi-client-ruby'
|
27
|
+
@desc = {}
|
28
|
+
end
|
30
29
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
30
|
+
# @return [:compatible] if perfectly compatible
|
31
|
+
# @return [:imperfect] if minor version differs
|
32
|
+
# @return [false] if not compatible
|
33
|
+
def compatible?
|
34
|
+
description_for(path_for, {describe: :versions})
|
35
|
+
@proto_version == HaveAPI::Client::PROTOCOL_VERSION ? :compatible
|
36
|
+
: :imperfect
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
38
|
+
rescue ProtocolError
|
39
|
+
false
|
40
|
+
end
|
40
41
|
|
41
|
-
|
42
|
-
|
43
|
-
|
42
|
+
# Authenticate user with selected +auth_method+.
|
43
|
+
# +auth_method+ is a name of registered authentication provider.
|
44
|
+
# +options+ are specific for each authentication provider.
|
45
|
+
def authenticate(auth_method, options = {})
|
46
|
+
desc = describe_api(@version)
|
44
47
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
+
@auth = self.class.auth_methods[auth_method].new(self, desc[:authentication][auth_method], options)
|
49
|
+
@rest = @auth.resource || @rest
|
50
|
+
end
|
48
51
|
|
49
|
-
|
50
|
-
|
52
|
+
def auth_save
|
53
|
+
@auth.save
|
54
|
+
end
|
51
55
|
|
52
|
-
|
53
|
-
|
56
|
+
def available_versions
|
57
|
+
description_for(path_for, {describe: :versions})
|
58
|
+
end
|
54
59
|
|
55
|
-
|
56
|
-
|
60
|
+
def describe_api(v=nil)
|
61
|
+
return @desc[v] if @desc.has_key?(v)
|
57
62
|
|
58
|
-
|
59
|
-
|
63
|
+
@desc[v] = description_for(path_for(v), v.nil? ? {describe: :default} : {})
|
64
|
+
end
|
60
65
|
|
61
|
-
|
62
|
-
|
66
|
+
def describe_resource(path)
|
67
|
+
tmp = describe_api(@version)
|
63
68
|
|
64
|
-
|
65
|
-
|
69
|
+
path.each do |r|
|
70
|
+
tmp = tmp[:resources][r.to_sym]
|
66
71
|
|
67
|
-
|
68
|
-
description_for(action.prepared_help)
|
72
|
+
return false unless tmp
|
69
73
|
end
|
70
74
|
|
71
|
-
|
72
|
-
|
75
|
+
tmp
|
76
|
+
end
|
73
77
|
|
74
|
-
|
75
|
-
|
78
|
+
def describe_action(action)
|
79
|
+
description_for(action.prepared_help)
|
80
|
+
end
|
76
81
|
|
77
|
-
|
78
|
-
|
82
|
+
def get_action(resources, action, args)
|
83
|
+
tmp = describe_api(@version)
|
79
84
|
|
80
|
-
|
85
|
+
resources.each do |r|
|
86
|
+
tmp = tmp[:resources][r.to_sym]
|
81
87
|
|
82
|
-
|
83
|
-
tmp[:actions].each do |_, v|
|
84
|
-
if v[:aliases].include?(action.to_s)
|
85
|
-
a = v
|
86
|
-
break
|
87
|
-
end
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
if a
|
92
|
-
obj = Action.new(self, action, a, args)
|
93
|
-
obj.resource_path = resources
|
94
|
-
obj
|
95
|
-
else
|
96
|
-
false
|
97
|
-
end
|
88
|
+
return false unless tmp
|
98
89
|
end
|
99
90
|
|
100
|
-
|
101
|
-
args = []
|
102
|
-
input_namespace = action.namespace(:input)
|
103
|
-
meta = nil
|
91
|
+
a = tmp[:actions][action]
|
104
92
|
|
105
|
-
|
106
|
-
|
107
|
-
|
93
|
+
unless a # search in aliases
|
94
|
+
tmp[:actions].each do |_, v|
|
95
|
+
if v[:aliases].include?(action.to_s)
|
96
|
+
a = v
|
97
|
+
break
|
98
|
+
end
|
108
99
|
end
|
100
|
+
end
|
109
101
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
102
|
+
if a
|
103
|
+
obj = Action.new(self, action, a, args)
|
104
|
+
obj.resource_path = resources
|
105
|
+
obj
|
106
|
+
else
|
107
|
+
false
|
108
|
+
end
|
109
|
+
end
|
114
110
|
|
115
|
-
|
116
|
-
|
111
|
+
def call(action, params, raw: false)
|
112
|
+
args = []
|
113
|
+
input_namespace = action.namespace(:input)
|
114
|
+
meta = nil
|
117
115
|
|
118
|
-
|
119
|
-
|
116
|
+
if params.is_a?(Hash) && params[:meta]
|
117
|
+
meta = params[:meta]
|
118
|
+
params.delete(:meta)
|
119
|
+
end
|
120
120
|
|
121
|
-
|
122
|
-
|
123
|
-
|
121
|
+
if %w(POST PUT).include?(action.http_method)
|
122
|
+
ns = {input_namespace => params}
|
123
|
+
ns[:_meta] = meta if meta
|
124
|
+
ns.update(@auth.request_payload)
|
124
125
|
|
125
|
-
|
126
|
-
|
126
|
+
args << ns.to_json
|
127
|
+
args << {content_type: :json, accept: :json, user_agent: @identity}.update(@auth.request_headers)
|
127
128
|
|
128
|
-
|
129
|
+
elsif %w(GET DELETE).include?(action.http_method)
|
130
|
+
get_params = {}
|
129
131
|
|
130
|
-
|
132
|
+
params.each do |k, v|
|
133
|
+
get_params["#{input_namespace}[#{k}]"] = v
|
131
134
|
end
|
132
135
|
|
133
|
-
|
134
|
-
|
136
|
+
meta.each do |k, v|
|
137
|
+
get_params["_meta[#{k}]"] = v # FIXME: read _meta namespace from the description
|
135
138
|
|
136
|
-
|
137
|
-
return error('Access forbidden. Bad user name or password? Not authorized?')
|
139
|
+
end if meta
|
138
140
|
|
139
|
-
|
140
|
-
|
141
|
-
end
|
141
|
+
args << {params: get_params.update(@auth.request_url_params), accept: :json, user_agent: @identity}.update(@auth.request_headers)
|
142
|
+
end
|
142
143
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
end
|
144
|
+
begin
|
145
|
+
response = parse(@rest[action.prepared_url].method(action.http_method.downcase.to_sym).call(*args))
|
146
|
+
|
147
|
+
rescue RestClient::Forbidden
|
148
|
+
return error('Access forbidden. Bad user name or password? Not authorized?')
|
149
149
|
|
150
|
+
rescue => e
|
151
|
+
return error("Fatal API error: #{e.inspect}")
|
152
|
+
end
|
153
|
+
|
154
|
+
if response[:status]
|
155
|
+
if raw
|
156
|
+
ok(JSON.pretty_generate(response[:response]))
|
150
157
|
else
|
151
|
-
|
158
|
+
ok(response[:response])
|
152
159
|
end
|
160
|
+
|
161
|
+
else
|
162
|
+
error(response[:message], response[:errors])
|
153
163
|
end
|
164
|
+
end
|
154
165
|
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
166
|
+
private
|
167
|
+
def ok(response)
|
168
|
+
{status: true, response: response}
|
169
|
+
end
|
159
170
|
|
160
|
-
|
161
|
-
|
162
|
-
|
171
|
+
def error(msg, errors={})
|
172
|
+
{status: false, message: msg, errors: errors}
|
173
|
+
end
|
163
174
|
|
164
|
-
|
165
|
-
|
175
|
+
def path_for(v=nil, r=nil)
|
176
|
+
ret = '/'
|
166
177
|
|
167
|
-
|
168
|
-
|
178
|
+
ret += "v#{v}/" if v
|
179
|
+
ret += r if r
|
169
180
|
|
170
|
-
|
171
|
-
|
181
|
+
ret
|
182
|
+
end
|
172
183
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
184
|
+
def description_for(path, query_params={})
|
185
|
+
ret = parse(@rest[path].get_options({
|
186
|
+
params: @auth.request_payload.update(@auth.request_url_params).update(query_params),
|
187
|
+
user_agent: @identity
|
188
|
+
}.update(@auth.request_headers)))
|
189
|
+
|
190
|
+
@proto_version = ret[:version]
|
191
|
+
p_v = HaveAPI::Client::PROTOCOL_VERSION
|
192
|
+
return ret[:response] if ret[:version] == p_v
|
193
|
+
|
194
|
+
unless ret[:version]
|
195
|
+
raise ProtocolError,
|
196
|
+
"Incompatible protocol version: the client uses v#{p_v} "+
|
197
|
+
"while the API server uses an unspecified version (pre 1.0)"
|
198
|
+
end
|
179
199
|
|
180
|
-
|
181
|
-
|
182
|
-
|
200
|
+
major1, minor1 = ret[:version].split('.')
|
201
|
+
major2, minor2 = p_v.split('.')
|
202
|
+
|
203
|
+
if major1 != major2
|
204
|
+
raise ProtocolError,
|
205
|
+
"Incompatible protocol version: the client uses v#{p_v} "+
|
206
|
+
"while the API server uses v#{ret[:version]}"
|
207
|
+
end
|
208
|
+
|
209
|
+
warn "The client uses protocol v#{p_v} while the API server uses v#{ret[:version]}"
|
210
|
+
ret[:response]
|
211
|
+
end
|
212
|
+
|
213
|
+
def parse(str)
|
214
|
+
JSON.parse(str, symbolize_names: true)
|
183
215
|
end
|
184
216
|
end
|
185
217
|
end
|
@@ -1,13 +1,26 @@
|
|
1
|
-
module HaveAPI
|
2
|
-
|
3
|
-
class ActionFailed < Exception
|
4
|
-
def initialize(response)
|
5
|
-
@response = response
|
6
|
-
end
|
1
|
+
module HaveAPI::Client
|
2
|
+
class ProtocolError < StandardError ; end
|
7
3
|
|
8
|
-
|
9
|
-
|
10
|
-
|
4
|
+
class ActionFailed < StandardError
|
5
|
+
def initialize(response)
|
6
|
+
@response = response
|
7
|
+
end
|
8
|
+
|
9
|
+
def message
|
10
|
+
"#{@response.action.name} failed: #{@response.message}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class ValidationError < ActionFailed
|
15
|
+
attr_reader :errors
|
16
|
+
|
17
|
+
def initialize(action, errors)
|
18
|
+
@action = action
|
19
|
+
@errors = errors
|
20
|
+
end
|
21
|
+
|
22
|
+
def message
|
23
|
+
"#{@action.name} failed: input parameters not valid"
|
11
24
|
end
|
12
25
|
end
|
13
|
-
end
|
26
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module HaveAPI::Client
|
2
|
+
class Parameters::Resource
|
3
|
+
attr_reader :errors
|
4
|
+
|
5
|
+
def initialize(params, desc, value)
|
6
|
+
@errors = []
|
7
|
+
@value = coerce(value)
|
8
|
+
end
|
9
|
+
|
10
|
+
def valid?
|
11
|
+
@errors.empty?
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_api
|
15
|
+
@value
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
def coerce(v)
|
20
|
+
if !v.is_a?(::Integer) && /\A\d+\z/ !~ v
|
21
|
+
@errors << 'not a valid resource id'
|
22
|
+
nil
|
23
|
+
|
24
|
+
else
|
25
|
+
v.to_i
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|