Skip to content
This repository was archived by the owner on Mar 16, 2025. It is now read-only.

Commit 49e0636

Browse files
committed
coalescing docs
1 parent 2a50077 commit 49e0636

5 files changed

Lines changed: 187 additions & 2 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ like those available in JSON (numbers, bools, objects, vectors etc.). A statemen
2929
* [Installation](#installation)
3030
* [Documentation](#documentation)
3131
* [Language Description](docs/language.md)
32+
* [Type Handling](docs/coalescing.md)
3233
* [Standard Library](docs/functions/README.md)
3334
* [Usage](#usage)
3435
* [Command Line](#command-line)
@@ -72,6 +73,7 @@ Make yourself familiar with Rudi using the documentation:
7273

7374
* The [Language Description](docs/language.md) describes the Rudi syntax and semantics.
7475
* All built-in functions are described in the [standard library](docs/functions/README.md).
76+
* [Type Handling](docs/coalescing.md) describes how Rudi handles, converts and compares values.
7577

7678
## Usage
7779

docs/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ Welcome to the Rudi documentation :smile:
55
<!-- BEGIN_TOC -->
66
## General
77

8-
* [language](language.md) – A short introduction to the Rudi language
8+
* [Type Handling & Conversions](coalescing.md) – A short introduction to the Rudi languageHow Rudi handles, converts and compares values
9+
* [The Rudi Language](language.md) – A short introduction to the Rudi language
910

1011
## Core Functions
1112

docs/coalescing.md

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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.

docs/embed.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
var embeddedFS embed.FS
1414

1515
type Topic struct {
16+
Title string
1617
CliNames []string
1718
Group string
1819
Description string
@@ -27,11 +28,19 @@ func (t *Topic) Content() ([]byte, error) {
2728
func Topics() []Topic {
2829
topics := []Topic{
2930
{
31+
Title: "The Rudi Language",
3032
CliNames: []string{"language", "lang", "rudi"},
3133
Group: "General",
3234
Description: "A short introduction to the Rudi language",
3335
Filename: "language.md",
3436
},
37+
{
38+
Title: "Type Handling & Conversions",
39+
CliNames: []string{"coalescing"},
40+
Group: "General",
41+
Description: "A short introduction to the Rudi languageHow Rudi handles, converts and compares values",
42+
Filename: "coalescing.md",
43+
},
3544
}
3645

3746
ignoredFunctions := map[string]struct{}{
@@ -67,6 +76,7 @@ func Topics() []Topic {
6776
}
6877

6978
topics = append(topics, Topic{
79+
Title: funcName,
7080
CliNames: []string{funcName, sanitized},
7181
Group: ucFirst(group) + " Functions",
7282
Description: function.Description(),

hack/docs-toc/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ func renderTopics(topics []docs.Topic, groups []string) string {
9898
topicNames := getTopicNames(topics, group)
9999
for _, topicName := range topicNames {
100100
topic := getTopic(topics, topicName)
101-
linkTitle := topicName
101+
linkTitle := topic.Title
102102

103103
if topic.IsFunction {
104104
linkTitle = fmt.Sprintf("`%s`", linkTitle)

0 commit comments

Comments
 (0)