Skip to content

Commit 100dd53

Browse files
petitclClément Petit
authored and
Clément Petit
committed
[KT-42716] Add support for serializing property files
1 parent f833852 commit 100dd53

File tree

4 files changed

+735
-1
lines changed

4 files changed

+735
-1
lines changed

formats/properties/api/kotlinx-serialization-properties.api

+47
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
public final class kotlinx/serialization/properties/KeyValueSeparator : java/lang/Enum {
2+
public static final field COLON Lkotlinx/serialization/properties/KeyValueSeparator;
3+
public static final field EQUALS Lkotlinx/serialization/properties/KeyValueSeparator;
4+
public final fun char ()C
5+
public static fun valueOf (Ljava/lang/String;)Lkotlinx/serialization/properties/KeyValueSeparator;
6+
public static fun values ()[Lkotlinx/serialization/properties/KeyValueSeparator;
7+
}
8+
9+
public final class kotlinx/serialization/properties/LineSeparator : java/lang/Enum {
10+
public static final field CR Lkotlinx/serialization/properties/LineSeparator;
11+
public static final field CRLF Lkotlinx/serialization/properties/LineSeparator;
12+
public static final field LF Lkotlinx/serialization/properties/LineSeparator;
13+
public final fun chars ()[C
14+
public static fun valueOf (Ljava/lang/String;)Lkotlinx/serialization/properties/LineSeparator;
15+
public static fun values ()[Lkotlinx/serialization/properties/LineSeparator;
16+
}
17+
118
public abstract class kotlinx/serialization/properties/Properties : kotlinx/serialization/SerialFormat {
219
public static final field Default Lkotlinx/serialization/properties/Properties$Default;
320
public synthetic fun <init> (Lkotlinx/serialization/modules/SerializersModule;Ljava/lang/Void;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -11,8 +28,38 @@ public abstract class kotlinx/serialization/properties/Properties : kotlinx/seri
1128
public final class kotlinx/serialization/properties/Properties$Default : kotlinx/serialization/properties/Properties {
1229
}
1330

31+
public final class kotlinx/serialization/properties/PropertiesBuilder {
32+
public final fun getKeyValueSeparator ()Lkotlinx/serialization/properties/KeyValueSeparator;
33+
public final fun getLineSeparator ()Lkotlinx/serialization/properties/LineSeparator;
34+
public final fun getModule ()Lkotlinx/serialization/modules/SerializersModule;
35+
public final fun getSpacesAfterSeparator ()I
36+
public final fun getSpacesBeforeSeparator ()I
37+
public final fun setKeyValueSeparator (Lkotlinx/serialization/properties/KeyValueSeparator;)V
38+
public final fun setLineSeparator (Lkotlinx/serialization/properties/LineSeparator;)V
39+
public final fun setModule (Lkotlinx/serialization/modules/SerializersModule;)V
40+
public final fun setSpacesAfterSeparator (I)V
41+
public final fun setSpacesBeforeSeparator (I)V
42+
}
43+
1444
public final class kotlinx/serialization/properties/PropertiesKt {
1545
public static final fun Properties (Lkotlinx/serialization/modules/SerializersModule;)Lkotlinx/serialization/properties/Properties;
1646
public static final fun noImpl ()Ljava/lang/Void;
1747
}
1848

49+
public abstract class kotlinx/serialization/properties/StringProperties : kotlinx/serialization/SerialFormat {
50+
public static final field Default Lkotlinx/serialization/properties/StringProperties$Default;
51+
public synthetic fun <init> (Lkotlinx/serialization/properties/PropertiesConf;Lkotlinx/serialization/properties/Properties;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
52+
public synthetic fun <init> (Lkotlinx/serialization/properties/PropertiesConf;Lkotlinx/serialization/properties/Properties;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
53+
public final fun decodeFromString (Lkotlinx/serialization/DeserializationStrategy;Ljava/lang/String;)Ljava/lang/Object;
54+
public final fun encodeToString (Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;)Ljava/lang/String;
55+
public fun getSerializersModule ()Lkotlinx/serialization/modules/SerializersModule;
56+
}
57+
58+
public final class kotlinx/serialization/properties/StringProperties$Default : kotlinx/serialization/properties/StringProperties {
59+
}
60+
61+
public final class kotlinx/serialization/properties/StringPropertiesKt {
62+
public static final fun StringProperties (Lkotlin/jvm/functions/Function1;)Lkotlinx/serialization/properties/StringProperties;
63+
public static synthetic fun StringProperties$default (Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/serialization/properties/StringProperties;
64+
}
65+

formats/properties/commonMain/src/kotlinx/serialization/properties/Properties.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import kotlinx.serialization.modules.*
2626
* @Serializable
2727
* class DataHolder(val data: Data, val property2: String)
2828
*
29-
* val map = Properties.store(DataHolder(Data("value1"), "value2"))
29+
* val map = Properties.encodeToMap(DataHolder(Data("value1"), "value2"))
3030
* // map contents will be the following:
3131
* // property2 = value2
3232
* // data.property1 = value1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
/*
2+
* Copyright 2017-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.serialization.properties
6+
7+
import kotlinx.serialization.*
8+
import kotlinx.serialization.descriptors.*
9+
import kotlinx.serialization.encoding.*
10+
import kotlinx.serialization.internal.*
11+
import kotlinx.serialization.modules.*
12+
13+
/**
14+
* Transforms a [Serializable] class' properties into a single flat [String] representing the class data
15+
* in the properties format.
16+
*
17+
* If the given class has non-primitive property `d` of arbitrary type `D`, `D` values are inserted
18+
* into the same map; keys for such values are prefixed with string `d.`:
19+
*
20+
* ```
21+
* @Serializable
22+
* class Data(val property1: String)
23+
*
24+
* @Serializable
25+
* class DataHolder(val data: Data, val property2: String)
26+
*
27+
* val string = StringProperties.encodeToString(properties)
28+
* // string contents will be the following:
29+
* """
30+
* property2 = value2
31+
* data.property1 = value1
32+
* """
33+
* ```
34+
*
35+
* If the given class has a [List] property `l`, each value from the list
36+
* would be prefixed with `l.N.`, where N is an index for a particular value.
37+
* [Map] is treated as a `[key,value,...]` list.
38+
39+
* Conversely, this class can convert a properties string into a [Serializable] class instance.
40+
* ```
41+
* @Serializable
42+
* class Data(val property1: String)
43+
*
44+
* @Serializable
45+
* class DataHolder(val data: Data, val property2: String)
46+
*
47+
* val string = """
48+
* property2 = value2
49+
* data.property1 = value1
50+
* """
51+
* val data = StringProperties.decodeToString(string, DataHolder.serializer())
52+
* // data contents will be the following:
53+
* // DataHolder(data = Data(property1 = "value1"), property2 = "value2")
54+
* ```
55+
*
56+
* @param conf A [PropertiesConf] which contain configuration for customising the output string.
57+
*/
58+
@ExperimentalSerializationApi
59+
@Suppress("UNUSED_PARAMETER")
60+
public sealed class StringProperties(
61+
private val conf: PropertiesConf,
62+
private val properties: Properties = Properties(conf.serializersModule),
63+
) : SerialFormat by properties, StringFormat {
64+
65+
/**
66+
* Encodes properties from the given [value] to a properties String using the given [serializer].
67+
* `null` values are omitted from the output.
68+
*/
69+
@ExperimentalSerializationApi
70+
public override fun <T> encodeToString(serializer: SerializationStrategy<T>, value: T): String {
71+
val map = properties.encodeToMap(serializer, value)
72+
val builder = StringBuilder()
73+
for ((k, v) in map) {
74+
builder.append(k)
75+
repeat(conf.spacesBeforeSeparator) {
76+
builder.append(' ')
77+
}
78+
builder.append(conf.keyValueSeparator.char())
79+
repeat(conf.spacesAfterSeparator) {
80+
builder.append(' ')
81+
}
82+
builder.append(v)
83+
builder.append(conf.lineSeparator.chars())
84+
}
85+
return builder.toString()
86+
}
87+
88+
/**
89+
* Decodes properties from the given [string] to a value of type [T] using the given [deserializer].
90+
* [String] values are converted to respective primitive types using default conversion methods.
91+
* [T] may contain properties of nullable types; they will be filled by non-null values from the [map], if present.
92+
*/
93+
public override fun <T> decodeFromString(deserializer: DeserializationStrategy<T>, string: String): T {
94+
val result = mutableMapOf<String, String>()
95+
for (line in string.logicalLines()) {
96+
val parsedLine = line.unescaped()
97+
var keyEnd = parsedLine.length
98+
for (i in parsedLine.indices) {
99+
if (parsedLine[i] in separators) {
100+
keyEnd = i
101+
break
102+
}
103+
}
104+
105+
var valueBegin = parsedLine.length
106+
var separatorFound = false
107+
for (i in keyEnd..parsedLine.lastIndex) {
108+
if (separatorFound && parsedLine[i] != ' ') {
109+
valueBegin = i
110+
break
111+
}
112+
if (parsedLine[i] in nonBlankSeparators) {
113+
separatorFound = true
114+
}
115+
if (parsedLine[i] !in separators) {
116+
valueBegin = i
117+
break
118+
}
119+
}
120+
121+
result[parsedLine.substring(0, keyEnd)] = parsedLine.substring(valueBegin)
122+
}
123+
return properties.decodeFromStringMap(deserializer, result)
124+
}
125+
126+
/**
127+
* A [Properties] instance that can be used as default and does not have any [SerializersModule] installed.
128+
*/
129+
@ExperimentalSerializationApi
130+
public companion object Default : StringProperties(PropertiesConf())
131+
}
132+
133+
@OptIn(ExperimentalSerializationApi::class)
134+
private class StringPropertiesImpl(conf: PropertiesConf) : StringProperties(conf)
135+
136+
/**
137+
* Creates an instance of [StringProperties] with a given [builderAction].
138+
* TODO: doc
139+
*/
140+
@ExperimentalSerializationApi
141+
public fun StringProperties(builderAction: StringPropertiesBuilder.() -> Unit = {}): StringProperties {
142+
val builder = StringPropertiesBuilder(PropertiesConf())
143+
builder.builderAction()
144+
return StringPropertiesImpl(builder.build())
145+
}
146+
147+
/**
148+
* Encodes properties from given [value] to a string using serializer for reified type [T] and returns this string.
149+
* Converts all primitive types to [String] using [toString] method.
150+
* `null` values are omitted from the output.
151+
*/
152+
@ExperimentalSerializationApi
153+
public inline fun <reified T> StringProperties.encodeToString(value: T): String =
154+
encodeToString(serializersModule.serializer(), value)
155+
156+
/**
157+
* Decodes properties from given [propertiesString], assigns them to an object using serializer for reified type [T] and returns this object.
158+
* [String] values are converted to respective primitive types using default conversion methods.
159+
* [T] may contain properties of nullable types; they will be filled by non-null values from the [map], if present.
160+
*/
161+
@ExperimentalSerializationApi
162+
public inline fun <reified T> StringProperties.decodeFromString(propertiesString: String): T =
163+
decodeFromString(serializersModule.serializer(), propertiesString)
164+
165+
/**
166+
* Builder of the [StringProperties] instance provided by `StringProperties { ... }` factory function.
167+
*/
168+
@ExperimentalSerializationApi
169+
public class StringPropertiesBuilder internal constructor(from: PropertiesConf) {
170+
171+
/**
172+
* A [LineSeparator] to be used for separating lines when encoding to a string.
173+
* Default value is [LineSeparator.LF].
174+
*/
175+
public var lineSeparator: LineSeparator = from.lineSeparator
176+
177+
/**
178+
* A [KeyValueSeparator] to be used for separating keys and values when encoding to a string.
179+
* Default value is [KeyValueSeparator.EQUALS].
180+
*/
181+
public var keyValueSeparator: KeyValueSeparator = from.keyValueSeparator
182+
183+
/**
184+
* A number of spaces to be inserted before the [keyValueSeparator] when encoding to a string.
185+
* Default value is `0`.
186+
*/
187+
public var spacesBeforeSeparator: Int = from.spacesBeforeSeparator
188+
189+
/**
190+
* A number of spaces to be inserted after the [keyValueSeparator] when encoding to a string.
191+
* Default value is `0`.
192+
*/
193+
public var spacesAfterSeparator: Int = from.spacesAfterSeparator
194+
195+
/**
196+
* A [SerializersModule] to be used for encoding and decoding.
197+
* Default value is [EmptySerializersModule].
198+
*/
199+
public var module: SerializersModule = from.serializersModule
200+
201+
internal fun build(): PropertiesConf {
202+
return PropertiesConf(
203+
lineSeparator,
204+
keyValueSeparator,
205+
spacesBeforeSeparator,
206+
spacesAfterSeparator,
207+
module
208+
)
209+
}
210+
}
211+
212+
@ExperimentalSerializationApi
213+
internal data class PropertiesConf(
214+
val lineSeparator: LineSeparator = LineSeparator.LF,
215+
val keyValueSeparator: KeyValueSeparator = KeyValueSeparator.EQUALS,
216+
val spacesBeforeSeparator: Int = 0,
217+
val spacesAfterSeparator: Int = 0,
218+
val serializersModule: SerializersModule = EmptySerializersModule()
219+
)
220+
221+
@ExperimentalSerializationApi
222+
public enum class LineSeparator(private val s: String) {
223+
LF("\n"),
224+
CR("\r"),
225+
CRLF("\r\n");
226+
227+
public fun chars(): CharArray {
228+
return s.toCharArray()
229+
}
230+
}
231+
232+
@ExperimentalSerializationApi
233+
public enum class KeyValueSeparator(private val c: Char) {
234+
EQUALS('='),
235+
COLON(':');
236+
237+
public fun char(): Char = c
238+
}
239+
240+
private val nonBlankSeparators = setOf('=', ':')
241+
private val separators = nonBlankSeparators + ' '
242+
private val wellKnownEscapeChars = mapOf(
243+
'\\' to '\\',
244+
'n' to '\n',
245+
'r' to '\r',
246+
't' to '\t'
247+
)
248+
249+
private fun String.unescaped(): String {
250+
val sb = StringBuilder(this.length)
251+
var i = 0
252+
while (i < this.length) {
253+
if (i < this.length - 1 && this[i] == '\\') {
254+
if (this[i + 1] in wellKnownEscapeChars) {
255+
sb.append(wellKnownEscapeChars[this[i + 1]])
256+
i += 2
257+
} else {
258+
i++
259+
}
260+
} else {
261+
sb.append(this[i])
262+
i++
263+
}
264+
}
265+
return sb.toString()
266+
}
267+
268+
private fun String.logicalLines(): List<String> {
269+
val commentFilter = "[ \\t\\f]*[#!].*".toRegex()
270+
val lines = lines()
271+
.filterNot { it.isBlank() || commentFilter.matches(it) }
272+
.toMutableList()
273+
val logicalLines = mutableListOf<String>()
274+
275+
var currentLine = ""
276+
for (line in lines) {
277+
val trimmedLine = line.trimStart()
278+
if (trimmedLine.endsWith("\\")) {
279+
currentLine += trimmedLine.dropLast(1)
280+
} else {
281+
currentLine += trimmedLine
282+
logicalLines.add(currentLine)
283+
currentLine = ""
284+
}
285+
}
286+
if (currentLine.isNotBlank()) {
287+
logicalLines.add(currentLine)
288+
}
289+
290+
return logicalLines
291+
}

0 commit comments

Comments
 (0)