haveapi 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +12 -0
  3. data/Gemfile +10 -10
  4. data/README.md +19 -12
  5. data/Rakefile +23 -0
  6. data/doc/hooks.erb +35 -0
  7. data/doc/index.md +1 -0
  8. data/doc/json-schema.erb +369 -0
  9. data/doc/protocol.md +178 -38
  10. data/doc/protocol.plantuml +220 -0
  11. data/haveapi.gemspec +6 -10
  12. data/lib/haveapi/action.rb +35 -6
  13. data/lib/haveapi/api.rb +22 -5
  14. data/lib/haveapi/common.rb +7 -0
  15. data/lib/haveapi/exceptions.rb +7 -0
  16. data/lib/haveapi/hooks.rb +19 -8
  17. data/lib/haveapi/model_adapters/active_record.rb +58 -19
  18. data/lib/haveapi/output_formatter.rb +8 -5
  19. data/lib/haveapi/output_formatters/json.rb +6 -1
  20. data/lib/haveapi/params/param.rb +33 -39
  21. data/lib/haveapi/params/resource.rb +20 -0
  22. data/lib/haveapi/params.rb +26 -4
  23. data/lib/haveapi/public/doc/protocol.png +0 -0
  24. data/lib/haveapi/resource.rb +2 -7
  25. data/lib/haveapi/server.rb +87 -26
  26. data/lib/haveapi/tasks/hooks.rb +3 -0
  27. data/lib/haveapi/tasks/yard.rb +12 -0
  28. data/lib/haveapi/validator.rb +134 -0
  29. data/lib/haveapi/validator_chain.rb +99 -0
  30. data/lib/haveapi/validators/acceptance.rb +38 -0
  31. data/lib/haveapi/validators/confirmation.rb +46 -0
  32. data/lib/haveapi/validators/custom.rb +21 -0
  33. data/lib/haveapi/validators/exclusion.rb +38 -0
  34. data/lib/haveapi/validators/format.rb +42 -0
  35. data/lib/haveapi/validators/inclusion.rb +42 -0
  36. data/lib/haveapi/validators/length.rb +71 -0
  37. data/lib/haveapi/validators/numericality.rb +104 -0
  38. data/lib/haveapi/validators/presence.rb +40 -0
  39. data/lib/haveapi/version.rb +2 -1
  40. data/lib/haveapi/views/doc_sidebars/json-schema.erb +7 -0
  41. data/lib/haveapi/views/main_layout.erb +11 -0
  42. data/lib/haveapi/views/version_page.erb +26 -3
  43. data/lib/haveapi.rb +7 -4
  44. metadata +45 -66
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 503ec7b15af7ae22f652f749407b54fa61c0fe91
4
- data.tar.gz: 9a1fe935c87601e227b3a1038bef6bd71c4d6280
3
+ metadata.gz: 97a0780e86ee6d9273d743c2c6b7c87ba053b19d
4
+ data.tar.gz: aa2f1bda2b113b1ea0452da5940dabc1617bf68e
5
5
  SHA512:
6
- metadata.gz: 7234a6fb2a44fd3ab8ed68efe5363565944ca191c4617b40474da3658f922681ae7a16b7706c1ab58b31f9c806ea2fe571531b4a3c5b03e567c3413e86bd7abd
7
- data.tar.gz: f994d620f9847d66e35fdc25dcb0f3857a4e4f1dfc2acf7e6b9d74b416d013af0b09fe2217957fecbf956a862e5a722c68550e42238093e8b5a49a8fb9fef9ef
6
+ metadata.gz: f665c8857e439f36280aae5634f3ff98cce618d645f6b4104a6d00c7a82727bd787edffed0ca7ed2f3daf60646711105e2d408b58f2557b40e773916a665afac
7
+ data.tar.gz: 29bcfa9af9295e10bde9ead47df70bca5d185ea79ea656660f80a26b4ed54cf3f35ea6e142b88f2f7ff6480f92080c4172d8e8986f912700bd6d57f0f1fcd34c
data/CHANGELOG ADDED
@@ -0,0 +1,12 @@
1
+ * Wed Jan 20 2016 - version 0.4.0
2
+ - Introduced protocol version, currently 1.0
3
+ - Renamed certain API configuration methods
4
+ - Document defined hooks in yardoc
5
+ - Implicit API version
6
+ - Include input parameter validators in the protocol
7
+ - Present validator from ActiveRecord is not imported to controller
8
+ - Clean-up dependencies
9
+ - Cross-link resources in online API documentation
10
+ - JSON schema of the documentation protocol
11
+ - UML diagram representing the documentation protocol
12
+ - Improved error reporting in action definition
data/Gemfile CHANGED
@@ -1,12 +1,12 @@
1
1
  source 'https://rubygems.org'
