sober_swag 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/CHANGELOG.md +5 -0
  4. data/bin/console +30 -10
  5. data/docs/reporting.md +190 -0
  6. data/example/Gemfile +2 -2
  7. data/example/Gemfile.lock +92 -101
  8. data/example/app/controllers/application_controller.rb +4 -0
  9. data/example/app/controllers/people_controller.rb +44 -28
  10. data/example/app/output_objects/identified_output.rb +7 -0
  11. data/example/app/output_objects/person_output_object.rb +37 -11
  12. data/example/app/output_objects/post_output_object.rb +0 -4
  13. data/example/app/output_objects/reporting_post_output.rb +18 -0
  14. data/example/bin/rspec +29 -0
  15. data/example/spec/requests/people/create_spec.rb +3 -2
  16. data/example/spec/requests/people/index_spec.rb +1 -1
  17. data/lib/sober_swag/compiler/path.rb +3 -1
  18. data/lib/sober_swag/compiler.rb +58 -12
  19. data/lib/sober_swag/controller/route.rb +44 -8
  20. data/lib/sober_swag/controller.rb +18 -5
  21. data/lib/sober_swag/reporting/compiler.rb +39 -0
  22. data/lib/sober_swag/reporting/input/base.rb +11 -0
  23. data/lib/sober_swag/reporting/input/bool.rb +19 -0
  24. data/lib/sober_swag/reporting/input/converting/bool.rb +24 -0
  25. data/lib/sober_swag/reporting/input/converting/date.rb +30 -0
  26. data/lib/sober_swag/reporting/input/converting/date_time.rb +28 -0
  27. data/lib/sober_swag/reporting/input/converting/decimal.rb +24 -0
  28. data/lib/sober_swag/reporting/input/converting/integer.rb +19 -0
  29. data/lib/sober_swag/reporting/input/converting.rb +16 -0
  30. data/lib/sober_swag/reporting/input/defer.rb +29 -0
  31. data/lib/sober_swag/reporting/input/described.rb +38 -0
  32. data/lib/sober_swag/reporting/input/dictionary.rb +37 -0
  33. data/lib/sober_swag/reporting/input/either.rb +51 -0
  34. data/lib/sober_swag/reporting/input/enum.rb +44 -0
  35. data/lib/sober_swag/reporting/input/format.rb +39 -0
  36. data/lib/sober_swag/reporting/input/interface.rb +87 -0
  37. data/lib/sober_swag/reporting/input/list.rb +44 -0
  38. data/lib/sober_swag/reporting/input/mapped.rb +36 -0
  39. data/lib/sober_swag/reporting/input/merge_objects.rb +72 -0
  40. data/lib/sober_swag/reporting/input/null.rb +34 -0
  41. data/lib/sober_swag/reporting/input/number.rb +19 -0
  42. data/lib/sober_swag/reporting/input/object/property.rb +53 -0
  43. data/lib/sober_swag/reporting/input/object.rb +100 -0
  44. data/lib/sober_swag/reporting/input/pattern.rb +46 -0
  45. data/lib/sober_swag/reporting/input/referenced.rb +38 -0
  46. data/lib/sober_swag/reporting/input/struct.rb +271 -0
  47. data/lib/sober_swag/reporting/input/text.rb +42 -0
  48. data/lib/sober_swag/reporting/input.rb +54 -0
  49. data/lib/sober_swag/reporting/invalid_schema_error.rb +21 -0
  50. data/lib/sober_swag/reporting/output/base.rb +25 -0
  51. data/lib/sober_swag/reporting/output/bool.rb +25 -0
  52. data/lib/sober_swag/reporting/output/defer.rb +69 -0
  53. data/lib/sober_swag/reporting/output/described.rb +42 -0
  54. data/lib/sober_swag/reporting/output/dictionary.rb +46 -0
  55. data/lib/sober_swag/reporting/output/interface.rb +83 -0
  56. data/lib/sober_swag/reporting/output/list.rb +54 -0
  57. data/lib/sober_swag/reporting/output/merge_objects.rb +97 -0
  58. data/lib/sober_swag/reporting/output/null.rb +25 -0
  59. data/lib/sober_swag/reporting/output/number.rb +25 -0
  60. data/lib/sober_swag/reporting/output/object/property.rb +45 -0
  61. data/lib/sober_swag/reporting/output/object.rb +54 -0
  62. data/lib/sober_swag/reporting/output/partitioned.rb +77 -0
  63. data/lib/sober_swag/reporting/output/pattern.rb +50 -0
  64. data/lib/sober_swag/reporting/output/referenced.rb +42 -0
  65. data/lib/sober_swag/reporting/output/struct.rb +262 -0
  66. data/lib/sober_swag/reporting/output/text.rb +25 -0
  67. data/lib/sober_swag/reporting/output/via_map.rb +67 -0
  68. data/lib/sober_swag/reporting/output/viewed.rb +72 -0
  69. data/lib/sober_swag/reporting/output.rb +54 -0
  70. data/lib/sober_swag/reporting/report/base.rb +57 -0
  71. data/lib/sober_swag/reporting/report/either.rb +36 -0
  72. data/lib/sober_swag/reporting/report/error.rb +15 -0
  73. data/lib/sober_swag/reporting/report/list.rb +28 -0
  74. data/lib/sober_swag/reporting/report/merged_object.rb +25 -0
  75. data/lib/sober_swag/reporting/report/object.rb +29 -0
  76. data/lib/sober_swag/reporting/report/output.rb +14 -0
  77. data/lib/sober_swag/reporting/report/value.rb +28 -0
  78. data/lib/sober_swag/reporting/report.rb +16 -0
  79. data/lib/sober_swag/reporting.rb +11 -0
  80. data/lib/sober_swag/version.rb +1 -1
  81. data/lib/sober_swag.rb +1 -0
  82. metadata +65 -2
