Annotations

    Users can define their own annotations using the annotation keyword, which works similarly to defining a class or struct.

    The annotation can then be applied to various items, including:

    • Instance and class methods
    • Instance variables
    • Classes, structs, enums, and modules
    1. annotation MyAnnotation
    2. end
    3. @[MyAnnotation]
    4. def foo
    5. "foo"
    6. end
    7. @[MyAnnotation]
    8. class Klass
    9. end
    10. @[MyAnnotation]
    11. module MyModule
    12. end

    Annotations are best used to store metadata about a given instance variable, type, or method so that it can be read at compile time using macros. One of the main benefits of annotations is that they are applied directly to instance variables/methods, which causes classes to look more natural since a standard macro is not needed to create these properties/methods.

    A few applications for annotations:

    Have an annotation that when applied to an instance variable determines if that instance variable should be serialized, or with what key. Crystal’s JSON::Serializable and are examples of this.

    An annotation could be used to designate a property as an ORM column. The name and type of the instance variable can be read off the TypeNode in addition to the annotation; removing the need for any ORM specific macro. The annotation itself could also be used to store metadata about the column, such as if it is nullable, the name of the column, or if it is the primary key.

    1. annotation MyAnnotaion
    2. end
    3. # The fields can either be a key/value pair
    4. @[MyAnnotation(key: "value", value: 123)]
    5. # Or positional

    The values of annotation key/value pairs can be accessed at compile time via the [] method.

    The named_args method can be used to read all key/value pairs on an annotation as a NamedTupleLiteral. This method is defined on all annotations by default, and is unique to each applied annotation.

    1. annotation MyAnnotation
    2. end
    3. @[MyAnnotation(value: 2, name: "Jim")]
    4. def annotation_named_args
    5. {{ @def.annotation(MyAnnotation).named_args }}
    6. end
    7. annotation_named_args # => {value: 2, name: "Jim"}

    Since this method returns a NamedTupleLiteral, all of the on that type are available for use. Especially which makes it easy to pass annotation arguments to methods.

    1. annotation MyAnnotation
    2. end
    3. class SomeClass
    4. def initialize(@value : Int32, @name : String); end
    5. end
    6. @[MyAnnotation(value: 2, name: "Jim")]
    7. def new_test
    8. {% begin %}
    9. SomeClass.new {{ @def.annotation(MyAnnotation).named_args.double_splat }}
    10. {% end %}
    11. end
    12. new_test # => #<SomeClass:0x5621a19ddf00 @name="Jim", @value=2>

    Positional values can be accessed at compile time via the [] method; however, only one index can be accessed at a time.

    The args method can be used to read all positional arguments on an annotation as a TupleLiteral. This method is defined on all annotations by default, and is unique to each applied annotation.

    1. annotation MyAnnotation
    2. end
    3. @[MyAnnotation(1, 2, 3, 4)]
    4. def annotation_args
    5. {{ @def.annotation(MyAnnotation).args }}
    6. end
    7. annotation_args # => {1, 2, 3, 4}

    Since the return type of TupleLiteral is iterable, we can rewrite the previous example in a better way. By extension, all of the on TupleLiteral are available for use as well.

    1. end
    2. @[MyAnnotation(1, "foo", true, 17.0)]
    3. def annotation_read
    4. {% for value, idx in @def.annotation(MyAnnotation).args %}
    5. pp "{{ idx }} = #{{{ value }}}"
    6. {% end %}
    7. end
    8. annotation_read
    9. # Which would print
    10. "0 = 1"
    11. "1 = foo"
    12. "2 = true"
    13. "3 = 17.0"

    Note

    If multiple annotations of the same type are applied, the .annotation method will return the last one.

    The @type and variables can be used to get a TypeNode or Def object to use the .annotation method on. However, it is also possible to get TypeNode/Def types using other methods on TypeNode. For example TypeNode.all_subclasses or TypeNode.methods, respectively.

    The TypeNode.instance_vars can be used to get an array of instance variable MetaVar objects that would allow reading annotations defined on those instance variables.

    Note

    TypeNode.instance_vars currently only works in the context of an instance/class method.

    1. annotation MyAnnotation
    2. end
    3. @[MyAnnotation("foo")]
    4. @[MyAnnotation(123)]
    5. @[MyAnnotation(123)]
    6. def annotation_read
    7. {% for ann, idx in @def.annotations(MyAnnotation) %}
    8. pp "Annotation {{ idx }} = {{ ann[0].id }}"
    9. {% end %}
    10. end
    11. annotation_read
    12. # Which would print
    13. "Annotation 0 = foo"
    14. "Annotation 1 = 123"