|
1 |
| -/* |
2 |
| - * Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. |
3 |
| - */ |
4 |
| - |
5 | 1 | package kotlinx.serialization.hocon
|
6 | 2 |
|
7 |
| -import com.typesafe.config.* |
8 |
| -import kotlin.time.* |
9 |
| -import kotlinx.serialization.* |
10 |
| -import kotlinx.serialization.builtins.serializer |
11 |
| -import kotlinx.serialization.descriptors.* |
12 |
| -import kotlinx.serialization.encoding.* |
13 |
| -import kotlinx.serialization.internal.* |
14 |
| -import kotlinx.serialization.modules.* |
15 |
| - |
16 |
| -@ExperimentalSerializationApi |
17 |
| -internal abstract class AbstractHoconEncoder( |
18 |
| - private val hocon: Hocon, |
19 |
| - private val valueConsumer: (ConfigValue) -> Unit, |
20 |
| -) : NamedValueEncoder() { |
21 |
| - |
22 |
| - override val serializersModule: SerializersModule |
23 |
| - get() = hocon.serializersModule |
24 |
| - |
25 |
| - private var writeDiscriminator: Boolean = false |
26 |
| - |
27 |
| - override fun elementName(descriptor: SerialDescriptor, index: Int): String { |
28 |
| - return descriptor.getConventionElementName(index, hocon.useConfigNamingConvention) |
29 |
| - } |
30 |
| - |
31 |
| - override fun composeName(parentName: String, childName: String): String = childName |
32 |
| - |
33 |
| - protected abstract fun encodeTaggedConfigValue(tag: String, value: ConfigValue) |
34 |
| - protected abstract fun getCurrent(): ConfigValue |
35 |
| - |
36 |
| - override fun encodeTaggedValue(tag: String, value: Any) = encodeTaggedConfigValue(tag, configValueOf(value)) |
37 |
| - override fun encodeTaggedNull(tag: String) = encodeTaggedConfigValue(tag, configValueOf(null)) |
38 |
| - override fun encodeTaggedChar(tag: String, value: Char) = encodeTaggedString(tag, value.toString()) |
39 |
| - |
40 |
| - override fun encodeTaggedEnum(tag: String, enumDescriptor: SerialDescriptor, ordinal: Int) { |
41 |
| - encodeTaggedString(tag, enumDescriptor.getElementName(ordinal)) |
42 |
| - } |
43 |
| - |
44 |
| - override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = hocon.encodeDefaults |
45 |
| - |
46 |
| - override fun <T> encodeSerializableValue(serializer: SerializationStrategy<T>, value: T) { |
47 |
| - when { |
48 |
| - serializer.descriptor == Duration.serializer().descriptor -> encodeDuration(value as Duration) |
49 |
| - serializer !is AbstractPolymorphicSerializer<*> || hocon.useArrayPolymorphism -> serializer.serialize(this, value) |
50 |
| - else -> { |
51 |
| - @Suppress("UNCHECKED_CAST") |
52 |
| - val casted = serializer as AbstractPolymorphicSerializer<Any> |
53 |
| - val actualSerializer = casted.findPolymorphicSerializer(this, value as Any) |
54 |
| - writeDiscriminator = true |
55 |
| - |
56 |
| - actualSerializer.serialize(this, value) |
57 |
| - } |
58 |
| - } |
59 |
| - } |
60 |
| - |
61 |
| - override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { |
62 |
| - val consumer = |
63 |
| - if (currentTagOrNull == null) valueConsumer |
64 |
| - else { value -> encodeTaggedConfigValue(currentTag, value) } |
65 |
| - val kind = descriptor.hoconKind(hocon.useArrayPolymorphism) |
66 |
| - |
67 |
| - return when { |
68 |
| - kind.listLike -> HoconConfigListEncoder(hocon, consumer) |
69 |
| - kind.objLike -> HoconConfigEncoder(hocon, consumer) |
70 |
| - kind == StructureKind.MAP -> HoconConfigMapEncoder(hocon, consumer) |
71 |
| - else -> this |
72 |
| - }.also { encoder -> |
73 |
| - if (writeDiscriminator) { |
74 |
| - encoder.encodeTaggedString(hocon.classDiscriminator, descriptor.serialName) |
75 |
| - writeDiscriminator = false |
76 |
| - } |
77 |
| - } |
78 |
| - } |
79 |
| - |
80 |
| - override fun endEncode(descriptor: SerialDescriptor) { |
81 |
| - valueConsumer(getCurrent()) |
82 |
| - } |
83 |
| - |
84 |
| - private fun configValueOf(value: Any?) = ConfigValueFactory.fromAnyRef(value) |
85 |
| - |
86 |
| - private fun encodeDuration(value: Duration) { |
87 |
| - val result = value.toComponents { seconds, nanoseconds -> |
88 |
| - when { |
89 |
| - nanoseconds == 0 -> { |
90 |
| - if (seconds % 60 == 0L) { // minutes |
91 |
| - if (seconds % 3600 == 0L) { // hours |
92 |
| - if (seconds % 86400 == 0L) { // days |
93 |
| - "${seconds / 86400} d" |
94 |
| - } else { |
95 |
| - "${seconds / 3600} h" |
96 |
| - } |
97 |
| - } else { |
98 |
| - "${seconds / 60} m" |
99 |
| - } |
100 |
| - } else { |
101 |
| - "$seconds s" |
102 |
| - } |
103 |
| - } |
104 |
| - nanoseconds % 1_000_000 == 0 -> "${seconds * 1_000 + nanoseconds / 1_000_000} ms" |
105 |
| - nanoseconds % 1_000 == 0 -> "${seconds * 1_000_000 + nanoseconds / 1_000} us" |
106 |
| - else -> "${value.inWholeNanoseconds} ns" |
107 |
| - } |
108 |
| - } |
109 |
| - encodeString(result) |
110 |
| - } |
111 |
| -} |
112 |
| - |
113 |
| -@ExperimentalSerializationApi |
114 |
| -internal class HoconConfigEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) : |
115 |
| - AbstractHoconEncoder(hocon, configConsumer) { |
116 |
| - |
117 |
| - private val configMap = mutableMapOf<String, ConfigValue>() |
118 |
| - |
119 |
| - override fun encodeTaggedConfigValue(tag: String, value: ConfigValue) { |
120 |
| - configMap[tag] = value |
121 |
| - } |
122 |
| - |
123 |
| - override fun getCurrent(): ConfigValue = ConfigValueFactory.fromMap(configMap) |
124 |
| -} |
125 |
| - |
126 |
| -@ExperimentalSerializationApi |
127 |
| -internal class HoconConfigListEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) : |
128 |
| - AbstractHoconEncoder(hocon, configConsumer) { |
129 |
| - |
130 |
| - private val values = mutableListOf<ConfigValue>() |
131 |
| - |
132 |
| - override fun elementName(descriptor: SerialDescriptor, index: Int): String = index.toString() |
133 |
| - |
134 |
| - override fun encodeTaggedConfigValue(tag: String, value: ConfigValue) { |
135 |
| - values.add(tag.toInt(), value) |
136 |
| - } |
137 |
| - |
138 |
| - override fun getCurrent(): ConfigValue = ConfigValueFactory.fromIterable(values) |
139 |
| -} |
140 |
| - |
| 3 | +import com.typesafe.config.ConfigValue |
| 4 | +import kotlinx.serialization.ExperimentalSerializationApi |
| 5 | + |
| 6 | +/** |
| 7 | + * Encoder used by Hocon during serialization. |
| 8 | + * This interface allows intercepting serialization process and insertion of arbitrary [ConfigValue] into the output. |
| 9 | + * |
| 10 | + * Usage example (nested config serialization): |
| 11 | + * ``` |
| 12 | + * @Serializable |
| 13 | + * data class Example( |
| 14 | + * @Serializable(NestedConfigSerializer::class) |
| 15 | + * val d: Config |
| 16 | + * ) |
| 17 | + * object NestedConfigSerializer : KSerializer<Config> { |
| 18 | + * override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("package.Config", PrimitiveKind.STRING) |
| 19 | + * |
| 20 | + * override fun deserialize(decoder: Decoder): Config = |
| 21 | + * if (decoder is HoconDecoder) decoder.decodeConfigValue { conf, path -> conf.getConfig(path) } |
| 22 | + * else throw SerializationException("This class can be decoded only by Hocon format") |
| 23 | + * |
| 24 | + * override fun serialize(encoder: Encoder, value: Config) { |
| 25 | + * if (encoder is HoconEncoder) encoder.encodeConfigValue(value.root()) |
| 26 | + * else throw SerializationException("This class can be encoded only by Hocon format") |
| 27 | + * } |
| 28 | + * } |
| 29 | + * val nestedConfig = ConfigFactory.parseString("nested { value = \"test\" }") |
| 30 | + * val globalConfig = Hocon.encodeToConfig(Example(nestedConfig)) // d: { nested: { value = "test" } } |
| 31 | + * val newNestedConfig = Hocon.decodeFromConfig(Example.serializer(), globalConfig) |
| 32 | + * ``` |
| 33 | + */ |
141 | 34 | @ExperimentalSerializationApi
|
142 |
| -internal class HoconConfigMapEncoder(hocon: Hocon, configConsumer: (ConfigValue) -> Unit) : |
143 |
| - AbstractHoconEncoder(hocon, configConsumer) { |
144 |
| - |
145 |
| - private val configMap = mutableMapOf<String, ConfigValue>() |
146 |
| - |
147 |
| - private lateinit var key: String |
148 |
| - private var isKey: Boolean = true |
149 |
| - |
150 |
| - override fun encodeTaggedConfigValue(tag: String, value: ConfigValue) { |
151 |
| - if (isKey) { |
152 |
| - key = when (value.valueType()) { |
153 |
| - ConfigValueType.OBJECT, ConfigValueType.LIST -> throw InvalidKeyKindException(value) |
154 |
| - else -> value.unwrappedNullable().toString() |
155 |
| - } |
156 |
| - isKey = false |
157 |
| - } else { |
158 |
| - configMap[key] = value |
159 |
| - isKey = true |
160 |
| - } |
161 |
| - } |
162 |
| - |
163 |
| - override fun getCurrent(): ConfigValue = ConfigValueFactory.fromMap(configMap) |
164 |
| - |
165 |
| - // Without cast to `Any?` Kotlin will assume unwrapped value as non-nullable by default |
166 |
| - // and will call `Any.toString()` instead of extension-function `Any?.toString()`. |
167 |
| - // We can't cast value in place using `(value.unwrapped() as Any?).toString()` because of warning "No cast needed". |
168 |
| - private fun ConfigValue.unwrappedNullable(): Any? = unwrapped() |
| 35 | +sealed interface HoconEncoder { |
| 36 | + |
| 37 | + /** |
| 38 | + * Appends the given [ConfigValue] element to the current output. |
| 39 | + * |
| 40 | + * @param value to insert |
| 41 | + */ |
| 42 | + fun encodeConfigValue(value: ConfigValue) |
169 | 43 | }
|
0 commit comments