sober_swag 0.18.0 → 0.22.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (128) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +15 -0
  3. data/.github/workflows/benchmark.yml +39 -0
  4. data/.github/workflows/lint.yml +2 -4
  5. data/.github/workflows/ruby.yml +1 -1
  6. data/.gitignore +3 -0
  7. data/.rubocop.yml +6 -1
  8. data/.yardopts +7 -0
  9. data/CHANGELOG.md +22 -0
  10. data/Gemfile +12 -0
  11. data/README.md +1 -1
  12. data/bench/benchmark.rb +34 -0
  13. data/bench/benchmarks/basic_field_serializer.rb +21 -0
  14. data/bench/benchmarks/view_selection.rb +47 -0
  15. data/bin/console +30 -10
  16. data/docs/reporting.md +190 -0
  17. data/docs/serializers.md +4 -1
  18. data/example/Gemfile +2 -2
  19. data/example/Gemfile.lock +116 -123
  20. data/example/app/controllers/application_controller.rb +4 -0
  21. data/example/app/controllers/people_controller.rb +44 -28
  22. data/example/app/output_objects/identified_output.rb +7 -0
  23. data/example/app/output_objects/person_output_object.rb +37 -11
  24. data/example/app/output_objects/post_output_object.rb +0 -4
  25. data/example/app/output_objects/reporting_post_output.rb +18 -0
  26. data/example/bin/rspec +29 -0
  27. data/example/config/environments/production.rb +1 -1
  28. data/example/spec/requests/people/create_spec.rb +3 -2
  29. data/example/spec/requests/people/index_spec.rb +1 -1
  30. data/lib/sober_swag/compiler/path.rb +45 -4
  31. data/lib/sober_swag/compiler/paths.rb +20 -0
  32. data/lib/sober_swag/compiler/primitive.rb +17 -0
  33. data/lib/sober_swag/compiler/type.rb +105 -22
  34. data/lib/sober_swag/compiler.rb +87 -15
  35. data/lib/sober_swag/controller/route.rb +147 -28
  36. data/lib/sober_swag/controller.rb +57 -17
  37. data/lib/sober_swag/input_object.rb +124 -7
  38. data/lib/sober_swag/nodes/array.rb +19 -0
  39. data/lib/sober_swag/nodes/attribute.rb +45 -4
  40. data/lib/sober_swag/nodes/base.rb +27 -7
  41. data/lib/sober_swag/nodes/binary.rb +30 -13
  42. data/lib/sober_swag/nodes/enum.rb +16 -1
  43. data/lib/sober_swag/nodes/list.rb +20 -0
  44. data/lib/sober_swag/nodes/nullable_primitive.rb +3 -0
  45. data/lib/sober_swag/nodes/object.rb +4 -1
  46. data/lib/sober_swag/nodes/one_of.rb +11 -3
  47. data/lib/sober_swag/nodes/primitive.rb +34 -2
  48. data/lib/sober_swag/nodes/sum.rb +8 -0
  49. data/lib/sober_swag/output_object/definition.rb +57 -1
  50. data/lib/sober_swag/output_object/field.rb +31 -11
  51. data/lib/sober_swag/output_object/field_syntax.rb +19 -3
  52. data/lib/sober_swag/output_object/view.rb +46 -1
  53. data/lib/sober_swag/output_object.rb +40 -19
  54. data/lib/sober_swag/parser.rb +7 -1
  55. data/lib/sober_swag/reporting/compiler.rb +39 -0
  56. data/lib/sober_swag/reporting/input/base.rb +11 -0
  57. data/lib/sober_swag/reporting/input/bool.rb +19 -0
  58. data/lib/sober_swag/reporting/input/converting/bool.rb +24 -0
  59. data/lib/sober_swag/reporting/input/converting/date.rb +30 -0
  60. data/lib/sober_swag/reporting/input/converting/date_time.rb +28 -0
  61. data/lib/sober_swag/reporting/input/converting/decimal.rb +24 -0
  62. data/lib/sober_swag/reporting/input/converting/integer.rb +19 -0
  63. data/lib/sober_swag/reporting/input/converting.rb +16 -0
  64. data/lib/sober_swag/reporting/input/defer.rb +29 -0
  65. data/lib/sober_swag/reporting/input/described.rb +38 -0
  66. data/lib/sober_swag/reporting/input/dictionary.rb +37 -0
  67. data/lib/sober_swag/reporting/input/either.rb +51 -0
  68. data/lib/sober_swag/reporting/input/enum.rb +44 -0
  69. data/lib/sober_swag/reporting/input/format.rb +39 -0
  70. data/lib/sober_swag/reporting/input/interface.rb +87 -0
  71. data/lib/sober_swag/reporting/input/list.rb +44 -0
  72. data/lib/sober_swag/reporting/input/mapped.rb +36 -0
  73. data/lib/sober_swag/reporting/input/merge_objects.rb +72 -0
  74. data/lib/sober_swag/reporting/input/null.rb +34 -0
  75. data/lib/sober_swag/reporting/input/number.rb +19 -0
  76. data/lib/sober_swag/reporting/input/object/property.rb +53 -0
  77. data/lib/sober_swag/reporting/input/object.rb +100 -0
  78. data/lib/sober_swag/reporting/input/pattern.rb +46 -0
  79. data/lib/sober_swag/reporting/input/referenced.rb +38 -0
  80. data/lib/sober_swag/reporting/input/struct.rb +271 -0
  81. data/lib/sober_swag/reporting/input/text.rb +42 -0
  82. data/lib/sober_swag/reporting/input.rb +54 -0
  83. data/lib/sober_swag/reporting/invalid_schema_error.rb +21 -0
  84. data/lib/sober_swag/reporting/output/base.rb +25 -0
  85. data/lib/sober_swag/reporting/output/bool.rb +25 -0
  86. data/lib/sober_swag/reporting/output/defer.rb +69 -0
  87. data/lib/sober_swag/reporting/output/described.rb +42 -0
  88. data/lib/sober_swag/reporting/output/dictionary.rb +46 -0
  89. data/lib/sober_swag/reporting/output/interface.rb +83 -0
  90. data/lib/sober_swag/reporting/output/list.rb +54 -0
  91. data/lib/sober_swag/reporting/output/merge_objects.rb +97 -0
  92. data/lib/sober_swag/reporting/output/null.rb +25 -0
  93. data/lib/sober_swag/reporting/output/number.rb +25 -0
  94. data/lib/sober_swag/reporting/output/object/property.rb +45 -0
  95. data/lib/sober_swag/reporting/output/object.rb +54 -0
  96. data/lib/sober_swag/reporting/output/partitioned.rb +77 -0
  97. data/lib/sober_swag/reporting/output/pattern.rb +50 -0
  98. data/lib/sober_swag/reporting/output/referenced.rb +42 -0
  99. data/lib/sober_swag/reporting/output/struct.rb +262 -0
  100. data/lib/sober_swag/reporting/output/text.rb +25 -0
  101. data/lib/sober_swag/reporting/output/via_map.rb +67 -0
  102. data/lib/sober_swag/reporting/output/viewed.rb +72 -0
  103. data/lib/sober_swag/reporting/output.rb +54 -0
  104. data/lib/sober_swag/reporting/report/base.rb +57 -0
  105. data/lib/sober_swag/reporting/report/either.rb +36 -0
  106. data/lib/sober_swag/reporting/report/error.rb +15 -0
  107. data/lib/sober_swag/reporting/report/list.rb +28 -0
  108. data/lib/sober_swag/reporting/report/merged_object.rb +25 -0
  109. data/lib/sober_swag/reporting/report/object.rb +29 -0
  110. data/lib/sober_swag/reporting/report/output.rb +14 -0
  111. data/lib/sober_swag/reporting/report/value.rb +28 -0
  112. data/lib/sober_swag/reporting/report.rb +16 -0
  113. data/lib/sober_swag/reporting.rb +11 -0
  114. data/lib/sober_swag/serializer/array.rb +27 -3
  115. data/lib/sober_swag/serializer/base.rb +75 -25
  116. data/lib/sober_swag/serializer/conditional.rb +33 -1
  117. data/lib/sober_swag/serializer/field_list.rb +23 -5
  118. data/lib/sober_swag/serializer/hash.rb +53 -0
  119. data/lib/sober_swag/serializer/mapped.rb +10 -1
  120. data/lib/sober_swag/serializer/optional.rb +18 -1
  121. data/lib/sober_swag/serializer/primitive.rb +3 -0
  122. data/lib/sober_swag/serializer.rb +1 -0
  123. data/lib/sober_swag/server.rb +27 -11
  124. data/lib/sober_swag/type/named.rb +14 -0
  125. data/lib/sober_swag/types/comma_array.rb +4 -0
  126. data/lib/sober_swag/version.rb +1 -1
  127. data/lib/sober_swag.rb +7 -1
  128. metadata +72 -2
