feat(config): implement config lexicon

Signed-off-by: Maxence Lange <maxence@artificial-owl.com>
pull/44371/merge^2
Maxence Lange 1 month ago
parent 5b85562784
commit 40ca27599f

@ -216,6 +216,9 @@ return array(
'OCP\\Comments\\MessageTooLongException' => $baseDir . '/lib/public/Comments/MessageTooLongException.php',
'OCP\\Comments\\NotFoundException' => $baseDir . '/lib/public/Comments/NotFoundException.php',
'OCP\\Common\\Exception\\NotFoundException' => $baseDir . '/lib/public/Common/Exception/NotFoundException.php',
'OCP\\ConfigLexicon\\ConfigLexiconEntry' => $baseDir . '/lib/public/ConfigLexicon/ConfigLexiconEntry.php',
'OCP\\ConfigLexicon\\IConfigLexicon' => $baseDir . '/lib/public/ConfigLexicon/IConfigLexicon.php',
'OCP\\ConfigLexicon\\IConfigLexiconEntry' => $baseDir . '/lib/public/ConfigLexicon/IConfigLexiconEntry.php',
'OCP\\Config\\BeforePreferenceDeletedEvent' => $baseDir . '/lib/public/Config/BeforePreferenceDeletedEvent.php',
'OCP\\Config\\BeforePreferenceSetEvent' => $baseDir . '/lib/public/Config/BeforePreferenceSetEvent.php',
'OCP\\Console\\ConsoleEvent' => $baseDir . '/lib/public/Console/ConsoleEvent.php',

@ -249,6 +249,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Comments\\MessageTooLongException' => __DIR__ . '/../../..' . '/lib/public/Comments/MessageTooLongException.php',
'OCP\\Comments\\NotFoundException' => __DIR__ . '/../../..' . '/lib/public/Comments/NotFoundException.php',
'OCP\\Common\\Exception\\NotFoundException' => __DIR__ . '/../../..' . '/lib/public/Common/Exception/NotFoundException.php',
'OCP\\ConfigLexicon\\ConfigLexiconEntry' => __DIR__ . '/../../..' . '/lib/public/ConfigLexicon/ConfigLexiconEntry.php',
'OCP\\ConfigLexicon\\IConfigLexicon' => __DIR__ . '/../../..' . '/lib/public/ConfigLexicon/IConfigLexicon.php',
'OCP\\ConfigLexicon\\IConfigLexiconEntry' => __DIR__ . '/../../..' . '/lib/public/ConfigLexicon/IConfigLexiconEntry.php',
'OCP\\Config\\BeforePreferenceDeletedEvent' => __DIR__ . '/../../..' . '/lib/public/Config/BeforePreferenceDeletedEvent.php',
'OCP\\Config\\BeforePreferenceSetEvent' => __DIR__ . '/../../..' . '/lib/public/Config/BeforePreferenceSetEvent.php',
'OCP\\Console\\ConsoleEvent' => __DIR__ . '/../../..' . '/lib/public/Console/ConsoleEvent.php',

@ -38,6 +38,9 @@ namespace OC;
use InvalidArgumentException;
use JsonException;
use OC\AppFramework\Bootstrap\Coordinator;
use OCP\ConfigLexicon\ConfigLexiconEntry;
use OCP\ConfigLexicon\IConfigLexiconEntry;
use OCP\DB\Exception as DBException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Exceptions\AppConfigIncorrectTypeException;
@ -82,6 +85,8 @@ class AppConfig implements IAppConfig {
private array $valueTypes = []; // type for all config values
private bool $fastLoaded = false;
private bool $lazyLoaded = false;
/** @var array<array-key, array{entries: array<array-key, IConfigLexiconEntry>, strict: bool}> ['app_id' => ['strict' => bool, 'entries' => ['config_key' => IConfigLexiconEntry[]]] */
private array $configLexiconDetails = [];
/**
* $migrationCompleted is only needed to manage the previous structure
@ -457,6 +462,7 @@ class AppConfig implements IAppConfig {
int $type
): string {
$this->assertParams($app, $key, valueType: $type);
$this->compareRegisteredConfigValues($app, $key, $lazy, $type, $default);
$this->loadConfig($lazy);
/**
@ -748,6 +754,7 @@ class AppConfig implements IAppConfig {
int $type
): bool {
$this->assertParams($app, $key);
$this->compareRegisteredConfigValues($app, $key, $lazy, $type);
$this->loadConfig($lazy);
$sensitive = $this->isTyped(self::VALUE_SENSITIVE, $type);
@ -1567,4 +1574,74 @@ class AppConfig implements IAppConfig {
public function clearCachedConfig(): void {
$this->clearCache();
}
/**
* verify and compare current use of config values with defined lexicon
*
* @throws AppConfigUnknownKeyException
* @throws AppConfigTypeConflictException
*/
private function compareRegisteredConfigValues(
string $app,
string $key,
bool &$lazy,
int &$type,
string &$default = '',
): void {
$configDetails = $this->getConfigDetailsFromLexicon($app);
if (!array_key_exists($key, $configDetails['entries'])) {
if ($configDetails['strict'] === true) {
throw new AppConfigUnknownKeyException('The key ' . $app . '/' . $key . ' is not defined in the config lexicon');
}
return;
}
/** @var ConfigLexiconEntry $configValue */
$configValue = $configDetails['entries'][$key];
$type &= ~self::VALUE_SENSITIVE;
if ($configValue->getValueType() !== match($type) {
self::VALUE_STRING => IConfigLexiconEntry::TYPE_STRING,
self::VALUE_INT => IConfigLexiconEntry::TYPE_INT,
self::VALUE_FLOAT => IConfigLexiconEntry::TYPE_FLOAT,
self::VALUE_BOOL => IConfigLexiconEntry::TYPE_BOOL,
self::VALUE_ARRAY => IConfigLexiconEntry::TYPE_ARRAY,
}) {
throw new AppConfigTypeConflictException('The key ' . $app . '/' . $key . ' is typed incorrectly in relation to the config lexicon');
}
$lazy = $configValue->isLazy();
$default = $configValue->getDefault() ?? $default;
if ($configValue->isSensitive()) {
$type |= self::VALUE_SENSITIVE;
}
if ($configValue->isDeprecated()) {
$this->logger->notice('config value ' . $app . '/' . $key . ' is set as deprecated.');
}
}
/**
* extract details from registered $appId's config lexicon
*
* @param string $appId
*
* @return array{entries: array<array-key, IConfigLexiconEntry>, strict: bool}
*/
private function getConfigDetailsFromLexicon(string $appId): array {
if (!array_key_exists($appId, $this->configLexiconDetails)) {
$entries = [];
$bootstrapCoordinator = \OCP\Server::get(Coordinator::class);
$configLexicon = $bootstrapCoordinator->getRegistrationContext()?->getConfigLexicon($appId);
foreach ($configLexicon?->getAppConfigs() ?? [] as $configEntry) {
$entries[$configEntry->getKey()] = $configEntry;
}
$this->configLexiconDetails[$appId] = [
'entries' => $entries,
'strict' => $configLexicon?->isStrict() ?? false
];
}
return $this->configLexiconDetails[$appId];
}
}

@ -41,6 +41,7 @@ use OCP\Calendar\Resource\IBackend as IResourceBackend;
use OCP\Calendar\Room\IBackend as IRoomBackend;
use OCP\Capabilities\ICapability;
use OCP\Collaboration\Reference\IReferenceProvider;
use OCP\ConfigLexicon\IConfigLexicon;
use OCP\Dashboard\IManager;
use OCP\Dashboard\IWidget;
use OCP\EventDispatcher\IEventDispatcher;
@ -160,6 +161,9 @@ class RegistrationContext {
/** @var ServiceRegistration<IDeclarativeSettingsForm>[] */
private array $declarativeSettings = [];
/** @var array<array-key, string> */
private array $configLexiconClasses = [];
/** @var ServiceRegistration<ITeamResourceProvider>[] */
private array $teamResourceProviders = [];
@ -411,6 +415,13 @@ class RegistrationContext {
$declarativeSettingsClass
);
}
public function registerConfigLexicon(string $configLexiconClass): void {
$this->context->registerConfigLexicon(
$this->appId,
$configLexiconClass
);
}
};
}
@ -590,6 +601,13 @@ class RegistrationContext {
$this->declarativeSettings[] = new ServiceRegistration($appId, $declarativeSettingsClass);
}
/**
* @psalm-param class-string<IConfigLexicon> $configLexiconClass
*/
public function registerConfigLexicon(string $appId, string $configLexiconClass): void {
$this->configLexiconClasses[$appId] = $configLexiconClass;
}
/**
* @param App[] $apps
*/
@ -920,4 +938,20 @@ class RegistrationContext {
public function getDeclarativeSettings(): array {
return $this->declarativeSettings;
}
/**
* returns IConfigLexicon registered by the app.
* null if none registered.
*
* @param string $appId
*
* @return IConfigLexicon|null
*/
public function getConfigLexicon(string $appId): ?IConfigLexicon {
if (!array_key_exists($appId, $this->configLexiconClasses)) {
return null;
}
return \OCP\Server::get($this->configLexiconClasses[$appId]);
}
}

