Browse Source

Refactor `FeatureConfig` to use `AcediaConfig`

pull/8/head
Anton Tarasenko 3 years ago
parent
commit
6c89f178f0
  1. 6
      sources/Features/Feature.uc
  2. 255
      sources/Features/FeatureConfig.uc
  3. 49
      sources/Features/Tests/MockFeature.uc
  4. 113
      sources/Features/Tests/TEST_FeatureConfig.uc
  5. 7
      sources/Manifest.uc

6
sources/Features/Feature.uc

@ -147,14 +147,16 @@ public final function ApplyConfig(Text newConfigName)
if (newConfigName == none) {
return;
}
newConfig = configClass.static.GetConfigInstance(newConfigName);
newConfig =
FeatureConfig(configClass.static.GetConfigInstance(newConfigName));
if (newConfig == none)
{
_.logger.Auto(errorBadConfigData).ArgClass(class);
// Fallback to "default" config
newConfigName = _.text.FromString(defaultConfigName);
configClass.static.NewConfig(newConfigName);
newConfig = configClass.static.GetConfigInstance(newConfigName);
newConfig =
FeatureConfig(configClass.static.GetConfigInstance(newConfigName));
}
else {
newConfigName = newConfigName.Copy();

255
sources/Features/FeatureConfig.uc

@ -2,26 +2,9 @@
* Acedia's `Feature`s store their configuration in separate classes
* derived from this one. They allow to provide `Feature`s with several config
* presets and, potentially, swap them on-the-fly.
* To create a new config object for a `Feature` use following template:
*
* ```unrealscript
* class <FEATURE_NAME> extends FeatureConfig
* perobjectconfig
* config(<FEATURE_CONFIG>);
*
* // ...
*
* defaultproperties
* {
* configName = "<FEATURE_CONFIG>"
* }
* ```
*
* For each `Feature` you need to define a new child class, along with
* implementing it's `FromData()`, `ToData()` and `DefaultIt()` methods.
* You will also need to implement `Feature`'s `SwapConfig()` method that take
* an instance of `FeatureConfig` as a parameter. Other than that you should
* avoid directly using objects of this class.
* Difference from regular `AcediaConfig` is that `FeatureConfig` can
* determine with what settings (if any) each feature should start
* (be auto-enabled).
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
@ -39,8 +22,8 @@
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class FeatureConfig extends AcediaObject
dependson(AssociativeArray)
class FeatureConfig extends AcediaConfig
dependson(LoggerAPI)
abstract;
// Name of the config object that was marked as "auto enabled".
@ -48,15 +31,6 @@ class FeatureConfig extends AcediaObject
// Only it's default value is ever used.
var private Text autoEnabledConfig;
// All config of a particular class only get loaded once per session
// (unless new one is created) and then accessed through this collection.
// Only it's default value is ever used.
var private AssociativeArray existingConfigs;
// Stores name of the config where settings are to be stored.
// Must correspond to value in `config(...)` modifier in class definition.
var protected string configName;
// Setting that tells Acedia whether or not to enable feature,
// corresponding to this config during initialization.
// Only one version of any specific class should have this flag set to
@ -67,59 +41,22 @@ var private config bool autoEnable;
var private LoggerAPI.Definition warningMultipleFeaturesAutoEnabled;
/* These methods must be overloaded to store and load all the config
* variables inside an `AssociativeArray` collection. How exactly to store
* them is up to each `Feature` to decide, as long as it allows conversion into
* JSON (see `JSONAPI.IsCompatible()` for details). Note, however, that boxes
* can value boxes and references should be considered interchangeable.
* For example, even if you always save `int` value as a `IntRef` in
* `ToData()` method, it might be stored as `IntBox` in `FromData()` call.
* And vice versa.
* NOTE: DO NOT use `P()`, `C()`, `F()` or `T()` methods for keys or
* values in collections you return. All keys and values will be automatically
* deallocated when necessary, so these methods for creating `Text` values are
* not suitable.
*/
protected function AssociativeArray ToData() { return none; }
protected function FromData(AssociativeArray source) {}
/**
* This method must be overloaded to setup default values for all config
* variables. You should use it instead of the `defaultproperties` block.
*/
protected function DefaultIt() {}
/**
* This loads all of the `FeatureConfig`'s settings objects into internal
* arrays. Must be called before any other methods.
*/
public static final function Initialize()
public static function Initialize()
{
local int i;
local Text nextName;
local array<Text> names;
local FeatureConfig nextConfig;
local array<string> names;
if (default.existingConfigs != none) {
return;
}
super.Initialize();
// Load every config, find the auto-enabled one
default.autoEnabledConfig = none;
default.existingConfigs = __().collections.EmptyAssociativeArray();
names = GetPerObjectNames( default.configName, string(default.class.name),
MaxInt);
names = AvailableConfigs();
for (i = 0; i < names.length; i += 1)
{
if (names[i] == "") {
continue;
}
nextName = __().text.FromString(names[i]);
nextConfig = new(none, nextName.ToPlainString()) default.class;
default.existingConfigs.SetItem(nextName.LowerCopy(), nextConfig);
if (nextConfig.autoEnable)
{
if (default.autoEnabledConfig == none)
{
default.autoEnabledConfig = nextName;
continue;
nextConfig = FeatureConfig(GetConfigInstance(names[i]));
if (nextConfig == none) continue;
if (nextConfig.autoEnable) continue;
if (default.autoEnabledConfig == none) {
default.autoEnabledConfig = names[i].Copy();
}
else
{
@ -129,8 +66,7 @@ public static final function Initialize()
.Arg(default.autoEnabledConfig.Copy());
}
}
nextName.FreeSelf();
}
__().memory.FreeMany(names);
}
/**
@ -163,25 +99,24 @@ public static function Text GetAutoEnabledConfig()
*/
public static function bool SetAutoEnabledConfig(Text autoEnabledConfigName)
{
local Iter I;
local int i;
local array<Text> names;
local bool wasAutoEnabled;
local bool enabledConfig;
local Text nextConfigName;
local bool enabledSomeConfig;
local FeatureConfig nextConfig;
if (default.existingConfigs == none) {
return false;
}
I = default.existingConfigs.Iterate();
for (I = default.existingConfigs.Iterate(); !I.HasFinished(); I.Next(true))
names = AvailableConfigs();
for (i = 0; i < names.length; i += 1)
{
nextConfigName = Text(I.GetKey());
nextConfig = FeatureConfig(I.Get());
nextConfig = FeatureConfig(GetConfigInstance(names[i]));
if (nextConfig == none) {
continue;
}
wasAutoEnabled = nextConfig.autoEnable;
if (nextConfigName.Compare(autoEnabledConfigName, SCASE_INSENSITIVE))
if (names[i].Compare(autoEnabledConfigName, SCASE_INSENSITIVE))
{
default.autoEnabledConfig = autoEnabledConfigName.LowerCopy();
default.autoEnabledConfig = autoEnabledConfigName.Copy();
nextConfig.autoEnable = true;
enabledConfig = true;
enabledSomeConfig = true;
}
else {
nextConfig.autoEnable = false;
@ -190,138 +125,8 @@ public static function bool SetAutoEnabledConfig(Text autoEnabledConfigName)
nextConfig.SaveConfig();
}
}
return enabledConfig;
}
/**
* Returns array containing names of all available config objects.
*
* @return Array with names of all available config objects.
*/
public static function array<Text> AvailableConfigs()
{
local array<Text> emptyResult;
if (default.existingConfigs != none) {
return default.existingConfigs.CopyTextKeys();
}
return emptyResult;
}
/**
* Returns `FeatureConfig` of caller class with name `name`.
*
* @param name Name of the config object, whos settings data is to
* be loaded. Case-insensitive.
* @return `FeatureConfig` of caller class with name `name`.
*/
public final static function FeatureConfig GetConfigInstance(Text name)
{
local FeatureConfig requiredConfig;
if (default.existingConfigs == none) {
return none;
}
if (name != none) {
name = name.LowerCopy();
}
requiredConfig = FeatureConfig(default.existingConfigs.GetItem(name));
__().memory.Free(name);
return requiredConfig;
}
/**
* Loads Acedia's representation of settings data of a particular config
* object, given by the `name`.
*
* @param name Name of the config object, whos settings data is to
* be loaded.
* @return Settings data of a particular config object, given by the `name`.
* Expected to be in format that allows for JSON serialization
* (see `JSONAPI.IsCompatible()` for details).
* For correctly implemented config objects should only return `none` if
* their class was not yet initialized (see `self.Initialize()` method).
*/
public final static function AssociativeArray LoadData(Text name)
{
local AssociativeArray result;
local FeatureConfig requiredConfig;
requiredConfig = GetConfigInstance(name);
if (requiredConfig != none) {
result = requiredConfig.ToData();
}
return result;
}
/**
* Saves Acedia's representation of settings data (`data`) for a particular
* config object, given by the `name`.
*
* @param name Name of the config object, whos settings data is to
* be modified.
* @param data New data for config variables. Expected to be in format that
* allows for JSON deserialization (see `JSONAPI.IsCompatible()` for
* details).
*/
public final static function SaveData(Text name, AssociativeArray data)
{
local FeatureConfig requiredConfig;
if (name != none) {
name = name.LowerCopy();
}
if (default.existingConfigs != none) {
requiredConfig = FeatureConfig(default.existingConfigs.GetItem(name));
}
if (requiredConfig != none)
{
requiredConfig.FromData(data);
requiredConfig.SaveConfig();
}
__().memory.Free(name);
}
/**
* Creates a brand new config object with a given name.
*
* Fails if config object with that name already exists.
* Names are case-insensitive.
*
* @param name Name of the new config object.
* @return `true` iff new config object was created.
*/
public final static function bool NewConfig(Text name)
{
local FeatureConfig oldConfig, newConfig;
if (name == none) return false;
if (default.existingConfigs == none) return false;
oldConfig = FeatureConfig(default.existingConfigs.GetItem(name));
if (oldConfig != none) return false;
newConfig = new(none, name.ToPlainString()) default.class;
newConfig.DefaultIt();
newConfig.SaveConfig();
default.existingConfigs.SetItem(name.LowerCopy(), newConfig);
return true;
}
/**
* Deletes config object with a given name.
* Names are case-insensitive.
*
* If given config object exists, this method cannot fail.
*
* @param name Name of the config object to delete.
*/
public final static function DeleteConfig(Text name)
{
local AssociativeArray.Entry entry;
if (default.existingConfigs == none) {
return;
}
entry = default.existingConfigs.TakeEntry(name);
if (entry.value != none) {
entry.value.ClearConfig();
}
__().memory.Free(entry.value);
__().memory.Free(entry.key);
__().memory.FreeMany(names);
return enabledSomeConfig;
}
defaultproperties

49
sources/Features/Tests/MockFeature.uc

@ -1,49 +0,0 @@
/**
* Mock object for testing config functionality of Acedia's `Feature`s.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class MockFeature extends FeatureConfig
perobjectconfig
config(AcediaMockFeature);
var public config int value;
protected function AssociativeArray ToData()
{
local AssociativeArray data;
data = __().collections.EmptyAssociativeArray();
data.SetInt(P("value").Copy(), value, true);
return data;
}
protected function FromData(AssociativeArray source)
{
if (source != none) {
value = source.GetIntBy(P("/value"));
}
}
protected function DefaultIt()
{
value = 13;
}
defaultproperties
{
configName = "AcediaMockFeature"
}

113
sources/Features/Tests/TEST_FeatureConfig.uc

@ -1,113 +0,0 @@
/**
* Set of tests for `FeatureConfig` class.
* Copyright 2021 Anton Tarasenko
*------------------------------------------------------------------------------
* This file is part of Acedia.
*
* Acedia is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, version 3 of the License, or
* (at your option) any later version.
*
* Acedia is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Acedia. If not, see <https://www.gnu.org/licenses/>.
*/
class TEST_FeatureConfig extends TestCase
abstract;
protected static function TESTS()
{
class'MockFeature'.static.Initialize();
Context("Testing `FeatureConfig` functionality.");
TEST_AvailableConfigs();
TEST_DataGetSet();
TEST_DataNew();
}
protected static function TEST_AvailableConfigs()
{
local int i;
local bool foundConfig;
local array<Text> configNames;
configNames = class'MockFeature'.static.AvailableConfigs();
Issue("Incorrect amount of configs are loaded.");
TEST_ExpectTrue(configNames.length == 3);
Issue("Configs with incorrect names or values are loaded.");
for (i = 0; i < configNames.length; i += 1)
{
if (configNames[i].CompareToPlainString("default", SCASE_INSENSITIVE)) {
foundConfig = true;
}
}
TEST_ExpectTrue(foundConfig);
foundConfig = false;
for (i = 0; i < configNames.length; i += 1)
{
if (configNames[i].CompareToPlainString("other", SCASE_INSENSITIVE)) {
foundConfig = true;
}
}
TEST_ExpectTrue(foundConfig);
foundConfig = false;
for (i = 0; i < configNames.length; i += 1)
{
if (configNames[i].CompareToPlainString("another", SCASE_INSENSITIVE)) {
foundConfig = true;
}
}
TEST_ExpectTrue(foundConfig);
}
protected static function TEST_DataGetSet()
{
local AssociativeArray data, newData;
data = class'MockFeature'.static.LoadData(P("other"));
Issue("Wrong value is loaded from config.");
TEST_ExpectTrue(data.GetIntBy(P("/value")) == 11);
newData = __().collections.EmptyAssociativeArray();
newData.SetItem(P("value"), __().box.int(903));
class'MockFeature'.static.SaveData(P("other"), newData);
data = class'MockFeature'.static.LoadData(P("other"));
Issue("Wrong value is loaded from config after saving another value.");
TEST_ExpectTrue(data.GetIntBy(P("/value")) == 903);
Issue("`FeatureConfig` returns `AssociativeArray` reference that was"
@ "passed in `SaveData()` call instead of a new collection.");
TEST_ExpectTrue(data != newData);
// Restore configs
data.SetItem(P("value"), __().box.int(11));
class'MockFeature'.static.SaveData(P("other"), data);
}
protected static function TEST_DataNew()
{
local AssociativeArray data;
Issue("Creating new config with existing name succeeds.");
TEST_ExpectFalse(class'MockFeature'.static.NewConfig(P("another")));
data = class'MockFeature'.static.LoadData(P("another"));
TEST_ExpectTrue(data.GetIntBy(P("/value")) == -2956);
Issue("Cannot create new config.");
TEST_ExpectTrue(class'MockFeature'.static.NewConfig(P("new_one")));
Issue("New config does not have expected default value.");
data = class'MockFeature'.static.LoadData(P("new_one"));
TEST_ExpectTrue(data.GetIntBy(P("/value")) == 13);
// Restore configs, cannot properly test `DeleteConfig()`
class'MockFeature'.static.DeleteConfig(P("new_one"));
}
defaultproperties
{
caseName = "FeatureConfig"
caseGroup = "Features"
}

7
sources/Manifest.uc

@ -56,8 +56,7 @@ defaultproperties
testCases(20) = class'TEST_CommandDataBuilder'
testCases(21) = class'TEST_LogMessage'
testCases(22) = class'TEST_LocalDatabase'
testCases(23) = class'TEST_FeatureConfig'
testCases(24) = class'TEST_AcediaConfig'
testCases(25) = class'TEST_UTF8EncoderDecoder'
testCases(26) = class'TEST_AvariceStreamReader'
testCases(23) = class'TEST_AcediaConfig'
testCases(24) = class'TEST_UTF8EncoderDecoder'
testCases(25) = class'TEST_AvariceStreamReader'
}
Loading…
Cancel
Save