Add a Node.js wrapper library

The implementation is very similar to the Python wrapper.
This commit is contained in:
Oliver Hamlet
2025-04-22 19:22:31 +01:00
parent cb4686b0b9
commit 5a33e16eb3
12 changed files with 4504 additions and 1183 deletions
+339
View File
@@ -0,0 +1,339 @@
use std::{
path::Path,
sync::{Arc, RwLock},
};
use libloot::{error::DatabaseLockPoisonError, WriteMode};
use libloot_ffi_errors::UnsupportedEnumValueError;
use napi_derive::napi;
use crate::{
error::VerboseError,
metadata::{Group, Message, PluginMetadata},
};
#[napi]
#[derive(Clone, Debug)]
pub struct Database(Arc<RwLock<libloot::Database>>);
#[napi]
impl Database {
#[napi]
pub fn load_masterlist(&self, path: String) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.load_masterlist(Path::new(&path))
.map_err(Into::into)
}
#[napi]
pub fn load_masterlist_with_prelude(
&self,
masterlist_path: String,
prelude_path: String,
) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.load_masterlist_with_prelude(Path::new(&masterlist_path), Path::new(&prelude_path))
.map_err(Into::into)
}
#[napi]
pub fn load_userlist(&self, path: String) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.load_userlist(Path::new(&path))
.map_err(Into::into)
}
#[napi]
pub fn write_user_metadata(
&self,
output_path: String,
overwrite: bool,
) -> Result<(), VerboseError> {
let write_mode = if overwrite {
WriteMode::CreateOrTruncate
} else {
WriteMode::Create
};
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.write_user_metadata(Path::new(&output_path), write_mode)
.map_err(Into::into)
}
#[napi]
pub fn write_minimal_list(
&self,
output_path: String,
overwrite: bool,
) -> Result<(), VerboseError> {
let write_mode = if overwrite {
WriteMode::CreateOrTruncate
} else {
WriteMode::Create
};
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.write_minimal_list(Path::new(&output_path), write_mode)
.map_err(Into::into)
}
#[napi]
pub fn evaluate(&self, condition: String) -> Result<bool, VerboseError> {
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.evaluate(&condition)
.map_err(Into::into)
}
#[napi]
pub fn known_bash_tags(&self) -> Result<Vec<String>, VerboseError> {
Ok(self
.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.known_bash_tags())
}
#[napi]
pub fn general_messages(
&self,
evaluate_conditions: bool,
) -> Result<Vec<Message>, VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.general_messages(evaluate_conditions)
.map(|v| v.into_iter().map(Into::into).collect())
.map_err(Into::into)
}
#[napi]
pub fn groups(&self, include_user_metadata: bool) -> Result<Vec<Group>, VerboseError> {
Ok(self
.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.groups(include_user_metadata)
.into_iter()
.map(Into::into)
.collect())
}
#[napi]
pub fn user_groups(&self) -> Result<Vec<Group>, VerboseError> {
Ok(self
.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.user_groups()
.iter()
.cloned()
.map(Into::into)
.collect())
}
#[napi]
pub fn set_user_groups(&self, groups: Vec<&Group>) -> Result<(), VerboseError> {
let groups = groups.into_iter().cloned().map(Into::into).collect();
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.set_user_groups(groups);
Ok(())
}
#[napi]
pub fn groups_path(
&self,
from_group_name: String,
to_group_name: String,
) -> Result<Vec<Vertex>, VerboseError> {
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.groups_path(&from_group_name, &to_group_name)
.map(|v| v.into_iter().map(Into::into).collect())
.map_err(Into::into)
}
#[napi]
pub fn plugin_metadata(
&self,
plugin_name: String,
include_user_metadata: bool,
evaluate_conditions: bool,
) -> Result<Option<PluginMetadata>, VerboseError> {
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.plugin_metadata(&plugin_name, include_user_metadata, evaluate_conditions)
.map(|p| p.map(Into::into))
.map_err(Into::into)
}
#[napi]
pub fn plugin_user_metadata(
&self,
plugin_name: String,
evaluate_conditions: bool,
) -> Result<Option<PluginMetadata>, VerboseError> {
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.plugin_user_metadata(&plugin_name, evaluate_conditions)
.map(|p| p.map(Into::into))
.map_err(Into::into)
}
#[napi]
pub fn set_plugin_user_metadata(
&mut self,
plugin_metadata: &PluginMetadata,
) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.set_plugin_user_metadata(plugin_metadata.clone().into());
Ok(())
}
#[napi]
pub fn discard_plugin_user_metadata(&self, plugin: String) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.discard_plugin_user_metadata(&plugin);
Ok(())
}
#[napi]
pub fn discard_all_user_metadata(&self) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.discard_all_user_metadata();
Ok(())
}
}
impl From<Arc<RwLock<libloot::Database>>> for Database {
fn from(value: Arc<RwLock<libloot::Database>>) -> Self {
Self(value)
}
}
#[napi]
#[derive(Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[repr(transparent)]
pub struct Vertex(libloot::Vertex);
#[napi]
impl Vertex {
#[napi(constructor)]
pub fn new(name: String) -> Self {
Self(libloot::Vertex::new(name))
}
#[napi(getter)]
pub fn name(&self) -> &str {
self.0.name()
}
#[napi(getter)]
pub fn out_edge_type(&self) -> Result<Option<EdgeType>, VerboseError> {
self.0
.out_edge_type()
.map(|e| e.try_into().map_err(Into::into))
.transpose()
}
#[napi(setter)]
pub fn set_out_edge_type(&mut self, out_edge_type: EdgeType) {
let out_edge_type = out_edge_type.into();
self.0.set_out_edge_type(out_edge_type);
}
}
impl From<libloot::Vertex> for Vertex {
fn from(value: libloot::Vertex) -> Self {
Self(value)
}
}
impl From<Vertex> for libloot::Vertex {
fn from(value: Vertex) -> Self {
value.0
}
}
#[napi]
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum EdgeType {
Hardcoded,
MasterFlag,
Master,
MasterlistRequirement,
UserRequirement,
MasterlistLoadAfter,
UserLoadAfter,
MasterlistGroup,
UserGroup,
RecordOverlap,
AssetOverlap,
TieBreak,
BlueprintMaster,
}
impl TryFrom<libloot::EdgeType> for EdgeType {
type Error = UnsupportedEnumValueError;
fn try_from(value: libloot::EdgeType) -> Result<Self, Self::Error> {
match value {
libloot::EdgeType::Hardcoded => Ok(EdgeType::Hardcoded),
libloot::EdgeType::MasterFlag => Ok(EdgeType::MasterFlag),
libloot::EdgeType::Master => Ok(EdgeType::Master),
libloot::EdgeType::MasterlistRequirement => Ok(EdgeType::MasterlistRequirement),
libloot::EdgeType::UserRequirement => Ok(EdgeType::UserRequirement),
libloot::EdgeType::MasterlistLoadAfter => Ok(EdgeType::MasterlistLoadAfter),
libloot::EdgeType::UserLoadAfter => Ok(EdgeType::UserLoadAfter),
libloot::EdgeType::MasterlistGroup => Ok(EdgeType::MasterlistGroup),
libloot::EdgeType::UserGroup => Ok(EdgeType::UserGroup),
libloot::EdgeType::RecordOverlap => Ok(EdgeType::RecordOverlap),
libloot::EdgeType::AssetOverlap => Ok(EdgeType::AssetOverlap),
libloot::EdgeType::TieBreak => Ok(EdgeType::TieBreak),
libloot::EdgeType::BlueprintMaster => Ok(EdgeType::BlueprintMaster),
_ => Err(UnsupportedEnumValueError),
}
}
}
impl From<EdgeType> for libloot::EdgeType {
fn from(value: EdgeType) -> Self {
match value {
EdgeType::Hardcoded => libloot::EdgeType::Hardcoded,
EdgeType::MasterFlag => libloot::EdgeType::MasterFlag,
EdgeType::Master => libloot::EdgeType::Master,
EdgeType::MasterlistRequirement => libloot::EdgeType::MasterlistRequirement,
EdgeType::UserRequirement => libloot::EdgeType::UserRequirement,
EdgeType::MasterlistLoadAfter => libloot::EdgeType::MasterlistLoadAfter,
EdgeType::UserLoadAfter => libloot::EdgeType::UserLoadAfter,
EdgeType::MasterlistGroup => libloot::EdgeType::MasterlistGroup,
EdgeType::UserGroup => libloot::EdgeType::UserGroup,
EdgeType::RecordOverlap => libloot::EdgeType::RecordOverlap,
EdgeType::AssetOverlap => libloot::EdgeType::AssetOverlap,
EdgeType::TieBreak => libloot::EdgeType::TieBreak,
EdgeType::BlueprintMaster => libloot::EdgeType::BlueprintMaster,
}
}
}
+63
View File
@@ -0,0 +1,63 @@
use libloot::{
error::{
ConditionEvaluationError, DatabaseLockPoisonError, GameHandleCreationError,
GroupsPathError, LoadOrderError, LoadOrderStateError, LoadPluginsError,
MetadataRetrievalError, PluginDataError, SortPluginsError,
},
metadata::error::{
LoadMetadataError, MultilingualMessageContentsError, RegexError, WriteMetadataError,
},
};
use libloot_ffi_errors::{fmt_error_chain, SystemError, UnsupportedEnumValueError};
#[derive(Debug)]
pub struct VerboseError(Box<dyn std::error::Error>);
impl std::fmt::Display for VerboseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt_error_chain(self.0.as_ref(), f)
}
}
macro_rules! box_from_error {
( $from_type:ident, $to_type:ident ) => {
impl From<$from_type> for $to_type {
fn from(value: $from_type) -> Self {
Self(Box::new(value))
}
}
};
}
box_from_error!(GameHandleCreationError, VerboseError);
box_from_error!(UnsupportedEnumValueError, VerboseError);
box_from_error!(DatabaseLockPoisonError, VerboseError);
box_from_error!(LoadPluginsError, VerboseError);
box_from_error!(SortPluginsError, VerboseError);
box_from_error!(LoadOrderStateError, VerboseError);
box_from_error!(LoadOrderError, VerboseError);
box_from_error!(LoadMetadataError, VerboseError);
box_from_error!(WriteMetadataError, VerboseError);
box_from_error!(ConditionEvaluationError, VerboseError);
box_from_error!(GroupsPathError, VerboseError);
box_from_error!(MetadataRetrievalError, VerboseError);
box_from_error!(MultilingualMessageContentsError, VerboseError);
box_from_error!(RegexError, VerboseError);
impl From<PluginDataError> for VerboseError {
fn from(value: PluginDataError) -> Self {
Self(Box::new(SystemError::from(value)))
}
}
impl From<VerboseError> for napi::Error {
fn from(value: VerboseError) -> Self {
napi::Error::from_reason(value.to_string())
}
}
impl From<VerboseError> for napi::JsError {
fn from(value: VerboseError) -> Self {
napi::JsError::from(napi::Error::from(value))
}
}
+197
View File
@@ -0,0 +1,197 @@
use std::path::Path;
use libloot_ffi_errors::UnsupportedEnumValueError;
use napi_derive::napi;
use crate::{database::Database, error::VerboseError, plugin::Plugin};
#[allow(non_camel_case_types)]
#[napi]
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum GameType {
tes4,
tes5,
fo3,
fonv,
fo4,
tes5se,
fo4vr,
tes5vr,
tes3,
starfield,
openmw,
}
impl TryFrom<libloot::GameType> for GameType {
type Error = UnsupportedEnumValueError;
fn try_from(value: libloot::GameType) -> Result<Self, Self::Error> {
match value {
libloot::GameType::TES4 => Ok(GameType::tes4),
libloot::GameType::TES5 => Ok(GameType::tes5),
libloot::GameType::FO3 => Ok(GameType::fo3),
libloot::GameType::FONV => Ok(GameType::fonv),
libloot::GameType::FO4 => Ok(GameType::fo4),
libloot::GameType::TES5SE => Ok(GameType::tes5se),
libloot::GameType::FO4VR => Ok(GameType::fo4vr),
libloot::GameType::TES5VR => Ok(GameType::tes5vr),
libloot::GameType::TES3 => Ok(GameType::tes3),
libloot::GameType::Starfield => Ok(GameType::starfield),
libloot::GameType::OpenMW => Ok(GameType::openmw),
_ => Err(UnsupportedEnumValueError),
}
}
}
impl From<GameType> for libloot::GameType {
fn from(value: GameType) -> Self {
match value {
GameType::tes4 => libloot::GameType::TES4,
GameType::tes5 => libloot::GameType::TES5,
GameType::fo3 => libloot::GameType::FO3,
GameType::fonv => libloot::GameType::FONV,
GameType::fo4 => libloot::GameType::FO4,
GameType::tes5se => libloot::GameType::TES5SE,
GameType::fo4vr => libloot::GameType::FO4VR,
GameType::tes5vr => libloot::GameType::TES5VR,
GameType::tes3 => libloot::GameType::TES3,
GameType::starfield => libloot::GameType::Starfield,
GameType::openmw => libloot::GameType::OpenMW,
}
}
}
#[napi]
#[derive(Debug)]
pub struct Game(libloot::Game);
#[napi]
impl Game {
#[napi(constructor)]
pub fn new(
game_type: GameType,
game_path: String,
local_path: Option<String>,
) -> Result<Self, VerboseError> {
match local_path {
Some(local_path) => Ok(Game(libloot::Game::with_local_path(
game_type.into(),
Path::new(&game_path),
Path::new(&local_path),
)?)),
None => Ok(Game(libloot::Game::new(
game_type.into(),
Path::new(&game_path),
)?)),
}
}
#[napi]
pub fn game_type(&self) -> Result<GameType, VerboseError> {
self.0.game_type().try_into().map_err(Into::into)
}
#[napi]
pub fn additional_data_paths(&self) -> Vec<String> {
self.0
.additional_data_paths()
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect()
}
#[napi]
pub fn set_additional_data_paths(&mut self, paths: Vec<String>) -> Result<(), VerboseError> {
self.0.set_additional_data_paths(&as_paths(&paths))?;
Ok(())
}
#[napi]
pub fn database(&self) -> Database {
self.0.database().into()
}
#[napi]
pub fn is_valid_plugin(&self, plugin_path: String) -> bool {
self.0.is_valid_plugin(Path::new(&plugin_path))
}
#[napi]
pub fn load_plugins(&mut self, plugin_paths: Vec<String>) -> Result<(), VerboseError> {
self.0.load_plugins(&as_paths(&plugin_paths))?;
Ok(())
}
#[napi]
pub fn load_plugin_headers(&mut self, plugin_paths: Vec<String>) -> Result<(), VerboseError> {
self.0.load_plugin_headers(&as_paths(&plugin_paths))?;
Ok(())
}
#[napi]
pub fn clear_loaded_plugins(&mut self) {
self.0.clear_loaded_plugins();
}
#[napi]
pub fn plugin(&self, plugin_name: String) -> Option<Plugin> {
self.0.plugin(&plugin_name).map(Into::into)
}
#[napi]
pub fn loaded_plugins(&self) -> Vec<Plugin> {
self.0
.loaded_plugins()
.into_iter()
.map(Into::into)
.collect()
}
#[napi]
pub fn sort_plugins(&self, plugin_names: Vec<String>) -> Result<Vec<String>, VerboseError> {
Ok(self.0.sort_plugins(&as_strs(&plugin_names))?)
}
#[napi]
pub fn load_current_load_order_state(&mut self) -> Result<(), VerboseError> {
self.0.load_current_load_order_state()?;
Ok(())
}
#[napi]
pub fn is_load_order_ambiguous(&self) -> Result<bool, VerboseError> {
Ok(self.0.is_load_order_ambiguous()?)
}
#[napi]
pub fn active_plugins_file_path(&self) -> String {
self.0
.active_plugins_file_path()
.to_string_lossy()
.to_string()
}
#[napi]
pub fn is_plugin_active(&self, plugin_name: String) -> bool {
self.0.is_plugin_active(&plugin_name)
}
#[napi]
pub fn load_order(&self) -> Vec<&str> {
self.0.load_order()
}
#[napi]
pub fn set_load_order(&mut self, load_order: Vec<String>) -> Result<(), VerboseError> {
self.0.set_load_order(&as_strs(&load_order))?;
Ok(())
}
}
fn as_paths(pathbufs: &[String]) -> Vec<&Path> {
pathbufs.iter().map(Path::new).collect()
}
fn as_strs(strings: &[String]) -> Vec<&str> {
strings.iter().map(String::as_ref).collect()
}
+100 -4
View File
@@ -1,9 +1,105 @@
#![deny(clippy::all)]
#[macro_use]
extern crate napi_derive;
mod database;
mod error;
mod game;
mod metadata;
mod plugin;
use napi::{
threadsafe_function::{ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode},
Either,
};
use napi_derive::napi;
pub use metadata::select_message_content;
#[napi]
pub fn sum(a: i32, b: i32) -> i32 {
a + b
pub fn is_compatible(major: u32, minor: u32, patch: u32) -> bool {
libloot::is_compatible(major, minor, patch)
}
#[napi]
pub fn libloot_revision() -> String {
libloot::libloot_revision()
}
#[napi]
pub fn libloot_version() -> String {
libloot::libloot_version()
}
#[napi]
pub const LIBLOOT_VERSION_MAJOR: u32 = libloot::LIBLOOT_VERSION_MAJOR;
#[napi]
pub const LIBLOOT_VERSION_MINOR: u32 = libloot::LIBLOOT_VERSION_MINOR;
#[napi]
pub const LIBLOOT_VERSION_PATCH: u32 = libloot::LIBLOOT_VERSION_PATCH;
#[napi]
pub enum LogLevel {
Trace,
Debug,
Info,
Warning,
Error,
Fatal,
}
impl From<LogLevel> for libloot::LogLevel {
fn from(value: LogLevel) -> Self {
match value {
LogLevel::Trace => libloot::LogLevel::Trace,
LogLevel::Debug => libloot::LogLevel::Debug,
LogLevel::Info => libloot::LogLevel::Info,
LogLevel::Warning => libloot::LogLevel::Warning,
LogLevel::Error => libloot::LogLevel::Error,
LogLevel::Fatal => libloot::LogLevel::Fatal,
}
}
}
impl From<libloot::LogLevel> for LogLevel {
fn from(value: libloot::LogLevel) -> Self {
match value {
libloot::LogLevel::Trace => LogLevel::Trace,
libloot::LogLevel::Debug => LogLevel::Debug,
libloot::LogLevel::Info => LogLevel::Info,
libloot::LogLevel::Warning => LogLevel::Warning,
libloot::LogLevel::Error => LogLevel::Error,
libloot::LogLevel::Fatal => LogLevel::Fatal,
}
}
}
#[napi]
pub fn set_log_level(level: LogLevel) {
libloot::set_log_level(level.into());
}
#[napi(ts_args_type = "callback: (logLevel: LogLevel, message: string) => void")]
pub fn set_logging_callback(callback: napi::JsFunction) -> napi::Result<()> {
let thread_safe_callback: ThreadsafeFunction<
(libloot::LogLevel, String),
ErrorStrategy::Fatal,
> = callback.create_threadsafe_function(0, |ctx| {
let (level, message): (libloot::LogLevel, String) = ctx.value;
Ok(vec![
Either::A::<LogLevel, _>(level.into()),
Either::B(message),
])
})?;
let rust_callback = move |level: libloot::LogLevel, message: &str| {
thread_safe_callback.call(
(level, message.to_owned()),
ThreadsafeFunctionCallMode::Blocking,
);
};
libloot::set_logging_callback(rust_callback);
Ok(())
}
File diff suppressed because it is too large Load Diff
+104
View File
@@ -0,0 +1,104 @@
use std::sync::Arc;
use napi_derive::napi;
use crate::error::VerboseError;
#[napi]
#[derive(Clone, Debug, Eq, PartialEq)]
#[repr(transparent)]
pub struct Plugin(Arc<libloot::Plugin>);
#[napi]
impl Plugin {
#[napi]
pub fn name(&self) -> &str {
self.0.name()
}
#[napi]
pub fn header_version(&self) -> Option<f32> {
self.0.header_version()
}
#[napi]
pub fn version(&self) -> Option<&str> {
self.0.version()
}
#[napi]
pub fn masters(&self) -> Result<Vec<String>, VerboseError> {
Ok(self.0.masters()?)
}
#[napi]
pub fn bash_tags(&self) -> Vec<String> {
self.0.bash_tags().to_vec()
}
#[napi]
pub fn crc(&self) -> Option<u32> {
self.0.crc()
}
#[napi]
pub fn is_master(&self) -> bool {
self.0.is_master()
}
#[napi]
pub fn is_light_plugin(&self) -> bool {
self.0.is_light_plugin()
}
#[napi]
pub fn is_medium_plugin(&self) -> bool {
self.0.is_medium_plugin()
}
#[napi]
pub fn is_update_plugin(&self) -> bool {
self.0.is_update_plugin()
}
#[napi]
pub fn is_blueprint_plugin(&self) -> bool {
self.0.is_blueprint_plugin()
}
#[napi]
pub fn is_valid_as_light_plugin(&self) -> Result<bool, VerboseError> {
Ok(self.0.is_valid_as_light_plugin()?)
}
#[napi]
pub fn is_valid_as_medium_plugin(&self) -> Result<bool, VerboseError> {
Ok(self.0.is_valid_as_medium_plugin()?)
}
#[napi]
pub fn is_valid_as_update_plugin(&self) -> Result<bool, VerboseError> {
Ok(self.0.is_valid_as_update_plugin()?)
}
#[napi]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[napi]
pub fn loads_archive(&self) -> bool {
self.0.loads_archive()
}
#[napi]
pub fn do_records_overlap(&self, plugin: &Plugin) -> Result<bool, VerboseError> {
Ok(self.0.do_records_overlap(&plugin.0)?)
}
}
impl From<Arc<libloot::Plugin>> for Plugin {
fn from(value: Arc<libloot::Plugin>) -> Self {
Self(value)
}
}