forked from apple/pkl-pantry
-
Notifications
You must be signed in to change notification settings - Fork 0
/
toml.pkl
217 lines (187 loc) · 8.46 KB
/
toml.pkl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
//===----------------------------------------------------------------------===//
// Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
/// A renderer for [TOML](https://toml.io) configuration files.
///
/// Basic usage:
/// ```
/// import "package://pkg.pkl-lang.org/pantry/[email protected]"
///
/// output {
/// renderer = new toml.Renderer {}
/// }
/// ```
///
/// To render TOML dates and times, use [Date], [Time], and [DateTime].
@ModuleInfo { minPklVersion = "0.25.0" }
module pkl.toml.toml
abstract class AbstractDateTime {
value: String
}
/// A TOML [Local Date](https://toml.io/en/v1.0.0#local-date) value.
class Date extends AbstractDateTime {
value: String(matches(Regex(#"(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])"#)))
}
/// A TOML [Local Time](https://toml.io/en/v1.0.0#local-time) value.
class Time extends AbstractDateTime {
value: String(matches(Regex(#"([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?"#)))
}
/// A TOML [Offset Date-Time](https://toml.io/en/v1.0.0#offset-date-time)
/// or [Local Date-Time](https://toml.io/en/v1.0.0#local-date-time) value.
class DateTime extends AbstractDateTime {
value: String(matches(Regex(#"(\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])[T ]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(Z|[+-]([01][0-9]|2[0-3]):([0-5][0-9]))?"#)))
}
/// Renders values as TOML.
class Renderer extends ValueRenderer {
/// Value converters to apply before values are rendered.
///
/// For further information see [PcfRenderer.converters].
/// For path converters, only "*" is supported.
converters: Mapping<Class|String, (unknown) -> Any>
function renderValue(value: Any) =
let (_value = getBasicValue(value, false))
doRenderValue(_value, List()).trim()
function renderDocument(value: Any) =
if (!isTableLike(value))
throw("""
Invalid input: TOML can only render object-types at the root level. Received: \(value)
""")
else
renderValue(value)
local jsonRenderer = new JsonRenderer {}
local function getConvertersForValue(value: Any): List<(Any) -> unknown> = new Listing {
when (convertersMap.containsKey(value.getClass())) {
convertersMap[value.getClass()]
}
when (convertersMap.containsKey("*")) {
convertersMap["*"]
}
}.toList()
local function applyConverters(value: Any) =
let (converters = getConvertersForValue(value))
converters.fold(value, (acc, converter) -> converter.apply(acc))
/// Traverses the object and casts it down to its basic type: Map, List, or the primitive value. Runs each
/// value through the converter if there is a match.
/// `skipConversion` is a helper flag to avoid infinite recursion in case the converter returns the same type.
local function getBasicValue(value: Any, skipConversion: Boolean) =
if (!skipConversion && !getConvertersForValue(value).isEmpty)
getBasicValue(applyConverters(value), true)
// If the value is Dynamic, and we have both elements and properties, it's ambiguous whether we should
// render as a table or an array.
else if (value is Dynamic && isTableLike(value) && isArrayLike(value))
throw("""
Cannot render object with both properties/entries and elements as TOML. Received: \(value)
""")
else if (isTableLike(value))
getMap(value)
.mapValues((_, elem) -> getBasicValue(elem, false))
else if (isArrayLike(value)) getList(value).map((elem) -> getBasicValue(elem, false))
else value
/// Underlying implementation for rendering values as toml
local function doRenderValue(value: Any, path: List<String>): String =
if (isTableArray(value))
renderTableArray(value, path)
else if (value is Map)
renderTable(value, path)
else
renderInlineValue(value)
/// Determine whether an object is map-like. We'll consider any Dynamic that doesn't have any elements as map-like.
local function isTableLike(obj: Any) = !(obj is AbstractDateTime) && ((obj is Dynamic && obj.toList().isEmpty) || obj is MapLike)
/// Determine whether an object is list-like. We'll consider any Dynamic that has elements as list-like.
local function isArrayLike(obj: Any) = (obj is Dynamic && !obj.toList().isEmpty) || obj is ListLike
/// Convert an object to its Map representation. Toml doesn't include null so we should filter out null properties.
local function getMap(obj: MapLike|Dynamic) = (if (obj is Map) obj else obj.toMap()).filter((_, elem) -> elem != null)
/// Convert an object to its List representation.
local function getList(obj: ListLike|Dynamic) = if (obj is List) obj else obj.toList()
/// Determine if we should render this value as an array of tables or not.
/// A value is an array of tables if all of the inhabitants are table-like.
local function isTableArray(value: Any) =
value is List && value.every((elem) -> elem is Map)
local function isTableTypeProp(value: Any) = value is Map || isTableArray(value)
local convertersMap = converters.toMap()
/// Render the value as an inline value (e.g. inline array, object, or primitive)
local function renderInlineValue(value: Any) =
if (value is Number && value.isNaN)
"nan"
else if (value == Infinity)
"inf"
else if (value is String)
renderString(value)
else if (value is Number|Boolean)
jsonRenderer.renderValue(value)
else if (value is AbstractDateTime)
value.value
else if (value is Map)
"{ " + new Listing {
for (k, v in value) {
"\(makeSingleKey(k)) = \(renderInlineValue(v))"
}
}.toList().join(", ") + " }"
else if (value is List)
"[ " + value.map((elem) -> renderInlineValue(elem)).join(", ") + " ]"
else
throw("Not sure how to render value: \(value). Try defining a converter for this type.")
/// Render a string making sure multi-line use the """ multi-line syntax for better readability.
local function renderString(value: String) =
if (value.contains("\n"))
("\"\"\"\n" +
value.split("\n")
.map((line) -> jsonRenderer.renderValue(line).drop(1).dropLast(1))
.join("\n")
+ "\"\"\"")
else jsonRenderer.renderValue(value)
local function renderSingleTableArray(map: Map, path: List<String>) =
let (nativeProps = map.filter((_, value) -> !isTableTypeProp(value)))
let (tableProps = map.filter((_, value) -> isTableTypeProp(value)))
new Listing {
"""
[[\(makeKey(path))]]
"""
for (k, v in nativeProps) {
"\(makeSingleKey(k)) = \(renderInlineValue(v))"
}
for (k, v in tableProps) {
doRenderValue(v, path.add(k))
}
}.toList().join("\n")
local function renderTableArray(value: List, path: List<String>) =
value.map((elem) -> renderSingleTableArray(getMap(elem), path)).join("\n")
local function makeSingleKey(key: String) = if (key.matches(Regex(#"[A-Za-z0-9_-]+"#)))
key
else
jsonRenderer.renderValue(key)
local function makeKey(path: List<String>): String = path.map((k) -> makeSingleKey(k)).join(".")
local function renderTable(m: Map, path: List<String>): String =
let (nativeProps = m.filter((_, value) -> !isTableTypeProp(value)))
let (tableProps = m.filter((_, value) -> isTableTypeProp(value)))
new Listing {
// If we are in an object's context, render the object header. Skip if all children are also objects.
when (!path.isEmpty && nativeProps.length > 0) {
"""
[\(makeKey(path))]
"""
}
for (k, v in nativeProps) {
"\(makeSingleKey(k)) = \(renderInlineValue(v))"
}
for (k, v in tableProps) {
doRenderValue(v, path.add(k))
}
}
.toList()
.join("\n")
}
local typealias MapLike = Typed|Map|Mapping
local typealias ListLike = List|Listing