diff --git a/CHANGELOG.md b/CHANGELOG.md
index 860265c094..b953d74ead 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@
* Minimum IDE version has been increased to 2024.3. This helps avert compatibility issues with future versions. ([#TODO](https://github.com/fwdekker/intellij-randomness/issues/TODO))
### Added
+* Added support for Nano ID. ([#TODO](https://github.com/fwdekker/intellij-randomness/issues/TODO))
+
### Changed
* Regex patterns now ignore named capturing groups instead of giving an error. Achieved by updating [RgxGen](https://github.com/curious-odd-man/RgxGen) to v3.1. ([#TODO](https://github.com/fwdekker/intellij-randomness/issues/TODO))
diff --git a/README.md b/README.md
index 1f67096bc5..f87c2dc464 100644
--- a/README.md
+++ b/README.md
@@ -42,7 +42,8 @@ Randomness can also be found in the main menu under Tools and under <
3. **Strings**, such as `"PaQDQqSBEH"`, with support for reverse regex.
4. **Words**, such as `"Bridge"`, with predefined or custom word lists.
5. **UUIDs**, such as `0caa7b28-fe58-4ba6-a25a-9e5beaaf8f4b`, with or without dashes.
- 6. **Date-times**, such as `2022-02-03 19:03`, or any other format you want.
+ 6. **Nano IDs**, such as `V1StGXR8_Z5jdHi6B-myT`, with customisable alphabets and sizes.
+ 7. **Date-times**, such as `2022-02-03 19:03`, or any other format you want.
* 🧬 **Templates**
For complex kinds of data, you can use templates.
A template is a list of data types that should be concatenated to create random data.
diff --git a/build.gradle.kts b/build.gradle.kts
index 0e6934556e..f6c77ae2c2 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -52,6 +52,7 @@ dependencies {
}
implementation(libs.rgxgen)
implementation(libs.github)
+ implementation(libs.nanoid)
scrambler(libs.kotlin.reflect)
testImplementation(libs.assertj.swing)
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index dd093ec7c3..0a96fd528d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -24,6 +24,7 @@ kotest = "5.9.1" # https://mvnrepository.com/artifact/io.kotest/kotest-assertio
kover = "0.9.1" # https://plugins.gradle.org/plugin/org.jetbrains.kotlinx.kover
rgxgen = "3.1" # https://mvnrepository.com/artifact/com.github.curious-odd-man/rgxgen
uuidGenerator = "5.1.0" # https://mvnrepository.com/artifact/com.fasterxml.uuid/java-uuid-generator
+nanoid = "1.0.1" # https://mvnrepository.com/artifact/io.viascom.nanoid/nanoid
[plugins]
changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" }
@@ -46,6 +47,7 @@ kotest-runner-junit5 = { module = "io.kotest:kotest-runner-junit5", version.ref
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" }
rgxgen = { module = "com.github.curious-odd-man:rgxgen", version.ref = "rgxgen" }
uuidGenerator = { module = "com.fasterxml.uuid:java-uuid-generator", version.ref = "uuidGenerator" }
+nanoid = { module = "io.viascom.nanoid:nanoid", version.ref = "nanoid" }
[bundles]
kotest = ["kotest-assertions-core", "kotest-framework-dataset", "kotest-runner-junit5"]
diff --git a/src/main/kotlin/com/fwdekker/randomness/Settings.kt b/src/main/kotlin/com/fwdekker/randomness/Settings.kt
index eebd281a48..c750e2b370 100644
--- a/src/main/kotlin/com/fwdekker/randomness/Settings.kt
+++ b/src/main/kotlin/com/fwdekker/randomness/Settings.kt
@@ -223,6 +223,62 @@ internal class PersistentSettings : PersistentStateComponent {
prop.setAttribute("value", max.value)
}
}
+ },
+ Version.parse("3.5.0") to
+ { settings ->
+ // Migrate UuidScheme to UidScheme
+ settings.getSchemes()
+ .filter { it.name == "UuidScheme" }
+ .forEach { scheme ->
+ // Change scheme name from UuidScheme to UidScheme
+ scheme.name = "UidScheme"
+
+ // Add idTypeKey property set to "uuid"
+ scheme.addProperty("idTypeKey", "uuid")
+
+ // Wrap existing UUID properties into uuidConfig
+ val uuidConfigElement = Element("UuidConfig")
+
+ // Move UUID-specific properties to uuidConfig
+ listOf("version", "minDateTime", "maxDateTime", "isUppercase", "addDashes").forEach { propName ->
+ scheme.getMultiProperty(propName).forEach { prop ->
+ scheme.children.remove(prop)
+ uuidConfigElement.addContent(prop.clone())
+ }
+ }
+
+ // Add uuidConfig as a property
+ scheme.addContent(
+ Element("option")
+ .setAttribute("name", "uuidConfig")
+ .addContent(uuidConfigElement)
+ )
+
+ // Add default nanoIdConfig
+ val nanoIdConfigElement = Element("NanoIdConfig")
+ scheme.addContent(
+ Element("option")
+ .setAttribute("name", "nanoIdConfig")
+ .addContent(nanoIdConfigElement)
+ )
+ }
+
+ // Add default UUID and NanoID templates if they don't exist
+ val templatesElement = settings.getPropertyByPath("templateList", null, "templates", null)
+ if (templatesElement != null) {
+ val existingTemplateNames = settings.getTemplates()
+ .mapNotNull { it.getPropertyValue("name") }
+
+ // Add UUID template if not present
+ if ("UUID" !in existingTemplateNames) {
+ templatesElement.addContent(createUidTemplateElement("UUID", "uuid"))
+ }
+
+ // Add Nano ID template if not present
+ if ("Nano ID" !in existingTemplateNames) {
+ templatesElement.addContent(createUidTemplateElement("Nano ID", "nanoid"))
+ }
+ }
}
)
@@ -230,6 +286,42 @@ internal class PersistentSettings : PersistentStateComponent {
* The settings format version of Randomness.
*/
val CURRENT_VERSION: Version = UPGRADES.keys.max()
+
+ /**
+ * Creates an XML Element representing a Template with a UidScheme.
+ *
+ * @param templateName The name of the template (e.g., "UUID" or "Nano ID")
+ * @param idTypeKey The ID type key (e.g., "uuid" or "nanoid")
+ */
+ private fun createUidTemplateElement(templateName: String, idTypeKey: String): Element {
+ val uidScheme = Element("UidScheme")
+ .apply {
+ addProperty("idTypeKey", idTypeKey)
+ addContent(
+ Element("option")
+ .setAttribute("name", "uuidConfig")
+ .addContent(Element("UuidConfig"))
+ )
+ addContent(
+ Element("option")
+ .setAttribute("name", "nanoIdConfig")
+ .addContent(Element("NanoIdConfig"))
+ )
+ }
+
+ val schemesOption = Element("option")
+ .setAttribute("name", "schemes")
+ .addContent(
+ Element("list")
+ .addContent(uidScheme)
+ )
+
+ return Element("Template")
+ .apply {
+ addProperty("name", templateName)
+ addContent(schemesOption)
+ }
+ }
}
}
diff --git a/src/main/kotlin/com/fwdekker/randomness/template/Template.kt b/src/main/kotlin/com/fwdekker/randomness/template/Template.kt
index 761c142201..dcdffcf361 100644
--- a/src/main/kotlin/com/fwdekker/randomness/template/Template.kt
+++ b/src/main/kotlin/com/fwdekker/randomness/template/Template.kt
@@ -12,7 +12,7 @@ import com.fwdekker.randomness.decimal.DecimalScheme
import com.fwdekker.randomness.integer.IntegerScheme
import com.fwdekker.randomness.string.StringScheme
import com.fwdekker.randomness.ui.ValidatorDsl.Companion.validators
-import com.fwdekker.randomness.uuid.UuidScheme
+import com.fwdekker.randomness.uid.UidScheme
import com.fwdekker.randomness.word.WordScheme
import com.intellij.ui.Gray
import com.intellij.util.xmlb.annotations.OptionTag
@@ -36,7 +36,7 @@ data class Template(
IntegerScheme::class,
StringScheme::class,
TemplateReference::class,
- UuidScheme::class,
+ UidScheme::class,
WordScheme::class,
]
)
diff --git a/src/main/kotlin/com/fwdekker/randomness/template/TemplateJTree.kt b/src/main/kotlin/com/fwdekker/randomness/template/TemplateJTree.kt
index 1ece19e1d4..d5e241616d 100644
--- a/src/main/kotlin/com/fwdekker/randomness/template/TemplateJTree.kt
+++ b/src/main/kotlin/com/fwdekker/randomness/template/TemplateJTree.kt
@@ -7,7 +7,7 @@ import com.fwdekker.randomness.datetime.DateTimeScheme
import com.fwdekker.randomness.decimal.DecimalScheme
import com.fwdekker.randomness.integer.IntegerScheme
import com.fwdekker.randomness.string.StringScheme
-import com.fwdekker.randomness.uuid.UuidScheme
+import com.fwdekker.randomness.uid.UidScheme
import com.fwdekker.randomness.word.WordScheme
import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionToolbarPosition
@@ -710,7 +710,7 @@ class TemplateJTree(
DecimalScheme(),
StringScheme(),
WordScheme(),
- UuidScheme(),
+ UidScheme(),
DateTimeScheme(),
TemplateReference(),
)
diff --git a/src/main/kotlin/com/fwdekker/randomness/template/TemplateList.kt b/src/main/kotlin/com/fwdekker/randomness/template/TemplateList.kt
index 436ba145dd..515ec90d83 100644
--- a/src/main/kotlin/com/fwdekker/randomness/template/TemplateList.kt
+++ b/src/main/kotlin/com/fwdekker/randomness/template/TemplateList.kt
@@ -12,7 +12,8 @@ import com.fwdekker.randomness.decimal.DecimalScheme
import com.fwdekker.randomness.integer.IntegerScheme
import com.fwdekker.randomness.string.StringScheme
import com.fwdekker.randomness.ui.ValidatorDsl.Companion.validators
-import com.fwdekker.randomness.uuid.UuidScheme
+import com.fwdekker.randomness.uid.IdType
+import com.fwdekker.randomness.uid.UidScheme
import com.fwdekker.randomness.word.DefaultWordList
import com.fwdekker.randomness.word.WordScheme
import com.intellij.util.xmlb.annotations.OptionTag
@@ -133,7 +134,8 @@ data class TemplateList(
)
)
),
- Template("UUID", mutableListOf(UuidScheme())),
+ Template("UUID", mutableListOf(UidScheme(idTypeKey = IdType.Uuid.key))),
+ Template("Nano ID", mutableListOf(UidScheme(idTypeKey = IdType.NanoId.key))),
Template("Date-Time", mutableListOf(DateTimeScheme())),
Template(
"IP address",
diff --git a/src/main/kotlin/com/fwdekker/randomness/template/TemplateListEditor.kt b/src/main/kotlin/com/fwdekker/randomness/template/TemplateListEditor.kt
index 4748666d7c..3ab3a4bb51 100644
--- a/src/main/kotlin/com/fwdekker/randomness/template/TemplateListEditor.kt
+++ b/src/main/kotlin/com/fwdekker/randomness/template/TemplateListEditor.kt
@@ -18,8 +18,8 @@ import com.fwdekker.randomness.ui.PreviewPanel
import com.fwdekker.randomness.ui.ValidationInfo
import com.fwdekker.randomness.ui.addChangeListenerTo
import com.fwdekker.randomness.ui.focusLater
-import com.fwdekker.randomness.uuid.UuidScheme
-import com.fwdekker.randomness.uuid.UuidSchemeEditor
+import com.fwdekker.randomness.uid.UidScheme
+import com.fwdekker.randomness.uid.UidSchemeEditor
import com.fwdekker.randomness.word.WordScheme
import com.fwdekker.randomness.word.WordSchemeEditor
import com.intellij.openapi.Disposable
@@ -153,7 +153,7 @@ class TemplateListEditor(
is IntegerScheme -> IntegerSchemeEditor(scheme)
is DecimalScheme -> DecimalSchemeEditor(scheme)
is StringScheme -> StringSchemeEditor(scheme)
- is UuidScheme -> UuidSchemeEditor(scheme)
+ is UidScheme -> UidSchemeEditor(scheme)
is WordScheme -> WordSchemeEditor(scheme)
is DateTimeScheme -> DateTimeSchemeEditor(scheme)
is TemplateReference -> TemplateReferenceEditor(scheme)
diff --git a/src/main/kotlin/com/fwdekker/randomness/uid/IdType.kt b/src/main/kotlin/com/fwdekker/randomness/uid/IdType.kt
new file mode 100644
index 0000000000..7c7b04d915
--- /dev/null
+++ b/src/main/kotlin/com/fwdekker/randomness/uid/IdType.kt
@@ -0,0 +1,57 @@
+package com.fwdekker.randomness.uid
+
+import com.fwdekker.randomness.Bundle
+
+
+/**
+ * Represents the type of unique identifier to generate.
+ *
+ * This sealed class allows for type-safe selection of ID types and is designed
+ * to be extensible for adding new ID types in the future.
+ */
+sealed class IdType {
+ /**
+ * The display name shown in the UI dropdown.
+ */
+ abstract val displayName: String
+
+ /**
+ * A unique key used for serialization and identification.
+ */
+ abstract val key: String
+
+
+ /**
+ * UUID (Universally Unique Identifier) type.
+ */
+ data object Uuid : IdType() {
+ override val displayName: String get() = Bundle("uid.type.uuid")
+ override val key: String = "uuid"
+ }
+
+ /**
+ * NanoID type - a tiny, secure, URL-friendly unique string ID generator.
+ */
+ data object NanoId : IdType() {
+ override val displayName: String get() = Bundle("uid.type.nanoid")
+ override val key: String = "nanoid"
+ }
+
+
+ companion object {
+ /**
+ * All available ID types.
+ */
+ val entries: List get() = listOf(Uuid, NanoId)
+
+ /**
+ * The default ID type.
+ */
+ val DEFAULT: IdType get() = Uuid
+
+ /**
+ * Returns the [IdType] with the given [key], or [DEFAULT] if not found.
+ */
+ fun fromKey(key: String): IdType = entries.find { it.key == key } ?: DEFAULT
+ }
+}
diff --git a/src/main/kotlin/com/fwdekker/randomness/uid/NanoIdConfig.kt b/src/main/kotlin/com/fwdekker/randomness/uid/NanoIdConfig.kt
new file mode 100644
index 0000000000..14c90b5284
--- /dev/null
+++ b/src/main/kotlin/com/fwdekker/randomness/uid/NanoIdConfig.kt
@@ -0,0 +1,55 @@
+package com.fwdekker.randomness.uid
+
+import io.viascom.nanoid.NanoId
+import kotlin.random.Random
+
+
+/**
+ * Configuration for generating Nano IDs.
+ *
+ * @property size The length of the generated Nano ID.
+ * @property alphabet The alphabet to use when generating the Nano ID.
+ */
+data class NanoIdConfig(
+ var size: Int = DEFAULT_SIZE,
+ var alphabet: String = DEFAULT_ALPHABET,
+) {
+ /**
+ * Generates [count] random Nano IDs using the given [random] instance.
+ *
+ * Note: The [random] parameter is currently unused as the NanoId library uses its own random source,
+ * but it's included for API consistency with other config classes.
+ */
+ @Suppress("UNUSED_PARAMETER") // random parameter kept for API consistency
+ fun generate(count: Int, random: Random): List =
+ List(count) { NanoId.generate(size, alphabet) }
+
+
+ /**
+ * Creates a deep copy of this configuration.
+ */
+ fun deepCopy() = copy()
+
+
+ companion object {
+ /**
+ * The minimum allowed value of [size].
+ */
+ const val MIN_SIZE = 1
+
+ /**
+ * The default value of [size].
+ */
+ const val DEFAULT_SIZE = 21
+
+ /**
+ * The default value of [alphabet].
+ */
+ const val DEFAULT_ALPHABET: String = "_-0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
+
+ /**
+ * The preset values for affix decorators.
+ */
+ val PRESET_AFFIX_DECORATOR_DESCRIPTORS = listOf("'", "\"", "`")
+ }
+}
diff --git a/src/main/kotlin/com/fwdekker/randomness/uid/UidScheme.kt b/src/main/kotlin/com/fwdekker/randomness/uid/UidScheme.kt
new file mode 100644
index 0000000000..645e549c14
--- /dev/null
+++ b/src/main/kotlin/com/fwdekker/randomness/uid/UidScheme.kt
@@ -0,0 +1,132 @@
+package com.fwdekker.randomness.uid
+
+import com.fwdekker.randomness.Bundle
+import com.fwdekker.randomness.Icons
+import com.fwdekker.randomness.Scheme
+import com.fwdekker.randomness.TypeIcon
+import com.fwdekker.randomness.affix.AffixDecorator
+import com.fwdekker.randomness.array.ArrayDecorator
+import com.fwdekker.randomness.uid.NanoIdConfig.Companion.MIN_SIZE
+import com.fwdekker.randomness.uid.UuidConfig.Companion.MAX_MAX_DATE_TIME
+import com.fwdekker.randomness.uid.UuidConfig.Companion.MIN_MIN_DATE_TIME
+import com.fwdekker.randomness.uid.UuidConfig.Companion.SUPPORTED_VERSIONS
+import com.fwdekker.randomness.uid.UuidConfig.Companion.TIME_BASED_VERSIONS
+import com.fwdekker.randomness.ui.ValidatorDsl.Companion.validators
+import com.intellij.ui.JBColor
+import com.intellij.util.xmlb.annotations.OptionTag
+import com.intellij.util.xmlb.annotations.Transient
+import java.awt.Color
+
+
+/**
+ * Contains settings for generating unique identifiers (UIDs).
+ *
+ * This scheme supports multiple ID types (UUID, NanoID) and delegates generation
+ * to the appropriate configuration based on the selected [idType].
+ *
+ * @property idTypeKey The key of the selected ID type, used for serialization.
+ * @property uuidConfig Configuration for UUID generation.
+ * @property nanoIdConfig Configuration for NanoID generation.
+ * @property affixDecorator The affixation to apply to the generated values.
+ * @property arrayDecorator Settings that determine whether the output should be an array of values.
+ */
+data class UidScheme(
+ var idTypeKey: String = IdType.DEFAULT.key,
+ @OptionTag val uuidConfig: UuidConfig = UuidConfig(),
+ @OptionTag val nanoIdConfig: NanoIdConfig = NanoIdConfig(),
+ @OptionTag val affixDecorator: AffixDecorator = DEFAULT_AFFIX_DECORATOR,
+ @OptionTag val arrayDecorator: ArrayDecorator = DEFAULT_ARRAY_DECORATOR,
+) : Scheme() {
+ /**
+ * The selected ID type.
+ */
+ @get:Transient
+ var idType: IdType
+ get() = IdType.fromKey(idTypeKey)
+ set(value) {
+ idTypeKey = value.key
+ }
+
+ @get:Transient
+ override val name = Bundle("uid.title")
+
+ override val typeIcon get() = BASE_ICON
+
+ override val decorators get() = listOf(affixDecorator, arrayDecorator)
+
+ override val validators = validators {
+ case({ idType == IdType.Uuid }) {
+ of(uuidConfig::version)
+ .check({ it in SUPPORTED_VERSIONS }, { Bundle("uuid.error.unknown_version", it) })
+ case({ uuidConfig.version in TIME_BASED_VERSIONS }) {
+ of(uuidConfig::minDateTime)
+ .checkNoException { it.epochMilli }
+ .check(
+ { !it.isBefore(MIN_MIN_DATE_TIME) },
+ { Bundle("timestamp.error.too_old", MIN_MIN_DATE_TIME.value) }
+ )
+ of(uuidConfig::maxDateTime)
+ .checkNoException { it.epochMilli }
+ .check(
+ { !it.isAfter(MAX_MAX_DATE_TIME) },
+ { Bundle("timestamp.error.too_new", MAX_MAX_DATE_TIME.value) }
+ )
+ .check(
+ { !it.isBefore(uuidConfig.minDateTime) },
+ { Bundle("datetime.error.min_datetime_above_max") }
+ )
+ }
+ }
+ case({ idType == IdType.NanoId }) {
+ of(nanoIdConfig::size)
+ .check({ it >= MIN_SIZE }, { Bundle("nanoid.error.size_too_low", MIN_SIZE) })
+ of(nanoIdConfig::alphabet)
+ .check({ it.isNotEmpty() }, { Bundle("nanoid.error.alphabet_empty") })
+ }
+ include(::affixDecorator)
+ include(::arrayDecorator)
+ }
+
+
+ /**
+ * Generates [count] random UIDs based on the selected [idType].
+ */
+ override fun generateUndecoratedStrings(count: Int): List =
+ when (idType) {
+ IdType.Uuid -> uuidConfig.generate(count, random)
+ IdType.NanoId -> nanoIdConfig.generate(count, random)
+ }
+
+
+ override fun deepCopy(retainUuid: Boolean) =
+ copy(
+ uuidConfig = uuidConfig.deepCopy(),
+ nanoIdConfig = nanoIdConfig.deepCopy(),
+ affixDecorator = affixDecorator.deepCopy(retainUuid),
+ arrayDecorator = arrayDecorator.deepCopy(retainUuid),
+ ).deepCopyTransient(retainUuid)
+
+
+ companion object {
+ /**
+ * The base icon for UIDs.
+ */
+ val BASE_ICON
+ get() = TypeIcon(Icons.SCHEME, "id", listOf(JBColor(Color(185, 155, 248, 154), Color(185, 155, 248, 154))))
+
+ /**
+ * The preset values for the [affixDecorator] field.
+ */
+ val PRESET_AFFIX_DECORATOR_DESCRIPTORS = listOf("'", "\"", "`")
+
+ /**
+ * The default value of the [affixDecorator] field.
+ */
+ val DEFAULT_AFFIX_DECORATOR get() = AffixDecorator(enabled = false, descriptor = "\"")
+
+ /**
+ * The default value of the [arrayDecorator] field.
+ */
+ val DEFAULT_ARRAY_DECORATOR get() = ArrayDecorator()
+ }
+}
diff --git a/src/main/kotlin/com/fwdekker/randomness/uid/UidSchemeEditor.kt b/src/main/kotlin/com/fwdekker/randomness/uid/UidSchemeEditor.kt
new file mode 100644
index 0000000000..9349c98657
--- /dev/null
+++ b/src/main/kotlin/com/fwdekker/randomness/uid/UidSchemeEditor.kt
@@ -0,0 +1,194 @@
+package com.fwdekker.randomness.uid
+
+import com.fwdekker.randomness.Bundle
+import com.fwdekker.randomness.SchemeEditor
+import com.fwdekker.randomness.affix.AffixDecoratorEditor
+import com.fwdekker.randomness.array.ArrayDecoratorEditor
+import com.fwdekker.randomness.ui.JDateTimeField
+import com.fwdekker.randomness.ui.JIntSpinner
+import com.fwdekker.randomness.ui.UIConstants
+import com.fwdekker.randomness.ui.bindDateTimes
+import com.fwdekker.randomness.ui.bindIntValue
+import com.fwdekker.randomness.ui.bindTimestamp
+import com.fwdekker.randomness.ui.isEditable
+import com.fwdekker.randomness.ui.loadMnemonic
+import com.fwdekker.randomness.ui.withFixedWidth
+import com.fwdekker.randomness.ui.withName
+import com.fwdekker.randomness.uid.NanoIdConfig.Companion.DEFAULT_SIZE
+import com.fwdekker.randomness.uid.UuidConfig.Companion.DEFAULT_MAX_DATE_TIME
+import com.fwdekker.randomness.uid.UuidConfig.Companion.DEFAULT_MIN_DATE_TIME
+import com.fwdekker.randomness.uid.UuidConfig.Companion.TIME_BASED_VERSIONS
+import com.intellij.ui.ColoredListCellRenderer
+import com.intellij.ui.SimpleTextAttributes
+import com.intellij.ui.dsl.builder.AlignX
+import com.intellij.ui.dsl.builder.BottomGap
+import com.intellij.ui.dsl.builder.bindItem
+import com.intellij.ui.dsl.builder.bindSelected
+import com.intellij.ui.dsl.builder.bindText
+import com.intellij.ui.dsl.builder.panel
+import com.intellij.ui.dsl.builder.toNullableProperty
+import com.intellij.ui.layout.ComponentPredicate
+import com.intellij.ui.layout.selectedValueMatches
+import javax.swing.JComboBox
+import javax.swing.JList
+
+
+/**
+ * Component for editing a [UidScheme].
+ *
+ * @param scheme the scheme to edit
+ */
+class UidSchemeEditor(scheme: UidScheme = UidScheme()) : SchemeEditor(scheme) {
+ private lateinit var idTypeComboBox: JComboBox
+
+ override val rootComponent = panel {
+ lateinit var isUuidSelected: ComponentPredicate
+ lateinit var isNanoIdSelected: ComponentPredicate
+
+ group(Bundle("uid.ui.value.header")) {
+ row(Bundle("uid.ui.type.option")) {
+ comboBox(IdType.entries, IdTypeRenderer())
+ .isEditable(false)
+ .withName("idType")
+ .bindItem(
+ getter = { scheme.idType },
+ setter = { value -> scheme.idType = value ?: IdType.DEFAULT }
+ )
+ .also { cell ->
+ idTypeComboBox = cell.component
+ isUuidSelected = cell.component.selectedValueMatches { it == IdType.Uuid }
+ isNanoIdSelected = cell.component.selectedValueMatches { it == IdType.NanoId }
+ }
+ }
+
+ // UUID settings panel
+ rowsRange {
+ lateinit var versionHasDateTime: ComponentPredicate
+ lateinit var minDateTimeField: JDateTimeField
+ lateinit var maxDateTimeField: JDateTimeField
+
+ row(Bundle("uuid.ui.value.version.option")) {
+ comboBox(UuidConfig.SUPPORTED_VERSIONS, UuidVersionRenderer())
+ .isEditable(false)
+ .withName("version")
+ .bindItem(scheme.uuidConfig::version.toNullableProperty())
+ .bindValidation(scheme.uuidConfig::version)
+ .also { cell ->
+ versionHasDateTime = cell.component.selectedValueMatches { it in TIME_BASED_VERSIONS }
+ }
+ }
+
+ indent {
+ row(Bundle("uuid.ui.value.min_datetime_option")) {
+ cell(JDateTimeField(DEFAULT_MIN_DATE_TIME))
+ .withFixedWidth(UIConstants.SIZE_VERY_LARGE)
+ .withName("minDateTime")
+ .bindTimestamp(scheme.uuidConfig::minDateTime)
+ .bindValidation(scheme.uuidConfig::minDateTime)
+ .also { minDateTimeField = it.component }
+ contextHelp(Bundle("uuid.ui.datetime_help"))
+ }.enabledIf(versionHasDateTime)
+
+ row(Bundle("uuid.ui.value.max_datetime_option")) {
+ cell(JDateTimeField(DEFAULT_MAX_DATE_TIME))
+ .withFixedWidth(UIConstants.SIZE_VERY_LARGE)
+ .withName("maxDateTime")
+ .bindTimestamp(scheme.uuidConfig::maxDateTime)
+ .bindValidation(scheme.uuidConfig::maxDateTime)
+ .also { maxDateTimeField = it.component }
+ contextHelp(Bundle("uuid.ui.datetime_help"))
+ }.enabledIf(versionHasDateTime).bottomGap(BottomGap.SMALL)
+
+ bindDateTimes(minDateTimeField, maxDateTimeField)
+ }
+
+ row {
+ checkBox(Bundle("uuid.ui.value.capitalization_option"))
+ .loadMnemonic()
+ .withName("isUppercase")
+ .bindSelected(scheme.uuidConfig::isUppercase)
+ .bindValidation(scheme.uuidConfig::isUppercase)
+ }
+
+ row {
+ checkBox(Bundle("uuid.add_dashes"))
+ .loadMnemonic()
+ .withName("addDashes")
+ .bindSelected(scheme.uuidConfig::addDashes)
+ .bindValidation(scheme.uuidConfig::addDashes)
+ }
+ }.visibleIf(isUuidSelected)
+
+ // NanoID settings panel
+ rowsRange {
+ row(Bundle("nanoid.ui.value.size_option")) {
+ cell(JIntSpinner(DEFAULT_SIZE, NanoIdConfig.MIN_SIZE, Int.MAX_VALUE))
+ .withFixedWidth(UIConstants.SIZE_SMALL)
+ .withName("size")
+ .bindIntValue(scheme.nanoIdConfig::size)
+ .bindValidation(scheme.nanoIdConfig::size)
+ }
+ row(Bundle("nanoid.ui.value.alphabet_option")) {
+ textField()
+ .withFixedWidth(UIConstants.SIZE_VERY_LARGE)
+ .withName("alphabet")
+ .bindText(scheme.nanoIdConfig::alphabet)
+ .bindValidation(scheme.nanoIdConfig::alphabet)
+ }
+ }.visibleIf(isNanoIdSelected)
+
+ row {
+ AffixDecoratorEditor(scheme.affixDecorator, UidScheme.PRESET_AFFIX_DECORATOR_DESCRIPTORS)
+ .also { decoratorEditors += it }
+ .let { cell(it.rootComponent) }
+ }
+ }
+
+ row {
+ ArrayDecoratorEditor(scheme.arrayDecorator)
+ .also { decoratorEditors += it }
+ .let { cell(it.rootComponent).align(AlignX.FILL) }
+ }
+ }.finalize(this)
+
+
+ init {
+ reset()
+ }
+
+
+ /**
+ * Renders an ID type in the dropdown.
+ */
+ private class IdTypeRenderer : ColoredListCellRenderer() {
+ override fun customizeCellRenderer(
+ list: JList,
+ value: IdType?,
+ index: Int,
+ selected: Boolean,
+ hasFocus: Boolean,
+ ) {
+ if (value == null) return
+ append(value.displayName)
+ }
+ }
+
+ /**
+ * Renders a supported UUID version.
+ */
+ private class UuidVersionRenderer : ColoredListCellRenderer() {
+ override fun customizeCellRenderer(
+ list: JList,
+ value: Int?,
+ index: Int,
+ selected: Boolean,
+ hasFocus: Boolean,
+ ) {
+ if (value == null) return
+
+ append("$value")
+ append(" ")
+ append(Bundle("uuid.ui.value.version.$value"), SimpleTextAttributes.GRAYED_ATTRIBUTES)
+ }
+ }
+}
diff --git a/src/main/kotlin/com/fwdekker/randomness/uuid/UuidScheme.kt b/src/main/kotlin/com/fwdekker/randomness/uid/UuidConfig.kt
similarity index 63%
rename from src/main/kotlin/com/fwdekker/randomness/uuid/UuidScheme.kt
rename to src/main/kotlin/com/fwdekker/randomness/uid/UuidConfig.kt
index 3645a94e4d..e7c8226bc5 100644
--- a/src/main/kotlin/com/fwdekker/randomness/uuid/UuidScheme.kt
+++ b/src/main/kotlin/com/fwdekker/randomness/uid/UuidConfig.kt
@@ -1,4 +1,4 @@
-package com.fwdekker.randomness.uuid
+package com.fwdekker.randomness.uid
import com.fasterxml.uuid.EthernetAddress
import com.fasterxml.uuid.NoArgGenerator
@@ -11,77 +11,35 @@ import com.fasterxml.uuid.impl.TimeBasedGenerator
import com.fasterxml.uuid.impl.TimeBasedReorderedGenerator
import com.fwdekker.randomness.Bundle
import com.fwdekker.randomness.CapitalizationMode
-import com.fwdekker.randomness.Icons
-import com.fwdekker.randomness.Scheme
import com.fwdekker.randomness.Timestamp
import com.fwdekker.randomness.TimestampConverter
-import com.fwdekker.randomness.TypeIcon
-import com.fwdekker.randomness.affix.AffixDecorator
-import com.fwdekker.randomness.array.ArrayDecorator
-import com.fwdekker.randomness.ui.ValidatorDsl.Companion.validators
-import com.intellij.ui.JBColor
import com.intellij.util.xmlb.annotations.OptionTag
-import com.intellij.util.xmlb.annotations.Transient
-import java.awt.Color
import java.util.UUID
import kotlin.random.Random
import kotlin.random.asJavaRandom
/**
- * Contains settings for generating random UUIDs.
+ * Configuration for generating UUIDs.
*
* @property version The version of UUIDs to generate.
* @property minDateTime The minimum date-time to use, applicable only for time-based UUIDs.
* @property maxDateTime The maximum date-time to use, applicable only for time-based UUIDs.
* @property isUppercase `true` if and only if all letters are uppercase.
* @property addDashes `true` if and only if the UUID should have dashes in it.
- * @property affixDecorator The affixation to apply to the generated values.
- * @property arrayDecorator Settings that determine whether the output should be an array of values.
*/
-data class UuidScheme(
+data class UuidConfig(
var version: Int = DEFAULT_VERSION,
@OptionTag(converter = TimestampConverter::class) var minDateTime: Timestamp = DEFAULT_MIN_DATE_TIME,
@OptionTag(converter = TimestampConverter::class) var maxDateTime: Timestamp = DEFAULT_MAX_DATE_TIME,
var isUppercase: Boolean = DEFAULT_IS_UPPERCASE,
var addDashes: Boolean = DEFAULT_ADD_DASHES,
- @OptionTag val affixDecorator: AffixDecorator = DEFAULT_AFFIX_DECORATOR,
- @OptionTag val arrayDecorator: ArrayDecorator = DEFAULT_ARRAY_DECORATOR,
-) : Scheme() {
- @get:Transient
- override val name = Bundle("uuid.title")
- override val typeIcon get() = BASE_ICON
- override val decorators get() = listOf(affixDecorator, arrayDecorator)
- override val validators = validators {
- of(::version).check({ it in SUPPORTED_VERSIONS }, { Bundle("uuid.error.unknown_version", it) })
- case({ version in TIME_BASED_VERSIONS }) {
- include(::minDateTime)
- include(::maxDateTime)
- of(::minDateTime)
- .check(
- { !it.isBefore(MIN_MIN_DATE_TIME) },
- { Bundle("timestamp.error.too_old", MIN_MIN_DATE_TIME.value) }
- )
- of(::maxDateTime)
- .check(
- { !it.isAfter(MAX_MAX_DATE_TIME) },
- { Bundle("timestamp.error.too_new", MAX_MAX_DATE_TIME.value) }
- )
- .check(
- { !it.isBefore(minDateTime) },
- { Bundle("datetime.error.min_datetime_above_max") }
- )
- }
- include(::affixDecorator)
- include(::arrayDecorator)
- }
-
-
+) {
/**
- * Returns [count] random UUIDs.
+ * Generates [count] random UUIDs using the given [random] instance.
*/
@Suppress("detekt:MagicNumber") // UUID versions are well-defined
- override fun generateUndecoratedStrings(count: Int): List {
+ fun generate(count: Int, random: Random): List {
val generator = when (version) {
1 -> TimeBasedGenerator(random.nextAddress(), random.uuidTimer(minDateTime, maxDateTime))
4 -> RandomBasedGenerator(random.asJavaRandom())
@@ -103,23 +61,13 @@ data class UuidScheme(
}
- override fun deepCopy(retainUuid: Boolean) =
- copy(
- affixDecorator = affixDecorator.deepCopy(retainUuid),
- arrayDecorator = arrayDecorator.deepCopy(retainUuid),
- ).deepCopyTransient(retainUuid)
-
-
/**
- * Holds constants.
+ * Creates a deep copy of this configuration.
*/
- companion object {
- /**
- * The base icon for UUIDs.
- */
- val BASE_ICON
- get() = TypeIcon(Icons.SCHEME, "id", listOf(JBColor(Color(185, 155, 248, 154), Color(185, 155, 248, 154))))
+ fun deepCopy() = copy()
+
+ companion object {
/**
* The default value of the [version] field.
*/
@@ -147,9 +95,6 @@ data class UuidScheme(
/**
* The minimum valid value of [minDateTime].
- *
- * This is a limitation of the underlying library, not of the UUID standard itself. See also
- * https://github.com/cowtowncoder/java-uuid-generator/issues/133.
*/
val MIN_MIN_DATE_TIME: Timestamp = Timestamp("1970-01-01 00:00:00.000")
@@ -160,9 +105,6 @@ data class UuidScheme(
/**
* The maximum valid value of [maxDateTime].
- *
- * This is a limitation of the underlying library, not of the UUID standard itself. See also
- * https://github.com/cowtowncoder/java-uuid-generator/issues/133.
*/
val MAX_MAX_DATE_TIME: Timestamp = Timestamp("5236-03-31 21:21:00.684")
@@ -172,22 +114,13 @@ data class UuidScheme(
val DEFAULT_MAX_DATE_TIME: Timestamp = MAX_MAX_DATE_TIME
/**
- * The preset values for the [affixDecorator] field.
+ * The preset values for affix decorators.
*/
val PRESET_AFFIX_DECORATOR_DESCRIPTORS = listOf("'", "\"", "`")
-
- /**
- * The default value of the [affixDecorator] field.
- */
- val DEFAULT_AFFIX_DECORATOR get() = AffixDecorator(enabled = false, descriptor = "\"")
-
- /**
- * The default value of the [arrayDecorator] field.
- */
- val DEFAULT_ARRAY_DECORATOR get() = ArrayDecorator()
}
}
+
/**
* Constants about UUIDs.
*/
@@ -209,7 +142,7 @@ object UuidMeta {
const val V6_TIMESTAMP_EPOCH: Long = V1_TIMESTAMP_EPOCH
/**
- * The modulo under which UUIDv1 timestamps are stored.
+ * The modulo under which UUIDv6 timestamps are stored.
*/
const val V6_TIMESTAMP_MODULO: Long = V1_TIMESTAMP_MODULO
@@ -219,7 +152,7 @@ object UuidMeta {
const val V7_TIMESTAMP_EPOCH: Long = 0L
/**
- * The modulo under which UUIDv1 timestamps are stored.
+ * The modulo under which UUIDv7 timestamps are stored.
*/
const val V7_TIMESTAMP_MODULO: Long = 0x1000000000000L
}
@@ -242,8 +175,6 @@ private fun Random.nextLongInclusive(min: Long = Long.MIN_VALUE, max: Long = Lon
/**
* Returns a [UUIDClock] that generates random times between [min] and [max] using this [Random] instance.
- *
- * Both [min] and [max] are inclusive, and are assumed to be valid.
*/
private fun Random.uuidClock(min: Timestamp, max: Timestamp) =
object : UUIDClock() {
@@ -252,8 +183,6 @@ private fun Random.uuidClock(min: Timestamp, max: Timestamp) =
/**
* Returns a [UUIDTimer] that generates random times between [min] and [max] using this [Random] instance.
- *
- * Both [min] and [max] are inclusive, and are assumed to be valid.
*/
private fun Random.uuidTimer(min: Timestamp, max: Timestamp) =
UUIDTimer(asJavaRandom(), null, uuidClock(min, max))
@@ -263,8 +192,6 @@ private fun Random.uuidTimer(min: Timestamp, max: Timestamp) =
* Generates v8 UUIDs.
*
* Works by generating a v4 UUID and then replacing the version nibble.
- *
- * TODO\[Workaround]: Remove class after https://github.com/cowtowncoder/java-uuid-generator/issues/47 has been fixed
*/
private class FreeFormGenerator(random: Random) : NoArgGenerator() {
/**
diff --git a/src/main/kotlin/com/fwdekker/randomness/uuid/UuidSchemeEditor.kt b/src/main/kotlin/com/fwdekker/randomness/uuid/UuidSchemeEditor.kt
deleted file mode 100644
index 28b1102b35..0000000000
--- a/src/main/kotlin/com/fwdekker/randomness/uuid/UuidSchemeEditor.kt
+++ /dev/null
@@ -1,137 +0,0 @@
-package com.fwdekker.randomness.uuid
-
-import com.fwdekker.randomness.Bundle
-import com.fwdekker.randomness.SchemeEditor
-import com.fwdekker.randomness.affix.AffixDecoratorEditor
-import com.fwdekker.randomness.array.ArrayDecoratorEditor
-import com.fwdekker.randomness.ui.JDateTimeField
-import com.fwdekker.randomness.ui.UIConstants
-import com.fwdekker.randomness.ui.bindDateTimes
-import com.fwdekker.randomness.ui.bindTimestamp
-import com.fwdekker.randomness.ui.isEditable
-import com.fwdekker.randomness.ui.loadMnemonic
-import com.fwdekker.randomness.ui.withFixedWidth
-import com.fwdekker.randomness.ui.withName
-import com.fwdekker.randomness.uuid.UuidScheme.Companion.DEFAULT_MAX_DATE_TIME
-import com.fwdekker.randomness.uuid.UuidScheme.Companion.DEFAULT_MIN_DATE_TIME
-import com.fwdekker.randomness.uuid.UuidScheme.Companion.PRESET_AFFIX_DECORATOR_DESCRIPTORS
-import com.fwdekker.randomness.uuid.UuidScheme.Companion.TIME_BASED_VERSIONS
-import com.intellij.ui.ColoredListCellRenderer
-import com.intellij.ui.SimpleTextAttributes
-import com.intellij.ui.dsl.builder.AlignX
-import com.intellij.ui.dsl.builder.BottomGap
-import com.intellij.ui.dsl.builder.bindItem
-import com.intellij.ui.dsl.builder.bindSelected
-import com.intellij.ui.dsl.builder.panel
-import com.intellij.ui.dsl.builder.toNullableProperty
-import com.intellij.ui.layout.ComponentPredicate
-import com.intellij.ui.layout.selectedValueMatches
-import javax.swing.JList
-
-
-/**
- * Component for editing a [UuidScheme].
- *
- * @param scheme the scheme to edit
- */
-class UuidSchemeEditor(scheme: UuidScheme = UuidScheme()) : SchemeEditor(scheme) {
- override val rootComponent = panel {
- group(Bundle("uuid.ui.value.header")) {
- panel {
- lateinit var versionHasDateTime: ComponentPredicate
- lateinit var minDateTimeField: JDateTimeField
- lateinit var maxDateTimeField: JDateTimeField
-
- panel {
- row(Bundle("uuid.ui.value.version.option")) {
- comboBox(UuidScheme.SUPPORTED_VERSIONS, UuidVersionRenderer())
- .isEditable(false)
- .withName("version")
- .bindItem(scheme::version.toNullableProperty())
- .bindValidation(scheme::version)
- .also { cell ->
- versionHasDateTime = cell.component.selectedValueMatches { it in TIME_BASED_VERSIONS }
- }
- }
- }
-
- indent {
- row(Bundle("uuid.ui.value.min_datetime_option")) {
- cell(JDateTimeField(DEFAULT_MIN_DATE_TIME))
- .withFixedWidth(UIConstants.SIZE_VERY_LARGE)
- .withName("minDateTime")
- .bindTimestamp(scheme::minDateTime)
- .bindValidation(scheme::minDateTime)
- .also { minDateTimeField = it.component }
- contextHelp(Bundle("uuid.ui.datetime_help"))
- }.enabledIf(versionHasDateTime)
-
- row(Bundle("uuid.ui.value.max_datetime_option")) {
- cell(JDateTimeField(DEFAULT_MAX_DATE_TIME))
- .withFixedWidth(UIConstants.SIZE_VERY_LARGE)
- .withName("maxDateTime")
- .bindTimestamp(scheme::maxDateTime)
- .bindValidation(scheme::maxDateTime)
- .also { maxDateTimeField = it.component }
- contextHelp(Bundle("uuid.ui.datetime_help"))
- }.enabledIf(versionHasDateTime).bottomGap(BottomGap.SMALL)
-
- bindDateTimes(minDateTimeField, maxDateTimeField)
- }
-
- row {
- checkBox(Bundle("uuid.ui.value.capitalization_option"))
- .loadMnemonic()
- .withName("isUppercase")
- .bindSelected(scheme::isUppercase)
- .bindValidation(scheme::isUppercase)
- }
-
- row {
- checkBox(Bundle("uuid.add_dashes"))
- .loadMnemonic()
- .withName("addDashes")
- .bindSelected(scheme::addDashes)
- .bindValidation(scheme::addDashes)
- }
-
- row {
- AffixDecoratorEditor(scheme.affixDecorator, PRESET_AFFIX_DECORATOR_DESCRIPTORS)
- .also { decoratorEditors += it }
- .let { cell(it.rootComponent) }
- }
- }
- }
-
- row {
- ArrayDecoratorEditor(scheme.arrayDecorator)
- .also { decoratorEditors += it }
- .let { cell(it.rootComponent).align(AlignX.FILL) }
- }
- }.finalize(this)
-
-
- init {
- reset()
- }
-
-
- /**
- * Renders a supported UUID version.
- */
- private class UuidVersionRenderer : ColoredListCellRenderer() {
- override fun customizeCellRenderer(
- list: JList,
- value: Int?,
- index: Int,
- selected: Boolean,
- hasFocus: Boolean,
- ) {
- if (value == null) return
-
- append("$value")
- append(" ")
- append(Bundle("uuid.ui.value.version.$value"), SimpleTextAttributes.GRAYED_ATTRIBUTES)
- }
- }
-}
diff --git a/src/main/resources/randomness.properties b/src/main/resources/randomness.properties
index 1cda554e85..9de4877b36 100644
--- a/src/main/resources/randomness.properties
+++ b/src/main/resources/randomness.properties
@@ -190,10 +190,8 @@ timestamp.error.too_old=Enter a date-time at or after %1$s.
uuid.add_dashes=&Add dashes
uuid.error.min_datetime_above_max=Minimum date-time should be less than or equal to maximum date-time.
uuid.error.unknown_version=Unknown UUID version '%1$s'.
-uuid.title=UUID
uuid.ui.datetime_help=Write the desired date and time, or write "NOW" without quotes to always refer to the moment at which this scheme is used.
uuid.ui.value.capitalization_option=Upper&case
-uuid.ui.value.header=Value
uuid.ui.value.max_datetime_option=Ma&ximum:
uuid.ui.value.min_datetime_option=Mi&nimum:
uuid.ui.value.version.1=Time
@@ -202,6 +200,15 @@ uuid.ui.value.version.6=Time, reordered
uuid.ui.value.version.7=Time, epoch
uuid.ui.value.version.8=Random
uuid.ui.value.version.option=Version:
+nanoid.ui.value.size_option=Le&ngth:
+nanoid.ui.value.alphabet_option=&Alphabet:
+nanoid.error.size_too_low=Length should be at least %1$s.
+nanoid.error.alphabet_empty=Alphabet must not be empty.
+uid.title=UID
+uid.ui.value.header=Value
+uid.ui.type.option=&Type:
+uid.type.uuid=UUID
+uid.type.nanoid=NanoID
word.error.empty_word_list=Enter at least one word.
word.title=Word
word.ui.format.capitalization_option=&Capitalization:
diff --git a/src/test/kotlin/com/fwdekker/randomness/SettingsTest.kt b/src/test/kotlin/com/fwdekker/randomness/SettingsTest.kt
index be5c990576..0fea29c282 100644
--- a/src/test/kotlin/com/fwdekker/randomness/SettingsTest.kt
+++ b/src/test/kotlin/com/fwdekker/randomness/SettingsTest.kt
@@ -285,6 +285,7 @@ object PersistentSettingsTest : FunSpec({
row("3.3.4", "3.3.5", "removes `generator` fields"),
row("3.3.6", "3.4.0", "patches epochs to timestamp strings"),
row("3.4.1", "3.4.2", "clamps timestamps in UUID settings"),
+ row("3.4.2", "3.5.0", "migrates UUID settings to new format"),
) { (from, to, _) ->
val unpatched = getTestConfig("/settings-upgrades/v$from-v$to-before.xml").parseXml()
diff --git a/src/test/kotlin/com/fwdekker/randomness/XmlHelpersTest.kt b/src/test/kotlin/com/fwdekker/randomness/XmlHelpersTest.kt
index 2cc0372876..513d273b3e 100644
--- a/src/test/kotlin/com/fwdekker/randomness/XmlHelpersTest.kt
+++ b/src/test/kotlin/com/fwdekker/randomness/XmlHelpersTest.kt
@@ -7,7 +7,7 @@ import com.fwdekker.randomness.template.TemplateList
import com.fwdekker.randomness.testhelpers.Tags
import com.fwdekker.randomness.testhelpers.beforeNonContainer
import com.fwdekker.randomness.testhelpers.shouldMatchXml
-import com.fwdekker.randomness.uuid.UuidScheme
+import com.fwdekker.randomness.uid.UidScheme
import com.intellij.openapi.util.JDOMUtil
import com.intellij.util.xmlb.XmlSerializer.serialize
import io.kotest.assertions.throwables.shouldThrow
@@ -603,7 +603,7 @@ object XmlHelpersTest : FunSpec({
templateList = TemplateList(
mutableListOf(
Template(name = "Foo", schemes = mutableListOf(IntegerScheme(), StringScheme())),
- Template(name = "Bar", schemes = mutableListOf(UuidScheme())),
+ Template(name = "Bar", schemes = mutableListOf(UidScheme())),
)
)
)
@@ -645,5 +645,21 @@ object XmlHelpersTest : FunSpec({
xmlUuids shouldContainExactlyInAnyOrder realUuids
}
+
+ test("UidScheme nested configs have uuids serialized") {
+ val uidScheme = settings.templates
+ .flatMap { it.schemes }
+ .filterIsInstance()
+ .single()
+
+ val uidSchemeXml = xml.getSchemes()
+ .single { it.getPropertyValue("uuid") == uidScheme.uuid }
+
+ val uuidConfigXml = uidSchemeXml.getProperty("uuidConfig")?.children?.singleOrNull()
+ val nanoIdConfigXml = uidSchemeXml.getProperty("nanoIdConfig")?.children?.singleOrNull()
+
+ uuidConfigXml?.getPropertyValue("uuid") shouldBe uidScheme.uuidConfig.uuid
+ nanoIdConfigXml?.getPropertyValue("uuid") shouldBe uidScheme.nanoIdConfig.uuid
+ }
}
})
diff --git a/src/test/kotlin/com/fwdekker/randomness/template/TemplateListEditorTest.kt b/src/test/kotlin/com/fwdekker/randomness/template/TemplateListEditorTest.kt
index ad29eefac4..5b7f8ef9b1 100644
--- a/src/test/kotlin/com/fwdekker/randomness/template/TemplateListEditorTest.kt
+++ b/src/test/kotlin/com/fwdekker/randomness/template/TemplateListEditorTest.kt
@@ -16,7 +16,7 @@ import com.fwdekker.randomness.testhelpers.shouldContainExactly
import com.fwdekker.randomness.testhelpers.shouldMatchBundle
import com.fwdekker.randomness.testhelpers.useBareIdeaFixture
import com.fwdekker.randomness.testhelpers.useEdtViolationDetection
-import com.fwdekker.randomness.uuid.UuidScheme
+import com.fwdekker.randomness.uid.UidScheme
import com.fwdekker.randomness.word.WordScheme
import com.intellij.openapi.util.Disposer
import io.kotest.assertions.throwables.shouldThrow
@@ -105,7 +105,7 @@ object TemplateListEditorTest : FunSpec({
"integer" to row(IntegerScheme()) { it.spinner("minValue") },
"decimal" to row(DecimalScheme()) { it.spinner("minValue") },
"string" to row(StringScheme()) { it.textBox("pattern") },
- "uuid" to row(UuidScheme()) { it.comboBox("version") },
+ "uid" to row(UidScheme()) { it.comboBox("idType") },
"word" to row(WordScheme()) { it.comboBox("presets") },
"date-time" to row(DateTimeScheme()) { it.textBox("minDateTime") },
"template reference" to row(TemplateReference()) { it.comboBox("template") },
diff --git a/src/test/kotlin/com/fwdekker/randomness/testhelpers/StateReflection.kt b/src/test/kotlin/com/fwdekker/randomness/testhelpers/StateReflection.kt
index a1402c4554..42f10fa0ef 100644
--- a/src/test/kotlin/com/fwdekker/randomness/testhelpers/StateReflection.kt
+++ b/src/test/kotlin/com/fwdekker/randomness/testhelpers/StateReflection.kt
@@ -7,6 +7,8 @@ import com.fwdekker.randomness.Timestamp
import com.fwdekker.randomness.Timestamp.Companion.FORMATTER
import com.fwdekker.randomness.getMod
import com.fwdekker.randomness.integer.IntegerScheme
+import com.fwdekker.randomness.uid.NanoIdConfig
+import com.fwdekker.randomness.uid.UuidConfig
import com.github.sisyphsu.dateparser.DateParserUtils
import com.intellij.util.xmlb.annotations.OptionTag
import com.intellij.util.xmlb.annotations.Transient
@@ -114,6 +116,9 @@ fun Any?.mutated(): Any {
if (epochMilli == null) Timestamp("foo_$value")
else Timestamp(DateParserUtils.parseDateTime(value).plusSeconds(1).format(FORMATTER))
+ is NanoIdConfig -> copy(size = size + 1, alphabet = "foo_$alphabet")
+ is UuidConfig -> copy(version = if (version == 4) 1 else 4, isUppercase = !isUppercase, addDashes = !addDashes)
+
is State ->
properties()
.filter { it.isSerialized() }
diff --git a/src/test/kotlin/com/fwdekker/randomness/uid/UidSchemeTest.kt b/src/test/kotlin/com/fwdekker/randomness/uid/UidSchemeTest.kt
new file mode 100644
index 0000000000..c4c2db76ef
--- /dev/null
+++ b/src/test/kotlin/com/fwdekker/randomness/uid/UidSchemeTest.kt
@@ -0,0 +1,181 @@
+package com.fwdekker.randomness.uid
+
+import com.fwdekker.randomness.affix.AffixDecorator
+import com.fwdekker.randomness.array.ArrayDecorator
+import com.fwdekker.randomness.testhelpers.Tags
+import com.fwdekker.randomness.testhelpers.shouldValidateAsBundle
+import com.fwdekker.randomness.testhelpers.stateDeepCopyTestFactory
+import com.fwdekker.randomness.testhelpers.stateSerializationTestFactory
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.data.row
+import io.kotest.datatest.withData
+import io.kotest.matchers.shouldBe
+import io.kotest.matchers.string.shouldMatch
+
+
+/**
+ * Unit tests for [UidScheme].
+ */
+object UidSchemeTest : FunSpec({
+ tags(Tags.PLAIN, Tags.SCHEME)
+
+
+ context("generateStrings") {
+ context("UUID") {
+ withData(
+ mapOf(
+ "generates lowercase UUIDv4 with dashes by default" to
+ row(
+ UidScheme(idTypeKey = IdType.Uuid.key),
+ Regex("[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")
+ ),
+ "generates uppercase UUID if enabled" to
+ row(
+ UidScheme(
+ idTypeKey = IdType.Uuid.key,
+ uuidConfig = UuidConfig(isUppercase = true)
+ ),
+ Regex("[0-9A-F]{8}-[0-9A-F]{4}-4[0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}")
+ ),
+ "generates UUID without dashes if disabled" to
+ row(
+ UidScheme(
+ idTypeKey = IdType.Uuid.key,
+ uuidConfig = UuidConfig(addDashes = false)
+ ),
+ Regex("[0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}")
+ ),
+ "generates UUIDv1" to
+ row(
+ UidScheme(
+ idTypeKey = IdType.Uuid.key,
+ uuidConfig = UuidConfig(version = 1)
+ ),
+ Regex("[0-9a-f]{8}-[0-9a-f]{4}-1[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")
+ ),
+ "generates UUIDv6" to
+ row(
+ UidScheme(
+ idTypeKey = IdType.Uuid.key,
+ uuidConfig = UuidConfig(version = 6)
+ ),
+ Regex("[0-9a-f]{8}-[0-9a-f]{4}-6[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")
+ ),
+ "generates UUIDv7" to
+ row(
+ UidScheme(
+ idTypeKey = IdType.Uuid.key,
+ uuidConfig = UuidConfig(version = 7)
+ ),
+ Regex("[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")
+ ),
+ "generates UUIDv8" to
+ row(
+ UidScheme(
+ idTypeKey = IdType.Uuid.key,
+ uuidConfig = UuidConfig(version = 8)
+ ),
+ Regex("[0-9a-f]{8}-[0-9a-f]{4}-8[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")
+ ),
+ )
+ ) { (scheme, pattern) -> scheme.generateStrings()[0] shouldMatch pattern }
+ }
+
+ context("NanoID") {
+ withData(
+ mapOf(
+ "generates NanoID with default size and alphabet" to
+ row(
+ UidScheme(idTypeKey = IdType.NanoId.key),
+ Regex("[_\\-0-9a-zA-Z]{21}")
+ ),
+ "generates NanoID with custom size" to
+ row(
+ UidScheme(
+ idTypeKey = IdType.NanoId.key,
+ nanoIdConfig = NanoIdConfig(size = 10)
+ ),
+ Regex("[_\\-0-9a-zA-Z]{10}")
+ ),
+ "generates NanoID with custom alphabet" to
+ row(
+ UidScheme(
+ idTypeKey = IdType.NanoId.key,
+ nanoIdConfig = NanoIdConfig(alphabet = "abc")
+ ),
+ Regex("[abc]{21}")
+ ),
+ )
+ ) { (scheme, pattern) -> scheme.generateStrings()[0] shouldMatch pattern }
+ }
+
+ test("applies decorators in order affix, array") {
+ val scheme = UidScheme(
+ idTypeKey = IdType.Uuid.key,
+ uuidConfig = UuidConfig(addDashes = false),
+ affixDecorator = AffixDecorator(enabled = true, descriptor = "@!"),
+ arrayDecorator = ArrayDecorator(enabled = true, minCount = 2, maxCount = 2, separator = ", "),
+ )
+
+ scheme.generateStrings()[0] shouldMatch
+ Regex("\\[[0-9a-f]{32}!, [0-9a-f]{32}!\\]")
+ }
+ }
+
+ context("doValidate") {
+ context("UUID") {
+ withData(
+ mapOf(
+ "succeeds for default UUID state" to
+ row(UidScheme(idTypeKey = IdType.Uuid.key), null),
+ "fails if UUID version is unsupported" to
+ row(
+ UidScheme(
+ idTypeKey = IdType.Uuid.key,
+ uuidConfig = UuidConfig(version = 3)
+ ),
+ "uuid.error.unknown_version"
+ ),
+ )
+ ) { (scheme, validation) -> scheme shouldValidateAsBundle validation }
+ }
+
+ context("NanoID") {
+ withData(
+ mapOf(
+ "succeeds for default NanoID state" to
+ row(UidScheme(idTypeKey = IdType.NanoId.key), null),
+ "fails if NanoID size is too low" to
+ row(
+ UidScheme(
+ idTypeKey = IdType.NanoId.key,
+ nanoIdConfig = NanoIdConfig(size = 0)
+ ),
+ "nanoid.error.size_too_low"
+ ),
+ "fails if NanoID alphabet is empty" to
+ row(
+ UidScheme(
+ idTypeKey = IdType.NanoId.key,
+ nanoIdConfig = NanoIdConfig(alphabet = "")
+ ),
+ "nanoid.error.alphabet_empty"
+ ),
+ )
+ ) { (scheme, validation) -> scheme shouldValidateAsBundle validation }
+ }
+
+ withData(
+ mapOf(
+ "fails if affix decorator is invalid" to
+ row(UidScheme(affixDecorator = AffixDecorator(enabled = true, descriptor = """\""")), ""),
+ "fails if array decorator is invalid" to
+ row(UidScheme(arrayDecorator = ArrayDecorator(enabled = true, minCount = -24)), ""),
+ )
+ ) { (scheme, validation) -> scheme shouldValidateAsBundle validation }
+ }
+
+ include(stateDeepCopyTestFactory { UidScheme() })
+
+ include(stateSerializationTestFactory { UidScheme() })
+})
diff --git a/src/test/kotlin/com/fwdekker/randomness/uuid/UuidSchemeEditorTest.kt b/src/test/kotlin/com/fwdekker/randomness/uuid/UuidSchemeEditorTest.kt
deleted file mode 100644
index 89c203f220..0000000000
--- a/src/test/kotlin/com/fwdekker/randomness/uuid/UuidSchemeEditorTest.kt
+++ /dev/null
@@ -1,136 +0,0 @@
-package com.fwdekker.randomness.uuid
-
-import com.fwdekker.randomness.Timestamp
-import com.fwdekker.randomness.testhelpers.Tags
-import com.fwdekker.randomness.testhelpers.afterNonContainer
-import com.fwdekker.randomness.testhelpers.beforeNonContainer
-import com.fwdekker.randomness.testhelpers.editorApplyTests
-import com.fwdekker.randomness.testhelpers.editorFieldsTests
-import com.fwdekker.randomness.testhelpers.find
-import com.fwdekker.randomness.testhelpers.isSelectedProp
-import com.fwdekker.randomness.testhelpers.itemProp
-import com.fwdekker.randomness.testhelpers.matcher
-import com.fwdekker.randomness.testhelpers.prop
-import com.fwdekker.randomness.testhelpers.runEdt
-import com.fwdekker.randomness.testhelpers.textProp
-import com.fwdekker.randomness.testhelpers.timestampProp
-import com.fwdekker.randomness.testhelpers.useBareIdeaFixture
-import com.fwdekker.randomness.testhelpers.useEdtViolationDetection
-import com.fwdekker.randomness.testhelpers.valueProp
-import com.fwdekker.randomness.ui.JDateTimeField
-import io.kotest.core.spec.style.FunSpec
-import io.kotest.data.row
-import io.kotest.matchers.shouldBe
-import org.assertj.swing.fixture.Containers.showInFrame
-import org.assertj.swing.fixture.FrameFixture
-
-
-/**
- * Unit tests for [UuidSchemeEditor].
- */
-object UuidSchemeEditorTest : FunSpec({
- tags(Tags.EDITOR)
-
-
- lateinit var frame: FrameFixture
-
- lateinit var scheme: UuidScheme
- lateinit var editor: UuidSchemeEditor
-
-
- useEdtViolationDetection()
- useBareIdeaFixture()
-
- beforeNonContainer {
- scheme = UuidScheme()
- editor = runEdt { UuidSchemeEditor(scheme) }
- frame = showInFrame(editor.rootComponent)
- }
-
- afterNonContainer {
- frame.cleanUp()
- }
-
-
- context("input handling") {
- test("expands entered date-times") {
- val min = runEdt { frame.find(matcher(JDateTimeField::class.java, matcher = { it.name == "minDateTime" })) }
-
- runEdt {
- min.text = "1982"
- min.commitEdit()
- }
-
- runEdt { min.value.value } shouldBe "1982-01-01 00:00:00.000"
- }
-
- test("binds the minimum and maximum times") {
- runEdt { frame.textBox("minDateTime").timestampProp().set(Timestamp("4970")) }
-
- runEdt { frame.textBox("maxDateTime").timestampProp().set(Timestamp("3972")) }
-
- runEdt { frame.textBox("minDateTime").timestampProp().get() } shouldBe Timestamp("3972")
- runEdt { frame.textBox("maxDateTime").timestampProp().get() } shouldBe Timestamp("3972")
- }
- }
-
-
- include(editorApplyTests { editor })
-
- include(
- editorFieldsTests(
- { editor },
- mapOf(
- "type" to {
- row(
- frame.comboBox("version").itemProp(),
- editor.scheme::version.prop(),
- 8,
- )
- },
- "isUppercase" to {
- row(
- frame.checkBox("isUppercase").isSelectedProp(),
- editor.scheme::isUppercase.prop(),
- true,
- )
- },
- "addDashes" to {
- row(
- frame.checkBox("addDashes").isSelectedProp(),
- editor.scheme::addDashes.prop(),
- false,
- )
- },
- "minDateTime" to {
- row(
- frame.textBox("minDateTime").timestampProp(),
- editor.scheme::minDateTime.prop(),
- Timestamp("1989-03-30 13:36:32"),
- )
- },
- "maxDateTime" to {
- row(
- frame.textBox("maxDateTime").timestampProp(),
- editor.scheme::maxDateTime.prop(),
- Timestamp("3656-11-05 20:58:41"),
- )
- },
- "affixDecorator" to {
- row(
- frame.comboBox("affixDescriptor").textProp(),
- editor.scheme.affixDecorator::descriptor.prop(),
- "[@]",
- )
- },
- "arrayDecorator" to {
- row(
- frame.spinner("arrayMaxCount").valueProp(),
- editor.scheme.arrayDecorator::maxCount.prop(),
- 7,
- )
- },
- )
- )
- )
-})
diff --git a/src/test/kotlin/com/fwdekker/randomness/uuid/UuidSchemeTest.kt b/src/test/kotlin/com/fwdekker/randomness/uuid/UuidSchemeTest.kt
deleted file mode 100644
index ed339d830f..0000000000
--- a/src/test/kotlin/com/fwdekker/randomness/uuid/UuidSchemeTest.kt
+++ /dev/null
@@ -1,182 +0,0 @@
-package com.fwdekker.randomness.uuid
-
-import com.fasterxml.uuid.impl.UUIDUtil.extractTimestamp
-import com.fwdekker.randomness.Timestamp
-import com.fwdekker.randomness.affix.AffixDecorator
-import com.fwdekker.randomness.array.ArrayDecorator
-import com.fwdekker.randomness.testhelpers.Tags
-import com.fwdekker.randomness.testhelpers.shouldValidateAsBundle
-import com.fwdekker.randomness.testhelpers.stateDeepCopyTestFactory
-import com.fwdekker.randomness.testhelpers.stateSerializationTestFactory
-import com.fwdekker.randomness.uuid.UuidScheme.Companion.MAX_MAX_DATE_TIME
-import com.fwdekker.randomness.uuid.UuidScheme.Companion.MIN_MIN_DATE_TIME
-import io.kotest.assertions.withClue
-import io.kotest.core.spec.style.FunSpec
-import io.kotest.data.row
-import io.kotest.datatest.withData
-import io.kotest.matchers.comparables.shouldBeGreaterThanOrEqualTo
-import io.kotest.matchers.comparables.shouldBeLessThanOrEqualTo
-import io.kotest.matchers.should
-import io.kotest.matchers.shouldBe
-import io.kotest.matchers.string.beLowerCase
-import io.kotest.matchers.string.beUpperCase
-import io.kotest.matchers.string.shouldContain
-import io.kotest.matchers.string.shouldNotContain
-import java.util.UUID
-
-
-/**
- * Unit tests for [UuidScheme].
- */
-object UuidSchemeTest : FunSpec({
- tags(Tags.PLAIN, Tags.SCHEME)
-
-
- /**
- * Utility method for generating a single UUID.
- *
- * @param version the version of UUID to generate
- * @param min the minimum date-time to use, or `null` to not set a minimum
- * @param max the maximum date-time to use, or `null` to not set a minimum; defaults to the [min] value
- */
- fun generateString(version: Int, min: Timestamp? = null, max: Timestamp? = min): String =
- UuidScheme(version = version)
- .apply {
- if (min != null) minDateTime = min
- if (max != null) maxDateTime = max
- }
- .generateStrings()[0]
-
- /**
- * Returns the [UUID] generates by [generateString].
- */
- fun generateUuid(version: Int, min: Timestamp? = null, max: Timestamp? = min): UUID =
- UUID.fromString(generateString(version, min, max))
-
-
- context("generateStrings") {
- context("generates a UUID for all supported versions") {
- withData(UuidScheme.SUPPORTED_VERSIONS) { version ->
- generateUuid(version).version() shouldBe version
- }
- }
-
- context("uses the specified date-time") {
- withData(UuidScheme.TIME_BASED_VERSIONS) { version ->
- val timestamp = Timestamp("2023-04-07 08:36:29")
-
- extractTimestamp(generateUuid(version, timestamp)) shouldBe timestamp.epochMilli
- }
- }
-
- context("generates in the specified date-time range") {
- withData(UuidScheme.TIME_BASED_VERSIONS) { version ->
- repeat(100) {
- val min = Timestamp("1975-11-25 22:38:21")
- val max = Timestamp("2084-03-22 19:10:44")
-
- val epoch = extractTimestamp(generateUuid(version, min, max))
- epoch shouldBeGreaterThanOrEqualTo min.epochMilli!!
- epoch shouldBeLessThanOrEqualTo max.epochMilli!!
- }
- }
- }
-
- context("generates at the minimum datetime, in 1970") {
- withData(UuidScheme.TIME_BASED_VERSIONS) { version ->
- extractTimestamp(generateUuid(version, MIN_MIN_DATE_TIME)) shouldBe MIN_MIN_DATE_TIME.epochMilli
- }
- }
-
- context("generates at the maximum datetime, in 5236") {
- withData(UuidScheme.TIME_BASED_VERSIONS) { version ->
- extractTimestamp(generateUuid(version, MAX_MAX_DATE_TIME)) shouldBe MAX_MAX_DATE_TIME.epochMilli
- }
- }
-
- context("generates datetimes in all centuries strictly between 1970 and 5236") {
- withData(UuidScheme.TIME_BASED_VERSIONS) { version ->
- (20..51).forEach { century ->
- withClue("Century $century") {
- val timestamp = Timestamp("${century}92-10-24 15:01:16")
- extractTimestamp(generateUuid(version, timestamp)) shouldBe timestamp.epochMilli
- }
- }
- }
- }
-
- test("returns uppercase string") {
- UuidScheme(isUppercase = true).generateStrings()[0] should beUpperCase()
- }
-
- test("returns lowercase string") {
- UuidScheme(isUppercase = false).generateStrings()[0] should beLowerCase()
- }
-
- test("returns string with dashes") {
- UuidScheme(addDashes = true).generateStrings()[0] shouldContain "-"
- }
-
- test("returns string without dashes") {
- UuidScheme(addDashes = false).generateStrings()[0] shouldNotContain "-"
- }
-
- test("applies decorators in order affix, array") {
- UuidScheme(
- affixDecorator = AffixDecorator(enabled = true, descriptor = "#@"),
- arrayDecorator = ArrayDecorator(enabled = true, minCount = 3, maxCount = 3),
- ).generateStrings()[0].count { it == '#' } shouldBe 3
- }
- }
-
- context("doValidate") {
- context("general validation") {
- withData(
- mapOf(
- "succeeds for default state" to
- row(UuidScheme(), null),
- "fails for unsupported version" to
- row(UuidScheme(version = 14), "uuid.error.unknown_version"),
- "fails if affix decorator is invalid" to
- row(UuidScheme(affixDecorator = AffixDecorator(enabled = true, descriptor = """\""")), ""),
- "fails if array decorator is invalid" to
- row(UuidScheme(arrayDecorator = ArrayDecorator(enabled = true, minCount = -539)), ""),
- )
- ) { (scheme, validation) -> scheme shouldValidateAsBundle validation }
- }
-
- context("time-based validation") {
- withData(
- mapOf(
- "fails for invalid min date-time" to
- row(UuidScheme(minDateTime = Timestamp("invalid")), "timestamp.error.parse"),
- "fails for invalid max date-time" to
- row(UuidScheme(minDateTime = Timestamp("invalid")), "timestamp.error.parse"),
- "fails if min date-time is before 1970" to
- row(UuidScheme(minDateTime = Timestamp("1960")), "timestamp.error.too_old"),
- "fails if max date-time is after 5236" to
- row(UuidScheme(maxDateTime = Timestamp("5258")), "timestamp.error.too_new"),
- "fails if min date-time is above max date-time" to
- row(
- UuidScheme(minDateTime = Timestamp("4157"), maxDateTime = Timestamp("3376")),
- "uuid.error.min_datetime_above_max"
- ),
- )
- ) { (scheme, validation) ->
- withClue("Should fail if time-based") {
- scheme.version = 1
- scheme shouldValidateAsBundle validation
- }
-
- withClue("Should ignore failure if not time-based") {
- scheme.version = 4
- scheme shouldValidateAsBundle null
- }
- }
- }
- }
-
- include(stateDeepCopyTestFactory { UuidScheme() })
-
- include(stateSerializationTestFactory { UuidScheme() })
-})
diff --git a/src/test/resources/settings-upgrades/v3.4.2-v3.5.0-after.xml b/src/test/resources/settings-upgrades/v3.4.2-v3.5.0-after.xml
new file mode 100644
index 0000000000..fbba37413c
--- /dev/null
+++ b/src/test/resources/settings-upgrades/v3.4.2-v3.5.0-after.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
diff --git a/src/test/resources/settings-upgrades/v3.4.2-v3.5.0-before.xml b/src/test/resources/settings-upgrades/v3.4.2-v3.5.0-before.xml
new file mode 100644
index 0000000000..002eef60fd
--- /dev/null
+++ b/src/test/resources/settings-upgrades/v3.4.2-v3.5.0-before.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+