-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
User-defined annotations #6063
User-defined annotations #6063
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really nice PR 😄
@@ -162,6 +162,10 @@ module Crystal | |||
self.is_a?(NilType) | |||
end | |||
|
|||
def nilable? | |||
self.is_a?(NilType) || (self.is_a?(UnionType) && self.union_types.any?(&.nil_type?)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't it self.union_types.any?(&.nilable?)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A union type can only have single types, it doesn't recurse (or it doesn't need to recurse).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh ok, didn't know that
I'm definitely not liking the mismatch of naming between annotations definition and their usage in code. It's confusing and has no presedent. If it takes a massive hack in the compiler (hardcode The rest is great. |
@RX14 As I explain somewhere above, I was actually going to do it that way (with the Plus, again, it's how it's done in C# (only in C# you can use the full attribute name too, when using it, but I don't think we need that in Crystal, or at least we could maybe easily support it later). But I wouldn't mind not using the suffix and waiting a bit. It's just that seeing Well, that's my argument anyway :-) |
I agree with RX14. I don't know about conflicts, maybe it's good to prevent having a class and an annotation under the same name? Or you could just make a hack that appends |
Your argument about discerning annotations in docs is not an argument towards particular naming, it's really just a feature request for docs generator. |
That said, if everyone else also dislikes this "hack", for now, I can hardcode that "Primitive" always refer to the primitive annotation, when used in annotations, and later remove that rule. |
thanks, this json serialization, i dream about: class JsonWithDefaults
include JSON::Serializable
property a = 11
property b = "Haha"
property c = true
property d = false
property e : Bool? = false
property f : Int32? = 1
property g : Int32?
property h = [1, 2, 3]
end if we also can generate initialize with named_tuple it would be super. |
@kostya that's possible. What do you mean? |
class JsonWithDefaults
include Initialize
include JSON::Serializable
property a : Int32
property b = "Haha"
end
JsonWithDefaults.new(a: 5, b: "bla")
JsonWithDefaults.new(a: 5)
JsonWithDefaults.from_json(%<{"a":5}>) |
Ah, yes, that's coming soon, too. Actually, it can already be done with the addition of knowing the default value of an instance var, only that the error message if used incorrectly are a bit ugly, but nothing too terrible. |
Please add an emoji in this comment for:
But if we go with 👎 , we'll have to think of another name of the attribute used on top of a type to let the parsing be |
I just pushed a commit that removes the requirement that annotation names must end with "Annotation". I had to hardcode two things in annotations, |
# just yet in annotations, we temporarily hardcode | ||
# that `Primitive` inside annotations means the top | ||
# level primitive. | ||
# We also have the same problem with File::Flags, which |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you put a TODO:
string somewhere in this comment here so we can grep for all todos and not miss this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
src/enum.cr
Outdated
@@ -26,7 +26,7 @@ | |||
# | |||
# ### Flags enum | |||
# | |||
# An enum can be marked with the `@[Flags]` attribute. This changes the default values: | |||
# An enum can be marked with the `FlagsAnnotation` annotation. This changes the default values: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think FlagsAnnotation
should be changed to Flags
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks! I reverted this change because annotations won't have (nice) docs for now, so better not to link them just yet.
src/compiler/crystal/program.cr
Outdated
types["RaisesAnnotation"] = @raises_annotation = AnnotationType.new self, self, "RaisesAnnotation" | ||
types["ReturnsTwiceAnnotation"] = @returns_twice_annotation = AnnotationType.new self, self, "ReturnsTwiceAnnotation" | ||
types["ThreadLocalAnnotation"] = @thread_local_annotation = AnnotationType.new self, self, "ThreadLocalAnnotation" | ||
types["AlwaysInline"] = @always_inline = AnnotationType.new self, self, "AlwaysInline" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So annotations conflicts with top level types? I would have thought they are in a separate category instead. Is it temporary?
So builtin annotations conflicts with top level types? I would have thought they are in a separate category |
They are in the same namespace, yes. Maybe later we can put them in a separate namespace, I don't know. But then, if they are in a different namespace, how would you show them in docs? That's a bit ugly. That's why I first suggested with stick with the "Annotation" suffix, but nobody liked that. But I don't think it's a big deal, really. |
We could put them in a special |
But annotations can appear under other namespaces, like in the |
@kostya Hey, I found a way to do what you want with the current (0.24.2) version of Crystal 😄 class Object
module Initialize
def initialize(**args : **T) forall T
{% for key in T.keys.map(&.id) %}
{% unless @type.instance_vars.map(&.id).includes?(key) %}
{% raise "no argument named '#{key}'" %}
{% end %}
@{{key}} = args[{{key.symbolize}}]
{% end %}
end
end
end
class Foo
include Initialize
property id : Int32
property x = 1
property y = "foo"
end
p Foo.new(id: 1)
p Foo.new(id: 2, y: "hi")
p Foo.new(id: 2, x: 3)
p Foo.new(id: 3, x: 10, y: "hello") And you even get a "nice" error message if you use it in a wrong way:
|
@asterite true, what about keeping the |
Then it will clash with stuff that ends with Annotation. Let's leave it like this, there's no problem, really. |
bug: https://play.crystal-lang.org/#/r/40ab (easy fixable), also when mismatch type bad error message: https://play.crystal-lang.org/#/r/40ao |
That mismatched type error message looks fine to me. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is good to merge though.
Things to specify can be:
|
Let's get a new JSON interface based on this PR merged, then we can cut 0.25 I think. |
@RX14 Based on the sheer amount of features it should be more like 0.30 🍘 |
@RX14 iirc you can't directly use the compiler feature until next release, so the new JSON interface will have to wait next release |
But maybe we can release a 0.24.3 with annotations (only?), so that we can develop that new serialization interface for 0.25 ? |
This can be done. If someone wants to work on this, let us know. Here's a diff to do it: diff --git a/src/json.cr b/src/json.cr
index ce859e2d7..58abfd904 100644
--- a/src/json.cr
+++ b/src/json.cr
@@ -93,3 +93,7 @@ module JSON
end
require "./json/*"
+
+{% if compare_versions(Crystal::VERSION, "0.25.0") >= 0 %}
+ require "./json/next/serialization"
+{% end %}
diff --git a/src/json/next/serialization.cr b/src/json/next/serialization.cr
new file mode 100644
index 000000000..0b14ba494
--- /dev/null
+++ b/src/json/next/serialization.cr
@@ -0,0 +1,7 @@
+module JSON
+ annotation Field
+ end
+
+ module Serializable
+ end
+end In short:
I put it in For specs it'll be a bit trickier. Alternatively, there's no reason why we can't release 0.25.0 "soon", and then work on 0.26.0 next and release it soon too. I used to released maybe once a month in the past. We can release as often as we'd like (except that I'm not in charge of releasing anymore). |
i add some initial work on it: #6082 |
It's been too long since last release... I thought the release process was supposed to be easier now 🤷♂️ |
Release is easier but it still needs someone from manas. @bcardiff should be around in a week or so to cut a release... |
Maybe we should start a release PR now though, to track anything left we want in |
Can we ask them to give you, RX14, or someone else from the core team, the ability to do releases? At this point this is the only thing that's holding us back from managing Crystal as a true open source community project. |
This PR adds user-defined annotations to the language.
Annotations are the old attributes:
I chose to use the name "annotation" instead of "attribute" because the latter is much more common in code: I searched usage of "annotation" and it was all over the place in Crystal projects, whereas I could just find one project that used the name "annotation" somewhere. So to avoid breaking a lot of projects, "annotation" might be a better name.
It works like this.
First you define an annotation (just like any other type):
Note that
annotation
now becomes a keyword, just likeclass
anddef
, so it's prohibited mostly everywhere, so that's why I chose to go with that name, because it's pretty rare.Once you define an annotation, you can use it:
And then you can query it:
In the above example it looks like JSON serialization becomes a lot more tedious than the current
JSON.mapping
. However, the way I imagine it, you would include aJSON::Serializable
module, which would define theinitialize
andto_json
methods, and by default will use all instance variables, even if they don't have theJSON::Field
annotation on them. The annotation is only used to control the serialization, like choosing thekey
, or whether to ignore a field.You can compile the compiler with this PR and try it out, here's a working prototype.
Annotations can also be placed on top of types, and later queried. Methods for now can't have (user-defined) annotations, but maybe in the future they will.
The benefits of having annotations is that it provides a way to attach metadata to instance variables. Right now one has to use a compile-time constant, add info to that, and later process it in a
finished
macro, which isn't ideal (though depending on what you want to do, because of this chicken and egg problem, you might need to still do it like that).Other benefits, which I will highlight just by using the
JSON::Serialization
implementation using annotations:record
includeJSON::Serializable
and not have to repeat theJSON.mapping
declarationOf course this will also apply to YAML serialization and any other formats. And I'm sure you will come up with great ways to use annotations.
Now, the annotation declaration is empty right now:
In the future, we could specify what things an annotation can be applied to, or what are the fields it can hold. But since it's mainly a compile-time thing that you'll interact with in macros, and macros are pretty lax (they feel like a dynamic Crystal), it makes sense not to restrict that. That's why you can still specify a converter like
@[JSON::Field(converter: Time::Format.new("%F %T"))]
, where it's not clear what typeconverter
has (like this, anything is basically an ASTNode, which is what macros work with).A few other things:
TypeNode#nilable?
macro method, which I think is very useful (and used in my prototype json serializable annotation)If we merge this, just in the next release we'll be able to define the
JSON::Serializable
module and all that stuff. We could do it now by usingcompare_version
and defining stuff conditionally based on the next compiler version, but I'd rather wait for a release to be able to try and review stuff more easily.See also: #3620, #3325, #5643