@ -410,4 +410,15 @@ interface IRegistrationContext {
* @since 29.0.0
*/
public function registerDeclarativeSettings(string $declarativeSettingsClass): void;
/**
* Register an implementation of \OCP\ConfigLexicon\IConfigLexicon that
* will handle the implementation of config lexicon
*
* @param string $configLexiconClass
* @psalm-param class-string<\OCP\ConfigLexicon\IConfigLexicon> $configLexiconClass
* @return void
* @since 30.0.0
*/
public function registerConfigLexicon(string $configLexiconClass): void;
}

@ -0,0 +1,197 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2024 Maxence Lange <maxence@artificial-owl.com>
*
* @author Maxence Lange <maxence@artificial-owl.com>
*
* @license AGPL-3.0 or later
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCP\ConfigLexicon;
/**
* Model that represent config values within an app config lexicon.
*
* @see IConfigLexicon
* @since 30.0.0
*/
class ConfigLexiconEntry implements IConfigLexiconEntry {
private string $definition = '';
private ?string $default = null;
/**
* @param string $key config key
* @param int $valueType type of config value ({@see self::TYPE_STRING} and others)
* @param string $definition optional description of config key available when using occ command
* @param bool $lazy set config value as lazy
* @param bool $sensitive set config value as sensitive
* @param bool $deprecated set config key as deprecated
* @since 30.0.0
*/
public function __construct(
private readonly string $key,
private readonly int $valueType,
string $definition = '',
private readonly bool $lazy = false,
private readonly bool $sensitive = false,
private readonly bool $deprecated = false
) {
/** @psalm-suppress UndefinedClass */
if (\OC::$CLI) { // only store definition if ran from CLI
$this->definition = $definition;
}
}
/**
* @inheritDoc
*
* @return string config key
* @since 30.0.0
*/
public function getKey(): string {
return $this->key;
}
/**
* @inheritDoc
*
* @return int
* @see self::TYPE_STRING and others
* @since 30.0.0
*/
public function getValueType(): int {
return $this->valueType;
}
/**
* @inheritDoc
*
* @param string $default
*
* @return self
* @since 30.0.0
*/
public function withDefaultString(string $default): self {
$this->default = $default;
return $this;
}
/**
* @inheritDoc
*
* @param int $default
*
* @return self
* @since 30.0.0
*/
public function withDefaultInt(int $default): self {
$this->default = (string) $default;
return $this;
}
/**
* @inheritDoc
*
* @param float $default
*
* @return self
* @since 30.0.0
*/
public function withDefaultFloat(float $default): self {
$this->default = (string) $default;
return $this;
}
/**
* @inheritDoc
*
* @param bool $default
*
* @return self
* @since 30.0.0
*/
public function withDefaultBool(bool $default): self {
$this->default = ($default) ? '1' : '0';
return $this;
}
/**
* @inheritDoc
*
* @param array $default
*
* @return self
* @since 30.0.0
*/
public function withDefaultArray(array $default): self {
$this->default = json_encode($default);
return $this;
}
/**
* @inheritDoc
*
* @return string|null NULL if no default is set
* @since 30.0.0
*/
public function getDefault(): ?string {
return $this->default;
}
/**
* @inheritDoc
*
* @return string
* @since 30.0.0
*/
public function getDefinition(): string {
return $this->definition;
}
/**
* @inheritDoc
*
* @see IAppConfig for details on lazy config values
* @return bool TRUE if config value is lazy
* @since 30.0.0
*/
public function isLazy(): bool {
return $this->lazy;
}
/**
* @inheritDoc
*
* @see IAppConfig for details on sensitive config values
* @return bool TRUE if config value is sensitive
* @since 30.0.0
*/
public function isSensitive(): bool {
return $this->sensitive;
}
/**
* @inheritDoc
*
* @return bool TRUE if config si deprecated
* @since 30.0.0
*/
public function isDeprecated(): bool {
return $this->deprecated;
}
}

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2024 Maxence Lange <maxence@artificial-owl.com>
*
* @author Maxence Lange <maxence@artificial-owl.com>
*
* @license AGPL-3.0 or later
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCP\ConfigLexicon;
/**
* This interface needs to be implemented if you want to define a config lexicon for your application
* The config lexicon is used to avoid conflicts and problems when storing/retrieving config values
*
* @since 30.0.0
*/
interface IConfigLexicon {
/**
* set your application config lexicon as strict or not.
* When set as strict, using a config key not set in the lexicon will throw an exception.
*
* @return bool
* @since 30.0.0
*/
public function isStrict(): bool;
/**
* define the list of entries of your application config lexicon, related to AppConfig.
*
* @return IConfigLexiconEntry[]
* @since 30.0.0
*/
public function getAppConfigs(): array;
/**
* define the list of entries of your application config lexicon, related to UserPreference.
*
* @return IConfigLexiconEntry[]
* @since 30.0.0
*/
public function getUserPreferences(): array;
}

