Reputation: 1086
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
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. Withrun
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