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