brainstem 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +147 -0
  3. data/Gemfile.lock +68 -39
  4. data/lib/brainstem/api_docs.rb +9 -4
  5. data/lib/brainstem/api_docs/atlas.rb +3 -3
  6. data/lib/brainstem/api_docs/controller.rb +12 -4
  7. data/lib/brainstem/api_docs/controller_collection.rb +11 -2
  8. data/lib/brainstem/api_docs/endpoint.rb +17 -7
  9. data/lib/brainstem/api_docs/endpoint_collection.rb +9 -1
  10. data/lib/brainstem/api_docs/formatters/open_api_specification/helper.rb +19 -16
  11. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/endpoint/param_definitions_formatter.rb +52 -80
  12. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/endpoint/response_definitions_formatter.rb +64 -84
  13. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/endpoint_formatter.rb +1 -1
  14. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/endpoint_param_formatter.rb +39 -0
  15. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/presenter_field_formatter.rb +147 -0
  16. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/response_field_formatter.rb +146 -0
  17. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/presenter_formatter.rb +53 -55
  18. data/lib/brainstem/api_docs/formatters/open_api_specification/version_2/tags_formatter.rb +1 -1
  19. data/lib/brainstem/api_docs/presenter.rb +16 -8
  20. data/lib/brainstem/api_docs/presenter_collection.rb +8 -5
  21. data/lib/brainstem/api_docs/sinks/open_api_specification_sink.rb +3 -1
  22. data/lib/brainstem/cli/generate_api_docs_command.rb +4 -0
  23. data/lib/brainstem/concerns/controller_dsl.rb +90 -20
  24. data/lib/brainstem/concerns/presenter_dsl.rb +16 -8
  25. data/lib/brainstem/dsl/association.rb +12 -0
  26. data/lib/brainstem/dsl/fields_block.rb +1 -1
  27. data/lib/brainstem/version.rb +1 -1
  28. data/spec/brainstem/api_docs/controller_spec.rb +127 -5
  29. data/spec/brainstem/api_docs/endpoint_spec.rb +489 -57
  30. data/spec/brainstem/api_docs/formatters/open_api_specification/helper_spec.rb +15 -4
  31. data/spec/brainstem/api_docs/formatters/open_api_specification/version_2/endpoint/param_definitions_formatter_spec.rb +112 -66
  32. data/spec/brainstem/api_docs/formatters/open_api_specification/version_2/endpoint/response_definitions_formatter_spec.rb +404 -32
  33. data/spec/brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/endpoint_param_formatter_spec.rb +335 -0
  34. data/spec/brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/presenter_field_formatter_spec.rb +237 -0
  35. data/spec/brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/response_field_formatter_spec.rb +413 -0
  36. data/spec/brainstem/api_docs/formatters/open_api_specification/version_2/presenter_formatter_spec.rb +116 -4
  37. data/spec/brainstem/api_docs/presenter_spec.rb +406 -24
  38. data/spec/brainstem/cli/generate_api_docs_command_spec.rb +8 -0
  39. data/spec/brainstem/concerns/controller_dsl_spec.rb +606 -45
  40. data/spec/brainstem/concerns/presenter_dsl_spec.rb +34 -2
  41. data/spec/brainstem/dsl/association_spec.rb +54 -3
  42. metadata +11 -2
