Class: Taro::Rails::ResponseValidator

Inherits:
Struct
  • Object
show all
Defined in:
lib/taro/rails/response_validator.rb

Overview

This runs on for every response, so we are using Struct instead of Data here for performance reasons: bugs.ruby-lang.org/issues/19693

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#controllerObject

Returns the value of attribute controller

Returns:

  • (Object)

    the current value of controller



4
5
6
# File 'lib/taro/rails/response_validator.rb', line 4

def controller
  @controller
end

#declarationObject

Returns the value of attribute declaration

Returns:

  • (Object)

    the current value of declaration



4
5
6
# File 'lib/taro/rails/response_validator.rb', line 4

def declaration
  @declaration
end

#renderedObject

Returns the value of attribute rendered

Returns:

  • (Object)

    the current value of rendered



4
5
6
# File 'lib/taro/rails/response_validator.rb', line 4

def rendered
  @rendered
end

Class Method Details

.call(controller, declaration, rendered) ⇒ Object



5
6
7
# File 'lib/taro/rails/response_validator.rb', line 5

def self.call(controller, declaration, rendered)
  new(controller, declaration, rendered).call
end

Instance Method Details

#callObject



9
10
11
12
13
14
15
16
17
18
# File 'lib/taro/rails/response_validator.rb', line 9

def call
  if declared_return_type.nil?
    fail_if_declaration_expected
  elsif declared_return_type < Taro::Types::NestedResponseType
    field = declared_return_type.nesting_field
    check(field.type, denest_rendered(field.name))
  else
    check(declared_return_type, rendered)
  end
end

#check(type, value) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/taro/rails/response_validator.rb', line 55

def check(type, value)
  if type < Taro::Types::ScalarType
    check_scalar(type, value)
  elsif type < Taro::Types::ListType &&
        type.item_type < Taro::Types::ScalarType
    check_scalar_array(type, value)
  elsif type < Taro::Types::EnumType
    check_enum(type, value)
  else
    check_custom_type(type, value)
  end
end

#check_custom_type(type, value) ⇒ Object

For complex/object types, we ensure conformance by checking whether the type was used for rendering. This has performance benefits compared to going over the structure a second time.



93
94
95
96
97
98
99
# File 'lib/taro/rails/response_validator.rb', line 93

def check_custom_type(type, value)
  # Ignore types without a specified structure.
  return if type <= Taro::Types::ObjectTypes::FreeFormType
  return if type <= Taro::Types::ObjectTypes::NoContentType

  strict_check_custom_type(type, value)
end

#check_enum(type, value) ⇒ Object



83
84
85
86
87
88
# File 'lib/taro/rails/response_validator.rb', line 83

def check_enum(type, value)
  # coercion checks non-emptyness + enum match
  type.new(value).cached_coerce_response
rescue Taro::Error => e
  fail_with(e.message)
end

#check_scalar(type, value) ⇒ Object

For scalar and enum types, we want to support e.g. ‘render json: 42`, and not require using the type as in `BeautifulNumbersEnum.render(42)`.



70
71
72
73
74
75
76
# File 'lib/taro/rails/response_validator.rb', line 70

def check_scalar(type, value)
  case type.openapi_type
  when :integer, :number then value.is_a?(Numeric)
  when :string           then value.is_a?(String) || value.is_a?(Symbol)
  when :boolean          then [true, false].include?(value)
  end || fail_with("Expected a #{type.openapi_type}, got: #{value.class}.")
end

#check_scalar_array(type, value) ⇒ Object



78
79
80
81
# File 'lib/taro/rails/response_validator.rb', line 78

def check_scalar_array(type, value)
  value.is_a?(Array) || fail_with('Expected an Array.')
  value.empty? || check_scalar(type.item_type, value.first)
end

#declared_return_typeObject



20
21
22
# File 'lib/taro/rails/response_validator.rb', line 20

def declared_return_type
  @declared_return_type ||= declaration.returns[controller.status]
end

#denest_rendered(nesting) ⇒ Object

support ‘returns :some_nesting, type: ’SomeType’‘ used like `render json: { some_nesting: SomeType.render(some_object) }`



45
46
47
48
49
50
51
52
53
# File 'lib/taro/rails/response_validator.rb', line 45

def denest_rendered(nesting)
  rendered.is_a?(Hash) || fail_with("Expected Hash, got #{rendered.class}.")

  if rendered.key?(nesting)
    rendered[nesting]
  else
    fail_with "Expected key :#{nesting}, got: #{rendered.keys}."
  end
end

#fail_if_declaration_expectedObject

Rack, Rails and gems commonly trigger rendering of 400, 404, 500 etc. Declaring these codes should be optional. Otherwise the api schema would get bloated as there are no “global” return declarations in OpenAPI v3, and we’d need to export all of these for every single endpoint. v4 might change this. github.com/OAI/OpenAPI-Specification/issues/521



29
30
31
32
33
# File 'lib/taro/rails/response_validator.rb', line 29

def fail_if_declaration_expected
  controller.status.to_s.match?(/^[123]|422/) && fail_with(<<~MSG)
    No return type declared for this status.
  MSG
end

#fail_with(message) ⇒ Object



35
36
37
38
39
40
41
# File 'lib/taro/rails/response_validator.rb', line 35

def fail_with(message)
  raise Taro::ResponseError.new(<<~MSG, rendered, self)
    Response validation error for
    #{controller.class}##{controller.action_name}, code #{controller.status}":
    #{message}
  MSG
end

#strict_check_custom_type(type, value) ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/taro/rails/response_validator.rb', line 101

def strict_check_custom_type(type, value)
  used_type, rendered_object_id = type.last_render
  used_type == type || used_type&.<(type) || fail_with(<<~MSG)
    Expected to use #{type}.render, but the last type rendered
    was: #{used_type || 'no type'}.
  MSG

  rendered_object_id == value.__id__ || fail_with(<<~MSG)
    #{type}.render was called, but the result
    of this call was not used in the response.
  MSG
end