Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Additional RequestBody annotation properties #486

Merged
merged 5 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ struct RequestBodyResolverTest < ASPEC::TestCase
object.name.should eq "Fred"
end

def test_it_supports_specifying_accepted_formats : Nil
expect_raises ATH::Exception::UnsupportedMediaType, %(Unsupported format, expects one of: 'json, xml', but got 'form'.) do
@target.resolve(
new_request(body: "id=10&name=Fred", format: "form"),
self.get_config(MockURISerializableEntity, configuration: ATHA::MapRequestBodyConfiguration.new(["json", "xml"]))
)
end
end

def test_it_supports_query_string_serializable : Nil
serializer = DeserializableMockSerializer(MockURISerializableEntity).new
serializer.deserialized_response = MockURISerializableEntity.new 10, "Fred"
Expand Down
2 changes: 1 addition & 1 deletion src/components/framework/src/athena.cr
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ module Athena::Framework
#
# 1. `ATHR::UUID` (105) - Attempts to resolve a value from the request attributes into a `::UUID` instance.
#
# 1. `ATHR::RequestBody` (105) - If enabled, attempts to deserialize the request body into the type of the related parameter, running any validations, if any.
# 1. `ATHR::RequestBody` (105) - If enabled, attempts to deserialize the request body/query string into the type of the related parameter, running any defined validations if applicable.
#
# 1. `ATHR::RequestAttribute` (100) - Provides a value stored in `ATH::Request#attributes` if one with the same name as the action parameter exists.
#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@
#
# In all of the examples so far, the resolvers could be applied to any parameter of any type and all of the logic to resolve a value would happen at runtime.
# In some cases a specific resolver may only support a single, or small subset of types.
# Such as how the `ATHR::RequestBody` resolver only allows `ASR::Serializable` or `JSON::Serializable` types.
# Such as how the `ATHR::RequestBody` resolver only allows `ASR::Serializable`, `JSON::Serializable`, or `URI::Params::Serializable` types.
# In this case, the `ATHR::Interface::Typed` module may be used to define the allowed parameter types.
#
# WARNING: Strict typing is _ONLY_ supported when a configuration annotation is used to enable the resolver.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,65 @@ struct Athena::Framework::Controller::ValueResolvers::RequestBody

# Enables the `ATHR::RequestBody` resolver for the parameter this annotation is applied to based on the request's body.
# See the related resolver documentation for more information.
configuration ::Athena::Framework::Annotations::MapRequestBody
#
# ```
# class UserController < ATH::Controller
# @[ARTA::Post("/user")]
# def new_user(
# @[ATHA::MapRequestBody]
# user_create : UserCreateDTO,
# ) : UserCreateDTO
# user_create
# end
# end
# ```
#
# # Configuration
#
# ## Optional Arguments
#
# ### accept_formats
#
# **Type:** `Array(String)?` **Default:** `nil`
#
# Allows whitelisting the allowed [request format(s)][ATH::Request::FORMATS].
# If the `ATH::Request#content_type_format` is not included in this list, a `ATH::Exception::UnsupportedMediaType` error will be raised.
#
# ### validation_groups
#
# **Type:** `Array(String) | AVD::Constraints::GroupSequence | Nil` **Default:** `nil`
#
# The [validation groups](/Validator/Constraint/#Athena::Validator::Constraint--validation-groups) that should be used when validating the resolved object.
configuration ::Athena::Framework::Annotations::MapRequestBody,
accept_formats : Array(String)? = nil,
validation_groups : Array(String) | AVD::Constraints::GroupSequence | Nil = nil

