sober_swag 0.21.0 → 0.22.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 (82) 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 +92 -101
  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/reporting/compiler.rb +39 -0
  22. data/lib/sober_swag/reporting/input/base.rb +11 -0
  23. data/lib/sober_swag/reporting/input/bool.rb +19 -0
  24. data/lib/sober_swag/reporting/input/converting/bool.rb +24 -0
  25. data/lib/sober_swag/reporting/input/converting/date.rb +30 -0
  26. data/lib/sober_swag/reporting/input/converting/date_time.rb +28 -0
  27. data/lib/sober_swag/reporting/input/converting/decimal.rb +24 -0
  28. data/lib/sober_swag/reporting/input/converting/integer.rb +19 -0
  29. data/lib/sober_swag/reporting/input/converting.rb +16 -0
  30. data/lib/sober_swag/reporting/input/defer.rb +29 -0
  31. data/lib/sober_swag/reporting/input/described.rb +38 -0
  32. data/lib/sober_swag/reporting/input/dictionary.rb +37 -0
  33. data/lib/sober_swag/reporting/input/either.rb +51 -0
  34. data/lib/sober_swag/reporting/input/enum.rb +44 -0
  35. data/lib/sober_swag/reporting/input/format.rb +39 -0
  36. data/lib/sober_swag/reporting/input/interface.rb +87 -0
  37. data/lib/sober_swag/reporting/input/list.rb +44 -0
  38. data/lib/sober_swag/reporting/input/mapped.rb +36 -0
  39. data/lib/sober_swag/reporting/input/merge_objects.rb +72 -0
  40. data/lib/sober_swag/reporting/input/null.rb +34 -0
  41. data/lib/sober_swag/reporting/input/number.rb +19 -0
  42. data/lib/sober_swag/reporting/input/object/property.rb +53 -0
  43. data/lib/sober_swag/reporting/input/object.rb +100 -0
  44. data/lib/sober_swag/reporting/input/pattern.rb +46 -0
  45. data/lib/sober_swag/reporting/input/referenced.rb +38 -0
  46. data/lib/sober_swag/reporting/input/struct.rb +271 -0
  47. data/lib/sober_swag/reporting/input/text.rb +42 -0
  48. data/lib/sober_swag/reporting/input.rb +54 -0
  49. data/lib/sober_swag/reporting/invalid_schema_error.rb +21 -0
  50. data/lib/sober_swag/reporting/output/base.rb +25 -0
  51. data/lib/sober_swag/reporting/output/bool.rb +25 -0
  52. data/lib/sober_swag/reporting/output/defer.rb +69 -0
  53. data/lib/sober_swag/reporting/output/described.rb +42 -0
  54. data/lib/sober_swag/reporting/output/dictionary.rb +46 -0
  55. data/lib/sober_swag/reporting/output/interface.rb +83 -0
  56. data/lib/sober_swag/reporting/output/list.rb +54 -0
  57. data/lib/sober_swag/reporting/output/merge_objects.rb +97 -0
  58. data/lib/sober_swag/reporting/output/null.rb +25 -0
  59. data/lib/sober_swag/reporting/output/number.rb +25 -0
  60. data/lib/sober_swag/reporting/output/object/property.rb +45 -0
  61. data/lib/sober_swag/reporting/output/object.rb +54 -0
  62. data/lib/sober_swag/reporting/output/partitioned.rb +77 -0
  63. data/lib/sober_swag/reporting/output/pattern.rb +50 -0
  64. data/lib/sober_swag/reporting/output/referenced.rb +42 -0
  65. data/lib/sober_swag/reporting/output/struct.rb +262 -0
  66. data/lib/sober_swag/reporting/output/text.rb +25 -0
  67. data/lib/sober_swag/reporting/output/via_map.rb +67 -0
  68. data/lib/sober_swag/reporting/output/viewed.rb +72 -0
  69. data/lib/sober_swag/reporting/output.rb +54 -0
  70. data/lib/sober_swag/reporting/report/base.rb +57 -0
  71. data/lib/sober_swag/reporting/report/either.rb +36 -0
  72. data/lib/sober_swag/reporting/report/error.rb +15 -0
  73. data/lib/sober_swag/reporting/report/list.rb +28 -0
  74. data/lib/sober_swag/reporting/report/merged_object.rb +25 -0
  75. data/lib/sober_swag/reporting/report/object.rb +29 -0
  76. data/lib/sober_swag/reporting/report/output.rb +14 -0
  77. data/lib/sober_swag/reporting/report/value.rb +28 -0
  78. data/lib/sober_swag/reporting/report.rb +16 -0
  79. data/lib/sober_swag/reporting.rb +11 -0
  80. data/lib/sober_swag/version.rb +1 -1
  81. data/lib/sober_swag.rb +1 -0
  82. metadata +65 -2