@@ -0,0 +1,69 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Defer loading of an output for mutual recursion and/or loading time speed.
6
+ # Probably just do this for mutual recursion though.
7
+ #
8
+ # Note: this *does not* save you from infinite schema generation.
9
+ # This type *must* return some sort of {Referenced} type in order to do that!
10
+ #
11
+ # The common use case for this is mutual recursion.
12
+ # Something like...
13
+ #
14
+ # ```ruby
15
+ # class PersonOutput < SoberSwag::Reporting::Output::Struct
16
+ # field :first_name, SoberSwag::Reporting::Output.text
17
+ # view :detail do
18
+ # field :classes, SoberSwag::Reporting::Output::Defer.new { ClassroomOutput.view(:base).array }
19
+ # end
20
+ # end
21
+ #
22
+ # class ClassroomOutut < SoberSwag::Reporting::Output::Struct
23
+ # field :class_name, SoberSwag::Reporting::Output.text
24
+ #
25
+ # view :detail do
26
+ # field :students, SoberSwag::Reporting::Output::Defer.new { PersonOutput.view(:base).array }
27
+ # end
28
+ # end
29
+ # ```
30
+ class Defer < Base
31
+ ##
32
+ # Nicer initialization: uses a block.
33
+ #
34
+ # @yieldreturn [Interface] serializer to use.
35
+ def self.defer(&block)
36
+ new(block)
37
+ end
38
+
39
+ def initialize(other_lazy)
40
+ @other_lazy = other_lazy
41
+ end
42
+
43
+ attr_reader :other_lazy
44
+
45
+ ##
46
+ # @return [Interface]
47
+ def other
48
+ @other ||= other_lazy.call
49
+ end
50
+
51
+ def call(input)
52
+ other.call(input)
53
+ end
54
+
55
+ def serialize_report(input)
56
+ other.serialize_report(input)
57
+ end
58
+
59
+ def view(view)
60
+ other.view(view)
61
+ end
62
+
63
+ def swagger_schema
64
+ other.swagger_schema
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,42 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Add a description onto an object.
6
+ class Described < Base
7
+ def initialize(output, description)
8
+ @output = output
9
+ @description = description
10
+ end
11
+
12
+ ##
13
+ # @return [Interface] output to describe
14
+ attr_reader :output
15
+
16
+ ##
17
+ # @return [String] description of output
18
+ attr_reader :description
19
+
20
+ def call(value)
21
+ output.call(value)
22
+ end
23
+
24
+ def serialize_report(value)
25
+ output.serialize_report(value)
26
+ end
27
+
28
+ def swagger_schema
29
+ schema, found = output.swagger_schema
30
+
31
+ merged =
32
+ if schema.key?(:$ref)
33
+ { allOf: [schema] }
34
+ else
35
+ schema
36
+ end.merge(description: description)
37
+ [merged, found]
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,46 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Output a dictionary of key-value pairs.
6
+ class Dictionary < Base
7
+ def self.of(valout)
8
+ new(valout)
9
+ end
10
+
11
+ def initialize(value_output)
12
+ @value_output = value_output
13
+ end
14
+
15
+ attr_reader :value_output
16
+
17
+ def call(item)
18
+ item.transform_values { |v| value_output.call(v) }
19
+ end
20
+
21
+ def serialize_report(item)
22
+ return Report::Base.new(['was not a dict']) unless item.is_a?(Hash)
23
+
24
+ bad, good = item.map { |k, v|
25
+ [k, value_output.serialize_report(v)]
26
+ }.compact.partition { |(_, v)| v.is_a?(Report::Base) }
27
+
28
+ return Report::Object.new(bad.to_h) if bad.any?
29
+
30
+ good.to_h
31
+ end
32
+
33
+ def swagger_schema
34
+ schema, found = value_output.swagger_schema
35
+ [
36
+ {
37
+ type: :object,
38
+ additionalProperties: schema
39
+ },
40
+ found
41
+ ]
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,83 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Output
4
+ ##
5
+ # Interface methods for all outputs.
6
+ module Interface
7
+ def call!(item)
8
+ res = serialize_report(item)
9
+
10
+ raise Report::Error.new(res) if res.is_a?(Report::Base) # rubocop:disable Style/RaiseArgs
11
+
12
+ res
13
+ end
14
+
15
+ ##
16
+ # Show off that this is a reporting output.
17
+ def reporting?
18
+ true
19
+ end
20
+
21
+ ##
22
+ # Delegates to {#call}
23
+ def serialize(item)
24
+ call(item)
25
+ end
26
+
27
+ def via_map(&block)
28
+ raise ArgumentError, 'block argument required' unless block
29
+
30
+ ViaMap.new(self, block)
31
+ end
32
+
33
+ def referenced(name)
34
+ Referenced.new(self, name)
35
+ end
36
+
37
+ def list
38
+ List.new(self)
39
+ end
40
+
41
+ ##
42
+ # Partition this serializer into two potentials.
43
+ # If the block given returns *false*, we will use `other` as the serializer.
44
+ # Otherwise, we will use `self`.
45
+ #
46
+ # This might be useful to serialize a sum type:
47
+ #
48
+ # ```ruby
49
+ # ResolutionOutput = TransferOutput.partitioned(RefundOutput) { |to_serialize| to_serialize.is_a?(Transfer)
50
+ # ```
51
+ #
52
+ # @param other [Interface] serializer to use if the block returns false
53
+ # @yieldreturn [true,false] false if we should use the other serializer
54
+ # @return [Interface]
55
+ def partitioned(other, &block)
56
+ raise ArgumentError, 'need a block' if block.nil?
57
+
58
+ Partitioned.new(
59
+ block,
60
+ self,
61
+ other
62
+ )
63
+ end
64
+
65
+ def nilable
66
+ Partitioned.new(
67
+ :nil?.to_proc,
68
+ Null.new,
69
+ self
70
+ )
71
+ end
72
+
73
+ def array
74
+ List.new(self)
75
+ end
76
+
77
+ def described(description)
78
+ Described.new(self, description)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -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