explicit 0.2.1 → 0.2.2

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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +75 -67
  3. data/app/helpers/explicit/application_helper.rb +32 -0
  4. data/app/views/explicit/documentation/_attribute.html.erb +38 -0
  5. data/app/views/explicit/documentation/_page.html.erb +166 -0
  6. data/app/views/explicit/documentation/_request.html.erb +87 -0
  7. data/app/views/explicit/documentation/request/_examples.html.erb +50 -0
  8. data/app/views/explicit/documentation/type/_agreement.html.erb +7 -0
  9. data/app/views/explicit/documentation/type/_array.html.erb +3 -0
  10. data/app/views/explicit/documentation/type/_big_decimal.html.erb +4 -0
  11. data/app/views/explicit/documentation/type/_boolean.html.erb +7 -0
  12. data/app/views/explicit/documentation/type/_date_time_iso8601.html.erb +3 -0
  13. data/app/views/explicit/documentation/type/_date_time_posix.html.erb +3 -0
  14. data/app/views/explicit/documentation/type/_enum.html.erb +7 -0
  15. data/app/views/explicit/documentation/type/_file.html.erb +9 -0
  16. data/app/views/explicit/documentation/type/_hash.html.erb +4 -0
  17. data/app/views/explicit/documentation/type/_integer.html.erb +25 -0
  18. data/app/views/explicit/documentation/type/_one_of.html.erb +11 -0
  19. data/app/views/explicit/documentation/type/_record.html.erb +9 -0
  20. data/app/views/explicit/documentation/type/_string.html.erb +21 -0
  21. data/config/locales/en.yml +27 -11
  22. data/lib/explicit/configuration.rb +1 -1
  23. data/lib/explicit/documentation/builder.rb +80 -0
  24. data/lib/explicit/documentation/markdown.rb +2 -13
  25. data/lib/explicit/documentation/output/swagger.rb +176 -0
  26. data/lib/explicit/documentation/output/webpage.rb +31 -0
  27. data/lib/explicit/documentation/page/partial.rb +20 -0
  28. data/lib/explicit/documentation/page/request.rb +27 -0
  29. data/lib/explicit/documentation/section.rb +9 -0
  30. data/lib/explicit/documentation.rb +12 -145
  31. data/lib/explicit/request/example.rb +50 -1
  32. data/lib/explicit/request/invalid_params_error.rb +1 -3
  33. data/lib/explicit/request/invalid_response_error.rb +2 -15
  34. data/lib/explicit/request/route.rb +18 -0
  35. data/lib/explicit/request.rb +43 -24
  36. data/lib/explicit/test_helper/example_recorder.rb +7 -2
  37. data/lib/explicit/test_helper.rb +25 -7
  38. data/lib/explicit/type/agreement.rb +39 -0
  39. data/lib/explicit/type/array.rb +56 -0
  40. data/lib/explicit/type/big_decimal.rb +58 -0
  41. data/lib/explicit/type/boolean.rb +47 -0
  42. data/lib/explicit/type/date_time_iso8601.rb +41 -0
  43. data/lib/explicit/type/date_time_posix.rb +44 -0
  44. data/lib/explicit/type/enum.rb +41 -0
  45. data/lib/explicit/type/file.rb +60 -0
  46. data/lib/explicit/type/hash.rb +57 -0
  47. data/lib/explicit/type/integer.rb +79 -0
  48. data/lib/explicit/type/literal.rb +45 -0
  49. data/lib/explicit/type/modifiers/default.rb +24 -0
  50. data/lib/explicit/type/modifiers/description.rb +11 -0
  51. data/lib/explicit/type/modifiers/nilable.rb +19 -0
  52. data/lib/explicit/type/modifiers/param_location.rb +11 -0
  53. data/lib/explicit/type/one_of.rb +46 -0
  54. data/lib/explicit/type/record.rb +96 -0
  55. data/lib/explicit/type/string.rb +68 -0
  56. data/lib/explicit/type.rb +112 -0
  57. data/lib/explicit/version.rb +1 -1
  58. data/lib/explicit.rb +28 -18
  59. metadata +47 -25
  60. data/app/views/explicit/application/_documentation.html.erb +0 -136
  61. data/app/views/explicit/application/_request.html.erb +0 -37
  62. data/lib/explicit/documentation/property.rb +0 -19
  63. data/lib/explicit/spec/agreement.rb +0 -17
  64. data/lib/explicit/spec/array.rb +0 -28
  65. data/lib/explicit/spec/bigdecimal.rb +0 -27
  66. data/lib/explicit/spec/boolean.rb +0 -30
  67. data/lib/explicit/spec/date_time_iso8601.rb +0 -17
  68. data/lib/explicit/spec/date_time_posix.rb +0 -21
  69. data/lib/explicit/spec/default.rb +0 -20
  70. data/lib/explicit/spec/error.rb +0 -63
  71. data/lib/explicit/spec/hash.rb +0 -30
  72. data/lib/explicit/spec/inclusion.rb +0 -15
  73. data/lib/explicit/spec/integer.rb +0 -53
  74. data/lib/explicit/spec/literal.rb +0 -15
  75. data/lib/explicit/spec/nilable.rb +0 -15
  76. data/lib/explicit/spec/one_of.rb +0 -40
  77. data/lib/explicit/spec/record.rb +0 -33
  78. data/lib/explicit/spec/string.rb +0 -50
  79. data/lib/explicit/spec.rb +0 -72
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Explicit::Type::BigDecimal < Explicit::Type
4
+ attr_reader :min, :max
5
+
6
+ def initialize(min: nil, max: nil)
7
+ @min = min
8
+ @max = max
9
+ end
10
+
11
+ def validate(value)
12
+ unless value.is_a?(::String) || value.is_a?(::Integer)
13
+ return [:error, error_i18n("bigdecimal")]
14
+ end
15
+
16
+ decimalvalue = BigDecimal(value)
17
+
18
+ if min && decimalvalue < min
19
+ return [:error, error_i18n("min", min:)]
20
+ end
21
+
22
+ if max && decimalvalue > max
23
+ return [:error, error_i18n("max", max:)]
24
+ end
25
+
26
+ [:ok, decimalvalue]
27
+ rescue ArgumentError
28
+ return [:error, error_i18n("bigdecimal")]
29
+ end
30
+
31
+ concerning :Webpage do
32
+ def summary
33
+ "string"
34
+ end
35
+
36
+ def partial
37
+ "explicit/documentation/type/big_decimal"
38
+ end
39
+
40
+ def has_details?
41
+ true
42
+ end
43
+ end
44
+
45
+ concerning :Swagger do
46
+ def swagger_schema
47
+ {
48
+ type: "string",
49
+ pattern: /^\d*\.?\d*$/.inspect,
50
+ format: "decimal number",
51
+ description: swagger_description([
52
+ min&.then { swagger_i18n("big_decimal_min", min: _1) },
53
+ max&.then { swagger_i18n("big_decimal_max", max: _1) }
54
+ ])
55
+ }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Explicit::Type::Boolean < Explicit::Type
4
+ VALUES = {
5
+ true => true,
6
+ "true" => true,
7
+ "on" => true,
8
+ "1" => true,
9
+ 1 => true,
10
+ false => false,
11
+ "false" => false,
12
+ "off" => false,
13
+ "0" => false,
14
+ 0 => false
15
+ }.freeze
16
+
17
+ def validate(value)
18
+ value = VALUES[value]
19
+
20
+ return [:error, error_i18n("boolean")] if value.nil?
21
+
22
+ [:ok, value]
23
+ end
24
+
25
+ concerning :Webpage do
26
+ def summary
27
+ "boolean"
28
+ end
29
+
30
+ def partial
31
+ "explicit/documentation/type/boolean"
32
+ end
33
+
34
+ def has_details?
35
+ true
36
+ end
37
+ end
38
+
39
+ concerning :Swagger do
40
+ def swagger_schema
41
+ {
42
+ type: "boolean",
43
+ description: swagger_description([])
44
+ }
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ class Explicit::Type::DateTimeISO8601 < Explicit::Type
6
+ def validate(value)
7
+ return [:error, error_i18n("date_time_iso8601")] if !value.is_a?(::String)
8
+
9
+ timeval = Time.iso8601(value)
10
+
11
+ [:ok, timeval]
12
+ rescue ArgumentError
13
+ [:error, error_i18n("date_time_iso8601")]
14
+ end
15
+
16
+ concerning :Webpage do
17
+ def summary
18
+ "string"
19
+ end
20
+
21
+ def partial
22
+ "explicit/documentation/type/date_time_iso8601"
23
+ end
24
+
25
+ def has_details?
26
+ true
27
+ end
28
+ end
29
+
30
+ concerning :Swagger do
31
+ def swagger_schema
32
+ {
33
+ type: "string",
34
+ format: "date-time",
35
+ description: swagger_description([
36
+ swagger_i18n("date_time_iso8601")
37
+ ])
38
+ }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ class Explicit::Type::DateTimePosix < Explicit::Type
6
+ def validate(value)
7
+ if !value.is_a?(::Integer) && !value.is_a?(::String)
8
+ return [:error, error_i18n("date_time_posix")]
9
+ end
10
+
11
+ datetimeval = DateTime.strptime(value.to_s, "%s")
12
+
13
+ [:ok, datetimeval]
14
+ rescue Date::Error
15
+ return [:error, error_i18n("date_time_posix")]
16
+ end
17
+
18
+ concerning :Webpage do
19
+ def summary
20
+ "integer"
21
+ end
22
+
23
+ def partial
24
+ "explicit/documentation/type/date_time_posix"
25
+ end
26
+
27
+ def has_details?
28
+ true
29
+ end
30
+ end
31
+
32
+ concerning :Swagger do
33
+ def swagger_schema
34
+ {
35
+ type: "integer",
36
+ minimum: 1,
37
+ format: "POSIX time",
38
+ description: swagger_description([
39
+ swagger_i18n("date_time_posix")
40
+ ])
41
+ }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Explicit::Type::Enum < Explicit::Type
4
+ attr_reader :allowed_values
5
+
6
+ def initialize(allowed_values)
7
+ @allowed_values = allowed_values
8
+ end
9
+
10
+ def validate(value)
11
+ if allowed_values.include?(value)
12
+ [:ok, value]
13
+ else
14
+ [:error, error_i18n("enum", allowed_values: allowed_values.inspect)]
15
+ end
16
+ end
17
+
18
+ concerning :Webpage do
19
+ def summary
20
+ "string"
21
+ end
22
+
23
+ def partial
24
+ "explicit/documentation/type/enum"
25
+ end
26
+
27
+ def has_details?
28
+ true
29
+ end
30
+ end
31
+
32
+ concerning :Swagger do
33
+ def swagger_schema
34
+ {
35
+ type: "string",
36
+ enum: allowed_values,
37
+ description: swagger_description([])
38
+ }
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Explicit::Type::File < Explicit::Type
4
+ include ActionView::Helpers::NumberHelper
5
+
6
+ attr_reader :max_size, :content_types
7
+
8
+ FILE_CLASSES = [
9
+ ActionDispatch::Http::UploadedFile,
10
+ Rack::Test::UploadedFile
11
+ ].freeze
12
+
13
+ def initialize(max_size: nil, content_types: nil)
14
+ @max_size = max_size
15
+ @content_types = Array(content_types)
16
+ end
17
+
18
+ def validate(value)
19
+ if !FILE_CLASSES.any? { |klass| value.is_a?(klass) }
20
+ return [:error, error_i18n("file")]
21
+ end
22
+
23
+ if max_size && value.size > max_size
24
+ return [:error, error_i18n("file_max_size", max_size: number_to_human_size(max_size))]
25
+ end
26
+
27
+ if content_types.any? && !content_types.include?(value.content_type)
28
+ return [:error, error_i18n("file_content_type", allowed_content_types: content_types.inspect)]
29
+ end
30
+
31
+ [:ok, value]
32
+ end
33
+
34
+ concerning :Webpage do
35
+ def summary
36
+ "file"
37
+ end
38
+
39
+ def partial
40
+ "explicit/documentation/type/file"
41
+ end
42
+
43
+ def has_details?
44
+ max_size.present? || content_types.any?
45
+ end
46
+ end
47
+
48
+ concerning :Swagger do
49
+ def swagger_schema
50
+ {
51
+ type: "string",
52
+ format: "binary",
53
+ description: swagger_description([
54
+ max_size&.then { swagger_i18n("file_max_size", max_size: number_to_human_size(_1)) },
55
+ content_types.any? ? swagger_i18n("file_content_types", content_types: content_types.join(', ')) : nil
56
+ ])
57
+ }
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Explicit::Type::Hash < Explicit::Type
4
+ attr_reader :keytype, :valuetype, :empty
5
+
6
+ def initialize(keytype:, valuetype:, empty: true)
7
+ @keytype = Explicit::Type.build(keytype)
8
+ @valuetype = Explicit::Type.build(valuetype)
9
+ @empty = empty
10
+ end
11
+
12
+ def validate(value)
13
+ return [:error, error_i18n("hash")] if !value.respond_to?(:[])
14
+ return [:error, error_i18n("empty")] if value.empty? && empty == false
15
+
16
+ validated_hash = {}
17
+
18
+ value.each do |key, value|
19
+ case [keytype.validate(key), valuetype.validate(value)]
20
+ in [[:ok, validated_key], [:ok, validated_value]]
21
+ validated_hash[validated_key] = validated_value
22
+ in [[:error, error], _]
23
+ return [:error, error_i18n("hash_key", key:, error:)]
24
+ in [_, [:error, error]]
25
+ return [:error, error_i18n("hash_value", key:, error:)]
26
+ end
27
+ end
28
+
29
+ [:ok, validated_hash]
30
+ end
31
+
32
+ concerning :Webpage do
33
+ def summary
34
+ "object"
35
+ end
36
+
37
+ def partial
38
+ "explicit/documentation/type/hash"
39
+ end
40
+
41
+ def has_details?
42
+ true
43
+ end
44
+ end
45
+
46
+ concerning :Swagger do
47
+ def swagger_schema
48
+ {
49
+ type: "object",
50
+ additionalProperties: valuetype.swagger_schema,
51
+ description: swagger_description([
52
+ empty == false ? swagger_i18n("hash_not_empty") : nil
53
+ ])
54
+ }
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Explicit::Type::Integer < Explicit::Type
4
+ attr_reader :min, :max, :negative, :positive
5
+
6
+ def initialize(min: nil, max: nil, negative: nil, positive: nil)
7
+ @min = min
8
+ @max = max
9
+ @negative = negative
10
+ @positive = positive
11
+ end
12
+
13
+ ParseFromString = ->(value) do
14
+ Integer(value)
15
+ rescue ::ArgumentError
16
+ nil
17
+ end
18
+
19
+ def validate(value)
20
+ value =
21
+ if value.is_a?(::Integer)
22
+ value
23
+ elsif value.is_a?(::String)
24
+ ParseFromString[value]
25
+ else
26
+ nil
27
+ end
28
+
29
+ return [:error, error_i18n("integer")] if value.nil?
30
+
31
+ if min && value < min
32
+ return [:error, error_i18n("min", min:)]
33
+ end
34
+
35
+ if max && value > max
36
+ return [:error, error_i18n("max", max:)]
37
+ end
38
+
39
+ if negative == false && value < 0
40
+ return [:error, error_i18n("not_negative")]
41
+ end
42
+
43
+ if positive == false && value > 0
44
+ return [:error, error_i18n("not_positive")]
45
+ end
46
+
47
+ [:ok, value]
48
+ end
49
+
50
+ concerning :Webpage do
51
+ def summary
52
+ "integer"
53
+ end
54
+
55
+ def partial
56
+ "explicit/documentation/type/integer"
57
+ end
58
+
59
+ def has_details?
60
+ min.present? || max.present? || !negative.nil? || !positive.nil?
61
+ end
62
+ end
63
+
64
+ concerning :Swagger do
65
+ def swagger_schema
66
+ {
67
+ type: "integer",
68
+ minimum: min,
69
+ maximum: max,
70
+ description: swagger_description([
71
+ positive == false ? swagger_i18n("integer_not_positive") : nil,
72
+ positive == true ? swagger_i18n("integer_only_positive") : nil,
73
+ negative == false ? swagger_i18n("integer_not_negative") : nil,
74
+ negative == true ? swagger_i18n("integer_only_negative") : nil
75
+ ])
76
+ }.compact_blank
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Explicit::Type::Literal < Explicit::Type
4
+ attr_reader :value
5
+
6
+ def initialize(value:)
7
+ if !value.is_a?(::String) && !value.is_a?(::Integer)
8
+ raise ArgumentError("literal must be a string or integer")
9
+ end
10
+
11
+ @value = value
12
+ end
13
+
14
+ def validate(value)
15
+ if value == @value
16
+ [:ok, value]
17
+ else
18
+ [:error, error_i18n("literal", value: @value.inspect)]
19
+ end
20
+ end
21
+
22
+ concerning :Webpage do
23
+ def summary
24
+ @value.inspect
25
+ end
26
+
27
+ def partial
28
+ nil
29
+ end
30
+
31
+ def has_details?
32
+ false
33
+ end
34
+ end
35
+
36
+ concerning :Swagger do
37
+ def swagger_schema
38
+ {
39
+ type: @value.is_a?(::String) ? "string" : "integer",
40
+ enum: [@value],
41
+ description: swagger_description([])
42
+ }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Type::Modifiers::Default
4
+ extend self
5
+
6
+ def apply(default, type)
7
+ Explicit::Type.build(type).tap do |type|
8
+ type.default = default if type.is_a?(Explicit::Type) # TODO: remove check
9
+
10
+ original_validate = type.method(:validate)
11
+
12
+ type.define_singleton_method(:validate, lambda do |value|
13
+ value =
14
+ if value.nil?
15
+ default.respond_to?(:call) ? default.call : default
16
+ else
17
+ value
18
+ end
19
+
20
+ original_validate.(value)
21
+ end)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Type::Modifiers::Description
4
+ extend self
5
+
6
+ def apply(description, type)
7
+ Explicit::Type.build(type).tap do |type|
8
+ type.description = description if type.is_a?(Explicit::Type) # TODO: remove check
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Type::Modifiers::Nilable
4
+ extend self
5
+
6
+ def apply(type)
7
+ Explicit::Type.build(type).tap do |type|
8
+ type.nilable = true if type.is_a?(Explicit::Type) # TODO: remove check
9
+
10
+ original_validate = type.method(:validate)
11
+
12
+ type.define_singleton_method(:validate, lambda do |value|
13
+ return [:ok, nil] if value.nil?
14
+
15
+ original_validate.(value)
16
+ end)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Explicit::Type::Modifiers::ParamLocation
4
+ extend self
5
+
6
+ def apply(param_location, type)
7
+ Explicit::Type.build(type).tap do |type|
8
+ type.param_location = param_location
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Explicit::Type::OneOf < Explicit::Type
4
+ attr_reader :subtypes
5
+
6
+ def initialize(subtypes:)
7
+ @subtypes = subtypes.map { Explicit::Type.build(_1) }
8
+ end
9
+
10
+ def validate(value)
11
+ errors = []
12
+
13
+ @subtypes.each do |subtype|
14
+ case subtype.validate(value)
15
+ in [:ok, validated_value]
16
+ return [:ok, validated_value]
17
+ in [:error, err]
18
+ errors << err
19
+ end
20
+ end
21
+
22
+ [:error, errors.join(" OR ")]
23
+ end
24
+
25
+ concerning :Webpage do
26
+ def summary
27
+ @subtypes.all? { _1.is_a?(Explicit::Type::Record) } ? "object" : "any"
28
+ end
29
+
30
+ def partial
31
+ "explicit/documentation/type/one_of"
32
+ end
33
+
34
+ def has_details?
35
+ true
36
+ end
37
+ end
38
+
39
+ concerning :Swagger do
40
+ def swagger_schema
41
+ return subtypes.first.swagger_schema if subtypes.one?
42
+
43
+ { oneOf: subtypes.map(&:swagger_schema) }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Explicit::Type::Record < Explicit::Type
4
+ attr_reader :attributes
5
+
6
+ def initialize(attributes:)
7
+ @attributes = attributes.map do |attribute_name, type|
8
+ type = Explicit::Type.build(type) if !type.is_a?(Explicit::Type)
9
+
10
+ [attribute_name, type]
11
+ end
12
+ end
13
+
14
+ def validate(data)
15
+ return [:error, error_i18n("hash")] if !data.respond_to?(:[])
16
+
17
+ validated_data = {}
18
+ errors = {}
19
+
20
+ @attributes.each do |attribute_name, type|
21
+ value = data[attribute_name]
22
+
23
+ case type.validate(value)
24
+ in [:ok, validated_value]
25
+ validated_data[attribute_name] = validated_value
26
+ in [:error, err]
27
+ errors[attribute_name] = err
28
+ end
29
+ end
30
+
31
+ return [:error, errors] if errors.any?
32
+
33
+ [:ok, validated_data]
34
+ end
35
+
36
+ def path_params_type
37
+ path_params = @attributes.filter do |name, type|
38
+ type.param_location_path?
39
+ end
40
+
41
+ self.class.new(attributes: path_params)
42
+ end
43
+
44
+ def body_params_type
45
+ body_params = @attributes.filter do |name, type|
46
+ !type.param_location_path?
47
+ end
48
+
49
+ self.class.new(attributes: body_params)
50
+ end
51
+
52
+ concerning :Webpage do
53
+ def summary
54
+ "object"
55
+ end
56
+
57
+ def partial
58
+ "explicit/documentation/type/record"
59
+ end
60
+
61
+ def has_details?
62
+ true
63
+ end
64
+ end
65
+
66
+ concerning :Swagger do
67
+ def swagger_parameters
68
+ attributes.map do |name, type|
69
+ {
70
+ name:,
71
+ in: type.param_location_path? ? "path" : "body",
72
+ description: type.description,
73
+ required: !type.nilable,
74
+ schema: type.swagger_schema
75
+ }
76
+ end
77
+ end
78
+
79
+ def swagger_schema
80
+ properties = attributes.to_h do |name, type|
81
+ [name, type.swagger_schema]
82
+ end
83
+
84
+ required = attributes.filter_map do |name, type|
85
+ type.required? ? name.to_s : nil
86
+ end
87
+
88
+ {
89
+ type: "object",
90
+ properties:,
91
+ required:,
92
+ description: swagger_description([])
93
+ }.compact_blank
94
+ end
95
+ end
96
+ end