Skip to content

Commit

Permalink
Merge pull request #477 from FWDekker/backwards-compatibility
Browse files Browse the repository at this point in the history
Add framework for settings backwards compatibility
  • Loading branch information
FWDekker authored Oct 3, 2023
2 parents 0ccf143 + 6c25739 commit 823f872
Show file tree
Hide file tree
Showing 10 changed files with 420 additions and 31 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Check the plugin description for more details and animated usage examples.
* In addition to a list of standard separators, you can now also choose your own separator for all data types, including
for arrays.
* You can automatically pad (or truncate) integers to a specific length.
* Future backwards compatibility, ensuring that your settings can always be imported into future versions.

### Changed
* Randomness now uses templates to generate data.
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
group=com.fwdekker
# TODO: Also remove `beta` from `PersistentSettings' annotation`!
# Version number should also be updated in `com.fwdekker.randomness.PersistentSettings.Companion.CURRENT_VERSION`.
version=3.0.0-beta.3

# Compatibility
Expand Down
62 changes: 49 additions & 13 deletions src/main/kotlin/com/fwdekker/randomness/Settings.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
package com.fwdekker.randomness

import com.fwdekker.randomness.PersistentSettings.Companion.CURRENT_VERSION
import com.fwdekker.randomness.template.Template
import com.fwdekker.randomness.template.TemplateList
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.SettingsCategory
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.util.xmlb.XmlSerializer
import com.intellij.util.xmlb.annotations.Transient
import org.jdom.Element
import java.lang.module.ModuleDescriptor
import com.intellij.openapi.components.State as JBState