2
+ gemspec
2
3
 
3
- gem 'activerecord'
4
- gem 'require_all'
5
- gem 'json'
6
- gem 'sinatra'
7
- gem 'mysql'
8
- gem 'sinatra-activerecord'
9
- gem 'rake'
10
- gem 'rspec'
11
- gem 'rack-test'
12
- gem 'railties'
4
+ group :test do
5
+ gem 'rspec'
6
+ gem 'rack-test'
7
+ end
8
+
9
+ group :activerecord do
10
+ gem 'activerecord', '~> 4.1.14'
11
+ gem 'sinatra-activerecord', '~> 2.0.9'
12
+ end
data/README.md CHANGED
@@ -2,7 +2,7 @@ HaveAPI
2
2
  =======
3
3
  A framework for creating self-describing APIs in Ruby.
4
4
 
5
- Note: HaveAPI is under heavy development. It is not stable, its interface may change.
5
+ Note: HaveAPI is in heavy development. It is not stable, its interface may change.
6
6
 
7
7
  ## What is a self-describing API?
8
8
  A self-describing API responds to HTTP method `OPTIONS` and returns description
@@ -14,29 +14,33 @@ Clients use the self-description to learn how to communicate with the API,
14
14
  which they otherwise know nothing about.
15
15
 
16
16
  ## Main features
17
- - Creates RESTful APIs
17
+ - Creates RESTful APIs usable even with simple HTTP client, should it be needed
18
18
  - Handles network communication, input/output formats and parameters
19
19
  on both server and client, you need only to define resources and actions
20
- - By writing the code you get the documentation which is available to all clients
20
+ - By writing the code you get the documentation for free, it is available to all clients
21
21
  - Auto-generated online HTML documentation
22
22
  - Generic interface for clients - one client can be used to access all APIs
23
23
  using this framework
24
24
  - Ruby, PHP and JavaScript clients already available
25
25
  - A change in the API is immediately reflected in all clients
26
26
  - Supports API versioning
27
- - Ready for ActiveRecord - validators from models are included in the
28
- self-description
27
+ - ORM integration
28
+ - ActiveRecord
29
+ - integrated with controller
30
+ - loads and documents validators from models
31
+ - eager loading
32
+ - easy to support other ORM frameworks
29
33
 
30
34
  ## Usage
31
35
  This text might not be complete or up-to-date, as things still often change.
32
36
  Full use of HaveAPI may be seen
