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,30 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ module Converting
5
+ ##
6
+ # Convert via a date.
7
+ #
8
+ # Note: unlike the swagger spec, we first try to convert
9
+ # rfc8601, then try rfc3339.
10
+ Date = (
11
+ SoberSwag::Reporting::Input::Text
12
+ .new
13
+ .mapped { |str|
14
+ begin
15
+ ::Date.rfc3339(str)
16
+ rescue ArgumentError
17
+ Report::Value.new(['was not an RFC 3339 date string'])
18
+ end
19
+ } |
20
+ SoberSwag::Reporting::Input::Text
21
+ .new
22
+ .mapped do |str|
23
+ ::Date.iso8601(str)
24
+ rescue ArgumentError
25
+ Report::Value.new(['was not an ISO8601 date string'])
26
+ end).format('date')
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,28 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ module Converting
5
+ ##
6
+ # Convert via a date.
7
+ DateTime =
8
+ SoberSwag::Reporting::Input::Text
9
+ .new
10
+ .mapped { |str|
11
+ begin
12
+ ::DateTime.rfc3339(str)
13
+ rescue ArgumentError
14
+ Report::Value.new(['was not an RFC 3339 date-time string'])
15
+ end
16
+ }.or(
17
+ SoberSwag::Reporting::Input::Text
18
+ .new
19
+ .mapped do |str|
20
+ ::DateTime.iso8601(str)
21
+ rescue ArgumentError
22
+ Report::Value.new(['was not an ISO8601 date-time string'])
23
+ end
24
+ ).format('date-time')
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,24 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ module Converting
5
+ ##
6
+ # Parse a decimal.
7
+ Decimal =
8
+ (SoberSwag::Reporting::Input::Number.new.mapped(&:to_d).format(:decimal) |
9
+ SoberSwag::Reporting::Input::Text
10
+ .new
11
+ .format('decimal')
12
+ .mapped do |v|
13
+ BigDecimal(v)
14
+ rescue ArgumentError
15
+ Report::Value.new('was not a decimal')
16
+ end).described(<<~MARKDOWN).referenced('SoberSwag.Converting.Decimal')
17
+ Decimal formatted input.
18
+ Will either convert a JSON number to a decimal, or accept a string representation.
19
+ The string representation allows for greater precision.
20
+ MARKDOWN
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,19 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ module Converting
5
+ Integer =
6
+ (SoberSwag::Reporting::Input.number.format('integer').mapped(&:to_i)) |
7
+ (SoberSwag::Reporting::Input.text.format('integer').mapped do |v|
8
+ Integer(v)
9
+ rescue ArgumentError
10
+ Report::Value.new(['was not an integer string'])
11
+ end).described(<<~MARKDOWN).referenced('SoberSwag.Converting.Integer')
12
+ Integer formatted input.
13
+
14
+ With either convert a JSON number to an integer, or accept a string representation of an integer.
15
+ MARKDOWN
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Namespace for things that can do conversion.
6
+ # These are really just compound types that kinda look nice.
7
+ module Converting
8
+ autoload(:Decimal, 'sober_swag/reporting/input/converting/decimal')
9
+ autoload(:Date, 'sober_swag/reporting/input/converting/date')
10
+ autoload(:DateTime, 'sober_swag/reporting/input/converting/date_time')
11
+ autoload(:Bool, 'sober_swag/reporting/input/converting/bool')
12
+ autoload(:Integer, 'sober_swag/reporting/input/converting/integer')
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,29 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Resolve circular references by deferring the loading of an input.
6
+ class Defer < Base
7
+ def initialize(other_lazy)
8
+ @other_lazy = other_lazy
9
+ end
10
+
11
+ attr_reader :other_lazy
12
+
13
+ def other
14
+ return @other if defined?(@other)
15
+
16
+ @other = other_lazy.call
17
+ end
18
+
19
+ def call(input)
20
+ other.call(input)
21
+ end
22
+
23
+ def swagger_schema
24
+ other.swagger_schema
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,38 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Node for things with descriptions.
6
+ # This describes the *type*, not the *object key*.
7
+ class Described < Base
8
+ def initialize(input, description)
9
+ @input = input
10
+ @description = description
11
+ end
12
+
13
+ ##
14
+ # @return [Interface] base input
15
+ attr_reader :input
16
+
17
+ ##
18
+ # @return [String] description of input
19
+ attr_reader :description
20
+
21
+ def call(value)
22
+ input.call(value)
23
+ end
24
+
25
+ def swagger_schema
26
+ val, other = input.swagger_schema
27
+ merged =
28
+ if val.key?(:$ref)
29
+ { allOf: [val] }
30
+ else
31
+ val
32
+ end.merge(description: description)
33
+ [merged, other]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,37 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Dictionary types: string keys, something else as a value.
6
+ class Dictionary < Base
7
+ def self.of(input_type)
8
+ new(input_type)
9
+ end
10
+
11
+ def initialize(value_input)
12
+ @value_input = value_input
13
+ end
14
+
15
+ attr_reader :value_input
16
+
17
+ def call(value)
18
+ return Report::Base.new(['was not an object']) unless value.is_a?(Hash)
19
+
20
+ bad, good = value.map { |k, v|
21
+ [k, value_input.call(v)]
22
+ }.compact.partition { |(_, v)| v.is_a?(Report::Base) }
23
+
24
+ return Report::Object.new(bad.to_h) if bad.any?
25
+
26
+ good.to_h
27
+ end
28
+
29
+ def swagger_schema
30
+ schema, found = value_input.swagger_schema
31
+
32
+ [{ type: :object, additionalProperties: schema }, found]
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,51 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Parses either one input, or another.
6
+ # Left-biased.
7
+ class Either < Base
8
+ ##
9
+ # @param lhs [Base] an input we will try first
10
+ # @param rhs [Base] an input we will try second
11
+ def initialize(lhs, rhs)
12
+ @lhs = lhs
13
+ @rhs = rhs
14
+ end
15
+
16
+ ##
17
+ # @return [Base] parser for LHS
18
+ attr_reader :lhs
19
+ ##
20
+ # @return [Base] parser for RHS
21
+ attr_reader :rhs
22
+
23
+ def call(value)
24
+ maybe_lhs = lhs.call(value)
25
+
26
+ return maybe_lhs unless maybe_lhs.is_a?(Report::Base)
27
+
28
+ maybe_rhs = rhs.call(value)
29
+
30
+ return maybe_rhs unless maybe_rhs.is_a?(Report::Base)
31
+
32
+ Report::Either.new(maybe_lhs, maybe_rhs)
33
+ end
34
+
35
+ def swagger_schema
36
+ lhs_val, lhs_set = lhs.swagger_schema
37
+ rhs_val, rhs_set = rhs.swagger_schema
38
+
39
+ val = { oneOf: defs_for(lhs_val) + defs_for(rhs_val) }
40
+ [val, lhs_set.merge(rhs_set)]
41
+ end
42
+
43
+ private
44
+
45
+ def defs_for(schema)
46
+ schema[:oneOf] || [schema]
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,44 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Specify that a value must be included in a list of possible values.
6
+ class Enum < Base
7
+ def initialize(input, values)
8
+ @input = input
9
+ @values = values
10
+ end
11
+
12
+ ##
13
+ # @return [Interface] base type
14
+ attr_reader :input
15
+
16
+ ##
17
+ # @return [Array<String>] acceptable types
18
+ attr_reader :values
19
+
20
+ def call(value)
21
+ inner = input.call(value)
22
+
23
+ return inner if inner.is_a?(Report::Base)
24
+
25
+ return Report::Value.new(['was not an acceptable enum member']) unless values.include?(inner)
26
+
27
+ inner
28
+ end
29
+
30
+ def swagger_schema
31
+ schema, found = input.swagger_schema
32
+
33
+ merged =
34
+ if schema.key?(:$ref)
35
+ { allOf: [schema] }
36
+ else
37
+ schema
38
+ end.merge(enum: values)
39
+ [merged, found]
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,39 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Specify that something must match a particular format.
6
+ # Note: said format is just a string.
7
+ class Format < Base
8
+ def initialize(input, format)
9
+ @input = input
10
+ @format = format
11
+ end
12
+
13
+ ##
14
+ # @return [Interface]
15
+ attr_reader :input
16
+
17
+ ##
18
+ # @return [String]
19
+ attr_reader :format
20
+
21
+ def call(object)
22
+ input.call(object)
23
+ end
24
+
25
+ def swagger_schema
26
+ schema, found = input.swagger_schema
27
+
28
+ merged =
29
+ if schema.key?(:$ref)
30
+ { allOf: [schema] }
31
+ else
32
+ schema
33
+ end.merge(format: format)
34
+ [merged, found]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,87 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Module for interface methods.
6
+ module Interface
7
+ ##
8
+ # Make a new input that is either this type or the argument.
9
+ #
10
+ # @argument other [Interface] other input type
11
+ # @return [Either] this input, or some other input.
12
+ def or(other)
13
+ Either.new(self, other)
14
+ end
15
+
16
+ ##
17
+ # @see {#or}
18
+ def |(other)
19
+ Either.new(self, other)
20
+ end
21
+
22
+ ##
23
+ # This, or null.
24
+ #
25
+ # @return [Either] an either type of this or nil.
26
+ def optional
27
+ self | Null.new
28
+ end
29
+
30
+ ##
31
+ # A list of this input.
32
+ #
33
+ # @return [List] the new input.
34
+ def list
35
+ List.new(self)
36
+ end
37
+
38
+ def referenced(name)
39
+ Referenced.new(self, name)
40
+ end
41
+
42
+ def format(format)
43
+ Format.new(self, format)
44
+ end
45
+
46
+ def described(desc)
47
+ Described.new(self, desc)
48
+ end
49
+
50
+ def enum(*cases)
51
+ Enum.new(self, cases)
52
+ end
53
+
54
+ ##
55
+ # Map a function after this input runs.
56
+ #
57
+ # @return [Mapped] the new input.
58
+ def mapped(&block)
59
+ Mapped.new(self, block)
60
+ end
61
+
62
+ def call!(value)
63
+ res = call(value)
64
+ raise Report::Error.new(res) if res.is_a?(Report::Base) # rubocop:disable Style/RaiseArgs
65
+
66
+ res
67
+ end
68
+
69
+ def swagger_path_schema
70
+ raise InvalidSchemaError::InvalidForPathError.new(self) # rubocop:disable Style/RaiseArgs
71
+ end
72
+
73
+ def swagger_query_schema
74
+ raise InvalidSchemaError::InvalidForQueryError.new(self) # rubocop:disable Style/RaiseArgs
75
+ end
76
+
77
+ def add_schema_key(base, addition)
78
+ if base.key?(:$ref)
79
+ { allOf: [base] }.merge(addition)
80
+ else
81
+ base.merge(addition)
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,44 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Class to parse an array, where each element has the same type.
6
+ #
7
+ # Called List to avoid name conflicts.
8
+ class List < Base
9
+ ##
10
+ # @param element [Base] the parser for elements
11
+ def initialize(element)
12
+ @element = element
13
+ end
14
+
15
+ ##
16
+ # @return [Base] the parser for elements
17
+ attr_reader :element
18
+
19
+ def call(value)
20
+ return Report::Value.new(['was not an array']) unless value.is_a?(Array)
21
+
22
+ # obtain a hash of indexes => errors
23
+ errs = {}
24
+ # yes, side effects in a map are evil, but we avoid traversal twice
25
+ mapped = value.map.with_index do |item, idx|
26
+ element.call(item).tap { |e| errs[idx] = e if e.is_a?(Report::Base) }
27
+ end
28
+
29
+ if errs.any?
30
+ Report::List.new(errs)
31
+ else
32
+ mapped
33
+ end
34
+ end
35
+
36
+ def swagger_schema
37
+ schema, found = element.swagger_schema
38
+
39
+ [{ type: 'list', items: schema }, found]
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,36 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Apply a mapping function over an input.
6
+ class Mapped < Base
7
+ ##
8
+ # @param mapper [#call] the mapping function
9
+ # @param input [Base] the base input
10
+ def initialize(input, mapper)
11
+ @mapper = mapper
12
+ @input = input
13
+ end
14
+
15
+ ##
16
+ # @return [#call] mapping function
17
+ attr_reader :mapper
18
+ ##
19
+ # @return [Base] base input
20
+ attr_reader :input
21
+
22
+ def call(value)
23
+ val = input.call(value)
24
+
25
+ return val if val.is_a?(Report::Base)
26
+
27
+ mapper.call(val)
28
+ end
29
+
30
+ def swagger_schema
31
+ input.swagger_schema
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,72 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Merge two object types together, in an allof stype relationship
6
+ class MergeObjects < Base
7
+ def initialize(parent, child)
8
+ @parent = parent
9
+ @child = child
10
+ end
11
+
12
+ ##
13
+ # @return [Interface] parent type
14
+ attr_reader :parent
15
+
16
+ ##
17
+ # @return [Interface] child type
18
+ attr_reader :child
19
+
20
+ def call(value)
21
+ parent_attrs = parent.call(value)
22
+
23
+ return parent_attrs if parent_attrs.is_a?(Report::Value)
24
+
25
+ # otherwise, object type, so we want to get a full error report
26
+
27
+ child_attrs = child.call(value)
28
+
29
+ return child_attrs if child_attrs.is_a?(Report::Value)
30
+
31
+ merge_results(parent_attrs, child_attrs)
32
+ end
33
+
34
+ def swagger_schema
35
+ parent_schema, parent_found = parent.swagger_schema
36
+ child_schema, child_found = child.swagger_schema
37
+
38
+ [
39
+ {
40
+ allOf: (parent_schema[:allOf] || [parent_schema]) + (child_schema[:allOf] || [child_schema])
41
+ },
42
+ parent_found.merge(child_found)
43
+ ]
44
+ end
45
+
46
+ def swagger_path_schema
47
+ parent.swagger_path_schema + child.swagger_path_schema
48
+ end
49
+
50
+ def swagger_query_schema
51
+ parent.swagger_query_schema + child.swagger_query_schema
52
+ end
53
+
54
+ private
55
+
56
+ def merge_results(par, chi) # rubocop:disable Metrics/MethodLength
57
+ if par.is_a?(Report::Base)
58
+ if chi.is_a?(Report::Base)
59
+ Report::MergedObject.new(par, chi)
60
+ else
61
+ par
62
+ end
63
+ elsif chi.is_a?(Report::Base)
64
+ chi
65
+ else
66
+ par.to_h.merge(chi.to_h)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,34 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Null input values.
6
+ # Validates that the input is null.
7
+ class Null < Base
8
+ def call(value)
9
+ return nil if value.nil?
10
+
11
+ Report::Value.new(['was not nil'])
12
+ end
13
+
14
+ def hash
15
+ [self.class.hash, 1].hash
16
+ end
17
+
18
+ def eql?(other)
19
+ other.class == self.class
20
+ end
21
+
22
+ def <=>(other)
23
+ eql?(other) ? 0 : nil
24
+ end
25
+
26
+ include Comparable
27
+
28
+ def swagger_schema
29
+ [{ type: 'null' }, {}]
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,19 @@
1
+ module SoberSwag
2
+ module Reporting
3
+ module Input
4
+ ##
5
+ # Parse some kind of number.
6
+ class Number < Base
7
+ def call(input)
8
+ return Report::Value.new(['is not a number']) unless input.is_a?(Numeric)
9
+
10
+ input
11
+ end
12
+
13
+ def swagger_schema
14
+ [{ type: 'number' }, {}]
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end