macroscrystal-lang

What does Crystal::ClassDef#resolved_type cannot be nil (NilAssertionError) mean?


I'm trying to add a method to the Routes class (under the # this throws the error comment). But the compiler throws the resolved_type cannot be nil error. What does it mean? Is there a solution to define the home_controller__show method somewhere which gives me back the "/" String?

Thank you.

require "http"
 
Routes.new.draw do
  get "/", HomeController.show
end
 
class Main
  @@routes : Routes = Routes.new
 
  def self.routes
    @@routes
  end
 
  def self.routes=(value)
    @@routes = value
  end
end
 
class HomeController
  def show
    "this is show"
  end
end
 
class Route
  getter path
  getter callback
 
  def initialize(@path : String, &@callback : HTTP::Server::Context -> String)
  end
end
 
class Routes
  getter routes
 
  def initialize
      @routes = [] of Route
    end
  
  def draw
    with self yield
    Main.routes = self
  end
  
  macro get(route, mapping)
      route = Route.new({{route}}) do |context|
        {{mapping.receiver}}.new.{{mapping.name}}
      end
    
    # this throws the error
    class Routes
      def {{mapping.id.underscore.gsub(/\./, "__")}}
        "/"
      end
    end
     
      Main.routes.routes << route
    end
end

Update: this is how I solved it:

require "http"

class Routes
  getter routes
 
  def initialize
    @routes = [] of Route
  end
  
  macro get(route, mapping)
    route = Route.new({{route}}) do |context|
      {{mapping.receiver}}.new.{{mapping.name}}
    end
    
    def self.{{mapping.id.underscore.gsub(/\./, "__")}}
      "/"
    end
  end
end

class Routes
  get "/", HomeController.show
end
 
class Main
  @@routes : Routes = Routes.new
 
  def self.routes
    @@routes
  end
 
  def self.routes=(value)
    @@routes = value
  end
end
 
class HomeController
  def show
    "this is show"
  end
end
 
class Route
  getter path
  getter callback
 
  def initialize(@path : String, &@callback : HTTP::Server::Context -> String)
  end
end
 
pp Routes.home_controller__show

Solution

  • This setup cannot work. Routes#draw is a method and its scope (via with self yield) is an instance of Routes, thus a runtime object. It's impossible to call a macro (Routes.get) on a runtime object. Macros evaluate at compile time.

    You can fix this by targeting the macro explicitly: Routes.get "/", HomeController.show. But then there'll be some other issues though. The macro must be defined before calling it, so you need to reorder the code accordingly. Furthermore, the macro generates code that reopens a type. This code needs to be at the top-level scope and cannot bet inside a method (the code is in the block of Routes#draw).

    So basically you have some architectural flaws in the API and should reconsider. As possible paths forward, maybe you can make draw a macro. Or maybe you don't need to use macros at all.