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.
- 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 +104 -113
- 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/input_object.rb +1 -0
- data/lib/sober_swag/output_object/field_syntax.rb +2 -0
- 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/in_range.rb +61 -0
- data/lib/sober_swag/reporting/input/interface.rb +113 -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/multiple_of.rb +36 -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 +272 -0
- data/lib/sober_swag/reporting/input/text.rb +42 -0
- data/lib/sober_swag/reporting/input.rb +56 -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/enum.rb +47 -0
- data/lib/sober_swag/reporting/output/in_range.rb +64 -0
- data/lib/sober_swag/reporting/output/interface.rb +98 -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 +287 -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 +56 -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 +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
|