-
Notifications
You must be signed in to change notification settings - Fork 44
/
Config.scala
271 lines (225 loc) · 10.4 KB
/
Config.scala
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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
/*
* Copyright 2012-2020 the original author or authors.
*
* 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
*
* http://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.
*/
package laika.api.config
import laika.api.config.Config.ConfigResult
import ConfigError.{ DecodingFailed, NotFound }
import ConfigValue.{ ArrayValue, ObjectValue }
import laika.internal.parse.hocon.{ IncludeResource, ObjectBuilderValue }
import scala.annotation.tailrec
import scala.util.Try
/** API for retrieving configuration values based on a string key and a decoder.
*
* Config instances are used in many places in this library, each `Document`,
* `DocumentTree` and `Directive` has a Config instance associated with it.
*
* One use case for configuration is controlling the behaviour of built-in features,
* like setting the navigation order or the depth for table of contents.
*
* A second use case is user configuration, where custom variables can be set
* in configuration files or headers and then referenced in templates or markup
* with the syntax `\${ref.path}`.
*
* The key is a path separated by '.', which allows to reference nested objects
* in the configuration.
*
* Built-in decoders are available for simple types like `String`, `Int`, `Double`, `Boolean`
* and any `Seq` consisting of those values.
*
* It also comes with a decoder for `Path`, which resolves relative paths in the configuration
* against the (virtual) path of the origin.
*
* This API is usually used with values obtained by parsing HOCON, as specified in
* [[https://github.com/lightbend/config/blob/master/HOCON.md]], but the API is generic
* and can also be used with values specified programmatically.
*
* Please note that Laika does not depend on the Typesafe Config library or any of its
* commonly used Scala wrappers or forks. It has its own HOCON parser, which implements
* the full spec while still being minimal and lightweight. It also ensures the FP
* properties are kept intact, e.g. it has full referential transparency and does
* not throw Exceptions like most of the alternatives.
*
* @author Jens Halm
*/
trait Config {
/** The origin of this configuration, which might come from programmatic configuration,
* a file in one of the input directories, a configuration header in a markup document
* or from an attribute section in a directive.
*/
def origin: Origin
/** Verifies whether this config instance contains a value mapped to the specified key.
*/
def hasKey(key: String): Boolean = hasKey(Key.parse(key))
/** Verifies whether this config instance contains a value mapped to the specified key.
*/
def hasKey(key: Key): Boolean
/** Retrieve a required value for the specified key and decoder.
*/
def get[T](key: Key)(implicit decoder: ConfigDecoder[T]): ConfigResult[T]
/** Retrieve a required value for the specified key and decoder.
*/
def get[T](key: String)(implicit decoder: ConfigDecoder[T]): ConfigResult[T] =
get[T](Key.parse(key))
/** Retrieve an optional value for the specified key and decoder, falling back to the
* given default if the value is missing.
*/
def get[T](key: Key, default: => T)(implicit decoder: ConfigDecoder[T]): ConfigResult[T] =
getOpt(key).map(_.getOrElse(default))
/** Retrieve an optional value for the specified key and decoder, falling back to the
* given default if the value is missing.
*/
def get[T](key: String, default: => T)(implicit decoder: ConfigDecoder[T]): ConfigResult[T] =
get[T](Key.parse(key), default)
/** Retrieve an optional value for the specified key and decoder.
* The result is still an Either as this method might still fail even if the value is present in
* case the decoding fails.
*/
def getOpt[T](key: Key)(implicit decoder: ConfigDecoder[T]): ConfigResult[Option[T]] =
get(key).fold(
e => if (e.isInstanceOf[NotFound]) Right(None) else Left(e),
r => Right(Some(r))
)
/** Retrieve an optional value for the specified implicit key and decoder.
*
* A defaultKey can be used for commonly used configuration objects like `AutonumberConfig`
* that are expected to be mapped to a specific key, like `autonumbering`.
*
* The result is still an Either as this method might still fail even if the value is present in
* case the decoding fails.
*/
def getOpt[T](implicit
decoder: ConfigDecoder[T],
defaultKey: DefaultKey[T]
): ConfigResult[Option[T]] =
getOpt(defaultKey.value)
/** Retrieve an optional value for the specified key and decoder.
* The result is still an Either as this method might still fail even if the value is present in
* case the decoding fails.
*/
def getOpt[T](key: String)(implicit decoder: ConfigDecoder[T]): ConfigResult[Option[T]] =
getOpt[T](Key.parse(key))
/** Retrieve a required value for the specified implicit key and decoder.
*
* A defaultKey can be used for commonly used configuration objects like `AutonumberConfig`
* that are expected to be mapped to a specific key, like `autonumbering`.
*/
def get[T](implicit decoder: ConfigDecoder[T], defaultKey: DefaultKey[T]): ConfigResult[T] =
get[T](defaultKey.value)
/** Creates a new configuration builder with the specified value and this instance as
* a fallback. The returned builder can be used to add further values before calling
* `build` to retrieve a new instance.
*/
def withValue[T](key: String, value: T)(implicit encoder: ConfigEncoder[T]): ConfigBuilder =
ConfigBuilder.withFallback(this).withValue(key, value)
/** Creates a new configuration builder with the specified value and this instance as
* a fallback. The returned builder can be used to add further values before calling
* `build` to retrieve a new instance.
*/
def withValue[T](key: Key, value: T)(implicit encoder: ConfigEncoder[T]): ConfigBuilder =
ConfigBuilder.withFallback(this).withValue(key, value)
/** Creates a new configuration builder with the specified value and this instance as
* a fallback. The returned builder can be used to add further values before calling
* `build` to retrieve a new instance.
*/
def withValue[T](
value: T
)(implicit encoder: ConfigEncoder[T], defaultKey: DefaultKey[T]): ConfigBuilder =
ConfigBuilder.withFallback(this).withValue(value)
/** Returns a new configuration instance using the specified instance as a fallback
* for keys not found in this instance.
*
* If this instance already has a fallback, the new fallback will be passed further down the chain.
*/
def withFallback(other: Config): Config
/** Returns a new configuration instance using the specified origin.
*/
def withOrigin(origin: Origin): Config
private[laika] def withoutFallback: Config
}
/** The default implementation of the Config API.
*/
private[laika] class ObjectConfig(
val root: ObjectValue,
val origin: Origin,
val fallback: Config = EmptyConfig
) extends Config {
private def lookup(
keySegments: Seq[String],
target: ArrayValue,
targetOrigin: Origin
): Option[Field] = {
Try(keySegments.head.toInt).toOption.flatMap { posKey =>
((target.values.drop(posKey).headOption, keySegments.tail) match {
case (res, Nil) => res.map(Field("", _, targetOrigin))
case (Some(ov: ObjectValue), rest) => lookup(rest, ov)
case _ => None
}): Option[Field]
}
}
@tailrec
private def lookup(keySegments: Seq[String], target: ObjectValue): Option[Field] = {
(target.values.find(_.key == keySegments.head), keySegments.tail) match {
case (res, Nil) => res
case (Some(Field(_, av: ArrayValue, fieldOrigin)), rest) => lookup(rest, av, fieldOrigin)
case (Some(Field(_, ov: ObjectValue, _)), rest) => lookup(rest, ov)
case _ => None
}
}
private def lookup(key: Key): Option[Field] =
if (key.segments.isEmpty) Some(Field("", root, origin)) else lookup(key.segments, root)
def hasKey(key: Key): Boolean = lookup(key).nonEmpty || fallback.hasKey(key)
def get[T](key: Key)(implicit decoder: ConfigDecoder[T]): ConfigResult[T] = {
lookup(key).fold(fallback.get[T](key)) { field =>
val res = field match {
case Field(_, ov: ObjectValue, _) =>
fallback.get[ConfigValue](key).toOption match {
case Some(parentOv: ObjectValue) => ov.merge(parentOv)
case _ => ov
}
case _ => field.value
}
decoder(Traced(res, field.origin)).left.map {
case de @ DecodingFailed(_, child) => de.withKey(child.fold(key)(key.child))
case other => other
}
}
}
def withFallback(other: Config): Config = other match {
case EmptyConfig => this
case _ => new ObjectConfig(root, origin, fallback.withFallback(other))
}
private[laika] def withoutFallback: Config = new ObjectConfig(root, origin)
def withOrigin(newOrigin: Origin): Config = new ObjectConfig(root, newOrigin, fallback)
override def hashCode: Int = (root, origin, fallback).hashCode
override def equals(obj: Any): Boolean = obj match {
case c: ObjectConfig => (c.root, c.origin, c.fallback).equals((root, origin, fallback))
case _ => false
}
}
/** An empty configuration instance.
*/
private[config] object EmptyConfig extends Config {
val origin: Origin = Origin.root
def hasKey(key: Key): Boolean = false
def get[T](key: Key)(implicit decoder: ConfigDecoder[T]): ConfigResult[T] = Left(NotFound(key))
def withFallback(other: Config): Config = other
private[laika] def withoutFallback: Config = this
def withOrigin(newOrigin: Origin): Config = this
}
object Config {
type ConfigResult[T] = Either[ConfigError, T]
private[laika] type IncludeMap = Map[IncludeResource, Either[ConfigError, ObjectBuilderValue]]
val empty: Config = EmptyConfig
}