33
- in [vpsadminapi](https://github.com/vpsfreecz/vpsadminapi), which may serve
34
- as an example of how are things meant to be used.
37
+ in [vpsadmin-api](https://github.com/vpsfreecz/vpsadmin-api)
38
+ ([online doc](https://api.vpsfree.cz)), which may serve as an example of how
39
+ are things meant to be used.
35
40
 
36
41
  All resources and actions are represented by classes. They all must be stored
37
- in a module, whose name is later given to HaveAPI.
38
-
39
- HaveAPI then searches all classes in that module and constructs your API.
42
+ in a module, whose name is later given to HaveAPI. HaveAPI then searches all
43
+ classes in this module and builds your API.
40
44
 
41
45
  For the purposes of this document, all resources will be in module `MyAPI`.
42
46
 
@@ -136,7 +140,7 @@ module MyAPI
136
140
 
137
141
  # Helper method returning a query for all users
138
142
  def query
139
- ::User.all
143
+ ::User.all
140
144
  end
141
145
 
142
146
  # This method is called if the request has meta[:count] = true
@@ -215,7 +219,6 @@ class BasicAuth < HaveAPI::Authentication::Basic::Provider
215
219
  end
216
220
 
217
221
  api.use_version(:all)
218
- api.set_default_version(1)
219
222
  api.auth_chain << BasicAuth
220
223
  api.mount('/')
221
224
 
@@ -254,6 +257,10 @@ is not self-described.
254
257
  If the user is authenticated when requesting self-description, only allowed
255
258
  resources, actions and parameters will be returned.
256
259
 
260
+ ## Validation of input data
261
+ Because validators are a part of the API's documentation, the clients can
262
+ perform client-side validation before the data is sent to the API server.
263
+
257
264
  ## Available clients
258
265
  These clients completely rely on the API description and can be used for all
259
266
  APIs that are using HaveAPI.
data/Rakefile CHANGED
@@ -1,7 +1,30 @@
1
1
  require 'bundler/gem_tasks'
2
2
  require 'rspec/core'
3
3
  require 'rspec/core/rake_task'
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'haveapi'
6
+ require 'haveapi/tasks/yard'
4
7
 
5
8
  RSpec::Core::RakeTask.new(:spec) do |spec|
6
9
  spec.pattern = FileList['spec/**/*_spec.rb']
7
10
  end
11
+
12
+ begin
13
+ require 'yard'
14
+
15
+ YARD::Rake::YardocTask.new do |t|
16
+ t.files = ['lib/**/*.rb']
17
+ t.options = [
18
+ '--protected',
19
+ '--output-dir=html_doc',
20
+ '--files=doc/*.md',
21
+ '--files=doc/*.html'
22
+ ]
23
+ t.before = Proc.new do
24
+ document_hooks.call
25
+ render_doc_file('doc/json-schema.erb', 'doc/JSON-Schema.html').call
26
+ end
27
+ end
28
+
29
+ rescue LoadError
30
+ end
data/doc/hooks.erb ADDED
@@ -0,0 +1,35 @@
1
+ <%
2
+ def render_hash(hash)
3
+ return 'none' unless hash
4
+ '<dl>' + hash.map { |k,v| "<dt>#{k}</dt><dd>#{v}</dd>" }.join('') + '</dl>'
5
+ end
6
+ %>
7
+ # Hooks
8
+ <% HaveAPI::Hooks.hooks.each do |klass, hooks| %>
9
+ ##<%= klass %>
10
+ <% hooks.each do |name, hook| %>
11
+ ### <%= name %>
12
+ <table>
13
+ <tr>
14
+ <td style="vertical-align: top;">Description:</td>
15
+ <td><%= hook[:desc] %></td>
16
+ </tr>
17
+ <tr>
18
+ <td style="vertical-align: top;">Context:</td>
19
+ <td><%= hook[:context] || 'current' %></td>
20
+ </tr>
21
+ <tr>
22
+ <td style="vertical-align: top;">Arguments:</td>
23
+ <td><%= render_hash(hook[:args]) %></td>
24
+ </tr>
25
+ <tr>
26
+ <td style="vertical-align: top;">Initial value:</td>
27
+ <td><%= render_hash(hook[:initial]) %></td>
28
+ </tr>
29
+ <tr>
30
+ <td style="vertical-align: top;">Return value:</td>
31
+ <td><%= render_hash(hook[:ret]) %></td>
32
+ </tr>
33
+ </table>
34
+ <% end %>
35
+ <% end %>
data/doc/index.md CHANGED
@@ -3,4 +3,5 @@ HaveAPI documentation
3
3
 
4
4
  - [README](doc/readme)
5
5
  - [Protocol definition](doc/protocol.md)
6
+ - [JSON schema of the documentation protocol](doc/json-schema)
6
7
  - [How to create a client](doc/create-client.md)
@@ -0,0 +1,369 @@
1
+ <%
2
+ require 'json'
3
+
4
+ DEFINITIONS = {
5
+ version: {
6
+ type: :object,
7
+ properties: {
8
+ authentication: {
9
+ type: :object,
10
+ properties: {
11
+ basic: { '$ref' => '#/definitions/auth_basic' },
12
+ token: { '$ref' => '#/definitions/auth_token' },
13
+ }
14
+ },
15
+ resources: {
16
+ type: :object,
17
+ '$ref' => '#/definitions/resources'
18
+ },
19
+ meta: {
20
+ type: :object,
21
+ properties: {
22
+ namespace: {
23
+ type: :string,
24
+ default: '_meta'
25
+ }
26
+ }
27
+ },
28
+ help: { type: :string }
29
+ }
30
+ },
31
+
32
+ auth_basic: {
33
+ type: :object,
34
+ },
35
+
36
+ auth_token: {
37
+ type: :object,
38
+ properties: {
39
+ http_header: {
40
+ type: :string,
41
+ default: 'X-HaveAPI-Auth-Token'
42
+ },
43
+ query_parameter: {
44
+ type: :string,
45
+ default: '_auth_token'
46
+ },
47
+ resources: {
48
+ type: :object,
49
+ '$ref' => '#/definitions/resources'
50
+ }
51
+ }
52
+ },
53
+
54
+ resources: {
55
+ type: :object,
56
+ patternProperties: {
57
+ '^[a-z_]+$' => {
58
+ type: :object,
59
+ properties: {
60
+ description: { type: :string },
61
+ actions: {
62
+ '$ref' => '#/definitions/actions'
63
+ },
64
+ resources: {
65
+ '$ref' => '#/definitions/resources'
66
+ }
67
+ }
68
+ }
69
+ }
70
+ },
71
+
72
+ actions: {
73
+ type: :object,
74
+ patternProperties: {
75
+ '^[a-z_]+$' => {
76
+ type: :object,
77
+ properties: {
78
+ auth: { type: :boolean },
79
+ description: { type: :string },
80
+ aliases: {
81
+ type: :array,
82
+ items: { type: :string }
83
+ },
84
+ input: { '$ref' => '#/definitions/input_parameters' },
85
+ output: { '$ref' => '#/definitions/output_parameters' },
86
+ meta: { '$ref' => '#/definitions/action_meta' },
87
+ examples: {
88
+ type: :object,
89
+ properties: {
90
+ title: { type: :string },
91
+ request: { type: :object },
92
+ response: { type: :object },
93
+ comment: { type: :string },
94
+ }
95
+ },
96
+ url: { type: :string },
97
+ method: { type: :string },
98
+ help: { type: :string }
99
+ }
100
+ }
101
+ }
102
+
103
+ },
104
+
105
+ input_parameters: {
106
+ type: :object,
107
+ properties: {
108
+ parameters: {
109
+ type: :object,
110
+ patternProperties: {
111
+ '^[a-z_]+$' => {
112
+ type: :object,
113
+ oneOf: [
114
+ {
115
+ title: 'Data type',
116
+ type: :object,
117
+ properties: {
118
+ required: { type: :boolean },
119
+ label: { type: :string },
120
+ description: { type: :string },
121
+ type: {
122
+ type: :string,
123
+ enum: %w(String Text Integer Float Datetime Boolean)
124
+ },
125
+ validators: { '$ref' => '#/definitions/input_validators' },
126
+ default: {}
127
+ }
128
+ },
129
+ {
130
+ title: 'Resource',
131
+ type: :object,
132
+ properties: {
133
+ required: { type: :boolean },
134
+ label: { type: :string },
135
+ description: { type: :string },
136
+ type: {
137
+ type: :string,
138
+ enum: %w(Resource)
139
+ },
140
+ resource: { type: :array },
141
+ value_id: { type: :string },
142
+ value_label: { type: :string },
143
+ value: {
144
+ type: :object,
145
+ properties: {
146
+ url: { type: :string },
147
+ method: { type: :string },
148
+ help: { type: :string },
149
+ }
150
+ },
151
+ choices: {
152
+ type: :object,
153
+ properties: {
154
+ url: { type: :string },
155
+ method: { type: :string },
156
+ help: { type: :string },
157
+ }
158
+ }
159
+ }
160
+ }
161
+ ]
162
+ }
163
+ }
164
+ },
165
+ layout: {},
166
+ namespace: {}
167
+ }
168
+ },
169
+
170
+ input_validators: {
171
+ type: :object,
172
+ properties: {
173
+ accept: {
174
+ type: :object,
175
+ properties: {
176
+ value: {},
177
+ message: { type: :string }
178
+ }
179
+ },
180
+ confirm: {
181
+ type: :object,
182
+ properties: {
183
+ equal: { type: :boolean },
184
+ parameter: { type: :string },
185
+ message: { type: :string }
186
+ }
187
+ },
188
+ custom: { type: :string },
189
+ exclude: {
190
+ type: :object,
191
+ properties: {
192
+ values: { type: :array },
193
+ message: { type: :string }
194
+ }
195
+ },
196
+ format: {
197
+ type: :object,
198
+ properties: {
199
+ rx: { type: :string },
200
+ match: { type: :boolean },
201
+ description: { type: :string },
202
+ message: { type: :string }
203
+ }
204
+ },
205
+ include: {
206
+ type: :object,
207
+ properties: {
208
+ values: {
209
+ oneOf: [
210
+ {
211
+ title: 'Array of allowed values',
212
+ type: :array
213
+ },
214
+ {
215
+ title: 'Hash of allowed values',
216
+ type: :object
217
+ }
218
+
219
+ ]
220
+ },
221
+ message: { type: :string }
222
+ }
223
+ },
224
+ length: {
225
+ oneOf: [
226
+ {
227
+ title: 'Equality',
228
+ type: :object,
229
+ properties: {
230
+ equals: { type: :integer },
231
+ message: { type: :string },
232
+ }
233
+ },
234
+ {
235
+ title: 'Interval',
236
+ type: :object,
237
+ properties: {
238
+ min: { type: :integer },
239
+ max: { type: :integer },
240
+ message: { type: :string },
241
+ }
242
+ }
243
+ ]
244
+ },
245
+ number: {
246
+ type: :object,
247
+ properties: {
248
+ min: { type: :number },
249
+ max: { type: :number },
250
+ step: { type: :number },
251
+ mod: { type: :integer },
252
+ odd: { type: :boolean },
253
+ even: { type: :boolean },
254
+ message: { type: :string },
255
+ }
256
+ },
257
+ present: {
258
+ type: :object,
259
+ properties: {
260
+ empty: { type: :boolean },
261
+ message: { type: :string },
262
+ }
263
+ }
264
+ }
265
+ },
266
+
267
+ output_parameters: {
268
+ type: :object,
269
+ properties: {
270
+ parameters: {},
271
+ layout: {},
272
+ namespace: {}
273
+ }
274
+ },
275
+
276
+ action_meta: {
277
+ type: :object,
278
+ properties: {
279
+ object: {
280
+ input: { '$ref' => '#/definitions/input_parameters' },
281
+ output: { '$ref' => '#/definitions/output_parameters' },
282
+ },
283
+ global: {
284
+ input: { '$ref' => '#/definitions/input_parameters' },
285
+ output: { '$ref' => '#/definitions/output_parameters' },
286
+ }
287
+ }
288
+ }
289
+ }
290
+
291
+ ROOTS = {
292
+ all: {
293
+ title: 'Describe all API versions',
294
+ type: :object,
295
+ properties: {
296
+ default_version: {},
297
+ versions: {
298
+ type: :object,
299
+ patternProperties: {
300
+ '^.+$' => { '$ref' => '#/definitions/version' }
301
+ },
302
+ properties: {
303
+ default: { '$ref' => '#/definitions/version' }
304
+ },
305
+ }
306
+ },
307
+ required: %i(default_version versions)
308
+ },
309
+
310
+ versions: {
311
+ title: 'Show available API versions',
312
+ type: :object,
313
+ properties: {
314
+ versions: { type: :array },
315
+ default: {}
316
+ },
317
+ required: %i(versions default)
318
+ },
319
+
320
+ default: {
321
+ title: 'Describe only the default version of the API',
322
+ '$ref' => '#/definitions/version'
323
+ },
324
+
325
+ envelope: {
326
+ title: 'All response are wrapped in this envelope',
327
+ type: :object,
328
+ properties: {
329
+ version: {},
330
+ status: { type: :boolean },
331
+ response: { type: :object },
332
+ message: { type: :string },
333
+ errors: {
334
+ type: :object,
335
+ patternProperties: {
336
+ '^.+$' => { type: :array }
337
+ },
338
+ },
339
+ },
340
+ required: ['status'],
341
+ }
342
+ }
343
+
344
+ urls = {
345
+ '/' => {
346
+ root: :all,
347
+ definitions: true
348
+ },
349
+ '/?describe=versions' => {
350
+ root: :versions
351
+ },
352
+ '/?describe=default' => {
353
+ root: :default,
354
+ definitions: true
355
+ },
356
+ }
357
+ %>
358
+
359
+ <h1 id="envelope">Envelope</h1>
360
+ <pre><code><%= JSON.pretty_generate(ROOTS[:envelope]) %></code></pre>
361
+
362
+ <%
363
+ urls.each do |url, opts|
364
+ hash = ROOTS[opts[:root]]
365
+ hash = hash.merge(DEFINITIONS) if opts[:definitions]
366
+ %>
367
+ <h1 id="<%= opts[:root] %>">OPTIONS <%= url %></h1>
368
+ <pre><code><%= JSON.pretty_generate(hash) %></code></pre>
369
+ <% end %>