brainstem 2.0.0 → 2.1.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 (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