@@ -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,262 @@
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
+ define_field(name, extract)
24
+
25
+ object_fields[name] = Object::Property.new(
26
+ output.view(:base).via_map(&name.to_proc),
27
+ description: description
28
+ )
29
+ end
30
+
31
+ def object_output
32
+ base = Object.new(object_fields).via_map { |o| new(o) }
33
+ if description
34
+ base.described(description)
35
+ else
36
+ base
37
+ end
38
+ end
39
+
40
+ ##
41
+ # Set a description for the *type* of this output.
42
+ # It will show up as a description in the component key for this output.
43
+ # Right now that unfortunately will not render with ReDoc, but it should eventually.
44
+ #
45
+ # @param val [String, nil] pass if you want to set, otherwise you will get the current value
46
+ # @return [String] the description assigned to this object, if any.
47
+ def description(val = nil)
48
+ return @description unless val
49
+
50
+ @description = val
51
+ end
52
+
53
+ ##
54
+ # An output for this specific schema type.
55
+ # If this schema has any views, it will be defined as a map of possible views to the actual views used.
56
+ # Otherwise, it will directly be the base definition.
57
+ def single_output
58
+ single =
59
+ if view_map.any?
60
+ Viewed.new(identified_view_map)
61
+ else
62
+ inherited_output
63
+ end
64
+ identifier ? single.referenced(identifier) : single
65
+ end
66
+
67
+ ##
68
+ # Used to generate 'allOf' subtyping relationships.
69
+ # Probably do not call this yourself.
70
+ #
71
+ # @return [Interface]
72
+ def identified_with_base
73
+ object_output.referenced([identifier, 'Base'].join('.'))
74
+ end
75
+
76
+ ##
77
+ # Used to generate 'allOf' subtyping relationships.
78
+ # Probably do not call this yourself.
79
+ def identified_without_base
80
+ if parent_struct
81
+ MergeObjects
82
+ .new(parent_struct.inherited_output, object_output)
83
+ else
84
+ object_output
85
+ end.referenced(identifier)
86
+ end
87
+
88
+ ##
89
+ # Used to generate 'allOf' subtyping relationships.
90
+ # Probably do not call this yourself!
91
+ # Use {#single_output} instead.
92
+ #
93
+ # This allows us to implement *inheritance*.
94
+ # So, if you inherit from another output object struct, you get its methods and attributes.
95
+ # Views behave as if they have inherited the base object.
96
+ #
97
+ # This means that any views added to any parent output objects *will* be visible in children.
98
+ # @return [Interface]
99
+ def inherited_output
100
+ inherited =
101
+ if parent_struct
102
+ MergeObjects
103
+ .new(parent_struct.inherited_output, object_output)
104
+ else
105
+ object_output
106
+ end
107
+
108
+ identifier ? inherited.referenced([identifier, 'Base'].join('.')) : inherited
109
+ end
110
+
111
+ ##
112
+ # Schema for this output.
113
+ # Will include views, if applicable.
114
+ def swagger_schema
115
+ single_output.swagger_schema
116
+ end
117
+
118
+ ##
119
+ # Serialize an object to a hash.
120
+ #
121
+ # @param value [Object] value to serialize
122
+ # @param view [Symbol] which view to use to serialize this output.
123
+ # @return [Hash] the serialized ruby hash, suitable for passing to JSON.generate
124
+ def call(value, view: :base)
125
+ view(view).output.call(value)
126
+ end
127
+
128
+ ##
129
+ # Serialize an object to a hash, with type-checking.
130
+ #
131
+ # @param value [Object] value to serialize
132
+ # @param view [Symbol] which view to use
133
+ # @return [Hash] the serialized ruby hash, suitable for passsing to JSON.generate
134
+ def serialize_report(value, view: :base)
135
+ view(view).output.serialize_report(value)
136
+ end
137
+
138
+ ##
139
+ # @return [Hash<Symbol, Object::Property>] the properties defined *directly* on this object.
140
+ # Does not include inherited fields!
141
+ def object_fields
142
+ @object_fields ||= {}
143
+ end
144
+
145
+ ##
146
+ # Define a view for this object.
147
+ #
148
+ # Views behave like their own output structs, which inherit the parent (or 'base' view).
149
+ # This means that fields *after* the definition of a view *will be present in the view*.
150
+ # This enables views to maintain a subtyping relationship.
151
+ #
152
+ # Your base view should thus serialize *as little as possible*.
153
+ #
154
+ # View classes get defined as child constants.
155
+ # So, if I write `define_view(:foo)` on a struct called `Person`,
156
+ # I will get `Person::Foo` as a class I can use if I want!
157
+ #
158
+ # @param name [Symbol] name of this view.
159
+ # @yieldself [self] a block in which you can add more fields to the view.
160
+ # @return [Class]
161
+ def define_view(name, &block) # rubocop:disable Metrics/MethodLength
162
+ raise ArgumentError, "duplicate view #{name}" if name == :base || views.include?(name)
163
+
164
+ classy_name = name.to_s.classify
165
+
166
+ Class.new(self).tap do |c|
167
+ c.instance_eval(&block)
168
+ c.define_singleton_method(:define_view) do |*|
169
+ raise ArgumentError, 'no nesting views'
170
+ end
171
+ c.define_singleton_method(:identifier) do
172
+ [parent_struct.identifier, classy_name.gsub('::', '.')].join('.')
173
+ end
174
+ const_set(classy_name, c)
175
+ view_map[name] = c
176
+ end
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
+ attr_accessor :parent_struct
203
+
204
+ ##
205
+ # When this class is inherited, it sets up a future subtyping relationship.
206
+ # This gets expressed with 'allOf' in the generated swagger.
207
+ def inherited(other)
208
+ other.parent_struct = self unless self == ::SoberSwag::Reporting::Output::Struct
209
+ end
210
+
211
+ ##
212
+ # Set a new identifier for this output object.
213
+ #
214
+ # @param value [String, nil] provide a new identifier to use.
215
+ # Stateful operation.
216
+ # @return [String] identifier key to use in the components hash.
217
+ # In rare cases (a class with no name and no set identifier) it can return nil.
218
+ # We consider this case "unsupported", IE, please do not do that.
219
+ def identifier(value = nil)
220
+ if value
221
+ @identifier = value
222
+ else
223
+ @identifier || name&.gsub('::', '.')
224
+ end
225
+ end
226
+
227
+ private
228
+
229
+ def identified_view_map
230
+ view_map.transform_values(&:identified_without_base).merge(base: inherited_output)
231
+ end
232
+
233
+ def define_field(method, extractor)
234
+ e =
235
+ if extractor.nil?
236
+ proc { _struct_serialized.public_send(method) }
237
+ elsif extractor.arity == 1
238
+ proc { extractor.call(_struct_serialized) }
239
+ else
240
+ extractor
241
+ end
242
+
243
+ define_method(method, &e)
244
+ end
245
+ end
246
+
247
+ def initialize(struct_serialized)
248
+ @_struct_serialized = struct_serialized
249
+ end
250
+
251
+ attr_reader :_struct_serialized
252
+
253
+ ##
254
+ # The object to serialize.
255
+ # Use this if you're defining your own methods.
256
+ def object_to_serialize
257
+ @_struct_serialized
258
+ end
259
+ end
260
+ end
261
+ end
262
+ 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
@@ -0,0 +1,54 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ ##
4
+ # Reporting outputs.
5
+ #
6
+ # These outputs can tell you what their acceptable views are.
7
+ module Output
8
+ autoload(:Base, 'sober_swag/reporting/output/base')
9
+ autoload(:Bool, 'sober_swag/reporting/output/bool')
10
+ autoload(:Defer, 'sober_swag/reporting/output/defer')
11
+ autoload(:Described, 'sober_swag/reporting/output/described')
12
+ autoload(:Dictionary, 'sober_swag/reporting/output/dictionary')
13
+ autoload(:Interface, 'sober_swag/reporting/output/interface')
14
+ autoload(:List, 'sober_swag/reporting/output/list')
15
+ autoload(:MergeObjects, 'sober_swag/reporting/output/merge_objects')
16
+ autoload(:Null, 'sober_swag/reporting/output/null')
17
+ autoload(:Number, 'sober_swag/reporting/output/number')
18
+ autoload(:Object, 'sober_swag/reporting/output/object')
19
+ autoload(:Partitioned, 'sober_swag/reporting/output/partitioned')
20
+ autoload(:Pattern, 'sober_swag/reporting/output/pattern')
21
+ autoload(:Referenced, 'sober_swag/reporting/output/referenced')
22
+ autoload(:Struct, 'sober_swag/reporting/output/struct')
23
+ autoload(:Text, 'sober_swag/reporting/output/text')
24
+ autoload(:ViaMap, 'sober_swag/reporting/output/via_map')
25
+ autoload(:Viewed, 'sober_swag/reporting/output/viewed')
26
+
27
+ class << self
28
+ ##
29
+ # @return [SoberSwag::Reporting::Output::Bool]
30
+ def bool
31
+ Bool.new
32
+ end
33
+
34
+ ##
35
+ # @return [SoberSwag::Reporting::Output::Number]
36
+ def number
37
+ Number.new
38
+ end
39
+
40
+ ##
41
+ # @return [SoberSwag::Reporting::Output::Text]
42
+ def text
43
+ Text.new
44
+ end
45
+
46
+ ##
47
+ # @return [SoberSwag::Reporting::Output::Null]
48
+ def null
49
+ Null.new
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,57 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Report
4
+ ##
5
+ # Base class for SoberSwag reports.
6
+ #
7
+ # These reports are what make these serializers and parsers *reporting*: they provide errors.
8
+ # For outputs, these are errors encountered during serialization, IE,
9
+ # places where we lied about what type we were going to serialize.
10
+ # This is mostly used for testing.
11
+ #
12
+ # For parsers, these are encountered during *parsing*.
13
+ # This can be easily converted into a hash of JSON path objects to individual errors,
14
+ # enabling developers to more easily see what's gone wrong.
15
+ class Base
16
+ ##
17
+ # @return [Array<[String]>]
18
+ # An array of error paths and error components, in the form of:
19
+ #
20
+ # ```ruby
21
+ # [
22
+ # 'foo.bar: was bad',
23
+ # 'foo.bar: was even worse'
24
+ # ]
25
+ # ```
26
+ def full_errors
27
+ each_error.map do |k, v|
28
+ [k, v].reject(&:blank?).join(': ')
29
+ end
30
+ end
31
+
32
+ ##
33
+ # Get a hash where each key is a JSON path, and each value is an array of errors for that path.
34
+ # @return [Hash<String,Array<String>>] hash of JSON path to errors
35
+ def path_hash
36
+ Hash.new { |h, k| h[k] = [] }.tap do |hash|
37
+ each_error do |k, v|
38
+ hash["$#{k}"] << v
39
+ end
40
+ end
41
+ end
42
+
43
+ ##
44
+ # @overload each_error() { |path, val| nil }
45
+ # Yields each error to the block.
46
+ # @yield [path, val] the JSON path to the error, and an error string
47
+ # @yieldparam [String, String]
48
+ # @overload each_error()
49
+ # @return [Enumerable<String, String>] an enum of two values: error keys and error values.
50
+ # Note: the same key can potentially occur more than once!
51
+ def each_error
52
+ return enum_for(:each_error) unless block_given?
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,36 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Report
4
+ ##
5
+ # Models either one set of errors or another.
6
+ # Will enumerate them in order with #each_error
7
+ class Either < Base
8
+ def initialize(lhs, rhs)
9
+ @lhs = lhs
10
+ @rhs = rhs
11
+ end
12
+
13
+ ##
14
+ # @return [Base] left reports
15
+ attr_reader :lhs
16
+ ##
17
+ # @return [Base] right reports
18
+ attr_reader :rhs
19
+
20
+ # rubocop:disable Style/ExplicitBlockArgument
21
+ def each_error
22
+ return enum_for(:each_error) unless block_given?
23
+
24
+ lhs.each_error do |key, value|
25
+ yield key, value
26
+ end
27
+
28
+ rhs.each_error do |key, value|
29
+ yield key, value
30
+ end
31
+ end
32
+ # rubocop:enable Style/ExplicitBlockArgument
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Report
4
+ ##
5
+ # Exception thrown when used with {Reporting::Input::Interface#call!}.
6
+ class Error < StandardError
7
+ def initialize(report)
8
+ @report = report
9
+ end
10
+
11
+ attr_reader :report
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,28 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Report
4
+ ##
5
+ # Report errors that arose while parsing a list.
6
+ class List < Base
7
+ ##
8
+ # @param element [Hash<Int, Base>] a hash of bad element indices to bad
9
+ # element values
10
+ def initialize(elements)
11
+ @elements = elements
12
+ end
13
+
14
+ attr_reader :elements
15
+
16
+ def each_error
17
+ return enum_for(:each_error) unless block_given?
18
+
19
+ elements.each do |k, v|
20
+ v.each_error do |nested, err|
21
+ yield ["[#{k}]", nested].reject(&:nil?).join(''), err
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,25 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Report
4
+ ##
5
+ # Report on problems with a merged object.
6
+ class MergedObject < Base
7
+ def initialize(parent, child)
8
+ @parent = parent
9
+ @child = child
10
+ end
11
+
12
+ attr_reader :parent, :child
13
+
14
+ def each_error
15
+ return enum_for(:each_error) unless block_given?
16
+
17
+ # rubocop:disable Style/ExplicitBlockArgument
18
+ parent.each_error { |k, v| yield k, v }
19
+ child.each_error { |k, v| yield k, v }
20
+ # rubocop:enable Style/ExplicitBlockArgument
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end