sober_swag 0.21.0 → 0.22.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +5 -0
- data/bin/console +30 -10
- data/docs/reporting.md +190 -0
- data/example/Gemfile +2 -2
- data/example/Gemfile.lock +92 -101
- data/example/app/controllers/application_controller.rb +4 -0
- data/example/app/controllers/people_controller.rb +44 -28
- data/example/app/output_objects/identified_output.rb +7 -0
- data/example/app/output_objects/person_output_object.rb +37 -11
- data/example/app/output_objects/post_output_object.rb +0 -4
- data/example/app/output_objects/reporting_post_output.rb +18 -0
- data/example/bin/rspec +29 -0
- data/example/spec/requests/people/create_spec.rb +3 -2
- data/example/spec/requests/people/index_spec.rb +1 -1
- data/lib/sober_swag/compiler/path.rb +3 -1
- data/lib/sober_swag/compiler.rb +58 -12
- data/lib/sober_swag/controller/route.rb +44 -8
- data/lib/sober_swag/controller.rb +18 -5
- data/lib/sober_swag/reporting/compiler.rb +39 -0
- data/lib/sober_swag/reporting/input/base.rb +11 -0
- data/lib/sober_swag/reporting/input/bool.rb +19 -0
- data/lib/sober_swag/reporting/input/converting/bool.rb +24 -0
- data/lib/sober_swag/reporting/input/converting/date.rb +30 -0
- data/lib/sober_swag/reporting/input/converting/date_time.rb +28 -0
- data/lib/sober_swag/reporting/input/converting/decimal.rb +24 -0
- data/lib/sober_swag/reporting/input/converting/integer.rb +19 -0
- data/lib/sober_swag/reporting/input/converting.rb +16 -0
- data/lib/sober_swag/reporting/input/defer.rb +29 -0
- data/lib/sober_swag/reporting/input/described.rb +38 -0
- data/lib/sober_swag/reporting/input/dictionary.rb +37 -0
- data/lib/sober_swag/reporting/input/either.rb +51 -0
- data/lib/sober_swag/reporting/input/enum.rb +44 -0
- data/lib/sober_swag/reporting/input/format.rb +39 -0
- data/lib/sober_swag/reporting/input/interface.rb +87 -0
- data/lib/sober_swag/reporting/input/list.rb +44 -0
- data/lib/sober_swag/reporting/input/mapped.rb +36 -0
- data/lib/sober_swag/reporting/input/merge_objects.rb +72 -0
- data/lib/sober_swag/reporting/input/null.rb +34 -0
- data/lib/sober_swag/reporting/input/number.rb +19 -0
- data/lib/sober_swag/reporting/input/object/property.rb +53 -0
- data/lib/sober_swag/reporting/input/object.rb +100 -0
- data/lib/sober_swag/reporting/input/pattern.rb +46 -0
- data/lib/sober_swag/reporting/input/referenced.rb +38 -0
- data/lib/sober_swag/reporting/input/struct.rb +271 -0
- data/lib/sober_swag/reporting/input/text.rb +42 -0
- data/lib/sober_swag/reporting/input.rb +54 -0
- data/lib/sober_swag/reporting/invalid_schema_error.rb +21 -0
- data/lib/sober_swag/reporting/output/base.rb +25 -0
- data/lib/sober_swag/reporting/output/bool.rb +25 -0
- data/lib/sober_swag/reporting/output/defer.rb +69 -0
- data/lib/sober_swag/reporting/output/described.rb +42 -0
- data/lib/sober_swag/reporting/output/dictionary.rb +46 -0
- data/lib/sober_swag/reporting/output/interface.rb +83 -0
- data/lib/sober_swag/reporting/output/list.rb +54 -0
- data/lib/sober_swag/reporting/output/merge_objects.rb +97 -0
- data/lib/sober_swag/reporting/output/null.rb +25 -0
- data/lib/sober_swag/reporting/output/number.rb +25 -0
- data/lib/sober_swag/reporting/output/object/property.rb +45 -0
- data/lib/sober_swag/reporting/output/object.rb +54 -0
- data/lib/sober_swag/reporting/output/partitioned.rb +77 -0
- data/lib/sober_swag/reporting/output/pattern.rb +50 -0
- data/lib/sober_swag/reporting/output/referenced.rb +42 -0
- data/lib/sober_swag/reporting/output/struct.rb +262 -0
- data/lib/sober_swag/reporting/output/text.rb +25 -0
- data/lib/sober_swag/reporting/output/via_map.rb +67 -0
- data/lib/sober_swag/reporting/output/viewed.rb +72 -0
- data/lib/sober_swag/reporting/output.rb +54 -0
- data/lib/sober_swag/reporting/report/base.rb +57 -0
- data/lib/sober_swag/reporting/report/either.rb +36 -0
- data/lib/sober_swag/reporting/report/error.rb +15 -0
- data/lib/sober_swag/reporting/report/list.rb +28 -0
- data/lib/sober_swag/reporting/report/merged_object.rb +25 -0
- data/lib/sober_swag/reporting/report/object.rb +29 -0
- data/lib/sober_swag/reporting/report/output.rb +14 -0
- data/lib/sober_swag/reporting/report/value.rb +28 -0
- data/lib/sober_swag/reporting/report.rb +16 -0
- data/lib/sober_swag/reporting.rb +11 -0
- data/lib/sober_swag/version.rb +1 -1
- data/lib/sober_swag.rb +1 -0
- 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
|