A reasonable configuration language
rcl-lang.org
configuration-language
json
1# Tutorial
2
3The main purpose of <abbr>RCL</abbr> is to reduce boilerplate in configuration.
4In this tutorial we will explore that use case through an example: defining
5cloud storage buckets for backups.
6
7## Setting
8
9In this tutorial we have two databases that need to be backed up to cloud
10storage: Alpha and Bravo. For both of them, we want to define three buckets: one
11for hourly, one for daily, and one for monthly backups. Each of them should have
12a lifecycle policy that deletes objects after 4, 30, and 365 days respectively.
13
14Furthermore, let’s say we have a script or tool that can set up the buckets from
15a <abbr>JSON</abbr> configuration file. That tool might be [Terraform][terraform]
16in practice, but in this tutorial we’ll assume a simpler schema to avoid
17distractions.
18
19[terraform]: https://www.terraform.io/
20
21The configuration file that defines our buckets might look like this:
22
23```yaml
24{
25 "buckets": [
26 {
27 "name": "alpha-hourly",
28 "region": "eu-west",
29 "lifecycle_policy": {
30 "delete_after_seconds": 345600
31 }
32 },
33 {
34 "name": "alpha-daily",
35 "region": "eu-west",
36 "lifecycle_policy": {
37 "delete_after_seconds": 2592000
38 }
39 },
40 {
41 "name": "alpha-monthly",
42 "region": "eu-west",
43 "lifecycle_policy": {
44 "delete_after_seconds": 31536000
45 }
46 },
47 {
48 "name": "bravo-hourly",
49 "region": "eu-west",
50 "lifecycle_policy": {
51 "delete_after_seconds": 34560
52 }
53 },
54 {
55 "name": "bravo-daily",
56 "region": "us-west",
57 "lifecycle_policy": {
58 "delete_after_seconds": 2592000
59 }
60 },
61 {
62 "name": "bravo-monthly",
63 "region": "eu-west",
64 "lifecycle_policy": {
65 "delete_after_seconds": 31536000
66 }
67 }
68 ]
69}
70```
71
72A configuration file like this is suboptimal in several ways. It is repetitive,
73difficult to read, and error-prone to edit. In fact, the above example contains
74two bugs that may not be obvious:
75
76 * The `bravo-daily` bucket is located in `us-west` rather than `eu-west` like
77 the other buckets.
78 * The `delete_after_seconds` of `bravo-hourly` is missing a zero and keeps
79 objects for only 10 hours, instead of the intended 4 days.
80
81Switching to a different format such as <abbr>YAML</abbr> or <abbr>TOML</abbr>
82may eliminate some of the line noise, but it does not make the file less
83repetitive, and therefore not less error-prone to edit. We're going to improve
84this by rewriting the configuration in <abbr>RCL</abbr>.
85
86## Installation
87
88Before we can start, follow the [installation instructions](installation.md),
89and if you like, [set up syntax highlighting](syntax_highlighting.md) for your
90editor. Save the file above as `buckets.json`. Because <abbr>RCL</abbr> is a
91superset of <abbr>JSON</abbr>, we can evaluate this file with <abbr>RCL</abbr>,
92and it should evaluate to itself.
93
94 rcl evaluate --format=json buckets.json
95
96This prints the document to stdout, formatted and colorized.
97
98## Record syntax
99
100The <abbr>JSON</abbr> format is great for data interchange, but when everything
101is quoted, the lack of visual distinction can make the document hard to read.
102In <abbr>RCL</abbr>, we can use [_record syntax_](syntax.md#dictionaries) to
103omit the quotes on the keys. In addition to writing `"key": value`, we can write
104`key = value` when the key is a valid [identifier](syntax.md#identifiers). Two
105other additions that <abbr>RCL</abbr> makes to <abbr>JSON</abbr> are allowing
106trailing commas, and underscores in numbers. With those changes, our
107configuration looks like this:
108
109```rcl
110{
111 buckets = [
112 {
113 name = "alpha-hourly",
114 region = "eu-west",
115 lifecycle_policy = {
116 delete_after_seconds = 345_600,
117 },
118 },
119 {
120 name = "alpha-daily",
121 region = "eu-west",
122 lifecycle_policy = {
123 delete_after_seconds = 2_592_000,
124 },
125 },
126 {
127 name = "alpha-monthly",
128 region = "eu-west",
129 lifecycle_policy = {
130 delete_after_seconds = 31_536_000,
131 },
132 },
133 {
134 name = "bravo-hourly",
135 region = "eu-west",
136 lifecycle_policy = {
137 delete_after_seconds = 34_560,
138 },
139 },
140 {
141 name = "bravo-daily",
142 region = "us-west",
143 lifecycle_policy = {
144 delete_after_seconds = 2_592_000,
145 },
146 },
147 {
148 name = "bravo-monthly",
149 region = "eu-west",
150 lifecycle_policy = {
151 delete_after_seconds = 31_536_000,
152 },
153 },
154 ],
155}
156```
157
158Evaluating the document should produce the same <abbr>JSON</abbr> output as before:
159
160 rcl evaluate --format=json buckets.rcl
161
162We can also output in <abbr>RCL</abbr> syntax with `--format=rcl`. This is the
163default, so when we are inspecting the configuration, and not feeding it into a
164tool that expects <abbr>JSON</abbr> or <abbr>YAML</abbr>, we can just run:
165
166 rcl evaluate buckets.rcl
167
168## Variables
169
170Next, let’s try to extract some duplicated values. Our document is an expression,
171and in expressions, we can use [let bindings](syntax.md#let-bindings) to bind
172values to names. This allows us to reuse them. We can extract the region, and
173ensure it’s the same everywhere:
174
175```rcl
176let region = "eu-west";
177{
178 buckets = [
179 // Other buckets and some fields omitted for brevity.
180 { name = "alpha-hourly", region = region },
181 { name = "alpha-daily", region = region },
182 ],
183}
184```
185
186A let-binding is itself an expression of the form `let name = value; expr`,
187where in the body `expr`, the variable `name` refers to the bound value. We can
188use a let-binding in any place where an expression is allowed. Here we put it at
189the top level, but we could put it before the bucket list for example:
190
191```rcl
192{
193 buckets = let region = "eu-west"; [
194 { name = "alpha-hourly", region = region },
195 { name = "alpha-daily", region = region },
196 ],
197}
198```
199
200Collections can also contain let bindings. In that case the variable is
201available to the element that follows.
202
203```rcl
204{
205 let region = "eu-west";
206 buckets = [
207 { name = "alpha-hourly", region = region },
208 { name = "alpha-daily", region = region },
209 ],
210}
211```
212
213## Arithmetic
214
215Now that we fixed the region bug, let’s try to eliminate the lifecycle bug.
216A number such as 31,536,000 seconds is not easily recognizable by humans, but
217most people will recognize 3600 as the number of seconds in an hour, and 24 as
218the number of hours in a day. We might write:
219
220```rcl
221{
222 let region = "eu-west";
223 let seconds_per_day = 3600 * 24;
224 buckets = [
225 // Again, some buckets omitted for brevity.
226 {
227 name = "alpha-hourly",
228 region = region,
229 lifecycle_policy = { delete_after_seconds = 4 * seconds_per_day },
230 },
231 {
232 name = "alpha-daily",
233 region = region,
234 lifecycle_policy = { delete_after_seconds = 30 * seconds_per_day },
235 },
236 ],
237}
238```
239
240## List comprehensions
241
242We managed to extract some duplicated values into variables, but the fact
243remains that our document consists of almost the same value repeated six times.
244We can improve that with a [_list comprehension_](syntax.md#comprehensions) and
245[_string interpolation_](strings.md#interpolation):
246
247```rcl
248{
249 let region = "eu-west";
250 let seconds_per_day = 3600 * 24;
251 let retention_days = {
252 hourly = 4,
253 daily = 30,
254 monthly = 365,
255 };
256 buckets = [
257 for period, days in retention_days: {
258 name = f"alpha-{period}",
259 region = region,
260 lifecycle_policy = { delete_after_seconds = days * seconds_per_day },
261 },
262 for period, days in retention_days: {
263 name = f"bravo-{period}",
264 region = region,
265 lifecycle_policy = { delete_after_seconds = days * seconds_per_day },
266 },
267 ],
268}
269```
270
271A collection can contain multiple separate loops. In the above example,
272`buckets` contains two loops, one for the Alpha database and one for Bravo.
273We can deduplicate this further with a nested loop. If we do that, our variables
274become single-use, so we can inline them again:
275
276```rcl
277{
278 buckets = [
279 let retention_days = {
280 hourly = 4,
281 daily = 30,
282 monthly = 365,
283 };
284 for database in ["alpha", "bravo"]:
285 for period, days in retention_days: {
286 name = f"{database}-{period}",
287 region = "eu-west",
288 lifecycle_policy = { delete_after_seconds = days * 24 * 3600 },
289 }
290 ],
291}
292```
293
294## Conclusion
295
296In this tutorial we replaced an error-prone repetitive <abbr>JSON</abbr>
297configuration file with an <abbr>RCL</abbr> file that avoids duplicating values
298by using loops, so the configuration is distilled down to its essence. This is a
299good introduction to <abbr>RCL</abbr> and highlights one of its use cases, but
300we haven’t explored the full language yet. While <abbr>RCL</abbr> is a simple
301language with comparatively few features, there are a few constructs we haven’t
302touched upon. In particular, assertions, imports, and functions. To learn more,
303continue on to [the language guide](syntax.md).