sober_swag 0.21.0 → 0.24.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/CHANGELOG.md +5 -0
  4. data/bin/console +30 -10
  5. data/docs/reporting.md +190 -0
  6. data/example/Gemfile +2 -2
  7. data/example/Gemfile.lock +104 -113
  8. data/example/app/controllers/application_controller.rb +4 -0
  9. data/example/app/controllers/people_controller.rb +44 -28
  10. data/example/app/output_objects/identified_output.rb +7 -0
  11. data/example/app/output_objects/person_output_object.rb +37 -11
  12. data/example/app/output_objects/post_output_object.rb +0 -4
  13. data/example/app/output_objects/reporting_post_output.rb +18 -0
  14. data/example/bin/rspec +29 -0
  15. data/example/spec/requests/people/create_spec.rb +3 -2
  16. data/example/spec/requests/people/index_spec.rb +1 -1
  17. data/lib/sober_swag/compiler/path.rb +3 -1
  18. data/lib/sober_swag/compiler.rb +58 -12
  19. data/lib/sober_swag/controller/route.rb +44 -8
  20. data/lib/sober_swag/controller.rb +18 -5
  21. data/lib/sober_swag/input_object.rb +1 -0
  22. data/lib/sober_swag/output_object/field_syntax.rb +2 -0
  23. data/lib/sober_swag/reporting/compiler.rb +39 -0
  24. data/lib/sober_swag/reporting/input/base.rb +11 -0
  25. data/lib/sober_swag/reporting/input/bool.rb +19 -0
  26. data/lib/sober_swag/reporting/input/converting/bool.rb +24 -0
  27. data/lib/sober_swag/reporting/input/converting/date.rb +30 -0
  28. data/lib/sober_swag/reporting/input/converting/date_time.rb +28 -0
  29. data/lib/sober_swag/reporting/input/converting/decimal.rb +24 -0
  30. data/lib/sober_swag/reporting/input/converting/integer.rb +19 -0
  31. data/lib/sober_swag/reporting/input/converting.rb +16 -0
  32. data/lib/sober_swag/reporting/input/defer.rb +29 -0
  33. data/lib/sober_swag/reporting/input/described.rb +38 -0
  34. data/lib/sober_swag/reporting/input/dictionary.rb +37 -0
  35. data/lib/sober_swag/reporting/input/either.rb +51 -0
  36. data/lib/sober_swag/reporting/input/enum.rb +44 -0
  37. data/lib/sober_swag/reporting/input/format.rb +39 -0
  38. data/lib/sober_swag/reporting/input/in_range.rb +61 -0
  39. data/lib/sober_swag/reporting/input/interface.rb +113 -0
  40. data/lib/sober_swag/reporting/input/list.rb +44 -0
  41. data/lib/sober_swag/reporting/input/mapped.rb +36 -0
  42. data/lib/sober_swag/reporting/input/merge_objects.rb +72 -0
  43. data/lib/sober_swag/reporting/input/multiple_of.rb +36 -0
  44. data/lib/sober_swag/reporting/input/null.rb +34 -0
  45. data/lib/sober_swag/reporting/input/number.rb +19 -0
  46. data/lib/sober_swag/reporting/input/object/property.rb +53 -0
  47. data/lib/sober_swag/reporting/input/object.rb +100 -0
  48. data/lib/sober_swag/reporting/input/pattern.rb +46 -0
  49. data/lib/sober_swag/reporting/input/referenced.rb +38 -0
  50. data/lib/sober_swag/reporting/input/struct.rb +272 -0
  51. data/lib/sober_swag/reporting/input/text.rb +42 -0
  52. data/lib/sober_swag/reporting/input.rb +56 -0
  53. data/lib/sober_swag/reporting/invalid_schema_error.rb +21 -0
  54. data/lib/sober_swag/reporting/output/base.rb +25 -0
  55. data/lib/sober_swag/reporting/output/bool.rb +25 -0
  56. data/lib/sober_swag/reporting/output/defer.rb +69 -0
  57. data/lib/sober_swag/reporting/output/described.rb +42 -0
  58. data/lib/sober_swag/reporting/output/dictionary.rb +46 -0
  59. data/lib/sober_swag/reporting/output/enum.rb +47 -0
  60. data/lib/sober_swag/reporting/output/in_range.rb +64 -0
  61. data/lib/sober_swag/reporting/output/interface.rb +98 -0
  62. data/lib/sober_swag/reporting/output/list.rb +54 -0
  63. data/lib/sober_swag/reporting/output/merge_objects.rb +97 -0
  64. data/lib/sober_swag/reporting/output/null.rb +25 -0
  65. data/lib/sober_swag/reporting/output/number.rb +25 -0
  66. data/lib/sober_swag/reporting/output/object/property.rb +45 -0
  67. data/lib/sober_swag/reporting/output/object.rb +54 -0
  68. data/lib/sober_swag/reporting/output/partitioned.rb +77 -0
  69. data/lib/sober_swag/reporting/output/pattern.rb +50 -0
  70. data/lib/sober_swag/reporting/output/referenced.rb +42 -0
  71. data/lib/sober_swag/reporting/output/struct.rb +287 -0
  72. data/lib/sober_swag/reporting/output/text.rb +25 -0
  73. data/lib/sober_swag/reporting/output/via_map.rb +67 -0
  74. data/lib/sober_swag/reporting/output/viewed.rb +72 -0
  75. data/lib/sober_swag/reporting/output.rb +56 -0
  76. data/lib/sober_swag/reporting/report/base.rb +57 -0
  77. data/lib/sober_swag/reporting/report/either.rb +36 -0
  78. data/lib/sober_swag/reporting/report/error.rb +15 -0
  79. data/lib/sober_swag/reporting/report/list.rb +28 -0
  80. data/lib/sober_swag/reporting/report/merged_object.rb +25 -0
  81. data/lib/sober_swag/reporting/report/object.rb +29 -0
  82. data/lib/sober_swag/reporting/report/output.rb +14 -0
  83. data/lib/sober_swag/reporting/report/value.rb +28 -0
  84. data/lib/sober_swag/reporting/report.rb +16 -0
  85. data/lib/sober_swag/reporting.rb +11 -0
  86. data/lib/sober_swag/version.rb +1 -1
  87. data/lib/sober_swag.rb +1 -0
  88. metadata +69 -2
