macro resolve(_type)
{% puts _type %}
end
module Models
class User
resolve Post
end
class Post
resolve User
end
end
# => Post
# => User
I want resolve to print Models::Post and Models::User accordingly. How to achieve this?
You can do {{ puts _type.resolve.name }}. This prints A::Foo
Path#resolve: https://crystal-lang.org/api/0.24.1/Crystal/Macros/Path.html#resolve%3AASTNode-instance-method
TypeNode#name: https://crystal-lang.org/api/0.24.1/Crystal/Macros/TypeNode.html#name%3AMacroId-instance-method
@lbguilherme please see updated question :blue_heart:
You pretty much cannot. But you can not use macros for this and go for somehow storing the structure at runtime:
MODELS = [] of BaseModel.class
class BaseModel
@@mentions = [] of Object.class
def self.mention(t)
@@mentions << t
end
def self.mentions
@@mentions
end
macro inherited
MODELS << self
end
end
module Models
class User < BaseModel
mention Post
mention UInt32
mention String
end
class Post < BaseModel
mention User
end
end
MODELS.each do |model|
puts "The model '#{model}' mentions #{model.mentions}"
end
Or, if you were going to actually use the macros to generate code, do so in-place so you don't need the fully qualified name of each type (unless you are really trying to print the type for the user).
Macros are executed as soon as they are seen/triggered, there is no way to delay them.
macro resolve(_type)
macro finished
\{% puts {{_type}} %}
end
end
module Models
class User
resolve Post
end
class Post
resolve User
end
end
@asterite that's just a hack which works if the full name is just supposed to be printed. But it won't work if it is supposed to be used as a value directly at the call site.
Maybe it would be possible to have a method to resolve a path relative to @type?
Well, the original use case is a hack, so a hacky solution is fine.
I have no idea what he's trying to do, but if that works, then why not?
@lbguilherme @asterite @straight-shoota big thanks for your fast responses. I appreciate it so much!
Sadly, as @straight-shoota said, finished hack would not work for latter class usage...
@lbguilherme I cannot find your solution universal; also I'm trying to solve an issue for the pretty mature project core.cr here, and it's structure already differs a lot from your proposal.
Being a Crystal passionate for half an year (:tada:), I found out that issues with non-minimal examples get very low attention and I totally get it. But now, having enough participants, I can share a slightly bigger example with you.
What I need is to get rid of `reference Models::Post` in favor of `reference Post`, because paths can get really long.
This is a simplified code of what's happening inside Core (and it's working):
macro schema(&block)
REFERENCES = [] of NamedTuple
{{yield}}
end
macro reference(name, _type)
{% REFERENCES.push({name: name, type: _type}) %}
end
module Models
class User
schema do
reference :post, Models::Post # Must specify full path to work
end
property primary_key : String? = nil
def self.primary_key
:uuid
end
end
class Post
schema do
reference :author, Models::User # Must specify full path to work
end
property primary_key : Int32? = nil
def self.primary_key
:id
end
end
end
class Query(T)
def where(**where)
where.to_h.each do |key, value|
{% begin %}
case key
{% for reference in T::REFERENCES %}
when {{reference[:name]}}
if value.nil?
return "WHERE IS NULL"
elsif value.is_a?({{reference[:type]}})
return "WHERE {{reference[:name].id.stringify.id}}.#{value.class.primary_key} = #{value.primary_key}"
end
{% end %}
else
raise "Invalid"
end
{% end %}
end
end
end
user = Models::User.new
user.primary_key = "foo"
puts Query(Models::Post).new.where(author: user)
# => WHERE author.uuid = foo
Again, thank you very much for this. I hope you can help me to solve it.
P.S: I'm sorry for constantly updating my comments :sweat_smile:
Well, I think something like that already exists and is working in Lucky. My first observation is that the references shouldn't be global, but instead stored in each class.
As a side note, I don't think this discussion belongs here, probably stack overflow, google groups, irc, etc.
@asterite Lucky has exactly the same associations structure:
macro inherited
ASSOCIATIONS = [] of {name: Symbol, foreign_key: Symbol}
end
macro association(table_name, foreign_key = nil)
{% ASSOCIATIONS << {name: table_name.id, foreign_key: foreign_key} %}
end
But it has another Query approach, so I'm not lucky here.
I do think it's related to Crystal itself. Resolving full path in macros is something about its core features. I was hoping to get a direct response from core members because noone knows macros better than you. Just copy the code and run it, please.
PS: Google groups & irc are past century, who uses them?
I already showed the answer in a comment above: use macro finished.
macro schema(&block)
REFERENCES = [] of NamedTuple
{{yield}}
end
macro reference(name, _type)
macro finished
\{% REFERENCES.push({name: {{name}}, type: {{_type}}}) %}
end
end
module Models
class User
schema do
reference :post, Post # Must specify full path to work
end
property primary_key : String? = nil
def self.primary_key
:uuid
end
end
class Post
schema do
reference :author, User # Must specify full path to work
end
property primary_key : Int32? = nil
def self.primary_key
:id
end
end
end
class Query(T)
def where(**where)
where.to_h.each do |key, value|
{% begin %}
case key
{% for reference in T::REFERENCES %}
when {{reference[:name]}}
if value.nil?
return "WHERE IS NULL"
elsif value.is_a?({{reference[:type]}})
return "WHERE {{reference[:name].id.stringify.id}}.#{value.class.primary_key} = #{value.primary_key}"
end
{% end %}
else
raise "Invalid"
end
{% end %}
end
end
end
user = Models::User.new
user.primary_key = "foo"
puts Query(Models::Post).new.where(author: user)
# => WHERE author.uuid = foo
I'm OK with using Slack, I think some others were against it.
@vladfaust if you don't like IRC, use gitter.
@asterite this fails if specifying generic type:
reference :author, User?
->
1. macro finished
> 2. {% REFERENCES.push({name: :author, type: ::Union(User, ::Nil)}) %}
3. end
can't execute Generic in a macro
Yeah, that currently doesn't work, sorry.
Thanks, @asterite! macro finished successfully implemented in https://github.com/vladfaust/core.cr/commit/822a77fb683a4445b921856eda635e2f4b13a83b (made the code a little dirtier, though).
Should I create a separate issue for can't execute Generic in a macro?
You probably should.
Most helpful comment
You can do
{{ puts _type.resolve.name }}. This printsA::FooPath#resolve: https://crystal-lang.org/api/0.24.1/Crystal/Macros/Path.html#resolve%3AASTNode-instance-methodTypeNode#name: https://crystal-lang.org/api/0.24.1/Crystal/Macros/TypeNode.html#name%3AMacroId-instance-method