@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2024 Maxence Lange <maxence@artificial-owl.com>
*
* @author Maxence Lange <maxence@artificial-owl.com>
*
* @license AGPL-3.0 or later
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCP\ConfigLexicon;
use OCP\IAppConfig;
/**
* Model that represent config values within an app config lexicon.
*
* @see IConfigLexicon
* @since 30.0.0
*/
interface IConfigLexiconEntry {
/** @since 30.0.0 */
public const TYPE_STRING = 1;
/** @since 30.0.0 */
public const TYPE_INT = 2;
/** @since 30.0.0 */
public const TYPE_FLOAT = 3;
/** @since 30.0.0 */
public const TYPE_BOOL = 4;
/** @since 30.0.0 */
public const TYPE_ARRAY = 5;
/**
* returns the config key.
*
* @return string config key
* @since 30.0.0
*/
public function getKey(): string;
/**
* returns the type of the config value.
*
* @return int
* @see self::TYPE_STRING and others
* @since 30.0.0
*/
public function getValueType(): int;
/**
* set default value (as string) for config value.
*
* @param string $default
*
* @return self
* @since 30.0.0
*/
public function withDefaultString(string $default): self;
/**
* set default value (as int) for config value.
*
* @param int $default
*
* @return self
* @since 30.0.0
*/
public function withDefaultInt(int $default): self;
/**
* set default value (as float) for config value.
*
* @param float $default
*
* @return self
* @since 30.0.0
*/
public function withDefaultFloat(float $default): self;
/**
* set default value (as bool) for config value.
*
* @param bool $default
*
* @return self
* @since 30.0.0
*/
public function withDefaultBool(bool $default): self;
/**
* set default value (as array) for config value.
*
* @param array $default
*
* @return self
* @since 30.0.0
*/
public function withDefaultArray(array $default): self;
/**
* returns the default value set for this config key.
* default value is returned as string or NULL if not set.
*
* @return string|null NULL if no default is set
* @since 30.0.0
*/
public function getDefault(): ?string;
/**
* returns the description for config key, only available when process is initiated from occ.
* returns empty string if not set or if process is not initiated from occ.
*
* @return string
* @since 30.0.0
*/
public function getDefinition(): string;
/**
* returns if config value is set as LAZY.
*
* @see IAppConfig for details on lazy config values
* @return bool TRUE if config value is lazy
* @since 30.0.0
*/
public function isLazy(): bool;
/**
* returns if config value is set as SENSITIVE.
*
* @see IAppConfig for details on sensitive config values
* @return bool TRUE if config value is sensitive
* @since 30.0.0
*/
public function isSensitive(): bool;
/**
* returns if config key is deprecated.
*
* @return bool TRUE if config si deprecated
* @since 30.0.0
*/
public function isDeprecated(): bool;
}

@ -25,6 +25,7 @@ namespace Test;
use InvalidArgumentException;
use OC\AppConfig;
use OC\AppFramework\Bootstrap\Coordinator;
use OCP\Exceptions\AppConfigTypeConflictException;
use OCP\Exceptions\AppConfigUnknownKeyException;
use OCP\IAppConfig;
@ -44,6 +45,8 @@ class AppConfigTest extends TestCase {
protected IDBConnection $connection;
private LoggerInterface $logger;
private ICrypto $crypto;
private Coordinator $coordinator;
private array $originalConfig;
/**
@ -104,6 +107,7 @@ class AppConfigTest extends TestCase {
$this->connection = \OCP\Server::get(IDBConnection::class);
$this->logger = \OCP\Server::get(LoggerInterface::class);
$this->crypto = \OCP\Server::get(ICrypto::class);
$this->coordinator = \OCP\Server::get(Coordinator::class);
// storing current config and emptying the data table
$sql = $this->connection->getQueryBuilder();
@ -194,6 +198,7 @@ class AppConfigTest extends TestCase {
$this->connection,
$this->logger,
$this->crypto,
$this->coordinator
);
$msg = ' generateAppConfig() failed to confirm cache status';

Loading…
Cancel
Save