@@ -0,0 +1,413 @@
1
+ require 'spec_helper'
2
+ require 'brainstem/api_docs/formatters/open_api_specification/version_2/field_definitions/response_field_formatter'
3
+
4
+ module Brainstem
5
+ module ApiDocs
6
+ module Formatters
7
+ module OpenApiSpecification
8
+ module Version2
9
+ module FieldDefinitions
10
+ describe ResponseFieldFormatter do
11
+ describe '#format' do
12
+ let(:endpoint) { OpenStruct.new(controller_name: 'Test', action: 'create') }
13
+ let(:field_name) { 'sprocket' }
14
+ let(:configuration_tree) { field_configuration_tree.with_indifferent_access }
15
+
16
+ subject { described_class.new(endpoint, field_name, configuration_tree).format }
17
+
18
+ context 'when formatting non-nested field' do
19
+ let(:field_configuration_tree) do
20
+ {
21
+ _config: {
22
+ type: 'string',
23
+ info: 'The name of the sprocket'
24
+ }
25
+ }
26
+ end
27
+
28
+ it 'returns the formatted field schema' do
29
+ expect(subject).to eq(
30
+ 'type' => 'string',
31
+ 'description' => 'The name of the sprocket.',
32
+ )
33
+ end
34
+ end
35
+
36
+ context 'when formatting nested field' do
37
+ context 'when formatting an array field' do
38
+ let(:field_configuration_tree) do
39
+ {
40
+ _config: {
41
+ type: 'array',
42
+ item_type: 'long',
43
+ },
44
+ }
45
+ end
46
+
47
+ it 'returns the formatted field schema' do
48
+ expect(subject).to eq(
49
+ 'type' => 'array',
50
+ 'items' => {
51
+ 'type' => 'integer', 'format' => 'int64'
52
+ }
53
+ )
54
+ end
55
+ end
56
+
57
+ context 'when formatting a nested array field with non nested data type' do
58
+ let(:field_configuration_tree) do
59
+ {
60
+ _config: {
61
+ type: 'array',
62
+ nested_levels: 3,
63
+ item_type: 'decimal',
64
+ },
65
+ }
66
+ end
67
+
68
+ it 'returns the formatted field schema' do
69
+ expect(subject).to eq(
70
+ 'type' => 'array',
71
+ 'items' => {
72
+ 'type' => 'array',
73
+ 'items' => {
74
+ 'type' => 'array',
75
+ 'items' => {
76
+ 'type' => 'number',
77
+ 'format' => 'float'
78
+ }
79
+ }
80
+ }
81
+ )
82
+ end
83
+ end
84
+
85
+ context 'when formatting a nested array field with objects' do
86
+ let(:field_configuration_tree) do
87
+ {
88
+ _config: {
89
+ type: 'array',
90
+ nested_levels: 2,
91
+ item_type: 'hash',
92
+ },
93
+ widget_name: {
94
+ _config: {
95
+ required: true,
96
+ type: 'string',
97
+ info: 'the name of the widget',
98
+ nodoc: false
99
+ },
100
+ },
101
+ widget_permissions: {
102
+ _config: {
103
+ type: 'array',
104
+ item_type: 'hash',
105
+ info: 'the permissions of the widget',
106
+ nodoc: false
107
+ },
108
+ can_edit: {
109
+ _config: {
110
+ type: 'array',
111
+ nested_levels: 3,
112
+ item_type: 'boolean',
113
+ nodoc: false
114
+ },
115
+ }
116
+ },
117
+ }
118
+ end
119
+
120
+ it 'formats a complicated tree with arrays and hashes as children' do
121
+ expect(subject).to eq(
122
+ 'type' => 'array',
123
+ 'items' => {
124
+ 'type' => 'array',
125
+ 'items' => {
126
+ 'type' => 'object',
127
+ 'properties' => {
128
+ 'widget_name' => {
129
+ 'type' => 'string',
130
+ 'description' => 'The name of the widget.',
131
+ },
132
+ 'widget_permissions' => {
133
+ 'type' => 'array',
134
+ 'description' => 'The permissions of the widget.',
135
+ 'items' => {
136
+ 'type' => 'object',
137
+ 'properties' => {
138
+ 'can_edit' => {
139
+ 'type' => 'array',
140
+ 'items' => {
141
+ 'type' => 'array',
142
+ 'items' => {
143
+ 'type' => 'array',
144
+ 'items' => {
145
+ 'type' => 'boolean'
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ }
156
+ )
157
+ end
158
+ end
159
+
160
+ context 'when formatting a hash field' do
161
+ let(:field_configuration_tree) do
162
+ {
163
+ _config: {
164
+ type: 'hash',
165
+ info: 'Details about the widget',
166
+ },
167
+ widget_name: {
168
+ _config: {
169
+ required: true,
170
+ type: 'string',
171
+ info: 'the name of the widget',
172
+ nodoc: false
173
+ },
174
+ },
175
+ widget_permissions: {
176
+ _config: {
177
+ type: 'array',
178
+ item_type: 'string',
179
+ info: 'the permissions of the widget',
180
+ nodoc: false
181
+ },
182
+ },
183
+ }
184
+ end
185
+
186
+ it 'returns the formatted field schema' do
187
+ expect(subject).to eq(
188
+ 'type' => 'object',
189
+ 'description' => 'Details about the widget.',
190
+ 'properties' => {
191
+ 'widget_name' => {
192
+ 'type' => 'string',
193
+ 'description' => 'The name of the widget.',
194
+ },
195
+ 'widget_permissions' => {
196
+ 'type' => 'array',
197
+ 'description' => 'The permissions of the widget.',
198
+ 'items' => {
199
+ 'type' => 'string',
200
+ }
201
+ }
202
+ }
203
+ )
204
+ end
205
+ end
206
+
207
+ context 'when formatting a multi nested hash field' do
208
+ let(:field_configuration_tree) do
209
+ {
210
+ _config: {
211
+ type: 'hash',
212
+ info: 'Details about the widget',
213
+ },
214
+ widget_name: {
215
+ _config: {
216
+ required: true,
217
+ type: 'string',
218
+ info: 'the name of the widget',
219
+ nodoc: false
220
+ },
221
+ },
222
+ widget_permissions: {
223
+ _config: {
224
+ type: 'hash',
225
+ info: 'the permissions of the widget',
226
+ nodoc: false
227
+ },
228
+ can_edit: {
229
+ _config: {
230
+ type: 'boolean',
231
+ info: 'can edit the widget',
232
+ nodoc: false
233
+ }
234
+ }
235
+ },
236
+ }
237
+ end
238
+
239
+ it 'returns the formatted field schema' do
240
+ expect(subject).to eq(
241
+ 'type' => 'object',
242
+ 'description' => 'Details about the widget.',
243
+ 'properties' => {
244
+ 'widget_name' => {
245
+ 'type' => 'string',
246
+ 'description' => 'The name of the widget.',
247
+ },
248
+ 'widget_permissions' => {
249
+ 'type' => 'object',
250
+ 'description' => 'The permissions of the widget.',
251
+ 'properties' => {
252
+ 'can_edit' => {
253
+ 'type' => 'boolean',
254
+ 'description' => 'Can edit the widget.',
255
+ }
256
+ }
257
+ }
258
+ }
259
+ )
260
+ end
261
+ end
262
+ end
263
+
264
+ context 'dynamic keys' do
265
+ context 'when formatting a hash field' do
266
+ let(:field_configuration_tree) do
267
+ {
268
+ _config: {
269
+ type: 'hash',
270
+ info: 'Dynamic keys hash.',
271
+ },
272
+ _dynamic_key: {
273
+ _config: {
274
+ nodoc: false,
275
+ type: 'hash',
276
+ dynamic_key: true,
277
+ info: 'a dynamic description.'
278
+ },
279
+ blah: {
280
+ _config: {
281
+ nodoc: false,
282
+ type: 'string',
283
+ },
284
+ },
285
+ },
286
+ }
287
+ end
288
+
289
+ it 'returns the formatted field schema' do
290
+ expect(subject).to eq({
291
+ 'type' => 'object',
292
+ 'description' => 'Dynamic keys hash.',
293
+ 'additionalProperties' => {
294
+ 'type' => 'object',
295
+ 'description' => 'A dynamic description.',
296
+ 'properties' => {
297
+ 'blah' => {
298
+ 'type' => 'string',
299
+ },
300
+ },
301
+ },
302
+ })
303
+ end
304
+ end
305
+
306
+ context 'when formatting a nested hash field' do
307
+ let(:field_configuration_tree) do
308
+ {
309
+ _config: {
310
+ type: 'hash',
311
+ info: 'Dynamic keys hash.',
312
+ },
313
+ non_dynamic_key: {
314
+ _config: {
315
+ nodoc: false,
316
+ type: 'hash',
317
+ info: 'A non-dynamic description.'
318
+ },
319
+ non_dynamic_property: {
320
+ _config: {
321
+ nodoc: false,
322
+ type: 'string',
323
+ },
324
+ },
325
+ },
326
+ _dynamic_key: {
327
+ _config: {
328
+ nodoc: false,
329
+ type: 'hash',
330
+ dynamic_key: true,
331
+ info: 'A dynamic description.'
332
+ },
333
+ dynamic_property1: {
334
+ _config: {
335
+ nodoc: false,
336
+ type: 'string',
337
+ },
338
+ },
339
+ _dynamic_key: {
340
+ _config: {
341
+ nodoc: false,
342
+ type: 'hash',
343
+ dynamic_key: true,
344
+ info: 'A 2nd dynamic description.'
345
+ },
346
+ _dynamic_key: {
347
+ _config: {
348
+ nodoc: false,
349
+ type: 'string',
350
+ dynamic_key: true,
351
+ info: 'A dynamic string.'
352
+ },
353
+ },
354
+ dynamic_property2: {
355
+ _config: {
356
+ nodoc: false,
357
+ type: 'string',
358
+ },
359
+ },
360
+ },
361
+ },
362
+ }
363
+ end
364
+
365
+ it 'returns the formatted field schema' do
366
+ expect(subject).to eq({
367
+ 'type' => 'object',
368
+ 'description' => 'Dynamic keys hash.',
369
+ 'properties' => {
370
+ 'non_dynamic_key' => {
371
+ 'type' => 'object',
372
+ 'description' => 'A non-dynamic description.',
373
+ 'properties' => {
374
+ 'non_dynamic_property' => {
375
+ 'type' => 'string',
376
+ }
377
+ }
378
+ },
379
+ },
380
+ 'additionalProperties' => {
381
+ 'type' => 'object',
382
+ 'description' => 'A dynamic description.',
383
+ 'properties' => {
384
+ 'dynamic_property1' => {
385
+ 'type' => 'string',
386
+ },
387
+ },
388
+ 'additionalProperties' => {
389
+ 'type' => 'object',
390
+ 'description' => 'A 2nd dynamic description.',
391
+ 'properties' => {
392
+ 'dynamic_property2' => {
393
+ 'type' => 'string',
394
+ },
395
+ },
396
+ 'additionalProperties' => {
397
+ 'type' => 'string',
398
+ 'description' => 'A dynamic string.',
399
+ },
400
+ },
401
+ },
402
+ })
403
+ end
404
+ end
405
+ end
406
+ end
407
+ end
408
+ end
409
+ end
410
+ end
411
+ end
412
+ end
413
+ end
@@ -30,9 +30,10 @@ module Brainstem
30
30
 
31
31
  describe '#call' do
32
32
  before do
33
- stub(presenter).format_title! { title }
34
- stub(presenter).format_fields! { fake_formatted_fields }
35
- stub(presenter).valid_fields { valid_fields }
33
+ stub(presenter).format_title! { title }
34
+ stub(presenter).format_fields! { fake_formatted_fields }
35
+ stub(presenter).valid_fields { valid_fields }
36
+ stub(presenter).valid_associations { {} }
36
37
  end
37
38
 
38
39
  context 'when nodoc' do
@@ -146,6 +147,83 @@ module Brainstem
146
147
  stub(presenter).conditionals { conditionals }
147
148
  end
148
149
 
150
+ context 'with associations present' do
151
+ context 'when association type is belongs_to or has_one' do
152
+ before do
153
+ presenter_class.associations do
154
+ association :task, Task,
155
+ type: :belongs_to
156
+
157
+ association :user, User,
158
+ response_key: :user_id,
159
+ type: :has_one
160
+ end
161
+ end
162
+
163
+ it 'outputs the foreign key in single formatted id' do
164
+ subject.send(:format_fields!)
165
+
166
+ expect(subject.definition).to have_key :properties
167
+ expect(subject.definition[:properties]).to eq({
168
+ 'task_id' => { 'type' => 'string', 'description' => "`task_id` will only be included in the response if `task` is in the list of included associations. See <a href='#section/Includes'>include</a> section for usage." },
169
+ 'user_id' => { 'type' => 'string', 'description' => "`user_id` will only be included in the response if `user` is in the list of included associations. See <a href='#section/Includes'>include</a> section for usage." }
170
+ })
171
+ end
172
+ end
173
+
174
+ context 'when association type is has_many' do
175
+ before do
176
+ presenter_class.associations do
177
+ association :task, Task,
178
+ type: :has_many
179
+ end
180
+ end
181
+
182
+ it 'outputs the foreign key in plural formatted id' do
183
+ subject.send(:format_fields!)
184
+
185
+ expect(subject.definition).to have_key :properties
186
+ expect(subject.definition[:properties]).to eq({
187
+ 'task_ids' => {
188
+ 'type' => 'array',
189
+ 'description' => "`task_ids` will only be included in the response if `task` is in the list of included associations. See <a href='#section/Includes'>include</a> section for usage.",
190
+ 'items' => {
191
+ 'type' => 'string'
192
+ },
193
+ },
194
+ })
195
+ end
196
+ end
197
+
198
+ context 'when association is polymorphic' do
199
+ before do
200
+ presenter_class.associations do
201
+ association :task, :polymorphic, polymorphic_classes: [User, Task]
202
+ end
203
+ end
204
+
205
+ it 'outputs an object that contains an id and key' do
206
+ subject.send(:format_fields!)
207
+
208
+ expect(subject.definition).to have_key :properties
209
+ expect(subject.definition[:properties]).to eq({
210
+ 'task_ref' => {
211
+ 'type' => 'object',
212
+ 'description' => "`task_ref` will only be included in the response if `task` is in the list of included associations. See <a href='#section/Includes'>include</a> section for usage.",
213
+ 'properties' => {
214
+ 'id' => {
215
+ 'type' => 'string'
216
+ },
217
+ 'key' => {
218
+ 'type' => 'string'
219
+ }
220
+ }
221
+ }
222
+ })
223
+ end
224
+ end
225
+ end
226
+
149
227
  context 'with fields present' do
150
228
  describe 'branch node' do
151
229
  context 'with single branch' do
@@ -165,7 +243,6 @@ module Brainstem
165
243
  'sprockets' => {
166
244
  'type' => 'object',
167
245
  'properties' => {
168
-
169
246
  'sprocket_name' => { 'type' => 'string', 'description' => 'Whatever.' }
170
247
  }
171
248
  }
@@ -225,6 +302,7 @@ module Brainstem
225
302
  expect(subject.definition[:properties]).to eq({
226
303
  'sprockets' => {
227
304
  'type' => 'array',
305
+ 'description' => 'Parent.',
228
306
  'items' => {
229
307
  'type' => 'object',
230
308
  'properties' => {
@@ -422,6 +500,40 @@ module Brainstem
422
500
  end
423
501
  end
424
502
  end
503
+
504
+ describe '#sort_properties!' do
505
+ let(:presenter_class) do
506
+ Class.new(Brainstem::Presenter) do
507
+ presents Workspace
508
+ end
509
+ end
510
+ let(:presenter) { Presenter.new(Object.new, const: presenter_class, target_class: 'Workspace') }
511
+ let(:conditionals) { {} }
512
+
513
+ before do
514
+ stub(presenter).conditionals { conditionals }
515
+
516
+ presenter_class.associations do
517
+ association :user, User,
518
+ response_key: :user_id,
519
+ type: :has_one
520
+
521
+ association :task, Task,
522
+ type: :belongs_to
523
+ end
524
+ end
525
+
526
+ it 'correctly sorts the properties' do
527
+ subject.send(:format_fields!)
528
+ subject.send(:sort_properties!)
529
+
530
+ expect(subject.definition).to have_key :properties
531
+ expect(subject.definition[:properties]).to eq({
532
+ 'task_id' => { 'type' => 'string', 'description' => "`task_id` will only be included in the response if `task` is in the list of included associations. See <a href='#section/Includes'>include</a> section for usage." },
533
+ 'user_id' => { 'type' => 'string', 'description' => "`user_id` will only be included in the response if `user` is in the list of included associations. See <a href='#section/Includes'>include</a> section for usage." }
534
+ })
535
+ end
536
+ end
425
537
  end
426
538
  end
427
539
  end