Replace WriteMode with MetadataWriteOptions

This is a breaking API change, but the addition of any future options won't be.
This commit is contained in:
Oliver Hamlet
2026-01-03 13:26:52 +00:00
parent dae06e1f10
commit b7363b0a29
13 changed files with 540 additions and 180 deletions
+93 -2
View File
@@ -36,6 +36,97 @@
#include "loot/metadata/plugin_metadata.h"
namespace loot {
struct MetadataWriteOptionsImpl;
/** @brief Options to configure how metadata files are written. */
class MetadataWriteOptions {
public:
/**
* @brief Creates a new set of options, all initially set to `false`.
*/
LOOT_API MetadataWriteOptions();
LOOT_API MetadataWriteOptions(const MetadataWriteOptions&);
LOOT_API MetadataWriteOptions(MetadataWriteOptions&&) noexcept;
LOOT_API ~MetadataWriteOptions();
LOOT_API MetadataWriteOptions& operator=(const MetadataWriteOptions&);
LOOT_API MetadataWriteOptions& operator=(MetadataWriteOptions&&) noexcept;
/**
* @brief Sets the option to overwrite the output file if it already exists.
* @details If true and the file path already exists, its contents will be
* replaced.
*
* If false and the file path already exists, an error will be
* returned.
*
* This setting has no effect if the file path does not exist.
* @param truncate
* The value to set.
*/
LOOT_API void SetTruncate(bool truncate);
/**
* @brief Sets the option to write YAML anchors and aliases.
* @details If true then conditions, constraints, files, file details, plugin
* cleaning data details, messages and message contents that appear
* more than once in the metadata will be deduplicated by including
* a YAML anchor when writing the first occurrence of the value, and
* writing YAML aliases in place of further occurrences.
*
* If false, YAML anchors and aliases will not be used, so no
* deduplication will occur.
* @param writeAnchors
* The value to set.
*/
LOOT_API void SetWriteAnchors(bool writeAnchors);
/**
* @brief Sets the option to write YAML anchors in a `common` section.
* @details If `writeAnchors` is true and this is also true, the document's
* root-level map will start with a `common` key. Its value will be
* a list of all the values for which YAML anchors will be written,
* so that all YAML anchors will appear within that list.
*
* This setting has no effect if `writeAnchors` is false.
* @param writeCommonSection
* The value to set.
*/
LOOT_API void SetWriteCommonSection(bool writeCommonSection);
/**
* @brief Sets the option to write anchors for File values that only have a
* name.
* @details If `writeAnchors` is true and this is also true, then all
* repeated File metadata values will be deduplicated using YAML
* anchors and aliases.
*
* If this is false, then only File metadata that is serialised as
* a YAML object will be deduplicated (i.e. File values that only
* have a name will not be deduplicated).
*
* This setting has no effect if `writeAnchors` is false.
* @param anchorFileStrings
* The value to set.
*/
LOOT_API void SetAnchorFileStrings(bool anchorFileStrings);
/** Gets the current value of the truncate option. */
LOOT_API bool GetTruncate() const;
/** Gets the current value of the writeAnchors option. */
LOOT_API bool GetWriteAnchors() const;
/** Gets the current value of the writeCommonSection option. */
LOOT_API bool GetWriteCommonSection() const;
/** Gets the current value of the anchorFileStrings option. */
LOOT_API bool GetAnchorFileStrings() const;
private:
std::unique_ptr<MetadataWriteOptionsImpl> pimpl_;
};
/** @brief The interface provided by API's database handle. */
class DatabaseInterface {
public:
@@ -98,7 +189,7 @@ public:
* written. Otherwise, data will be written.
*/
virtual void WriteUserMetadata(const std::filesystem::path& outputFile,
const bool overwrite) const = 0;
const MetadataWriteOptions& options) const = 0;
/**
* @brief Writes a minimal metadata file that only contains plugins with
@@ -111,7 +202,7 @@ public:
* written. Otherwise, data will be written.
*/
virtual void WriteMinimalList(const std::filesystem::path& outputFile,
const bool overwrite) const = 0;
const MetadataWriteOptions& options) const = 0;
/**
* @brief Evaluate the given condition string.
+72 -4
View File
@@ -4,7 +4,75 @@
#include "api/convert.h"
#include "api/exception/exception.h"
namespace {
loot::MetadataWriteOptionsImpl convert(
const loot::MetadataWriteOptions& options) {
loot::MetadataWriteOptionsImpl output;
output.truncate = options.GetTruncate();
output.write_anchors = options.GetWriteAnchors();
output.write_common_section = options.GetWriteCommonSection();
output.anchor_file_strings = options.GetAnchorFileStrings();
return output;
}
}
namespace loot {
MetadataWriteOptions::MetadataWriteOptions() :
pimpl_(std::make_unique<MetadataWriteOptionsImpl>()) {}
MetadataWriteOptions::MetadataWriteOptions(const MetadataWriteOptions& other) :
pimpl_(std::make_unique<MetadataWriteOptionsImpl>(*other.pimpl_)) {}
MetadataWriteOptions::MetadataWriteOptions(
MetadataWriteOptions&& other) noexcept :
pimpl_(std::move(other).pimpl_) {}
MetadataWriteOptions::~MetadataWriteOptions() {}
MetadataWriteOptions& MetadataWriteOptions::operator=(
const MetadataWriteOptions& other) {
*pimpl_ = *other.pimpl_;
return *this;
}
MetadataWriteOptions& MetadataWriteOptions::operator=(
MetadataWriteOptions&& other) noexcept {
pimpl_ = std::move(other.pimpl_);
return *this;
}
void MetadataWriteOptions::SetTruncate(bool truncate) {
pimpl_->truncate = truncate;
}
void MetadataWriteOptions::SetWriteAnchors(bool writeAnchors) {
pimpl_->write_anchors = writeAnchors;
}
void MetadataWriteOptions::SetWriteCommonSection(bool writeCommonSection) {
pimpl_->write_common_section = writeCommonSection;
}
void MetadataWriteOptions::SetAnchorFileStrings(bool anchorFileStrings) {
pimpl_->anchor_file_strings = anchorFileStrings;
}
bool MetadataWriteOptions::GetTruncate() const { return pimpl_->truncate; }
bool MetadataWriteOptions::GetWriteAnchors() const {
return pimpl_->write_anchors;
}
bool MetadataWriteOptions::GetWriteCommonSection() const {
return pimpl_->write_common_section;
}
bool MetadataWriteOptions::GetAnchorFileStrings() const {
return pimpl_->anchor_file_strings;
}
Database::Database(::rust::Box<loot::rust::Database>&& database) :
database_(std::move(database)) {}
@@ -36,9 +104,9 @@ void Database::LoadUserlist(const std::filesystem::path& userlistPath) {
}
void Database::WriteUserMetadata(const std::filesystem::path& outputFile,
const bool overwrite) const {
const MetadataWriteOptions& options) const {
try {
database_->write_user_metadata(outputFile.u8string(), overwrite);
database_->write_user_metadata(outputFile.u8string(), ::convert(options));
} catch (const ::rust::Error& e) {
std::rethrow_exception(mapError(e));
}
@@ -209,9 +277,9 @@ void Database::DiscardAllUserMetadata() {
}
void Database::WriteMinimalList(const std::filesystem::path& outputFile,
const bool overwrite) const {
const MetadataWriteOptions& options) const {
try {
database_->write_minimal_list(outputFile.u8string(), overwrite);
database_->write_minimal_list(outputFile.u8string(), ::convert(options));
} catch (const ::rust::Error& e) {
std::rethrow_exception(mapError(e));
}
+5 -5
View File
@@ -10,19 +10,19 @@ class Database final : public DatabaseInterface {
public:
explicit Database(::rust::Box<loot::rust::Database>&& database);
void LoadMasterlist(const std::filesystem::path& masterlist_path) override;
void LoadMasterlist(const std::filesystem::path& masterlistPath) override;
void LoadMasterlistWithPrelude(
const std::filesystem::path& masterlist_path,
const std::filesystem::path& masterlist_prelude_path) override;
const std::filesystem::path& masterlistPath,
const std::filesystem::path& masterlistPreludePath) override;
void LoadUserlist(const std::filesystem::path& userlist_path) override;
void WriteUserMetadata(const std::filesystem::path& outputFile,
const bool overwrite) const override;
const MetadataWriteOptions& options) const override;
void WriteMinimalList(const std::filesystem::path& outputFile,
const bool overwrite) const override;
const MetadataWriteOptions& options) const override;
bool Evaluate(const std::string& condition) const override;
+6 -18
View File
@@ -4,12 +4,12 @@ use std::{
};
use delegate::delegate;
use libloot::{EvalMode, MergeMode, WriteMode, error::DatabaseLockPoisonError};
use libloot::{EvalMode, MergeMode, error::DatabaseLockPoisonError};
use libloot_ffi_errors::UnsupportedEnumValueError;
use crate::{
OptionalPluginMetadata, VerboseError,
ffi::EdgeType,
ffi::{EdgeType, MetadataWriteOptionsImpl},
metadata::{Group, Message, PluginMetadata, to_vec_of_unwrapped},
};
@@ -53,36 +53,24 @@ impl Database {
pub fn write_user_metadata(
&self,
output_path: &str,
overwrite: bool,
options: MetadataWriteOptionsImpl,
) -> Result<(), VerboseError> {
let write_mode = if overwrite {
WriteMode::CreateOrTruncate
} else {
WriteMode::Create
};
self.0
.read()
.map_err(DatabaseLockPoisonError::from)?
.write_user_metadata(Path::new(output_path), write_mode)
.write_user_metadata(Path::new(output_path), &options.into())
.map_err(Into::into)
}
pub fn write_minimal_list(
&self,
output_path: &str,
overwrite: bool,
options: MetadataWriteOptionsImpl,
) -> Result<(), VerboseError> {
let write_mode = if overwrite {
WriteMode::CreateOrTruncate
} else {
WriteMode::Create
};
self.0
.read()
.map_err(DatabaseLockPoisonError::from)?
.write_minimal_list(Path::new(output_path), write_mode)
.write_minimal_list(Path::new(output_path), &options.into())
.map_err(Into::into)
}
+32 -4
View File
@@ -17,7 +17,7 @@ mod plugin;
use database::{Database, Vertex, new_vertex};
use error::{EmptyOptionalError, VerboseError};
use ffi::OptionalMessageContentRef;
use ffi::{MetadataWriteOptionsImpl, OptionalMessageContentRef};
use game::{Game, new_game, new_game_with_local_path};
use libloot_ffi_errors::UnsupportedEnumValueError;
use metadata::{
@@ -112,6 +112,18 @@ impl TryFrom<ffi::LogLevel> for libloot::LogLevel {
}
}
impl From<MetadataWriteOptionsImpl> for libloot::MetadataWriteOptions {
fn from(value: MetadataWriteOptionsImpl) -> Self {
let mut options = libloot::MetadataWriteOptions::new();
options.set_truncate(value.truncate);
options.set_write_anchors(value.write_anchors);
options.set_write_common_section(value.write_common_section);
options.set_anchor_file_strings(value.anchor_file_strings);
options
}
}
#[allow(
let_underscore_drop,
missing_debug_implementations,
@@ -123,7 +135,6 @@ impl TryFrom<ffi::LogLevel> for libloot::LogLevel {
)]
#[cxx::bridge(namespace = "loot::rust")]
mod ffi {
pub enum GameType {
Oblivion,
Skyrim,
@@ -181,6 +192,15 @@ mod ffi {
pointer: *const MessageContent,
}
#[namespace = "loot"]
#[derive(Debug, Copy, Clone)]
struct MetadataWriteOptionsImpl {
truncate: bool,
write_anchors: bool,
write_common_section: bool,
anchor_file_strings: bool,
}
extern "Rust" {
pub fn is_some(self: &OptionalMessageContentRef) -> bool;
@@ -260,9 +280,17 @@ mod ffi {
pub fn load_userlist(&self, path: &str) -> Result<()>;
pub fn write_user_metadata(&self, output_path: &str, overwrite: bool) -> Result<()>;
pub fn write_user_metadata(
&self,
output_path: &str,
options: MetadataWriteOptionsImpl,
) -> Result<()>;
pub fn write_minimal_list(&self, output_path: &str, overwrite: bool) -> Result<()>;
pub fn write_minimal_list(
&self,
output_path: &str,
options: MetadataWriteOptionsImpl,
) -> Result<()>;
pub fn evaluate(&self, condition: &str) -> Result<bool>;
@@ -198,46 +198,55 @@ TEST_P(DatabaseInterfaceTest, loadUserlistShouldSucceedIfTheUserlistIsPresent) {
TEST_P(
DatabaseInterfaceTest,
writeUserMetadataShouldThrowIfTheFileAlreadyExistsAndTheOverwriteArgumentIsFalse) {
ASSERT_NO_THROW(
handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, false));
ASSERT_NO_THROW(handle_->GetDatabase().WriteUserMetadata(
minimalOutputPath_, MetadataWriteOptions()));
ASSERT_TRUE(std::filesystem::exists(minimalOutputPath_));
EXPECT_THROW(
handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, false),
std::runtime_error);
EXPECT_THROW(handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_,
MetadataWriteOptions()),
std::runtime_error);
}
TEST_P(
DatabaseInterfaceTest,
writeUserMetadataShouldReturnOkAndWriteToFileIfTheArgumentsAreValidAndTheOverwriteArgumentIsTrue) {
MetadataWriteOptions options;
options.SetTruncate(true);
EXPECT_NO_THROW(
handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, true));
handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, options));
EXPECT_TRUE(std::filesystem::exists(minimalOutputPath_));
}
TEST_P(
DatabaseInterfaceTest,
writeUserMetadataShouldReturnOkIfTheFileAlreadyExistsAndTheOverwriteArgumentIsTrue) {
ASSERT_NO_THROW(
handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, false));
ASSERT_NO_THROW(handle_->GetDatabase().WriteUserMetadata(
minimalOutputPath_, MetadataWriteOptions()));
ASSERT_TRUE(std::filesystem::exists(minimalOutputPath_));
MetadataWriteOptions options;
options.SetTruncate(true);
EXPECT_NO_THROW(
handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, true));
handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, options));
}
TEST_P(DatabaseInterfaceTest,
writeUserMetadataShouldThrowIfPathGivenExistsAndIsReadOnly) {
ASSERT_NO_THROW(
handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, false));
ASSERT_NO_THROW(handle_->GetDatabase().WriteUserMetadata(
minimalOutputPath_, MetadataWriteOptions()));
ASSERT_TRUE(std::filesystem::exists(minimalOutputPath_));
std::filesystem::permissions(minimalOutputPath_,
std::filesystem::perms::owner_read,
std::filesystem::perm_options::replace);
MetadataWriteOptions options;
options.SetTruncate(true);
EXPECT_THROW(
handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, true),
handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, options),
std::runtime_error);
}
@@ -246,8 +255,11 @@ TEST_P(DatabaseInterfaceTest,
ASSERT_NO_THROW(GenerateMasterlist());
ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath));
MetadataWriteOptions options;
options.SetTruncate(true);
EXPECT_NO_THROW(
handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, true));
handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, options));
EXPECT_EQ("{}", GetFileContent(minimalOutputPath_));
}
@@ -263,8 +275,11 @@ TEST_P(DatabaseInterfaceTest, writeUserMetadataShouldShouldWriteUserMetadata) {
ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath));
ASSERT_NO_THROW(handle_->GetDatabase().LoadUserlist(userlistPath_));
MetadataWriteOptions options;
options.SetTruncate(true);
EXPECT_NO_THROW(
handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, true));
handle_->GetDatabase().WriteUserMetadata(minimalOutputPath_, options));
EXPECT_FALSE(GetFileContent(minimalOutputPath_).empty());
}
@@ -899,54 +914,63 @@ TEST_P(
TEST_P(DatabaseInterfaceTest,
writeMinimalListShouldReturnOkAndWriteToFileIfArgumentsGivenAreValid) {
EXPECT_NO_THROW(
handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, false));
EXPECT_NO_THROW(handle_->GetDatabase().WriteMinimalList(
minimalOutputPath_, MetadataWriteOptions()));
EXPECT_TRUE(std::filesystem::exists(minimalOutputPath_));
}
TEST_P(
DatabaseInterfaceTest,
writeMinimalListShouldThrowIfTheFileAlreadyExistsAndTheOverwriteArgumentIsFalse) {
ASSERT_NO_THROW(
handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, false));
ASSERT_NO_THROW(handle_->GetDatabase().WriteMinimalList(
minimalOutputPath_, MetadataWriteOptions()));
ASSERT_TRUE(std::filesystem::exists(minimalOutputPath_));
EXPECT_THROW(
handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, false),
std::runtime_error);
EXPECT_THROW(handle_->GetDatabase().WriteMinimalList(minimalOutputPath_,
MetadataWriteOptions()),
std::runtime_error);
}
TEST_P(
DatabaseInterfaceTest,
writeMinimalListShouldReturnOkAndWriteToFileIfTheArgumentsAreValidAndTheOverwriteArgumentIsTrue) {
MetadataWriteOptions options;
options.SetTruncate(true);
EXPECT_NO_THROW(
handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, true));
handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, options));
EXPECT_TRUE(std::filesystem::exists(minimalOutputPath_));
}
TEST_P(
DatabaseInterfaceTest,
writeMinimalListShouldReturnOkIfTheFileAlreadyExistsAndTheOverwriteArgumentIsTrue) {
ASSERT_NO_THROW(
handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, false));
ASSERT_NO_THROW(handle_->GetDatabase().WriteMinimalList(
minimalOutputPath_, MetadataWriteOptions()));
ASSERT_TRUE(std::filesystem::exists(minimalOutputPath_));
MetadataWriteOptions options;
options.SetTruncate(true);
EXPECT_NO_THROW(
handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, true));
handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, options));
}
TEST_P(DatabaseInterfaceTest,
writeMinimalListShouldThrowIfPathGivenExistsAndIsReadOnly) {
ASSERT_NO_THROW(
handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, false));
ASSERT_NO_THROW(handle_->GetDatabase().WriteMinimalList(
minimalOutputPath_, MetadataWriteOptions()));
ASSERT_TRUE(std::filesystem::exists(minimalOutputPath_));
std::filesystem::permissions(minimalOutputPath_,
std::filesystem::perms::owner_read,
std::filesystem::perm_options::replace);
MetadataWriteOptions options;
options.SetTruncate(true);
EXPECT_THROW(
handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, true),
handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, options),
std::runtime_error);
}
@@ -957,8 +981,11 @@ TEST_P(DatabaseInterfaceTest,
ASSERT_NO_THROW(GenerateMasterlist());
ASSERT_NO_THROW(handle_->GetDatabase().LoadMasterlist(masterlistPath));
MetadataWriteOptions options;
options.SetTruncate(true);
EXPECT_NO_THROW(
handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, true));
handle_->GetDatabase().WriteMinimalList(minimalOutputPath_, options));
const auto content = GetFileContent(minimalOutputPath_);