Skip to content

Commit

Permalink
Prep work for query/request parameter refactor (#425)
Browse files Browse the repository at this point in the history
  • Loading branch information
Blacksmoke16 authored Aug 1, 2024
1 parent 48478b3 commit ea68da6
Show file tree
Hide file tree
Showing 11 changed files with 49 additions and 46 deletions.
2 changes: 1 addition & 1 deletion docs/why_athena.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,7 @@ class UserController < ATH::Controller
@[ARTA::Post("/user")]
@[ATHA::View(status: :created)]
def new_user(
@[ATHR::RequestBody::Extract]
@[ATHA::MapRequestBody]
user_create : UserCreate
) : UserCreate
# Use the provided UserCreate instance to create an actual User DB record.
Expand Down
4 changes: 2 additions & 2 deletions src/components/framework/spec/compiler_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -379,12 +379,12 @@ describe Athena::Framework do

describe ATHR::RequestBody do
it "when the action parameter is not serializable" do
assert_error " The annotation '@[ATHR::RequestBody::Extract]' cannot be applied to 'CompileController#action:foo : Foo' since the 'Athena::Framework::Controller::ValueResolvers::RequestBody' resolver only supports parameters of type 'Athena::Serializer::Serializable | JSON::Serializable'.", <<-CODE
assert_error " The annotation '@[ATHA::MapRequestBody]' cannot be applied to 'CompileController#action:foo : Foo' since the 'Athena::Framework::Controller::ValueResolvers::RequestBody' resolver only supports parameters of type 'Athena::Serializer::Serializable | JSON::Serializable'.", <<-CODE
record Foo, text : String
class CompileController < ATH::Controller
@[ARTA::Get(path: "/")]
def action(@[ATHR::RequestBody::Extract] foo : Foo) : Foo
def action(@[ATHA::MapRequestBody] foo : Foo) : Foo
foo
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ struct RequestBodyResolverTest < ASPEC::TestCase
ATH::Controller::ParameterMetadata(T).new(
"foo",
annotation_configurations: ADI::AnnotationConfigurations.new({
ATHR::RequestBody::Extract => [
ATHR::RequestBody::ExtractConfiguration.new,
ATHA::MapRequestBody => [
ATHA::MapRequestBodyConfiguration.new,
] of ADI::AnnotationConfigurations::ConfigurationBase,
} of ADI::AnnotationConfigurations::Classes => Array(ADI::AnnotationConfigurations::ConfigurationBase))
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ describe ATHR::Time do
parameter = ATH::Controller::ParameterMetadata(::Time).new(
"foo",
annotation_configurations: ADI::AnnotationConfigurations.new({
ATHR::Time::Format => [
ATHR::Time::FormatConfiguration.new(format: "%Y--%m//%d %T"),
ATHA::MapTime => [
ATHA::MapTimeConfiguration.new(format: "%Y--%m//%d %T"),
] of ADI::AnnotationConfigurations::ConfigurationBase,
} of ADI::AnnotationConfigurations::Classes => Array(ADI::AnnotationConfigurations::ConfigurationBase))
)
Expand All @@ -90,8 +90,8 @@ describe ATHR::Time do
parameter = ATH::Controller::ParameterMetadata(::Time).new(
"foo",
annotation_configurations: ADI::AnnotationConfigurations.new({
ATHR::Time::Format => [
ATHR::Time::FormatConfiguration.new(format: "%Y--%m//%d %T", location: Time::Location.fixed(9001)),
ATHA::MapTime => [
ATHA::MapTimeConfiguration.new(format: "%Y--%m//%d %T", location: Time::Location.fixed(9001)),
] of ADI::AnnotationConfigurations::ConfigurationBase,
} of ADI::AnnotationConfigurations::Classes => Array(ADI::AnnotationConfigurations::ConfigurationBase))
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@
class GenericAnnotationEnabledCustomResolver
include ATHR::Interface

configuration Enable
configuration ::MyResolverAnnotation

def resolve(request : ATH::Request, parameter : ATH::Controller::ParameterMetadata(Float64)) : Float64?
return unless parameter.annotation_configurations.has? Enable
return unless parameter.annotation_configurations.has? MyResolverAnnotation

3.14
end

def resolve(request : ATH::Request, parameter : ATH::Controller::ParameterMetadata(String)) : String?
return unless parameter.annotation_configurations.has? Enable
return unless parameter.annotation_configurations.has? MyResolverAnnotation

"fooo"
end
Expand All @@ -24,15 +24,15 @@ end
class ArgumentResolverController < ATH::Controller
@[ARTA::Post("/float")]
def happy_path1(
@[GenericAnnotationEnabledCustomResolver::Enable]
@[MyResolverAnnotation]
value : Float64
) : Float64
value
end

@[ARTA::Post("/string")]
def happy_path2(
@[GenericAnnotationEnabledCustomResolver::Enable]
@[MyResolverAnnotation]
value : String
) : String
value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class QueryParamController < ATH::Controller
@[ATHA::QueryParam("value")]
@[ARTA::Get("/param/enabled/resolver")]
def annotation_enabled_resolver_on_query_param_parameter(
@[GenericAnnotationEnabledCustomResolver::Enable]
@[MyResolverAnnotation]
value : String
) : String
value
Expand All @@ -71,7 +71,7 @@ class QueryParamController < ATH::Controller
@[ATHA::QueryParam("time")]
@[ARTA::Get("/nt_time")]
def nt_time(
@[ATHR::Time::Format(format: "%Y--%m//%d %T")]
@[ATHA::MapTime(format: "%Y--%m//%d %T")]
time : Time
) : String
"Today is: #{time}"
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 @@ -76,7 +76,7 @@ module Athena::Framework
#
# 1. `ATHR::Time` (105) - Attempts to resolve a value from the request attributes into a `::Time` instance,
# defaulting to [RFC 3339](https://crystal-lang.org/api/Time.html#parse_rfc3339%28time:String%29-class-method).
# Format/location can be customized via the `ATHR::Time::Format` annotation.
# Format/location can be customized via the `ATHA::MapTime` annotation.
#
# 1. `ATHR::UUID` (105) - Attempts to resolve a value from the request attributes into a `::UUID` instance.
#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,13 +273,17 @@
# This feature pairs nicely with the [free var][Athena::Framework::Controller::ValueResolvers::Interface--free-vars] section as it essentially allows
# scoping the possible types of `T` to the set of types defined as part of the module.
module Athena::Framework::Controller::ValueResolvers::Interface
# :nodoc:
ANNOTATION_RESOLVER_MAP = {} of Nil => Nil

# The tag name for `ATHR::Interface` services.
TAG = "athena.controller.value_resolver"

# Helper macro around `ADI.configuration_annotation` that allows defining resolver specific annotations.
# See the underlying macro and the [configuration][Athena::Framework::Controller::ValueResolvers::Interface--configuration] section for more information.
macro configuration(name, *args)
ADI.configuration_annotation ::{{@type}}::{{name.id}}{% unless args.empty? %}, {{args.splat}}{% end %}
ADI.configuration_annotation {{name.id}}{% unless args.empty? %}, {{args.splat}}{% end %}
{% ANNOTATION_RESOLVER_MAP[name.id] = @type.resolve %}
end

# Represents an `ATHR::Interface` that only supports a subset of types.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@[ADI::Register(tags: [{name: ATHR::Interface::TAG, priority: 105}])]
# Attempts to resolve the value of any parameter with the `ATHR::RequestBody::Extract` annotation by
# Attempts to resolve the value of any parameter with the `ATHA::MapRequestBody` annotation by
# deserializing the request body into an object of the type of the related parameter.
# Also handles running any validations defined on it, if it is `AVD::Validatable`.
# Requires the type of the related parameter to include either `ASR::Serializable` or `JSON::Serializable`.
Expand Down Expand Up @@ -31,7 +31,7 @@
# @[ARTA::Post("/user")]
# @[ATHA::View(status: :created)]
# def new_user(
# @[ATHR::RequestBody::Extract]
# @[ATHA::MapRequestBody]
# user_create : UserCreate
# ) : UserCreate
# # Use the provided UserCreate instance to create an actual User DB record.
Expand Down Expand Up @@ -89,33 +89,25 @@ struct Athena::Framework::Controller::ValueResolvers::RequestBody

# Enables the `ATHR::RequestBody` resolver for the parameter this annotation is applied to.
# See the related resolver documentation for more information.
configuration Extract
configuration ::Athena::Framework::Annotations::MapRequestBody

def initialize(
@serializer : ASR::SerializerInterface,
@validator : AVD::Validator::ValidatorInterface
); end

# :inherit:
def resolve(request : ATH::Request, parameter : ATH::Controller::ParameterMetadata(T)) : T? forall T
return unless parameter.annotation_configurations.has? Extract
def resolve(request : ATH::Request, parameter : ATH::Controller::ParameterMetadata)
return unless parameter.annotation_configurations.has? ATHA::MapRequestBody

if !(body = request.body) || body.peek.try &.empty?
raise ATH::Exceptions::BadRequest.new "Request does not have a body."
end

object = nil

begin
{% begin %}
{% if T.instance <= ASR::Serializable %}
object = @serializer.deserialize T, body, :json
{% elsif T.instance <= JSON::Serializable %}
object = T.from_json body
{% else %}
return
{% end %}
{% end %}
unless object = self.map_request_body body, parameter.type
return
end
rescue ex : JSON::ParseException | ASR::Exceptions::DeserializationException
raise ATH::Exceptions::BadRequest.new "Malformed JSON payload.", cause: ex
end
Expand All @@ -125,6 +117,17 @@ struct Athena::Framework::Controller::ValueResolvers::RequestBody
raise AVD::Exceptions::ValidationFailed.new errors unless errors.empty?
end

object.as T
object
end

private def map_request_body(body : IO, klass : ASR::Serializable.class)
@serializer.deserialize klass, body, :json
end

private def map_request_body(body : IO, klass : JSON::Serializable.class)
klass.from_json body
end

private def map_request_body(body : IO, klass : _) : Nil
end
end
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@[ADI::Register(tags: [{name: ATHR::Interface::TAG, priority: 105}])]
# Attempts to parse a date(time) string into a `::Time` instance.
#
# Optionally allows specifying the *format* and *location* to use when parsing the string via the `ATHR::Time::Format` annotation.
# Optionally allows specifying the *format* and *location* to use when parsing the string via the `ATHA::MapTime` annotation.
# If no *format* is specified, defaults to [RFC 3339](https://crystal-lang.org/api/Time.html#parse_rfc3339%28time:String%29-class-method).
# Defaults to `UTC` if no *location* is specified with the annotation.
#
Expand All @@ -15,7 +15,7 @@
# class ExampleController < ATH::Controller
# @[ARTA::Get(path: "/event/{start_time}/{end_time}")]
# def event(
# @[ATHR::Time::Format("%F", location: Time::Location.load("Europe/Berlin"))]
# @[ATHA::MapTime("%F", location: Time::Location.load("Europe/Berlin"))]
# start_time : Time,
# end_time : Time
# ) : Nil
Expand All @@ -33,7 +33,7 @@ struct Athena::Framework::Controller::ValueResolvers::Time

# Allows customizing the time format and/or location used to parse the string datetime as part of the `ATHR::Time` resolver.
# See the related resolver documentation for more information.
configuration Format, format : String? = nil, location : ::Time::Location = ::Time::Location::UTC
configuration ::Athena::Framework::Annotations::MapTime, format : String? = nil, location : ::Time::Location = ::Time::Location::UTC

# :inherit:
def resolve(request : ATH::Request, parameter : ATH::Controller::ParameterMetadata) : ::Time?
Expand All @@ -45,7 +45,7 @@ struct Athena::Framework::Controller::ValueResolvers::Time

return unless value = request.attributes.get? parameter.name, String?

if !(configuration = parameter.annotation_configurations[Format]?) || !(format = configuration.format)
if !(configuration = parameter.annotation_configurations[ATHA::MapTime]?) || !(format = configuration.format)
return ::Time.parse_rfc3339(value)
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,16 +197,12 @@ module Athena::Framework::Routing::AnnotationRouteLoader

# Process custom annotation types
ADI::CUSTOM_ANNOTATIONS.each do |ann_class|
ann_class = ann_class.resolve
annotations = [] of Nil

(arg.annotations ann_class).each do |ann|
(arg.annotations ann_class.resolve).each do |ann|
# See if this annotation relates to a typed resolver interface,
# checking the namespace the configuration annotation was defined in for the interface.
#
# If there is only 1 part to the annotation's name, we know it was defined in the top level,
# and as such, does not related to a resolver type
resolver = 1 == ann.name.names.size ? false : parse_type(ann.name.names[0..-2].join "::").resolve
resolver = ATH::Controller::ValueResolvers::Interface::ANNOTATION_RESOLVER_MAP[ann_class.id]

if resolver && (interface = resolver.resolve.ancestors.find &.<=(ATHR::Interface::Typed))
supported_types = interface.type_vars.first.type_vars
Expand All @@ -219,7 +215,7 @@ module Athena::Framework::Routing::AnnotationRouteLoader
annotations << "#{ann_class}Configuration.new(#{ann.args.empty? ? "".id : "#{ann.args.splat},".id}#{ann.named_args.double_splat})".id
end

parameter_annotation_configurations[ann_class] = "(#{annotations} of ADI::AnnotationConfigurations::ConfigurationBase)".id unless annotations.empty?
parameter_annotation_configurations[ann_class.resolve] = "(#{annotations} of ADI::AnnotationConfigurations::ConfigurationBase)".id unless annotations.empty?
end

arg.raise "Route action parameter '#{klass.name}##{m.name}:#{arg.name}' must have a type restriction." if arg.restriction.is_a? Nop
Expand Down

0 comments on commit ea68da6

Please sign in to comment.