From bb1f0c82152b3c8d4da242c358eab7bfa4660712 Mon Sep 17 00:00:00 2001 From: itsmefox Date: Fri, 17 Oct 2025 15:33:15 +0200 Subject: [PATCH 1/4] Add support for Nano ID --- README.md | 3 +- build.gradle.kts | 1 + gradle.properties | 1 + .../randomness/nanoid/NanoIdScheme.kt | 80 +++++++++++++++++++ .../randomness/nanoid/NanoIdSchemeEditor.kt | 55 +++++++++++++ .../randomness/template/TemplateJTree.kt | 2 + .../randomness/template/TemplateList.kt | 2 + .../randomness/template/TemplateListEditor.kt | 3 + src/main/resources/randomness.properties | 6 ++ .../nanoid/NanoIdSchemeEditorTest.kt | 80 +++++++++++++++++++ .../randomness/nanoid/NanoIdSchemeTest.kt | 77 ++++++++++++++++++ 11 files changed, 309 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdScheme.kt create mode 100644 src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeEditor.kt create mode 100644 src/test/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeEditorTest.kt create mode 100644 src/test/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeTest.kt diff --git a/README.md b/README.md index 1eb3460ba6..a41fb84439 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 8ce11f0343..b880039049 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -64,6 +64,7 @@ dependencies { } implementation("com.github.curious-odd-man", "rgxgen", properties("rgxgenVersion")) implementation("org.eclipse.mylyn.github", "org.eclipse.egit.github.core", properties("githubCore")) + implementation("io.viascom.nanoid", "nanoid", properties("nanoidVersion")) scrambler("org.jetbrains.kotlin:kotlin-reflect") testImplementation("org.assertj", "assertj-swing-junit", properties("assertjSwingVersion")) diff --git a/gradle.properties b/gradle.properties index 0e1c33c8ff..a3979a55e4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -37,6 +37,7 @@ githubCore=2.1.5 kotestVersion=5.9.1 rgxgenVersion=3.1 uuidGeneratorVersion=5.1.0 +nanoidVersion=1.0.1 # Gradle org.gradle.caching=true diff --git a/src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdScheme.kt b/src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdScheme.kt new file mode 100644 index 0000000000..0c5b734170 --- /dev/null +++ b/src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdScheme.kt @@ -0,0 +1,80 @@ +package com.fwdekker.randomness.nanoid + +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.ui.ValidatorDsl.Companion.validators +import com.intellij.ui.JBColor +import com.intellij.util.xmlb.annotations.OptionTag +import io.viascom.nanoid.NanoId +import java.awt.Color + +/** + * Contains settings for generating Nano IDs. + * + * @property size The length of the generated Nano ID. + * @property alphabet The alphabet to use when generating the Nano ID. + * @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 NanoIdScheme( + var size: Int = DEFAULT_SIZE, + var alphabet: String = DEFAULT_ALPHABET, + @OptionTag val affixDecorator: AffixDecorator = DEFAULT_AFFIX_DECORATOR, + @OptionTag val arrayDecorator: ArrayDecorator = DEFAULT_ARRAY_DECORATOR, +) : Scheme() { + override val name = Bundle("nanoid.title") + override val typeIcon get() = BASE_ICON + override val decorators get() = listOf(affixDecorator, arrayDecorator) + override val validators = validators { + of(::size) + .check({ it >= MIN_SIZE }, { Bundle("nanoid.error.size_too_low", MIN_SIZE) }) + of(::alphabet) + .check({ it.isNotEmpty() }, { Bundle("nanoid.error.alphabet_empty") }) + include(::affixDecorator) + include(::arrayDecorator) + } + + override fun generateUndecoratedStrings(count: Int): List = + List(count) { NanoId.generate(size, alphabet) } + + override fun deepCopy(retainUuid: Boolean) = + copy( + affixDecorator = affixDecorator.deepCopy(retainUuid), + arrayDecorator = arrayDecorator.deepCopy(retainUuid), + ).deepCopyTransient(retainUuid) + + /** + * Holds constants. + */ + companion object { + /** The base icon for Nano IDs. */ + val BASE_ICON + get() = TypeIcon( + Icons.SCHEME, + "id", + listOf(JBColor(Color(120, 200, 120, 154), Color(120, 200, 120, 154))) + ) + + /** 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 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/nanoid/NanoIdSchemeEditor.kt b/src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeEditor.kt new file mode 100644 index 0000000000..c57f0e11a1 --- /dev/null +++ b/src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeEditor.kt @@ -0,0 +1,55 @@ +package com.fwdekker.randomness.nanoid + +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.nanoid.NanoIdScheme.Companion.DEFAULT_SIZE +import com.fwdekker.randomness.nanoid.NanoIdScheme.Companion.PRESET_AFFIX_DECORATOR_DESCRIPTORS +import com.fwdekker.randomness.ui.JIntSpinner +import com.fwdekker.randomness.ui.UIConstants +import com.fwdekker.randomness.ui.bindIntValue +import com.fwdekker.randomness.ui.withFixedWidth +import com.fwdekker.randomness.ui.withName +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.bindText +import com.intellij.ui.dsl.builder.panel + +/** + * Component for editing a [NanoIdScheme]. + * + * @param scheme the scheme to edit + */ +class NanoIdSchemeEditor(scheme: NanoIdScheme = NanoIdScheme()) : SchemeEditor(scheme) { + override val rootComponent = panel { + group(Bundle("nanoid.ui.value.header")) { + row(Bundle("nanoid.ui.value.size_option")) { + cell(JIntSpinner(DEFAULT_SIZE, NanoIdScheme.MIN_SIZE, Int.MAX_VALUE)) + .withFixedWidth(UIConstants.SIZE_SMALL) + .withName("size") + .bindIntValue(scheme::size) + .bindValidation(scheme::size) + } + row(Bundle("nanoid.ui.value.alphabet_option")) { + textField() + .withName("alphabet") + .bindText(scheme::alphabet) + .bindValidation(scheme::alphabet) + } + 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() + } +} diff --git a/src/main/kotlin/com/fwdekker/randomness/template/TemplateJTree.kt b/src/main/kotlin/com/fwdekker/randomness/template/TemplateJTree.kt index 1ece19e1d4..71823770ed 100644 --- a/src/main/kotlin/com/fwdekker/randomness/template/TemplateJTree.kt +++ b/src/main/kotlin/com/fwdekker/randomness/template/TemplateJTree.kt @@ -6,6 +6,7 @@ import com.fwdekker.randomness.Scheme import com.fwdekker.randomness.datetime.DateTimeScheme import com.fwdekker.randomness.decimal.DecimalScheme import com.fwdekker.randomness.integer.IntegerScheme +import com.fwdekker.randomness.nanoid.NanoIdScheme import com.fwdekker.randomness.string.StringScheme import com.fwdekker.randomness.uuid.UuidScheme import com.fwdekker.randomness.word.WordScheme @@ -711,6 +712,7 @@ class TemplateJTree( StringScheme(), WordScheme(), UuidScheme(), + NanoIdScheme(), 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..ce96c496df 100644 --- a/src/main/kotlin/com/fwdekker/randomness/template/TemplateList.kt +++ b/src/main/kotlin/com/fwdekker/randomness/template/TemplateList.kt @@ -10,6 +10,7 @@ import com.fwdekker.randomness.array.ArrayDecorator import com.fwdekker.randomness.datetime.DateTimeScheme import com.fwdekker.randomness.decimal.DecimalScheme import com.fwdekker.randomness.integer.IntegerScheme +import com.fwdekker.randomness.nanoid.NanoIdScheme import com.fwdekker.randomness.string.StringScheme import com.fwdekker.randomness.ui.ValidatorDsl.Companion.validators import com.fwdekker.randomness.uuid.UuidScheme @@ -134,6 +135,7 @@ data class TemplateList( ) ), Template("UUID", mutableListOf(UuidScheme())), + Template("Nano ID", mutableListOf(NanoIdScheme())), 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..0727dbb690 100644 --- a/src/main/kotlin/com/fwdekker/randomness/template/TemplateListEditor.kt +++ b/src/main/kotlin/com/fwdekker/randomness/template/TemplateListEditor.kt @@ -10,6 +10,8 @@ import com.fwdekker.randomness.decimal.DecimalScheme import com.fwdekker.randomness.decimal.DecimalSchemeEditor import com.fwdekker.randomness.integer.IntegerScheme import com.fwdekker.randomness.integer.IntegerSchemeEditor +import com.fwdekker.randomness.nanoid.NanoIdScheme +import com.fwdekker.randomness.nanoid.NanoIdSchemeEditor import com.fwdekker.randomness.setAll import com.fwdekker.randomness.string.StringScheme import com.fwdekker.randomness.string.StringSchemeEditor @@ -154,6 +156,7 @@ class TemplateListEditor( is DecimalScheme -> DecimalSchemeEditor(scheme) is StringScheme -> StringSchemeEditor(scheme) is UuidScheme -> UuidSchemeEditor(scheme) + is NanoIdScheme -> NanoIdSchemeEditor(scheme) is WordScheme -> WordSchemeEditor(scheme) is DateTimeScheme -> DateTimeSchemeEditor(scheme) is TemplateReference -> TemplateReferenceEditor(scheme) diff --git a/src/main/resources/randomness.properties b/src/main/resources/randomness.properties index 4832db0460..2d1d578e2c 100644 --- a/src/main/resources/randomness.properties +++ b/src/main/resources/randomness.properties @@ -202,6 +202,12 @@ 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.title=Nano ID +nanoid.ui.value.header=Value +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. 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/nanoid/NanoIdSchemeEditorTest.kt b/src/test/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeEditorTest.kt new file mode 100644 index 0000000000..fe2e0daa75 --- /dev/null +++ b/src/test/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeEditorTest.kt @@ -0,0 +1,80 @@ +package com.fwdekker.randomness.nanoid + +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.prop +import com.fwdekker.randomness.testhelpers.runEdt +import com.fwdekker.randomness.testhelpers.textProp +import com.fwdekker.randomness.testhelpers.useBareIdeaFixture +import com.fwdekker.randomness.testhelpers.useEdtViolationDetection +import com.fwdekker.randomness.testhelpers.valueProp +import io.kotest.core.spec.style.FunSpec +import io.kotest.data.row +import org.assertj.swing.fixture.Containers.showInFrame +import org.assertj.swing.fixture.FrameFixture + +/** + * Unit tests for [NanoIdSchemeEditor]. + */ +object NanoIdSchemeEditorTest : FunSpec({ + tags(Tags.EDITOR) + + lateinit var frame: FrameFixture + + lateinit var scheme: NanoIdScheme + lateinit var editor: NanoIdSchemeEditor + + useEdtViolationDetection() + useBareIdeaFixture() + + beforeNonContainer { + scheme = NanoIdScheme() + editor = runEdt { NanoIdSchemeEditor(scheme) } + frame = showInFrame(editor.rootComponent) + } + + afterNonContainer { + frame.cleanUp() + } + + include(editorApplyTests { editor }) + + include( + editorFieldsTests( + { editor }, + mapOf( + "size" to { + row( + frame.spinner("size").valueProp(), + editor.scheme::size.prop(), + 37, + ) + }, + "alphabet" to { + row( + frame.textBox("alphabet").textProp(), + editor.scheme::alphabet.prop(), + "abc123", + ) + }, + "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/nanoid/NanoIdSchemeTest.kt b/src/test/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeTest.kt new file mode 100644 index 0000000000..9c42e01b30 --- /dev/null +++ b/src/test/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeTest.kt @@ -0,0 +1,77 @@ +package com.fwdekker.randomness.nanoid + +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.matchers.collections.shouldBeUnique +import io.kotest.matchers.shouldBe + +/** + * Unit tests for [NanoIdScheme]. + */ +object NanoIdSchemeTest : FunSpec({ + tags(Tags.PLAIN, Tags.SCHEME) + + context("generateStrings") { + test("generates IDs of configured length") { + val scheme = NanoIdScheme(size = 13) + val id = scheme.generateStrings()[0] + id.length shouldBe 13 + } + + test("generated characters are from configured alphabet") { + val scheme = NanoIdScheme(size = 100, alphabet = "abc") + val id = scheme.generateStrings()[0] + id.all { it in scheme.alphabet } shouldBe true + } + + test("generates the requested count") { + val scheme = NanoIdScheme(size = 8) + val values = scheme.generateStrings(10) + values.size shouldBe 10 + // Most likely unique; not strictly required but a good smoke test + values.shouldBeUnique() + } + + test("applies decorators in order affix, array") { + val scheme = NanoIdScheme( + size = 5, + affixDecorator = AffixDecorator(enabled = true, descriptor = "#@"), + arrayDecorator = ArrayDecorator(enabled = true, minCount = 3, maxCount = 3), + ) + val out = scheme.generateStrings()[0] + // Expect the affix applied to each element produced by the array decorator -> 3 prefixes + out.count { it == '#' } shouldBe 3 + } + } + + context("doValidate") { + test("succeeds for default state") { + NanoIdScheme() shouldValidateAsBundle null + } + + test("fails for too small size") { + NanoIdScheme(size = 0) shouldValidateAsBundle "nanoid.error.size_too_low" + } + + test("fails for empty alphabet") { + NanoIdScheme(alphabet = "") shouldValidateAsBundle "nanoid.error.alphabet_empty" + } + + test("fails if affix decorator is invalid") { + NanoIdScheme(affixDecorator = AffixDecorator(enabled = true, descriptor = "\\")) shouldValidateAsBundle "" + } + + test("fails if array decorator is invalid") { + NanoIdScheme(arrayDecorator = ArrayDecorator(enabled = true, minCount = -1)) shouldValidateAsBundle "" + } + } + + include(stateDeepCopyTestFactory { NanoIdScheme() }) + + include(stateSerializationTestFactory { NanoIdScheme() }) +}) From 06be91f710d7594154e14c520a580aa211bbcb4b Mon Sep 17 00:00:00 2001 From: itsmefox Date: Fri, 17 Oct 2025 15:40:05 +0200 Subject: [PATCH 2/4] Add support for Nano ID --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) 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)) From 88751965420a2a7663b2aee7d7d561b6c0bccd65 Mon Sep 17 00:00:00 2001 From: itsmefox Date: Fri, 17 Oct 2025 16:17:05 +0200 Subject: [PATCH 3/4] Update the color to match the "id" color --- .../kotlin/com/fwdekker/randomness/nanoid/NanoIdScheme.kt | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdScheme.kt b/src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdScheme.kt index 0c5b734170..6ec82f6ace 100644 --- a/src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdScheme.kt +++ b/src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdScheme.kt @@ -53,11 +53,8 @@ data class NanoIdScheme( companion object { /** The base icon for Nano IDs. */ val BASE_ICON - get() = TypeIcon( - Icons.SCHEME, - "id", - listOf(JBColor(Color(120, 200, 120, 154), Color(120, 200, 120, 154))) - ) + get() = TypeIcon(Icons.SCHEME, "id", listOf(JBColor(Color(185, 155, 248, 154), Color(185, 155, 248, 154)))) + /** The minimum allowed value of [size]. */ const val MIN_SIZE = 1 From 40412d6436a4fc5c90747e3b47dba1748e6cbdc7 Mon Sep 17 00:00:00 2001 From: itsmefox Date: Sun, 7 Dec 2025 00:30:19 +0100 Subject: [PATCH 4/4] Change from UUID and Nano ID implementations to generic Uid (work in progress) --- gradle/libs.versions.toml | 4 +- .../com/fwdekker/randomness/Settings.kt | 92 +++++++++ .../randomness/nanoid/NanoIdScheme.kt | 77 ------- .../randomness/nanoid/NanoIdSchemeEditor.kt | 55 ----- .../fwdekker/randomness/template/Template.kt | 4 +- .../randomness/template/TemplateJTree.kt | 6 +- .../randomness/template/TemplateList.kt | 8 +- .../randomness/template/TemplateListEditor.kt | 9 +- .../com/fwdekker/randomness/uid/IdType.kt | 57 +++++ .../fwdekker/randomness/uid/NanoIdConfig.kt | 55 +++++ .../com/fwdekker/randomness/uid/UidScheme.kt | 132 ++++++++++++ .../randomness/uid/UidSchemeEditor.kt | 194 ++++++++++++++++++ .../{uuid/UuidScheme.kt => uid/UuidConfig.kt} | 101 ++------- .../randomness/uuid/UuidSchemeEditor.kt | 137 ------------- src/main/resources/randomness.properties | 9 +- .../com/fwdekker/randomness/SettingsTest.kt | 1 + .../com/fwdekker/randomness/XmlHelpersTest.kt | 20 +- .../nanoid/NanoIdSchemeEditorTest.kt | 80 -------- .../randomness/nanoid/NanoIdSchemeTest.kt | 77 ------- .../template/TemplateListEditorTest.kt | 4 +- .../randomness/testhelpers/StateReflection.kt | 5 + .../fwdekker/randomness/uid/UidSchemeTest.kt | 181 ++++++++++++++++ .../randomness/uuid/UuidSchemeEditorTest.kt | 136 ------------ .../randomness/uuid/UuidSchemeTest.kt | 182 ---------------- .../settings-upgrades/v3.4.2-v3.5.0-after.xml | 49 +++++ .../v3.4.2-v3.5.0-before.xml | 25 +++ 26 files changed, 843 insertions(+), 857 deletions(-) delete mode 100644 src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdScheme.kt delete mode 100644 src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeEditor.kt create mode 100644 src/main/kotlin/com/fwdekker/randomness/uid/IdType.kt create mode 100644 src/main/kotlin/com/fwdekker/randomness/uid/NanoIdConfig.kt create mode 100644 src/main/kotlin/com/fwdekker/randomness/uid/UidScheme.kt create mode 100644 src/main/kotlin/com/fwdekker/randomness/uid/UidSchemeEditor.kt rename src/main/kotlin/com/fwdekker/randomness/{uuid/UuidScheme.kt => uid/UuidConfig.kt} (63%) delete mode 100644 src/main/kotlin/com/fwdekker/randomness/uuid/UuidSchemeEditor.kt delete mode 100644 src/test/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeEditorTest.kt delete mode 100644 src/test/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeTest.kt create mode 100644 src/test/kotlin/com/fwdekker/randomness/uid/UidSchemeTest.kt delete mode 100644 src/test/kotlin/com/fwdekker/randomness/uuid/UuidSchemeEditorTest.kt delete mode 100644 src/test/kotlin/com/fwdekker/randomness/uuid/UuidSchemeTest.kt create mode 100644 src/test/resources/settings-upgrades/v3.4.2-v3.5.0-after.xml create mode 100644 src/test/resources/settings-upgrades/v3.4.2-v3.5.0-before.xml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 24bc9b67f6..0a96fd528d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,8 +23,8 @@ github = "2.1.5" # https://mvnrepository.com/artifact/org.eclipse.mylyn.github/ kotest = "5.9.1" # https://mvnrepository.com/artifact/io.kotest/kotest-assertions-core-jvm 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/io.viascom.nanoid/nanoid -nanoid = "1.0.1" # +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" } 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/nanoid/NanoIdScheme.kt b/src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdScheme.kt deleted file mode 100644 index 6ec82f6ace..0000000000 --- a/src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdScheme.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.fwdekker.randomness.nanoid - -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.ui.ValidatorDsl.Companion.validators -import com.intellij.ui.JBColor -import com.intellij.util.xmlb.annotations.OptionTag -import io.viascom.nanoid.NanoId -import java.awt.Color - -/** - * Contains settings for generating Nano IDs. - * - * @property size The length of the generated Nano ID. - * @property alphabet The alphabet to use when generating the Nano ID. - * @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 NanoIdScheme( - var size: Int = DEFAULT_SIZE, - var alphabet: String = DEFAULT_ALPHABET, - @OptionTag val affixDecorator: AffixDecorator = DEFAULT_AFFIX_DECORATOR, - @OptionTag val arrayDecorator: ArrayDecorator = DEFAULT_ARRAY_DECORATOR, -) : Scheme() { - override val name = Bundle("nanoid.title") - override val typeIcon get() = BASE_ICON - override val decorators get() = listOf(affixDecorator, arrayDecorator) - override val validators = validators { - of(::size) - .check({ it >= MIN_SIZE }, { Bundle("nanoid.error.size_too_low", MIN_SIZE) }) - of(::alphabet) - .check({ it.isNotEmpty() }, { Bundle("nanoid.error.alphabet_empty") }) - include(::affixDecorator) - include(::arrayDecorator) - } - - override fun generateUndecoratedStrings(count: Int): List = - List(count) { NanoId.generate(size, alphabet) } - - override fun deepCopy(retainUuid: Boolean) = - copy( - affixDecorator = affixDecorator.deepCopy(retainUuid), - arrayDecorator = arrayDecorator.deepCopy(retainUuid), - ).deepCopyTransient(retainUuid) - - /** - * Holds constants. - */ - companion object { - /** The base icon for Nano IDs. */ - val BASE_ICON - get() = TypeIcon(Icons.SCHEME, "id", listOf(JBColor(Color(185, 155, 248, 154), Color(185, 155, 248, 154)))) - - - /** 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 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/nanoid/NanoIdSchemeEditor.kt b/src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeEditor.kt deleted file mode 100644 index c57f0e11a1..0000000000 --- a/src/main/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeEditor.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.fwdekker.randomness.nanoid - -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.nanoid.NanoIdScheme.Companion.DEFAULT_SIZE -import com.fwdekker.randomness.nanoid.NanoIdScheme.Companion.PRESET_AFFIX_DECORATOR_DESCRIPTORS -import com.fwdekker.randomness.ui.JIntSpinner -import com.fwdekker.randomness.ui.UIConstants -import com.fwdekker.randomness.ui.bindIntValue -import com.fwdekker.randomness.ui.withFixedWidth -import com.fwdekker.randomness.ui.withName -import com.intellij.ui.dsl.builder.AlignX -import com.intellij.ui.dsl.builder.bindText -import com.intellij.ui.dsl.builder.panel - -/** - * Component for editing a [NanoIdScheme]. - * - * @param scheme the scheme to edit - */ -class NanoIdSchemeEditor(scheme: NanoIdScheme = NanoIdScheme()) : SchemeEditor(scheme) { - override val rootComponent = panel { - group(Bundle("nanoid.ui.value.header")) { - row(Bundle("nanoid.ui.value.size_option")) { - cell(JIntSpinner(DEFAULT_SIZE, NanoIdScheme.MIN_SIZE, Int.MAX_VALUE)) - .withFixedWidth(UIConstants.SIZE_SMALL) - .withName("size") - .bindIntValue(scheme::size) - .bindValidation(scheme::size) - } - row(Bundle("nanoid.ui.value.alphabet_option")) { - textField() - .withName("alphabet") - .bindText(scheme::alphabet) - .bindValidation(scheme::alphabet) - } - 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() - } -} 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 71823770ed..d5e241616d 100644 --- a/src/main/kotlin/com/fwdekker/randomness/template/TemplateJTree.kt +++ b/src/main/kotlin/com/fwdekker/randomness/template/TemplateJTree.kt @@ -6,9 +6,8 @@ import com.fwdekker.randomness.Scheme import com.fwdekker.randomness.datetime.DateTimeScheme import com.fwdekker.randomness.decimal.DecimalScheme import com.fwdekker.randomness.integer.IntegerScheme -import com.fwdekker.randomness.nanoid.NanoIdScheme 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 @@ -711,8 +710,7 @@ class TemplateJTree( DecimalScheme(), StringScheme(), WordScheme(), - UuidScheme(), - NanoIdScheme(), + 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 ce96c496df..515ec90d83 100644 --- a/src/main/kotlin/com/fwdekker/randomness/template/TemplateList.kt +++ b/src/main/kotlin/com/fwdekker/randomness/template/TemplateList.kt @@ -10,10 +10,10 @@ import com.fwdekker.randomness.array.ArrayDecorator import com.fwdekker.randomness.datetime.DateTimeScheme import com.fwdekker.randomness.decimal.DecimalScheme import com.fwdekker.randomness.integer.IntegerScheme -import com.fwdekker.randomness.nanoid.NanoIdScheme 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 @@ -134,8 +134,8 @@ data class TemplateList( ) ) ), - Template("UUID", mutableListOf(UuidScheme())), - Template("Nano ID", mutableListOf(NanoIdScheme())), + 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 0727dbb690..3ab3a4bb51 100644 --- a/src/main/kotlin/com/fwdekker/randomness/template/TemplateListEditor.kt +++ b/src/main/kotlin/com/fwdekker/randomness/template/TemplateListEditor.kt @@ -10,8 +10,6 @@ import com.fwdekker.randomness.decimal.DecimalScheme import com.fwdekker.randomness.decimal.DecimalSchemeEditor import com.fwdekker.randomness.integer.IntegerScheme import com.fwdekker.randomness.integer.IntegerSchemeEditor -import com.fwdekker.randomness.nanoid.NanoIdScheme -import com.fwdekker.randomness.nanoid.NanoIdSchemeEditor import com.fwdekker.randomness.setAll import com.fwdekker.randomness.string.StringScheme import com.fwdekker.randomness.string.StringSchemeEditor @@ -20,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 @@ -155,8 +153,7 @@ class TemplateListEditor( is IntegerScheme -> IntegerSchemeEditor(scheme) is DecimalScheme -> DecimalSchemeEditor(scheme) is StringScheme -> StringSchemeEditor(scheme) - is UuidScheme -> UuidSchemeEditor(scheme) - is NanoIdScheme -> NanoIdSchemeEditor(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 8bd0a74d89..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,12 +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.title=Nano ID -nanoid.ui.value.header=Value 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/nanoid/NanoIdSchemeEditorTest.kt b/src/test/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeEditorTest.kt deleted file mode 100644 index fe2e0daa75..0000000000 --- a/src/test/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeEditorTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.fwdekker.randomness.nanoid - -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.prop -import com.fwdekker.randomness.testhelpers.runEdt -import com.fwdekker.randomness.testhelpers.textProp -import com.fwdekker.randomness.testhelpers.useBareIdeaFixture -import com.fwdekker.randomness.testhelpers.useEdtViolationDetection -import com.fwdekker.randomness.testhelpers.valueProp -import io.kotest.core.spec.style.FunSpec -import io.kotest.data.row -import org.assertj.swing.fixture.Containers.showInFrame -import org.assertj.swing.fixture.FrameFixture - -/** - * Unit tests for [NanoIdSchemeEditor]. - */ -object NanoIdSchemeEditorTest : FunSpec({ - tags(Tags.EDITOR) - - lateinit var frame: FrameFixture - - lateinit var scheme: NanoIdScheme - lateinit var editor: NanoIdSchemeEditor - - useEdtViolationDetection() - useBareIdeaFixture() - - beforeNonContainer { - scheme = NanoIdScheme() - editor = runEdt { NanoIdSchemeEditor(scheme) } - frame = showInFrame(editor.rootComponent) - } - - afterNonContainer { - frame.cleanUp() - } - - include(editorApplyTests { editor }) - - include( - editorFieldsTests( - { editor }, - mapOf( - "size" to { - row( - frame.spinner("size").valueProp(), - editor.scheme::size.prop(), - 37, - ) - }, - "alphabet" to { - row( - frame.textBox("alphabet").textProp(), - editor.scheme::alphabet.prop(), - "abc123", - ) - }, - "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/nanoid/NanoIdSchemeTest.kt b/src/test/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeTest.kt deleted file mode 100644 index 9c42e01b30..0000000000 --- a/src/test/kotlin/com/fwdekker/randomness/nanoid/NanoIdSchemeTest.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.fwdekker.randomness.nanoid - -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.matchers.collections.shouldBeUnique -import io.kotest.matchers.shouldBe - -/** - * Unit tests for [NanoIdScheme]. - */ -object NanoIdSchemeTest : FunSpec({ - tags(Tags.PLAIN, Tags.SCHEME) - - context("generateStrings") { - test("generates IDs of configured length") { - val scheme = NanoIdScheme(size = 13) - val id = scheme.generateStrings()[0] - id.length shouldBe 13 - } - - test("generated characters are from configured alphabet") { - val scheme = NanoIdScheme(size = 100, alphabet = "abc") - val id = scheme.generateStrings()[0] - id.all { it in scheme.alphabet } shouldBe true - } - - test("generates the requested count") { - val scheme = NanoIdScheme(size = 8) - val values = scheme.generateStrings(10) - values.size shouldBe 10 - // Most likely unique; not strictly required but a good smoke test - values.shouldBeUnique() - } - - test("applies decorators in order affix, array") { - val scheme = NanoIdScheme( - size = 5, - affixDecorator = AffixDecorator(enabled = true, descriptor = "#@"), - arrayDecorator = ArrayDecorator(enabled = true, minCount = 3, maxCount = 3), - ) - val out = scheme.generateStrings()[0] - // Expect the affix applied to each element produced by the array decorator -> 3 prefixes - out.count { it == '#' } shouldBe 3 - } - } - - context("doValidate") { - test("succeeds for default state") { - NanoIdScheme() shouldValidateAsBundle null - } - - test("fails for too small size") { - NanoIdScheme(size = 0) shouldValidateAsBundle "nanoid.error.size_too_low" - } - - test("fails for empty alphabet") { - NanoIdScheme(alphabet = "") shouldValidateAsBundle "nanoid.error.alphabet_empty" - } - - test("fails if affix decorator is invalid") { - NanoIdScheme(affixDecorator = AffixDecorator(enabled = true, descriptor = "\\")) shouldValidateAsBundle "" - } - - test("fails if array decorator is invalid") { - NanoIdScheme(arrayDecorator = ArrayDecorator(enabled = true, minCount = -1)) shouldValidateAsBundle "" - } - } - - include(stateDeepCopyTestFactory { NanoIdScheme() }) - - include(stateSerializationTestFactory { NanoIdScheme() }) -}) 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 @@ + + +