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.
- 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
|