Crystal: [Question] Get full type path in outer macro

Created on 2 Jan 2018  路  15Comments  路  Source: crystal-lang/crystal

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?

Most helpful comment

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

All 15 comments

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:

https://github.com/luckyframework/lucky_record/blob/c2166c48b0f294bf59d50d4580cd19ae806e8eec/src/lucky_record/model.cr#L9-L11

  macro inherited
    ASSOCIATIONS = [] of {name: Symbol, foreign_key: Symbol}
  end

https://github.com/luckyframework/lucky_record/blob/c2166c48b0f294bf59d50d4580cd19ae806e8eec/src/lucky_record/model.cr#L109-L111

  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.

Was this page helpful?
0 / 5 - 0 ratings