Source Code Templates
The first, extremely simple Build-time Metaprogramming example that we'll look at is one way to create and use source code templates at Build time.
As a contrived example, imagine that you're writing an
HTTP Service's homepage
Endpoint Handler where you want to
just serve some hardcoded static HTML representing our site's trivial homepage. Of course, you may decide to just
directly write out the HTML string inline, but it would probably be more useful to create a separate homepage.html
file so that you can get IDE support for your HTML. You could of course depend on homepage.html
as a Resource file to
read at runtime, but we could also opt to directly embed the file's HTML contents into the source code at Build time so
that we don't have to spend any time reading the file while the program is running.
To do this, we could make our source file a template with a format string {{HOMEPAGE_HTML}}
to be replaced:
Fig 1:
# ex1.claro.tmpl
provider homepageHtml() -> string {
return "{{HOMEPAGE_HTML}}";
}
expand_template(...)
Macro
Bazel provides ample tooling for you to write this functionality entirely from scratch, but to make it easier to get up
and running, Claro provides an expand_template(...)
Bazel macro out of the box. The BUILD
file below expands the
template by converting the given homepage.html
file to a single line with all "
escaped and inserting it into the
ex1.claro.tmpl
:
Fig 2:
# BUILD
load("@claro-lang//stdlib/utils/expand_template:expand_template.bzl", "expand_template")
expand_template(
name = "ex1",
template = "ex1.claro.tmpl",
out = "ex1.claro",
substitutions = {
# Replace {{HOMEPAGE_HTML}} with the contents of the html file generated below.
"HOMEPAGE_HTML": "homepage_single_line.html",
},
)
# Learn more about genrule at: https://bazel.build/reference/be/general#genrule
genrule(
name = "homepage_single_line",
srcs = ["homepage.html"],
outs = ["homepage_single_line.html"],
# Bash script to remove all newlines and escape double-quotes.
cmd = "cat $(SRCS) | tr '\\n' ' ' | sed 's/\"/\\\\\"/g' > $(OUTS)",
)
And now you end up with a valid Claro source file:
Fig 3:
# ex1.claro
provider homepageHtml() -> string {
return "<!DOCTYPE html> <html lang=\"en\"> <head> <meta charset=\"UTF-8\"> <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"> <title>My Website</title> </head> <body> <h1>Welcome to my website!</h1> <p>This is a very basic homepage.</p> </body> </html>";
}
(Aside) Claro's Entire Docs Site is Generated at Build Time Using This Templating Approach!
Bazel's support for this Build time execution is an extremely powerful tool that can be used for all sorts of things where you'd like to derive some files (program source code or otherwise) from some other file(s) representing canonical source(s) of truth.
In fact, this docs site was dynamically generated at Build time by first executing each and every sample Claro code snippet and templating the snippet's output into the markdown file that eventually gets converted to HTML. To make this example more explicit, this site's Hello, World! page was generated from literally the below template:
Fig 4:
# Hello, World!
{{EX1}}
As you can already see from the most minimal program possible, Claro programs eliminate unnecessary boilerplate. Every
Claro program is simply a sequence of statements that are executed from top-to-bottom as if it were a "script". You
don't need to specify a "main" method" as in other languages like Java, instead, much like Python, you simply specify a
starting file which will execute top-down at program start.
And the corresponding BUILD file contains the following doc_with_validated_examples(...)
declaration which is built
on top of the expand_template(...)
macro described above:
Fig 5:
# BUILD
load("//mdbook_docs:docs_with_validated_examples.bzl", "doc_with_validated_examples")
doc_with_validated_examples(
name = "hello_world",
doc_template = "hello_world.tmpl.md",
examples = ["hello_world.claro"],
)
Which generates this final output markdown:
Fig 6:
[0.001s][warning][perf,memops] Cannot use file /tmp/hsperfdata_runner/3 because it is locked by another process (errno = 11)
# Hello, World!
#### _Fig 1:_
---
```claro
print("Hello, world!");
```
_Output:_
```
Hello, world!
```
---
As you can already see from the most minimal program possible, Claro programs eliminate unnecessary boilerplate. Every
Claro program is simply a sequence of statements that are executed from top-to-bottom as if it were a "script". You
don't need to specify a "main" method" as in other languages like Java, instead, much like Python, you simply specify a
starting file which will execute top-down at program start.
This is a powerful example of Build-time Metaprogramming being put to good use. This approach is not just convenient, but provides some legitimate guarantees that wherever this approach was used, if the site deploys, then the sample code on the site is guaranteed to actually be valid because otherwise Bazel would have failed the build! You can imagine how useful this is for an actively in-development language where things are subject to potentially change at any time.
Feel free to dig into the source code of Claro's docs site here if you want to get more into the weeds on this example.