haveapi 0.3.2 → 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.
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 %>