-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Description
I wanted to get an issue opened for tracking/discussion regarding the future of annotations. This issue will essentially be a reiteration of my thoughts from https://forum.crystal-lang.org/t/rfc-annotations-metadata-declaration-dsl/1312/7?u=blacksmoke16 and some of my thoughts regarding annotations in general.
Background
The concept of annotations and how they fit into the macro system works quite well at the moment. Annotations can be used to store metadata that can then be acted upon via macro code. However, things aren't as smooth when you want to access that annotation data at runtime. #7694 helped quite a bit in this regard as it is now possible to do ann.named_args.double_splat, where that double_splat could be into the constructor of a struct for example. This is a pattern I use quite a bit. But there is still an area where this pattern lacks: matching the annotation to its corresponding struct.
In the past this hasn't been too big of a problem for me as my use cases were mainly static mappings that are not use configurable. Or have been simple enough where I could get away with abstractions like https://github.com/athena-framework/config/blob/master/src/athena-config.cr#L61-L70. Unfortunately with my recent work on https://github.com/athena-framework/validator, this issue has become harder and harder to work around.
Example problem
The validator implementation uses constraints like NotBlank or EqualTo. These are represented as classes. You can then apply an annotation to a property to have it be validated against that specific constraint, e.x. Assert::NotBlank. The major challenge with this, especially when dealing with custom constraints that are nested, i.e. #9766; is there is not a good way to know which constraint a given annotation should be "handled" by.
Suggested Solution
The best way I can think of to solve this would be to remove the need for there to be two types in the first place, or in other words, make the constraint the annotation itself. I.e. something like:
@[Annotation]
class NotBlank
def initialize(...); end
endalternatively:
annotation NotBlank
# Allow defining initializers/methods/etc within the `annotation` type
def initialize(...); end
endIn either case the end goal would be to more tightly couple the type that gets applied to something and the logic that annotation should represent at runtime. The initializer (or some other DSL) would define the specific fields that an annotation supports, which if an unexpected one is provided a compile time error would be raised.
I'm personally in favor of the @[Annotation] annotation because it makes annotations less unique and would inherently support methods, inheritance, modules, etc.
I think ideally the current semantics around how annotations work in macro land would remain. I could see there being a new method added to Crystal::Macros::Annotation like #build or something along those lines. This method would essentially be doing like @type.new self.args.splat, self.named_args.double_splat where @type is the type the annotation is related to (assuming the first example). Or maybe just have the annotation expand to that when doing like {{ann}} and remove the need for the method (if that would even be possible).
Implementation
My, probably naive thinking, is that not much would need to change on the compiler side of things; mainly given the macro land representation wouldn't be affected only how an annotation is defined. Then of course the logic around the handling type. Probably another argument on AnnotationDef? I guess my main question would be how to implement the @[Annotation] annotation, but I'm thinking it wouldn't be much different than the other built in annotation types like @[Flags], as it could directly instantiate an AnnotationType?
IMO, this, #8148, #9246, and #8835 would be a game changer in the usability/effectiveness of annotations, and macros in general.
WDYT?