# Enables the `ATHR::RequestBody` resolver for the parameter this annotation is applied to based on the request's query string.
# See the related resolver documentation for more information.
configuration ::Athena::Framework::Annotations::MapQueryString
#
# ```
# class ArticleController < ATH::Controller
# @[ARTA::Get("/articles")]
# def articles(
# @[ATHA::MapQueryString]
# pagination_context : PaginationContext,
# ) : Array(Article)
# # ...
# end
# end
# ```
#
# # Configuration
#
# ## Optional Arguments
#
# ### validation_groups
#
# **Type:** `Array(String) | AVD::Constraints::GroupSequence | Nil` **Default:** `nil`
#
# The [validation groups](/Validator/Constraint/#Athena::Validator::Constraint--validation-groups) that should be used when validating the resolved object.
configuration ::Athena::Framework::Annotations::MapQueryString,
validation_groups : Array(String) | AVD::Constraints::GroupSequence | Nil = nil

def initialize(
@serializer : ASR::SerializerInterface,
Expand All @@ -112,23 +166,27 @@ struct Athena::Framework::Controller::ValueResolvers::RequestBody

# :inherit:
def resolve(request : ATH::Request, parameter : ATH::Controller::ParameterMetadata)
object = if parameter.annotation_configurations.has?(ATHA::MapQueryString)
self.map_query_string request, parameter
elsif parameter.annotation_configurations.has?(ATHA::MapRequestBody)
self.map_request_body request, parameter
validation_groups = nil

object = if configuration = parameter.annotation_configurations[ATHA::MapQueryString]?
validation_groups = configuration.validation_groups
self.map_query_string request, parameter, configuration
elsif configuration = parameter.annotation_configurations[ATHA::MapRequestBody]?
validation_groups = configuration.validation_groups
self.map_request_body request, parameter, configuration
else
return
end

if object.is_a? AVD::Validatable
errors = @validator.validate object
errors = @validator.validate object, groups: validation_groups
raise AVD::Exception::ValidationFailed.new errors unless errors.empty?
end

object
end

private def map_query_string(request : ATH::Request, parameter : ATH::Controller::ParameterMetadata)
private def map_query_string(request : ATH::Request, parameter : ATH::Controller::ParameterMetadata, configuration : ATHA::MapQueryStringConfiguration)
return unless query = request.query
return if query.nil? && (parameter.nilable? || parameter.has_default?)

Expand All @@ -137,13 +195,19 @@ struct Athena::Framework::Controller::ValueResolvers::RequestBody
raise ATH::Exception::BadRequest.new "Malformed query string.", cause: ex
end

private def map_request_body(request : ATH::Request, parameter : ATH::Controller::ParameterMetadata)
private def map_request_body(request : ATH::Request, parameter : ATH::Controller::ParameterMetadata, configuration : ATHA::MapRequestBodyConfiguration)
if !(body = request.body) || body.peek.try &.empty?
raise ATH::Exception::BadRequest.new "Request does not have a body."
end

format = request.content_type_format

if (accept_formats = configuration.accept_formats) && !accept_formats.includes? format
raise ATH::Exception::UnsupportedMediaType.new "Unsupported format, expects one of: '#{accept_formats.join(", ")}', but got '#{format}'."
end

# We have to use separate deserialization methods with the case such that a type that includes multiple modules is handled as expected.
case request.content_type_format
case format
when "form"
self.deserialize_form body, parameter.type
when "json"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ require "uuid"
# # GET /uuid/b115c7a5-0a13-47b4-b4ac-55b3e2686946 # => "Version: V4 - Variant: RFC4122"
# ```
#
# TIP: Checkout `ART::Requirement` for an easy way to restrict/validate the version of the UUID that is allowed.
# TIP: Checkout [ART::Requirement](/Routing/Requirement/) for an easy way to restrict/validate the version of the UUID that is allowed.
struct Athena::Framework::Controller::ValueResolvers::UUID
include Athena::Framework::Controller::ValueResolvers::Interface

Expand Down
3 changes: 3 additions & 0 deletions src/components/framework/src/ext/routing.cr
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
require "athena-routing"

# :nodoc:
module Athena::Framework::Routing; end

require "./routing/*"
Loading