@@ -0,0 +1,54 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Serialize out a JSON object.
6
+ class Object < Base
7
+ autoload(:Property, 'sober_swag/reporting/output/object/property')
8
+
9
+ ##
10
+ # @param properties [Hash<Symbol,Property>] the properties to serialize
11
+ def initialize(properties)
12
+ @properties = properties
13
+ end
14
+
15
+ ##
16
+ # @param properties [Hash<Symbol,Property>]
17
+ attr_reader :properties
18
+
19
+ def call(item)
20
+ properties.each.with_object({}) do |(k, v), hash|
21
+ hash[k] = v.output.call(item)
22
+ end
23
+ end
24
+
25
+ def serialize_report(item)
26
+ bad, good = properties.map { |k, prop|
27
+ [k, prop.output.serialize_report(item)]
28
+ }.partition { |(_, v)| v.is_a?(Report::Base) }
29
+
30
+ return Report::Object.new(bad.to_h) if bad.any?
31
+
32
+ good.to_h
33
+ end
34
+
35
+ def swagger_schema # rubocop:disable Metrics/MethodLength
36
+ props, found = properties.each.with_object([{}, {}]) do |(k, v), (field, f)|
37
+ prop_type, prop_found = v.property_schema
38
+ field[k] = prop_type
39
+ f.merge!(prop_found)
40
+ end
41
+
42
+ [
43
+ {
44
+ type: 'object',
45
+ properties: props,
46
+ required: properties.keys
47
+ },
48
+ found
49
+ ]
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,77 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Partition output into one of two possible cases.
6
+ # We use a block to decide if we should use the first or the second.
7
+ # If the block returns a truthy value, we use the first output.
8
+ # If it returns a falsy value, we use the second.
9
+ #
10
+ # This is useful to serialize sum types, or types where it can be EITHER one thing OR another.
11
+ # IE, if I can resolve a dispute by EITHER transfering money OR refunding a customer, I can do this:
12
+ #
13
+ # ```ruby
14
+ # ResolutionOutput = SoberSwag::Reporting::Output.new(
15
+ # proc { |x| x.is_a?(Transfer) },
16
+ # TransferOutput,
17
+ # RefundOutput
18
+ # )
19
+ # ```
20
+ class Partitioned < Base
21
+ ##
22
+ # @param partition [#call] block that returns true or false for the input type
23
+ # @param true_output [Interface] serializer to use if block is true
24
+ # @param false_output [Interface] serializer to use if block is false
25
+ def initialize(partition, true_output, false_output)
26
+ @partition = partition
27
+ @true_output = true_output
28
+ @false_output = false_output
29
+ end
30
+
31
+ ##
32
+ # @return [#call] partitioning block
33
+ attr_reader :partition
34
+
35
+ ##
36
+ # @return [Interface]
37
+ attr_reader :true_output
38
+
39
+ ##
40
+ # @return [Interface]
41
+ attr_reader :false_output
42
+
43
+ def call(item)
44
+ serializer_for(item).call(item)
45
+ end
46
+
47
+ def serialize_report(item)
48
+ serializer_for(item).serialize_report(item)
49
+ end
50
+
51
+ def swagger_schema
52
+ true_schema, true_found = true_output.swagger_schema
53
+ false_schema, false_found = false_output.swagger_schema
54
+
55
+ [
56
+ {
57
+ oneOf: (true_schema[:oneOf] || [true_schema]) + (false_schema[:oneOf] || [false_schema])
58
+ },
59
+ true_found.merge(false_found)
60
+ ]
61
+ end
62
+
63
+ private
64
+
65
+ ##
66
+ # @return [Interface]
67
+ def serializer_for(item)
68
+ if partition.call(item)
69
+ true_output
70
+ else
71
+ false_output
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,50 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Output with a particular pattern.
6
+ class Pattern < Base
7
+ def initialize(output, pattern)
8
+ @output = output
9
+ @pattern = pattern
10
+ end
11
+
12
+ ##
13
+ # @return [Interface]
14
+ attr_reader :output
15
+
16
+ ##
17
+ # @return [Regexp]
18
+ attr_reader :pattern
19
+
20
+ def call(input)
21
+ output.call(input)
22
+ end
23
+
24
+ def serialize_report(value)
25
+ base = output.serialize_report(value)
26
+
27
+ return base if base.is_a?(Report::Error)
28
+
29
+ if pattern.match?(base)
30
+ base
31
+ else
32
+ Report::Value.new(['did not match pattern'])
33
+ end
34
+ end
35
+
36
+ def swagger_schema
37
+ schema, defs = output.swagger_schema
38
+
39
+ merged =
40
+ if schema.key?(:$ref)
41
+ { oneOf: [schema] }
42
+ else
43
+ schema
44
+ end.merge(pattern: pattern.to_s.gsub('?-mix:', ''))
45
+ [merged, defs]
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,42 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Referenced: An input that will be referred to via reference in the
6
+ # final schema.
7
+ class Referenced < Base
8
+ def initialize(output, reference)
9
+ @output = output
10
+ @reference = reference
11
+ end
12
+
13
+ ##
14
+ # @return [Interface] the actual output type to use
15
+ attr_reader :output
16
+
17
+ ##
18
+ # @return [String] key in the components hash
19
+ attr_reader :reference
20
+
21
+ def call(input)
22
+ output.call(input)
23
+ end
24
+
25
+ def serialize_report(input)
26
+ output.serialize_report(input)
27
+ end
28
+
29
+ def ref_path
30
+ "#/components/schemas/#{reference}"
31
+ end
32
+
33
+ def swagger_schema
34
+ [
35
+ { "$ref": ref_path },
36
+ { reference => proc { output.swagger_schema } }
37
+ ]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,287 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # A DSL for building "output object structs."
6
+ class Struct # rubocop:disable Metrics/ClassLength
7
+ class << self
8
+ include Interface
9
+
10
+ ##
11
+ # Define a new field to be serialized.
12
+ #
13
+ # @param name [Symbol] name of this field.
14
+ # @param output [Interface] reporting output to use to serialize.
15
+ # @param description [String,nil] description for this field.
16
+ # @param block [Proc, nil]
17
+ # If a block is given, it will be defined as a method on the output object struct.
18
+ # If the block takes an argument, the object being serialized will be passed to it.
19
+ # Otherwise, it will be accessible as `#object_to_serialize` from within the body.
20
+ #
21
+ # You can access other methods from this method.
22
+ def field(name, output, description: nil, &extract)
23
+ raise ArgumentError, "output of field #{name} is not a SoberSwag::Reporting::Output::Interface" unless output.is_a?(Interface)
24
+
25
+ define_field(name, extract)
26
+
27
+ object_fields[name] = Object::Property.new(
28
+ output.view(:base).via_map(&name.to_proc),
29
+ description: description
30
+ )
31
+ end
32
+
33
+ def object_output
34
+ base = Object.new(object_fields).via_map { |o| new(o) }
35
+ if description
36
+ base.described(description)
37
+ else
38
+ base
39
+ end
40
+ end
41
+
42
+ ##
43
+ # Set a description for the *type* of this output.
44
+ # It will show up as a description in the component key for this output.
45
+ # Right now that unfortunately will not render with ReDoc, but it should eventually.
46
+ #
47
+ # @param val [String, nil] pass if you want to set, otherwise you will get the current value
48
+ # @return [String] the description assigned to this object, if any.
49
+ def description(val = nil)
50
+ return @description unless val
51
+
52
+ @description = val
53
+ end
54
+
55
+ ##
56
+ # An output for this specific schema type.
57
+ # If this schema has any views, it will be defined as a map of possible views to the actual views used.
58
+ # Otherwise, it will directly be the base definition.
59
+ def single_output
60
+ single =
61
+ if view_map.any?
62
+ Viewed.new(identified_view_map)
63
+ else
64
+ inherited_output
65
+ end
66
+ identifier ? single.referenced(identifier) : single
67
+ end
68
+
69
+ ##
70
+ # Used to generate 'allOf' subtyping relationships.
71
+ # Probably do not call this yourself.
72
+ #
73
+ # @return [Interface]
74
+ def identified_with_base
75
+ object_output.referenced([identifier, 'Base'].join('.'))
76
+ end
77
+
78
+ ##
79
+ # Used to generate 'allOf' subtyping relationships.
80
+ # Probably do not call this yourself.
81
+ def identified_without_base
82
+ if parent_struct
83
+ MergeObjects
84
+ .new(parent_struct.inherited_output, object_output)
85
+ else
86
+ object_output
87
+ end.referenced(identifier)
88
+ end
89
+
90
+ ##
91
+ # Used to generate 'allOf' subtyping relationships.
92
+ # Probably do not call this yourself!
93
+ # Use {#single_output} instead.
94
+ #
95
+ # This allows us to implement *inheritance*.
96
+ # So, if you inherit from another output object struct, you get its methods and attributes.
97
+ # Views behave as if they have inherited the base object.
98
+ #
99
+ # This means that any views added to any parent output objects *will* be visible in children.
100
+ # @return [Interface]
101
+ def inherited_output
102
+ inherited =
103
+ if parent_struct
104
+ MergeObjects
105
+ .new(parent_struct.inherited_output, object_output)
106
+ else
107
+ object_output
108
+ end
109
+
110
+ identifier ? inherited.referenced([identifier, 'Base'].join('.')) : inherited
111
+ end
112
+
113
+ ##
114
+ # Schema for this output.
115
+ # Will include views, if applicable.
116
+ def swagger_schema
117
+ single_output.swagger_schema
118
+ end
119
+
120
+ ##
121
+ # Serialize an object to a hash.
122
+ #
123
+ # @param value [Object] value to serialize
124
+ # @param view [Symbol] which view to use to serialize this output.
125
+ # @return [Hash] the serialized ruby hash, suitable for passing to JSON.generate
126
+ def call(value, view: :base)
127
+ view(view).output.call(value)
128
+ end
129
+
130
+ ##
131
+ # Serialize an object to a hash, with type-checking.
132
+ #
133
+ # @param value [Object] value to serialize
134
+ # @param view [Symbol] which view to use
135
+ # @return [Hash] the serialized ruby hash, suitable for passsing to JSON.generate
136
+ def serialize_report(value, view: :base)
137
+ view(view).output.serialize_report(value)
138
+ end
139
+
140
+ ##
141
+ # @return [Hash<Symbol, Object::Property>] the properties defined *directly* on this object.
142
+ # Does not include inherited fields!
143
+ def object_fields
144
+ @object_fields ||= {}
145
+ end
146
+
147
+ ##
148
+ # Define a view for this object.
149
+ #
150
+ # Views behave like their own output structs, which inherit the parent (or 'base' view).
151
+ # This means that fields *after* the definition of a view *will be present in the view*.
152
+ # This enables views to maintain a subtyping relationship.
153
+ #
154
+ # Your base view should thus serialize *as little as possible*.
155
+ #
156
+ # View classes get defined as child constants.
157
+ # So, if I write `define_view(:foo)` on a struct called `Person`,
158
+ # I will get `Person::Foo` as a class I can use if I want!
159
+ #
160
+ # @param name [Symbol] name of this view.
161
+ # @yieldself [self] a block in which you can add more fields to the view.
162
+ # @return [Class]
163
+ def define_view(name, &block)
164
+ define_view_with_parent(name, self, block)
165
+ end
166
+
167
+ ##
168
+ # Defines a view for this object, which "inherits" another view.
169
+ # @see #define_view for how views behave.
170
+ #
171
+ # @param name [Symbol] name of this view
172
+ # @param inherits [Symbol] name of the view this view inherits
173
+ # @yieldself [self] a block in which you can add more fields to this view
174
+ # @return [Class]
175
+ def define_inherited_view(name, inherits:, &block)
176
+ define_view_with_parent(name, view_class(inherits), block)
177
+ end
178
+
179
+ ##
180
+ # @return Hash<Symbol,Class> map of potential views.
181
+ # Does not include the 'base' view.
182
+ def view_map
183
+ @view_map ||= {}
184
+ end
185
+
186
+ ##
187
+ # @return [Set<Symbol>] all applicable views.
188
+ # Will always include `:base`.
189
+ def views
190
+ [:base, *view_map.keys].to_set
191
+ end
192
+
193
+ ##
194
+ # @param name [Symbol] which view to use.
195
+ # @return [Interface] a serializer suitable for this interface.
196
+ def view(name)
197
+ return inherited_output if name == :base
198
+
199
+ view_map.fetch(name).view(:base)
200
+ end
201
+
202
+ ##
203
+ # Equivalent to .view, but returns the raw view class.
204
+ #
205
+ # @return [Class]
206
+ def view_class(name)
207
+ return self if name == :base
208
+
209
+ view_map.fetch(name)
210
+ end
211
+
212
+ attr_accessor :parent_struct
213
+
214
+ ##
215
+ # When this class is inherited, it sets up a future subtyping relationship.
216
+ # This gets expressed with 'allOf' in the generated swagger.
217
+ def inherited(other)
218
+ other.parent_struct = self unless self == ::SoberSwag::Reporting::Output::Struct
219
+ end
220
+
221
+ ##
222
+ # Set a new identifier for this output object.
223
+ #
224
+ # @param value [String, nil] provide a new identifier to use.
225
+ # Stateful operation.
226
+ # @return [String] identifier key to use in the components hash.
227
+ # In rare cases (a class with no name and no set identifier) it can return nil.
228
+ # We consider this case "unsupported", IE, please do not do that.
229
+ def identifier(value = nil)
230
+ if value
231
+ @identifier = value
232
+ else
233
+ @identifier || name&.gsub('::', '.')
234
+ end
235
+ end
236
+
237
+ private
238
+
239
+ def define_view_with_parent(name, parent, block)
240
+ raise ArgumentError, "duplicate view #{name}" if name == :base || views.include?(name)
241
+
242
+ classy_name = name.to_s.classify
243
+ us = self # grab this so its identifier doesn't get nested under whatever parent it inherits from, since its our view
244
+
245
+ Class.new(parent).tap do |c|
246
+ c.instance_eval(&block)
247
+ c.define_singleton_method(:define_view) { |*| raise ArgumentError, 'no nesting views' }
248
+ c.define_singleton_method(:identifier) { [us.identifier, classy_name.gsub('::', '.')].join('.') }
249
+ const_set(classy_name, c)
250
+ view_map[name] = c
251
+ end
252
+ end
253
+
254
+ def identified_view_map
255
+ view_map.transform_values(&:identified_without_base).merge(base: inherited_output)
256
+ end
257
+
258
+ def define_field(method, extractor)
259
+ e =
260
+ if extractor.nil?
261
+ proc { _struct_serialized.public_send(method) }
262
+ elsif extractor.arity == 1
263
+ proc { extractor.call(_struct_serialized) }
264
+ else
265
+ extractor
266
+ end
267
+
268
+ define_method(method, &e)
269
+ end
270
+ end
271
+
272
+ def initialize(struct_serialized)
273
+ @_struct_serialized = struct_serialized
274
+ end
275
+
276
+ attr_reader :_struct_serialized
277
+
278
+ ##
279
+ # The object to serialize.
280
+ # Use this if you're defining your own methods.
281
+ def object_to_serialize
282
+ @_struct_serialized
283
+ end
284
+ end
285
+ end
286
+ end
287
+ end
@@ -0,0 +1,25 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Output raw text.
6
+ class Text < Base
7
+ def call(input)
8
+ input
9
+ end
10
+
11
+ def serialize_report(input)
12
+ result = call(input)
13
+
14
+ return Report::Value.new(['was not a string']) unless result.is_a?(String)
15
+
16
+ result
17
+ end
18
+
19
+ def swagger_schema
20
+ [{ type: 'string' }, {}]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,67 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Apply a mapping function before calling
6
+ # a base output.
7
+ #
8
+ # Note that this is applied *before* the base output.
9
+ # This is different than {SoberSwag::Reporting::Input::Mapped}, which does the reverse.
10
+ # IE, this class does `call block -> pass result to base output`,
11
+ # while the other does `call serializer -> pass result to block`.
12
+ #
13
+ # If you want to get *really* nerdy, this is *contravariant* to `Mapped`.
14
+ #
15
+ # This lets you do things like making an output that serializes to strings via `to_s`:
16
+ #
17
+ # ```ruby
18
+ # ToSTextOutput = SoberSwag::Reporting::Output::ViaMap.new(
19
+ # SoberSwag::Reporting::Output.text,
20
+ # proc { |arg| arg.to_s }
21
+ # )
22
+ #
23
+ # class Person
24
+ # def to_s
25
+ # 'Person'
26
+ # end
27
+ # end
28
+ #
29
+ # ToSTextOutput.call(Person.new) # => 'Person'
30
+ # ```
31
+ class ViaMap < Base
32
+ def initialize(output, mapper)
33
+ @output = output
34
+ @mapper = mapper
35
+ end
36
+
37
+ ##
38
+ # @return [Interface] base output
39
+ attr_reader :output
40
+
41
+ ##
42
+ # @return [#call] mapping function
43
+ attr_reader :mapper
44
+
45
+ def call(input)
46
+ output.call(mapper.call(input))
47
+ end
48
+
49
+ def serialize_report(input)
50
+ output.serialize_report(mapper.call(input))
51
+ end
52
+
53
+ def view(view)
54
+ ViaMap.new(output.view(view), mapper)
55
+ end
56
+
57
+ def views
58
+ output.views
59
+ end
60
+
61
+ def swagger_schema
62
+ output.swagger_schema
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,72 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Augment outputs with the ability to select views.
6
+ # This models a 'oneOf' relationship, where the choice picked is controlled by the 'view' parameter.
7
+ #
8
+ # This is "optional choice," in the sense that you *must* provide a default `:base` key.
9
+ # This key will be used in almost all cases.
10
+ class Viewed < Base
11
+ ##
12
+ # @param views [Hash<Symbol,Interface>] a map of view key to view.
13
+ # Note: this map *must* include the base view.
14
+ def initialize(views)
15
+ @view_map = views
16
+
17
+ raise ArgumentError, 'views must have a base key' unless views.key?(:base)
18
+ end
19
+
20
+ attr_reader :view_map
21
+
22
+ ##
23
+ # Serialize out an object.
24
+ # If the view key is not provided, use the base view.
25
+ #
26
+ # @param input [Object] object to serialize
27
+ # @param view [Symbol] which view to use.
28
+ # If view is not valid, an exception will be thrown
29
+ # @raise [KeyError] if view is not valid
30
+ # @return [Object,String,Array,Numeric] JSON-serializable object.
31
+ # Suitable for use with #to_json.
32
+ def call(input, view: :base)
33
+ view(view).call(input)
34
+ end
35
+
36
+ def serialize_report(input)
37
+ view(:base).call(input)
38
+ end
39
+
40
+ ##
41
+ # Get a view with a particular key.
42
+ def view(view)
43
+ view_map.fetch(view)
44
+ end
45
+
46
+ ##
47
+ # @return [Set<Symbol>] all of the views applicable.
48
+ def views
49
+ view_map.keys.to_set
50
+ end
51
+
52
+ ##
53
+ # Add (or override) the possible views.
54
+ #
55
+ # @return [Viewed] a new view map, with one more view.
56
+ def with_view(name, val)
57
+ Viewed.new(views.merge(name => val))
58
+ end
59
+
60
+ def swagger_schema
61
+ found = {}
62
+ possibles = view_map.values.flat_map do |v|
63
+ view_item, view_found = v.swagger_schema
64
+ found.merge!(view_found)
65
+ view_item[:oneOf] || [view_item]
66
+ end
67
+ [{ oneOf: possibles }, found]
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end