@@ -0,0 +1,54 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Serialize a list of some other output type.
6
+ # Passes views down.
7
+ class List < Base
8
+ def initialize(element_output)
9
+ @element_output = element_output
10
+ end
11
+
12
+ attr_reader :element_output
13
+
14
+ def view(view)
15
+ List.new(element_output.view(view))
16
+ end
17
+
18
+ def views
19
+ element_output.views
20
+ end
21
+
22
+ def call(input)
23
+ input.map { |i| element_output.call(i) }
24
+ end
25
+
26
+ def swagger_schema
27
+ schema, found = element_output.swagger_schema
28
+ [
29
+ {
30
+ type: 'array',
31
+ items: schema
32
+ },
33
+ found
34
+ ]
35
+ end
36
+
37
+ def serialize_report(input)
38
+ return Report::Value.new(['could not be made an array']) unless input.respond_to?(:map)
39
+
40
+ errs = {}
41
+ mapped = input.map.with_index do |item, idx|
42
+ element_output.serialize_report(item).tap { |e| errs[idx] = e if e.is_a?(Report::Base) }
43
+ end
44
+
45
+ if errs.any?
46
+ Report::List.new(errs)
47
+ else
48
+ mapped
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,97 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Represents object that are marged with `allOf` in swagger.
6
+ #
7
+ # These *have* to be objects, due to how `allOf` works.
8
+ # This expresses a subtyping relationship.
9
+ #
10
+ # Note: non-careful use of this can generate impossible objects,
11
+ # IE, objects where a certain field has to be *both* a string and an integer or something.
12
+ # Subtyping is dangerous and should be used with care!
13
+ #
14
+ # This class is used in the implementation of {SoberSwag::Reporting::Output::Struct},
15
+ # in order to model the inheritence relationship structs have.
16
+ class MergeObjects < Base
17
+ ##
18
+ # @param parent [Interface] parent interface to use.
19
+ # Should certainly be some sort of object, or a reference to it.
20
+ # @param child [Interface] child interface to use.
21
+ # Should certainly be some sort of object, or a reference to it.
22
+ def initialize(parent, child)
23
+ @parent = parent
24
+ @child = child
25
+ end
26
+
27
+ ##
28
+ # @return [Interface] first object to merge
29
+ attr_reader :parent
30
+ ##
31
+ # @return [Interface] second object to merge
32
+ attr_reader :child
33
+
34
+ ##
35
+ # Serialize with the parent first, then merge in the child.
36
+ # This *does* mean that parent keys override child keys.
37
+ #
38
+ # If `parent` or `child` does not serialize some sort of object, this will result in an error.
39
+ def call(input)
40
+ parent.call(input).merge(child.call(input))
41
+ end
42
+
43
+ ##
44
+ # Child views.
45
+ def views
46
+ child.views
47
+ end
48
+
49
+ ##
50
+ # Passes on view to the *child object*.
51
+ def view(view)
52
+ MergeObjects.new(parent, child.view(view))
53
+ end
54
+
55
+ def serialize_report(value)
56
+ parent_attrs = parent.serialize_report(value)
57
+
58
+ return parent_attrs if parent_attrs.is_a?(Report::Value)
59
+
60
+ child_attrs = child.serialize_report(value)
61
+
62
+ return child_attrs if child_attrs.is_a?(Report::Value)
63
+
64
+ merge_results(parent_attrs, child_attrs)
65
+ end
66
+
67
+ ##
68
+ # Swagger schema.
69
+ #
70
+ # This will collapse 'allOf' keys, so a chain of parent methods will be
71
+ def swagger_schema # rubocop:disable Metrics/MethodLength
72
+ found = {}
73
+ mapped = [parent, child].flat_map do |i|
74
+ schema, item_found = i.swagger_schema
75
+ found.merge!(item_found)
76
+ if schema.key?(:allOf)
77
+ schema[:allOf]
78
+ else
79
+ [schema]
80
+ end
81
+ end
82
+ [{ allOf: mapped }, found]
83
+ end
84
+
85
+ private
86
+
87
+ def merge_results(par, chi)
88
+ return Report::MergedObject.new(par, chi) if [par, chi].all? { |c| c.is_a?(Report::Base) }
89
+ return par if par.is_a?(Report::Base)
90
+ return chi if chi.is_a?(Report::Base)
91
+
92
+ par.to_h.merge(chi.to_h)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,25 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Output JSON nulls.
6
+ class Null < 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 null']) unless result.nil?
15
+
16
+ result
17
+ end
18
+
19
+ def swagger_schema
20
+ [{ type: 'null' }, {}]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,25 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Output numbers of some variety.
6
+ class Number < 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 number']) unless result.is_a?(Numeric)
15
+
16
+ result
17
+ end
18
+
19
+ def swagger_schema
20
+ [{ type: 'number' }, {}]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,45 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ class Object
5
+ ##
6
+ # Definitions for a specific property of an object.
7
+ class Property
8
+ def initialize(output, description: nil)
9
+ @output = output
10
+ @description = description
11
+ end
12
+ ##
13
+ # @return [Interface]
14
+ attr_reader :output
15
+
16
+ ##
17
+ # @return [String,nil]
18
+ attr_reader :description
19
+
20
+ def call(item, view: :base)
21
+ output.call(item, view: view)
22
+ end
23
+
24
+ def property_schema
25
+ direct, refined = output.swagger_schema
26
+
27
+ if description
28
+ [add_description(direct), refined]
29
+ else
30
+ [direct, refined]
31
+ end
32
+ end
33
+
34
+ def add_description(dir)
35
+ if dir.key?(:$ref)
36
+ { allOf: [dir] }
37
+ else
38
+ dir
39
+ end.merge(description: description)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -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