A reasonable configuration language rcl-lang.org
configuration-language json
at master 303 lines 8.9 kB view raw view rendered
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).