Maximilian Stroh
Maximilian Stroh

Reputation: 1086

How can I generate code from file at compile time using a macro?

I have a CSV file that looks like this:

CountryCode,CountryName
AD,Andorra
AE,United Arab Emirates
AF,Afghanistan
AG,Antigua and Barbuda
// -- snip -- //

and a class that looks like this:

module OpenData
  class Country
    def initialize(@code : String, @name : String)
    end
  end
end

and I want to have a class variable within the module automatically loaded at compile time like this:

module OpenData
  @@countries : Array(Country) = {{ run "./sources/country_codes.cr" }}
end

I tried to use the "run" macro above with the following code:

require "csv"
require "./country"

content = File.read "#{__DIR__}/country-codes.csv"
result = [] of OpenData::Country
CSV.new(content, headers: true).each do |row|
  result.push OpenData::Country.new(row["CountryCode"], row["CountryName"])
end
result

but this results in

@@countries : Array(Country) = {{ run "./sources/country_codes.cr" }}
                                    ^
Error: class variable '@@countries' of OpenData must be Array(OpenData::Country), not Nil

All my other attempts somehow failed due to various reasons, like not being able to call .new within a macro or stuff like that. This is something I regularly do in Elixir and other languages that support macros and is something I would suspect Crystal can also achieve... I'd also take any other way that accomplishes the task at compile time!

Basically there are several more files I want to process this way, and they`re longer/more complex... thanks in advance!

EDIT:

Found the issue. It seems that I have to return a string that includes actual crystal code from the "run" macro. So, the code in the "run" file becomes:

require "csv"

content = File.read "#{__DIR__}/country-codes.csv"
lines = [] of String
CSV.new(content, headers: true).each do |row|
  lines << "Country.new(\"#{row["CountryCode"]}\", \"#{row["CountryName"]}\")"
end

puts "[#{lines.join(", ")}]"

and everything works!

Upvotes: 1

Views: 173

Answers (1)

Johannes M&#252;ller
Johannes M&#252;ller

Reputation: 5661

You already found your answer, but for completeness, here are the docs, from: https://crystal-lang.org/api/1.2.2/Crystal/Macros.html#run%28filename%2C%2Aargs%29%3AMacroId-instance-method

Compiles and execute a Crystal program and returns its output as a MacroId.

The file denoted by filename must be a valid Crystal program. This macro invocation passes args to the program as regular program arguments. The program must output a valid Crystal expression. This output is the result of this macro invocation, as a MacroId.

The run macro is useful when the subset of available macro methods are not enough for your purposes and you need something more powerful. With run you can read files at compile time, connect to the internet or to a database.

A simple example:

# read.cr
puts File.read(ARGV[0])
# main.cr
macro read_file_at_compile_time(filename)
  {{ run("./read", filename).stringify }}
end

puts read_file_at_compile_time("some_file.txt")

The above generates a program that will have the contents of some_file.txt. The file, however, is read at compile time and will not be needed at runtime.

Upvotes: 3

Related Questions