/**
* Contains references to various [State] objects.
*
* @property version The version of Randomness with which these settings were created.
* @property templateList The template list.
*/
data class Settings(var templateList: TemplateList = TemplateList()) : State() {
data class Settings(
var version: String = CURRENT_VERSION,
var templateList: TemplateList = TemplateList(),
) : State() {
/**
* @see TemplateList.templates
*/
Expand Down Expand Up @@ -43,45 +51,73 @@ data class Settings(var templateList: TemplateList = TemplateList()) : State() {
/**
* The persistent [Settings] instance.
*/
val DEFAULT: Settings by lazy { PersistentSettings.default.state }
val DEFAULT: Settings by lazy { service<PersistentSettings>().settings }
}
}

/**
* The actual user's actual stored actually-serialized settings (actually).
* The persistent [Settings] instance, stored as an [Element] to allow custom conversion for backwards compatibility.
*
* @see Settings.DEFAULT Preferred method of accessing the persistent settings instance.
*/
@JBState(
name = "Randomness",
storages = [
Storage("\$APP_CONFIG\$/randomness.xml", deprecated = true),
Storage("\$APP_CONFIG\$/randomness-beta.xml", exportable = true),
Storage("randomness.xml", deprecated = true),
Storage("randomness-beta.xml", exportable = true),
],
category = SettingsCategory.PLUGINS,
)
class PersistentSettings : PersistentStateComponent<Settings> {
private val settings = Settings()
class PersistentSettings : PersistentStateComponent<Element> {
/**
* The persistent settings instance.
*
* @see Settings.DEFAULT Preferred method of accessing the persistent settings instance.
*/
val settings = Settings()


/**
* Returns the template list.
* Returns the [settings] as an [Element].
*/
override fun getState() = settings
override fun getState(): Element = XmlSerializer.serialize(settings)

/**
* Copies [settings] into `this`.
* Deserializes [element] into a [Settings] instance, which is then copied into the [settings] instance.
*
* @see TemplateList.copyFrom
*/
override fun loadState(settings: Settings) = this.settings.copyFrom(settings)
override fun loadState(element: Element) =
settings.copyFrom(XmlSerializer.deserialize(upgrade(element), Settings::class.java))


/**
* Silently upgrades the format of the settings contained in [element] to the format of the latest version.
*/
private fun upgrade(element: Element): Element {
val elementVersion = element.getAttributeValueByName("version")?.let { ModuleDescriptor.Version.parse(it) }

when {
elementVersion == null -> Unit

// Placeholder to show how an upgrade might work. Remove this once an actual upgrade has been added.
elementVersion < ModuleDescriptor.Version.parse("0.0.0-placeholder") ->
element.getContentByPath("templateList", null, "templates", null)?.getElements()
?.forEachIndexed { idx, template -> template.setAttributeValueByName("name", "Template$idx") }
}

element.setAttributeValueByName("version", CURRENT_VERSION)
return element
}


/**
* Holds constants.
*/
companion object {
/**
* The persistent instance.
* The currently-running version of Randomness.
*/
val default: PersistentSettings get() = service()
const val CURRENT_VERSION: String = "3.0.0-beta.3"
}
}
49 changes: 49 additions & 0 deletions src/main/kotlin/com/fwdekker/randomness/XmlHelpers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.fwdekker.randomness

import org.jdom.Element


/**
* Returns a list of all [Element]s contained in this `Element`.
*/
fun Element.getElements(): List<Element> =
content().toList().filterIsInstance<Element>()

/**
* Returns the [Element] contained in this `Element` that has attribute `name="[name]"`, or `null` if no single such
* `Element` exists.
*/
fun Element.getContentByName(name: String): Element? =
getContent<Element> { (it as Element).getAttribute("name")?.value == name }.singleOrNull()

/**
* Returns the value of the `value` attribute of the single [Element] contained in this `Element` that has attribute
* `name="[name]"`, or `null` if no single such `Element` exists.
*/
fun Element.getAttributeValueByName(name: String): String? =
getContentByName(name)?.getAttribute("value")?.value

/**
* Sets the value of the `value` attribute of the single [Element] contained in this `Element` that has attribute
* `name="[name]"` to [value], or does nothing if no single such `Element` exists.
*/
fun Element.setAttributeValueByName(name: String, value: String) {
getContentByName(name)?.setAttribute("value", value)
}

/**
* Returns the single [Element] that is contained in this `Element`, or `null` if this `Element` does not contain
* exactly one `Element`.
*/
fun Element.getSingleContent(): Element? =
content.singleOrNull() as? Element

/**
* Traverses a path of [Element]s based on their [names] by monadically calling either [getContentByName] (if the name
* is not `null`) or [getSingleContent] (if the name is `null`).
*/
fun Element.getContentByPath(vararg names: String?): Element? =
names.fold(this as? Element) { acc, name ->
if (name == null) acc?.getSingleContent()
else acc?.getContentByName(name)
}
13 changes: 9 additions & 4 deletions src/test/kotlin/com/fwdekker/randomness/SettingsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ object SettingsTest : FunSpec({
"succeeds for default state" to
row(Settings(), null),
"fails if template list is invalid" to
row(Settings(TemplateList(mutableListOf(Template("Duplicate"), Template("Duplicate")))), ""),
row(
Settings(
templateList = TemplateList(mutableListOf(Template("Duplicate"), Template("Duplicate"))),
),
"",
),
)
) { (scheme, validation) ->
scheme shouldValidateAsBundle validation
Expand All @@ -31,7 +36,7 @@ object SettingsTest : FunSpec({

context("deepCopy") {
test("deep-copies the template list") {
val settings = Settings(TemplateList(mutableListOf(Template("old"))))
val settings = Settings(templateList = TemplateList(mutableListOf(Template("old"))))

val copy = settings.deepCopy()
copy.templates[0].name = "new"
Expand All @@ -49,8 +54,8 @@ object SettingsTest : FunSpec({
}

test("deep-copies the template list") {
val settings = Settings(TemplateList(mutableListOf(Template("old"))))
val other = Settings(TemplateList(mutableListOf(Template("new"))))
val settings = Settings(templateList = TemplateList(mutableListOf(Template("old"))))
val other = Settings(templateList = TemplateList(mutableListOf(Template("new"))))

settings.copyFrom(other)

Expand Down
Loading

0 comments on commit 823f872

Please sign in to comment.