diff --git a/Cargo.toml b/Cargo.toml index 7b27322e..ebb125ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,7 +49,7 @@ categories = ["games"] ambiguous_negative_literals = "forbid" impl_trait_overcaptures = "forbid" let_underscore_drop = "deny" -missing_copy_implementations = "forbid" +missing_copy_implementations = "deny" missing_debug_implementations = "deny" non_ascii_idents = "forbid" redundant_imports = "forbid" diff --git a/cpp/include/loot/database_interface.h b/cpp/include/loot/database_interface.h index 3a8810ca..23329eb4 100644 --- a/cpp/include/loot/database_interface.h +++ b/cpp/include/loot/database_interface.h @@ -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 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. diff --git a/cpp/src/api/database.cpp b/cpp/src/api/database.cpp index 34f379c1..ecc37098 100644 --- a/cpp/src/api/database.cpp +++ b/cpp/src/api/database.cpp @@ -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()) {} + +MetadataWriteOptions::MetadataWriteOptions(const MetadataWriteOptions& other) : + pimpl_(std::make_unique(*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&& 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)); } diff --git a/cpp/src/api/database.h b/cpp/src/api/database.h index 9ca97f48..baf42b6f 100644 --- a/cpp/src/api/database.h +++ b/cpp/src/api/database.h @@ -10,19 +10,19 @@ class Database final : public DatabaseInterface { public: explicit Database(::rust::Box&& 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; diff --git a/cpp/src/database.rs b/cpp/src/database.rs index 0bffed41..e7251857 100644 --- a/cpp/src/database.rs +++ b/cpp/src/database.rs @@ -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) } diff --git a/cpp/src/lib.rs b/cpp/src/lib.rs index 88859e66..9590d83a 100644 --- a/cpp/src/lib.rs +++ b/cpp/src/lib.rs @@ -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 for libloot::LogLevel { } } +impl From 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 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; diff --git a/cpp/src/tests/api/interface/database_interface_test.h b/cpp/src/tests/api/interface/database_interface_test.h index a4ed1e65..b0aafd86 100644 --- a/cpp/src/tests/api/interface/database_interface_test.h +++ b/cpp/src/tests/api/interface/database_interface_test.h @@ -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_); diff --git a/nodejs/src/database.rs b/nodejs/src/database.rs index f7993914..99b3fb15 100644 --- a/nodejs/src/database.rs +++ b/nodejs/src/database.rs @@ -3,7 +3,7 @@ use std::{ sync::{Arc, RwLock}, }; -use libloot::{WriteMode, error::DatabaseLockPoisonError}; +use libloot::error::DatabaseLockPoisonError; use libloot_ffi_errors::UnsupportedEnumValueError; use napi_derive::napi; @@ -44,6 +44,59 @@ impl From for libloot::MergeMode { } } +#[napi] +#[repr(transparent)] +#[derive(Clone, Debug, Default)] +pub struct MetadataWriteOptions(libloot::MetadataWriteOptions); + +#[napi] +impl MetadataWriteOptions { + #[napi(constructor)] + pub fn new() -> Self { + MetadataWriteOptions(libloot::MetadataWriteOptions::new()) + } + + #[napi(setter)] + pub fn set_truncate(&mut self, truncate: bool) { + self.0.set_truncate(truncate); + } + + #[napi(setter)] + pub fn set_write_anchors(&mut self, write_anchors: bool) { + self.0.set_write_anchors(write_anchors); + } + + #[napi(setter)] + pub fn set_write_common_section(&mut self, write_common_section: bool) { + self.0.set_write_common_section(write_common_section); + } + + #[napi(setter)] + pub fn set_anchor_file_strings(&mut self, anchor_file_strings: bool) { + self.0.set_anchor_file_strings(anchor_file_strings); + } + + #[napi(getter)] + pub fn truncate(&self) -> bool { + self.0.truncate() + } + + #[napi(getter)] + pub fn write_anchors(&self) -> bool { + self.0.write_anchors() + } + + #[napi(getter)] + pub fn write_common_section(&self) -> bool { + self.0.write_common_section() + } + + #[napi(getter)] + pub fn anchor_file_strings(&self) -> bool { + self.0.anchor_file_strings() + } +} + #[napi] #[derive(Clone, Debug)] pub struct Database(Arc>); @@ -85,18 +138,12 @@ impl Database { pub fn write_user_metadata( &self, output_path: String, - overwrite: bool, + options: &MetadataWriteOptions, ) -> 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.0) .map_err(Into::into) } @@ -104,18 +151,12 @@ impl Database { pub fn write_minimal_list( &self, output_path: String, - overwrite: bool, + options: &MetadataWriteOptions, ) -> 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.0) .map_err(Into::into) } diff --git a/python/src/database.rs b/python/src/database.rs index 41495635..0fcaeaf9 100644 --- a/python/src/database.rs +++ b/python/src/database.rs @@ -3,7 +3,7 @@ use std::{ sync::{Arc, RwLock}, }; -use libloot::{EvalMode, MergeMode, WriteMode, error::DatabaseLockPoisonError}; +use libloot::{EvalMode, MergeMode, error::DatabaseLockPoisonError}; use libloot_ffi_errors::UnsupportedEnumValueError; use pyo3::{ Bound, PyResult, pyclass, pymethods, @@ -15,6 +15,59 @@ use crate::{ metadata::{Group, Message, NONE_REPR, PluginMetadata}, }; +#[pyclass(str = "{0:?}")] +#[repr(transparent)] +#[derive(Clone, Debug, Default)] +pub struct MetadataWriteOptions(libloot::MetadataWriteOptions); + +#[pymethods] +impl MetadataWriteOptions { + #[new] + pub fn new() -> Self { + MetadataWriteOptions(libloot::MetadataWriteOptions::new()) + } + + #[setter] + pub fn set_truncate(&mut self, truncate: bool) { + self.0.set_truncate(truncate); + } + + #[setter] + pub fn set_write_anchors(&mut self, write_anchors: bool) { + self.0.set_write_anchors(write_anchors); + } + + #[setter] + pub fn set_write_common_section(&mut self, write_common_section: bool) { + self.0.set_write_common_section(write_common_section); + } + + #[setter] + pub fn set_anchor_file_strings(&mut self, anchor_file_strings: bool) { + self.0.set_anchor_file_strings(anchor_file_strings); + } + + #[getter] + pub fn truncate(&self) -> bool { + self.0.truncate() + } + + #[getter] + pub fn write_anchors(&self) -> bool { + self.0.write_anchors() + } + + #[getter] + pub fn write_common_section(&self) -> bool { + self.0.write_common_section() + } + + #[getter] + pub fn anchor_file_strings(&self) -> bool { + self.0.anchor_file_strings() + } +} + #[pyclass] #[derive(Clone, Debug)] pub struct Database(Arc>); @@ -56,18 +109,12 @@ impl Database { pub fn write_user_metadata( &self, output_path: PathBuf, - overwrite: bool, + options: &MetadataWriteOptions, ) -> Result<(), VerboseError> { - let write_mode = if overwrite { - WriteMode::CreateOrTruncate - } else { - WriteMode::Create - }; - self.0 .read() .map_err(DatabaseLockPoisonError::from)? - .write_user_metadata(&output_path, write_mode) + .write_user_metadata(&output_path, &options.0) .map_err(Into::into) } @@ -75,18 +122,12 @@ impl Database { pub fn write_minimal_list( &self, output_path: PathBuf, - overwrite: bool, + options: &MetadataWriteOptions, ) -> Result<(), VerboseError> { - let write_mode = if overwrite { - WriteMode::CreateOrTruncate - } else { - WriteMode::Create - }; - self.0 .read() .map_err(DatabaseLockPoisonError::from)? - .write_minimal_list(&output_path, write_mode) + .write_minimal_list(&output_path, &options.0) .map_err(Into::into) } diff --git a/python/src/lib.rs b/python/src/lib.rs index c742ee21..d6ca7c94 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -4,7 +4,7 @@ mod game; mod metadata; mod plugin; -use database::{Database, EdgeType, Vertex}; +use database::{Database, EdgeType, MetadataWriteOptions, Vertex}; use game::{Game, GameType}; use metadata::{ File, Filename, Group, Location, Message, MessageContent, MessageType, PluginCleaningData, @@ -64,6 +64,7 @@ fn libloot_pyo3(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add( "CyclicInteractionError", diff --git a/src/database/mod.rs b/src/database/mod.rs index 31226ee5..8a43d4d0 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -20,22 +20,6 @@ use crate::{ }; pub use error::{ConditionEvaluationError, MetadataRetrievalError}; -const WRITE_OPTIONS: MetadataWriteOptions = MetadataWriteOptions { - write_anchors: true, - write_common_section: true, - anchor_file_strings: true, -}; - -/// Control behaviour when writing to files. -#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] -#[non_exhaustive] -pub enum WriteMode { - /// Create the file if it does not exist, otherwise error. - Create, - /// Create the file if it does not exist, otherwise replace its contents. - CreateOrTruncate, -} - /// Control whether user metadata is included or not when retrieving metadata. #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] #[expect(clippy::exhaustive_enums, reason = "It's effectively a boolean")] @@ -116,11 +100,11 @@ impl Database { pub fn write_user_metadata( &self, output_path: &Path, - mode: WriteMode, + options: &MetadataWriteOptions, ) -> Result<(), WriteMetadataError> { - validate_write_path(output_path, mode)?; + validate_write_path(output_path, options)?; - self.userlist.save(output_path, WRITE_OPTIONS) + self.userlist.save(output_path, options) } /// Writes a metadata file that only contains plugin Bash Tag suggestions @@ -131,9 +115,9 @@ impl Database { pub fn write_minimal_list( &self, output_path: &Path, - mode: WriteMode, + options: &MetadataWriteOptions, ) -> Result<(), WriteMetadataError> { - validate_write_path(output_path, mode)?; + validate_write_path(output_path, options)?; let mut doc = MetadataDocument::default(); @@ -145,7 +129,7 @@ impl Database { doc.set_plugin_metadata(minimal_plugin); } - doc.save(output_path, WRITE_OPTIONS) + doc.save(output_path, options) } /// Evaluate the given condition string. @@ -351,7 +335,10 @@ impl Database { } } -fn validate_write_path(output_path: &Path, mode: WriteMode) -> Result<(), WriteMetadataError> { +fn validate_write_path( + output_path: &Path, + options: &MetadataWriteOptions, +) -> Result<(), WriteMetadataError> { if !output_path .parent() .is_some_and(|p| p.as_os_str().is_empty() || p.exists()) @@ -360,7 +347,7 @@ fn validate_write_path(output_path: &Path, mode: WriteMode) -> Result<(), WriteM output_path.into(), WriteMetadataErrorReason::ParentDirectoryNotFound, )) - } else if mode == WriteMode::Create && output_path.exists() { + } else if !options.truncate() && output_path.exists() { Err(WriteMetadataError::new( output_path.into(), WriteMetadataErrorReason::PathAlreadyExists, @@ -518,6 +505,12 @@ plugins: } } + fn truncate_options() -> MetadataWriteOptions { + let mut options = MetadataWriteOptions::new(); + options.set_truncate(true); + options + } + #[test] fn load_masterlist_should_succeed_if_given_a_valid_path() { let fixture = Fixture::new(GameType::Oblivion); @@ -587,7 +580,7 @@ plugins: let output_path = fixture.inner.local_path.join("userlist.yaml"); database - .write_user_metadata(&output_path, WriteMode::Create) + .write_user_metadata(&output_path, &MetadataWriteOptions::new()) .unwrap(); let content = std::fs::read_to_string(output_path).unwrap(); @@ -603,7 +596,7 @@ plugins: assert!( database - .write_user_metadata(&output_path, WriteMode::Create) + .write_user_metadata(&output_path, &MetadataWriteOptions::new()) .is_ok() ); } @@ -616,7 +609,7 @@ plugins: assert!( database - .write_user_metadata(&output_path, WriteMode::CreateOrTruncate) + .write_user_metadata(&output_path, &truncate_options()) .is_ok() ); } @@ -631,7 +624,7 @@ plugins: assert!( database - .write_user_metadata(&output_path, WriteMode::CreateOrTruncate) + .write_user_metadata(&output_path, &truncate_options()) .is_ok() ); } @@ -644,7 +637,7 @@ plugins: assert!( database - .write_user_metadata(output_path, WriteMode::Create) + .write_user_metadata(output_path, &MetadataWriteOptions::new()) .is_ok() ); @@ -657,7 +650,7 @@ plugins: let database = fixture.database(); let err = database - .write_user_metadata(Path::new("/"), WriteMode::Create) + .write_user_metadata(Path::new("/"), &MetadataWriteOptions::new()) .unwrap_err(); assert_eq!( @@ -673,7 +666,7 @@ plugins: let output_path = fixture.inner.local_path; let err = database - .write_user_metadata(&output_path, WriteMode::CreateOrTruncate) + .write_user_metadata(&output_path, &truncate_options()) .unwrap_err(); assert_eq!("an I/O error occurred", err.to_string()); @@ -687,7 +680,7 @@ plugins: assert!( database - .write_user_metadata(&output_path, WriteMode::Create) + .write_user_metadata(&output_path, &MetadataWriteOptions::new()) .is_err() ); } @@ -706,7 +699,7 @@ plugins: assert!( database - .write_user_metadata(&output_path, WriteMode::CreateOrTruncate) + .write_user_metadata(&output_path, &truncate_options()) .is_err() ); } @@ -721,7 +714,7 @@ plugins: assert!( database - .write_user_metadata(&output_path, WriteMode::Create) + .write_user_metadata(&output_path, &MetadataWriteOptions::new()) .is_err() ); } @@ -740,7 +733,7 @@ plugins: assert!( database - .write_minimal_list(&output_path, WriteMode::Create) + .write_minimal_list(&output_path, &MetadataWriteOptions::new()) .is_ok() ); @@ -770,7 +763,7 @@ plugins: assert!( database - .write_minimal_list(&output_path, WriteMode::Create) + .write_minimal_list(&output_path, &MetadataWriteOptions::new()) .is_ok() ); } @@ -783,7 +776,7 @@ plugins: assert!( database - .write_minimal_list(&output_path, WriteMode::CreateOrTruncate) + .write_minimal_list(&output_path, &truncate_options()) .is_ok() ); } @@ -798,7 +791,7 @@ plugins: assert!( database - .write_minimal_list(&output_path, WriteMode::CreateOrTruncate) + .write_minimal_list(&output_path, &truncate_options()) .is_ok() ); } @@ -811,7 +804,7 @@ plugins: assert!( database - .write_minimal_list(output_path, WriteMode::Create) + .write_minimal_list(output_path, &MetadataWriteOptions::new()) .is_ok() ); @@ -824,7 +817,7 @@ plugins: let database = fixture.database(); let err = database - .write_minimal_list(Path::new("/"), WriteMode::Create) + .write_minimal_list(Path::new("/"), &MetadataWriteOptions::new()) .unwrap_err(); assert_eq!( @@ -840,7 +833,7 @@ plugins: let output_path = fixture.inner.local_path; let err = database - .write_minimal_list(&output_path, WriteMode::CreateOrTruncate) + .write_minimal_list(&output_path, &truncate_options()) .unwrap_err(); assert_eq!("an I/O error occurred", err.to_string()); @@ -854,7 +847,7 @@ plugins: assert!( database - .write_minimal_list(&output_path, WriteMode::Create) + .write_minimal_list(&output_path, &MetadataWriteOptions::new()) .is_err() ); } @@ -873,7 +866,7 @@ plugins: assert!( database - .write_minimal_list(&output_path, WriteMode::CreateOrTruncate) + .write_minimal_list(&output_path, &truncate_options()) .is_err() ); } @@ -888,7 +881,7 @@ plugins: assert!( database - .write_minimal_list(&output_path, WriteMode::Create) + .write_minimal_list(&output_path, &MetadataWriteOptions::new()) .is_err() ); } diff --git a/src/lib.rs b/src/lib.rs index 6a784c4b..64b6c4dd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,9 +31,10 @@ use std::{path::Path, slice::EscapeAscii}; use regress::{Error as RegexImplError, Regex}; -pub use database::{Database, EvalMode, MergeMode, WriteMode}; +pub use database::{Database, EvalMode, MergeMode}; pub use game::{Game, GameType}; pub use logging::{LogLevel, set_log_level, set_logging_callback}; +pub use metadata::metadata_document::MetadataWriteOptions; pub use plugin::Plugin; pub use sorting::vertex::{EdgeType, Vertex}; pub use version::{ diff --git a/src/metadata/metadata_document.rs b/src/metadata/metadata_document.rs index f77ad712..81073390 100644 --- a/src/metadata/metadata_document.rs +++ b/src/metadata/metadata_document.rs @@ -28,11 +28,106 @@ use super::{ }, }; -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub(crate) struct MetadataWriteOptions { - pub write_anchors: bool, - pub write_common_section: bool, - pub anchor_file_strings: bool, +/// Options to configure how metadata files are written. +#[derive(Clone, Debug, Default)] +#[expect( + missing_copy_implementations, + reason = "Omitted Copy to allow this to become non-Copy-compatible without breaking backwards compatibility." +)] +#[expect( + clippy::struct_excessive_bools, + reason = "It's a config object, the bools make sense." +)] +pub struct MetadataWriteOptions { + truncate: bool, + write_anchors: bool, + write_common_section: bool, + anchor_file_strings: bool, +} + +impl MetadataWriteOptions { + /// Creates a new set of options, all initially set to `false`. + pub fn new() -> Self { + MetadataWriteOptions { + truncate: false, + write_anchors: false, + write_common_section: false, + anchor_file_strings: false, + } + } + + /// Sets the option to overwrite the output file if it already exists. + /// + /// 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. + pub fn set_truncate(&mut self, truncate: bool) { + self.truncate = truncate; + } + + /// Sets the option to write YAML anchors and aliases. + /// + /// 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. + pub fn set_write_anchors(&mut self, write_anchors: bool) { + self.write_anchors = write_anchors; + } + + /// Sets the option to write YAML anchors in a `common` section. + /// + /// If `write_anchors` 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 `write_anchors` is false. + pub fn set_write_common_section(&mut self, write_common_section: bool) { + self.write_common_section = write_common_section; + } + + /// Sets the option to write anchors for [File] values that only have a + /// name. + /// + /// If `write_anchors` 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 `write_anchors` is false. + pub fn set_anchor_file_strings(&mut self, anchor_file_strings: bool) { + self.anchor_file_strings = anchor_file_strings; + } + + /// Gets the current value of the `truncate` option. + pub fn truncate(&self) -> bool { + self.truncate + } + + /// Gets the current value of the `write_anchors` option. + pub fn write_anchors(&self) -> bool { + self.write_anchors + } + + /// Gets the current value of the `write_common_section` option. + pub fn write_common_section(&self) -> bool { + self.write_common_section + } + + /// Gets the current value of the `anchor_file_strings` option. + pub fn anchor_file_strings(&self) -> bool { + self.anchor_file_strings + } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -233,7 +328,7 @@ impl MetadataDocument { pub(crate) fn save( &self, file_path: &Path, - options: MetadataWriteOptions, + options: &MetadataWriteOptions, ) -> Result<(), WriteMetadataError> { logging::trace!("Saving metadata list to: \"{}\"", escape_ascii(file_path)); @@ -1298,7 +1393,7 @@ plugins: let other_path = tmp_dir.path().join("other.yaml"); metadata - .save(&other_path, MetadataWriteOptions::default()) + .save(&other_path, &MetadataWriteOptions::default()) .unwrap(); let mut other_metadata = MetadataDocument::default(); @@ -1382,16 +1477,11 @@ plugins: let metadata = metadata_with_repeated_values(); - metadata - .save( - &path, - MetadataWriteOptions { - write_anchors: true, - write_common_section: false, - anchor_file_strings: true, - }, - ) - .unwrap(); + let mut options = MetadataWriteOptions::new(); + options.set_write_anchors(true); + options.set_anchor_file_strings(true); + + metadata.save(&path, &options).unwrap(); let content = std::fs::read_to_string(&path).unwrap(); @@ -1464,16 +1554,12 @@ plugins: let metadata = metadata_with_repeated_values(); - metadata - .save( - &path, - MetadataWriteOptions { - write_anchors: true, - write_common_section: true, - anchor_file_strings: true, - }, - ) - .unwrap(); + let mut options = MetadataWriteOptions::new(); + options.set_write_anchors(true); + options.set_write_common_section(true); + options.set_anchor_file_strings(true); + + metadata.save(&path, &options).unwrap(); let content = std::fs::read_to_string(&path).unwrap(); @@ -1555,16 +1641,11 @@ plugins: let metadata = metadata_with_repeated_values(); - metadata - .save( - &path, - MetadataWriteOptions { - write_anchors: true, - write_common_section: true, - anchor_file_strings: false, - }, - ) - .unwrap(); + let mut options = MetadataWriteOptions::new(); + options.set_write_anchors(true); + options.set_write_common_section(true); + + metadata.save(&path, &options).unwrap(); let content = std::fs::read_to_string(&path).unwrap();