|
| 1 | +# Type Handling & Conversions |
| 2 | + |
| 3 | +Rudi has pluggable coalescers, which is the term it uses for the component that is responsible for |
| 4 | +handling implicit type conversions (e.g. deciding whether `1` (integer) is the same as `"1"` (string)) |
| 5 | +and by extension also for equality checks between two values. |
| 6 | + |
| 7 | +* [Background](#background) |
| 8 | +* [Coalescers](#coalescers) |
| 9 | + + [Strict](#strict-coalescer) |
| 10 | + + [Pedantic](#pedantic-coalescer) |
| 11 | + + [Humane](#humane-coalescer) |
| 12 | +* [Conversion Functions](#conversion-functions) |
| 13 | +* [Comparisons](#comparisons) |
| 14 | + |
| 15 | +## Background |
| 16 | + |
| 17 | +Rudi programs are meant to transform data structures, often YAML/JSON files. In some cases, these |
| 18 | +are based on strictly typed datastructures, like [Kubernetes](https://kubernetes.io) objects that |
| 19 | +are based on an OpenAPI schema. Sometimes however these data structures are much more loosey goosey, |
| 20 | +like [Helm](https://helm.sh/) values, where a flag like `enabled: "false"` would ultimately be rendered |
| 21 | +using `--enabled={{ .enabled }}` to `--enabled=false`. In these cases, types are less important than |
| 22 | +the user's actual intent. |
| 23 | + |
| 24 | +The built-in functions in Rudi already contain helpers like [`to-string`](functions/types-to-string.md) |
| 25 | +or [`to-int`](functions/types-to-int.md) to deal with conversions, but it can be tedious to have |
| 26 | +type conversions in many places in a Rudi program, just to deal with untyped data. |
| 27 | + |
| 28 | +To help with this, Rudi offers a choice, both to the Rudi program author by having, as well as |
| 29 | +when embedding Rudi into other Go programs: Choose your own adventure, or, "coalescer". |
| 30 | + |
| 31 | +When running `rudi`, the coalescing can be changed using `--coalesce`: |
| 32 | + |
| 33 | +```bash |
| 34 | +$ rudi --coalesce humane '(+ .foo 2)' data.yaml |
| 35 | +``` |
| 36 | + |
| 37 | +## Coalescers |
| 38 | + |
| 39 | +A coalescer is a Go interface similar to this: |
| 40 | + |
| 41 | +```go |
| 42 | +type Coalescer interface { |
| 43 | + ToBool(val any) (bool, error) |
| 44 | + ToInt64(val any) (int64, error) |
| 45 | + ToString(val any) (string, error) |
| 46 | + // ... |
| 47 | +} |
| 48 | +``` |
| 49 | + |
| 50 | +Its task is to handle type conversions/ensurances across all Rudi functions. When `(add 1 2)` wants |
| 51 | +to turn its two arguments into numbers, the coalescer is used. The coalescer decides which values |
| 52 | +of which types to convert into the desired target type. |
| 53 | + |
| 54 | +Rudi comes with 3 coalescers to choose from: |
| 55 | + |
| 56 | +* **Strict** is the default. This coalescer does not allow any type conversions, except turning |
| 57 | + `null` into the empty value of any type (i.e. `null` can turn int `0`) and allowing to turn |
| 58 | + floating point numbers into integer numbers if no precision is lost (i.e. `2.0` can turn into `2`). |
| 59 | +* **Pedantic** is even more strict than **strict** and does not allow any type conversion whatsoever. |
| 60 | +* **Humane** is inspired by, but less forgiving than PHP's type system. This coalescer allows much |
| 61 | + more conversions, like turning `"2.0"` (string) into `2` (int). |
| 62 | + |
| 63 | +You can of course also implement your own coalescer by implementing `pkg/coalescing.Coalescer`. |
| 64 | + |
| 65 | +### Strict Coalescer |
| 66 | + |
| 67 | +A safe default with minimal conversion support. The following list shows which values/types are |
| 68 | +allowed for each target type. The left column shows the target type, the other columns list |
| 69 | +acceptable source types |
| 70 | + |
| 71 | +| | null | bool | int64 | float64 | string | vector | object | |
| 72 | +| ------: | :-------: | :--: | :---: | :-----: | :----: | :----: | :----: | |
| 73 | +| null | ✅ | – | – | – | – | – | – | |
| 74 | +| bool | ✅ | ✅ | – | – | – | – | – | |
| 75 | +| int64 | ✅ | – | ✅ | ✅(*) | – | – | – | |
| 76 | +| float64 | ✅ | – | ✅ | ✅ | – | – | – | |
| 77 | +| string | ✅ (`""`) | – | – | – | ✅ | – | – | |
| 78 | +| vector | ✅ | – | – | – | – | ✅ | – | |
| 79 | +| object | ✅ | – | – | – | – | – | ✅ | |
| 80 | + |
| 81 | +\*) only if lossless (e.g. `2.0` can be turned into an int64, `2.1` cannot) |
| 82 | + |
| 83 | +### Pedantic Coalescer |
| 84 | + |
| 85 | +If you really want to be super extra explicit and have strongly typed source data, maybe the pedantic |
| 86 | +coalescer is more your style. |
| 87 | + |
| 88 | +| | null | bool | int64 | float64 | string | vector | object | |
| 89 | +| ------: | :---: | :--: | :---: | :-----: | :----: | :----: | :----: | |
| 90 | +| null | ✅ | – | – | – | – | – | – | |
| 91 | +| bool | – | ✅ | – | – | – | – | – | |
| 92 | +| int64 | – | – | ✅ | – | – | – | – | |
| 93 | +| float64 | – | – | – | ✅ | – | – | – | |
| 94 | +| string | – | – | – | – | ✅ | – | – | |
| 95 | +| vector | – | – | – | – | – | ✅ | – | |
| 96 | +| object | – | – | – | – | – | – | ✅ | |
| 97 | + |
| 98 | +### Humane Coalescer |
| 99 | + |
| 100 | +For less-than-strongly typed data, sometimes it's easier to just accept humans for what they are and |
| 101 | +that `replicas: "2"` really meant `replicas: 2`. For cases like this, use the humane coalescer, which |
| 102 | +is inspired by PHP, but a bit less flexible. Also instead of turning `true` into `"1"` like in PHP, |
| 103 | +this coalescer returns `"true"` (and `"false"` for `false`). |
| 104 | + |
| 105 | +| | null | bool | int64 | float64 | string | vector | object | |
| 106 | +| ------: | :---: | :--: | :---: | :-----: | :----: | :----: | :----: | |
| 107 | +| null | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
| 108 | +| bool | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
| 109 | +| int64 | ✅ | ✅ | ✅ | ✅ | ✅ | – | – | |
| 110 | +| float64 | ✅ | ✅ | ✅ | ✅ | ✅ | – | – | |
| 111 | +| string | ✅ | ✅ | ✅ | ✅ | ✅ | – | – | |
| 112 | +| vector | ✅ | – | – | – | – | ✅ | ✅ | |
| 113 | +| object | ✅ | – | – | – | – | ✅ | ✅ | |
| 114 | + |
| 115 | +Let's look closer at each type's conversion logic: |
| 116 | + |
| 117 | +* **null**: All conversions to `null` are only allowed for the empty value of each source type |
| 118 | + (meaning that `false` and `0` are convertible, but `true` and `"foo"` are not). |
| 119 | +* **bool**: Empty values are considered `false`, all others `true`. The string `"0"` and `"false"` |
| 120 | + are also considered `false`. |
| 121 | +* **int64**: Empty values are `0`, `true` becomes `1`. If lossless conversion from float64 to int64 |
| 122 | + is possible, it's performed, otherwise an error is returned. Strings have their whitespace trimmed; |
| 123 | + if the resulting string is empty, `0` is returned, otherwise the string is parsed as an integer; |
| 124 | + if not successful, parsing as float is attempted and if the resulting float can be losslessly |
| 125 | + converted, it's returned (e.g. `"2.0"` is valid, `"2.1"` is not). Otherwise an error is returned. |
| 126 | +* **float64**: Empty values are `0.0`, `true` becomes `1.0`. Integers are converted to floats. |
| 127 | + Strings have their whitespace trimmed; if the resulting string is empty, `0.0` is returned, |
| 128 | + otherwise the string is parsed as a float; if not possible to parse as float, an error is returned. |
| 129 | +* **string**: works as expected; floats have their trailing zeros trimmed (`3.12000` becomes |
| 130 | + `"3.12"`). `true` becomes `"true"` and `false` becomes `"false"`. `null` becomes an empty string. |
| 131 | +* **vector**: `null` turns into an empty vector and objects can only be converted to an empty vector |
| 132 | + if they are empty, otherwise an error is retuned. |
| 133 | +* **object**: `null` turns into an empty object and vectors can only be converted to an empty object |
| 134 | + if they are empty, otherwise an error is retuned. |
| 135 | + |
| 136 | +## Conversion Functions |
| 137 | + |
| 138 | +Rudi offers explicit conversion functions. These always apply the humane coalescing logic. |
| 139 | + |
| 140 | +* [`to-int`](functions/types-to-int.md) converts its argument to an int64 or returns an error if not possible. |
| 141 | +* [`to-float`](functions/types-to-float.md) does the same for float64. |
| 142 | +* [`to-string`](functions/types-to-string.md) does the same for strings. |
| 143 | +* [`to-bool`](functions/types-to-bool.md) does the same for booleans. |
| 144 | + |
| 145 | +## Comparisons |
| 146 | + |
| 147 | +Rudi has 3 functions built-in to check for equality between 2 values: |
| 148 | + |
| 149 | +* [`eq?`](functions/comparisons-eq.md) uses the current coalescer (i.e. by default, strict). If a Rudi program is configured to |
| 150 | + use humane coalescing however, this function will use that coalescing to determine equality. |
| 151 | +* [`like?`](functions/comparisons-like.md) always uses humane coalescing. |
| 152 | +* [`identical?`](functions/comparisons-identical.md) always uses strict coalescing. |
| 153 | + |
| 154 | +Comparisons work by converting the two values into (hopefully) compatible types that can be compared. |
| 155 | +This is done in steps: |
| 156 | + |
| 157 | +1. If either of the arguments is `null`, try to convert to other to `null`. |
| 158 | +1. Do the same with `bool`. |
| 159 | +1. Do the same with `int64`. |
| 160 | +1. Do the same with `float64`. |
| 161 | +1. Do the same with `string`. |
| 162 | +1. Do the same with `vector`. |
| 163 | +1. Do the same with `object`. |
| 164 | + |
| 165 | +**NB:** Equality rules are associative (if `a == b`, then `b == a`), but not transitive, which is |
| 166 | +especially apparent with humane coalescing: |
| 167 | + |
| 168 | +* `" " == true` because the string is not empty. |
| 169 | +* `" " == 0` because empty strings can turn into `0`. |
| 170 | +* `0 == false` because both are the empty values of their types. |
| 171 | + |
| 172 | +If rules were transitive, `0` could not both be `false` and `true` at the same time. |
0 commit comments