Skip to content

Commit

Permalink
WIP: Implement config feature
Browse files Browse the repository at this point in the history
  • Loading branch information
SpriteOvO committed Nov 14, 2023
1 parent fd2c3cf commit 94fb844
Show file tree
Hide file tree
Showing 15 changed files with 618 additions and 20 deletions.
4 changes: 4 additions & 0 deletions spdlog/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ release-level-info = []
release-level-debug = []
release-level-trace = []

config = ["serde", "erased-serde"]
source-location = []
native = []
libsystemd = ["libsystemd-sys"]
Expand All @@ -45,11 +46,13 @@ cfg-if = "1.0.0"
chrono = "0.4.22"
crossbeam = { version = "0.8.2", optional = true }
dyn-clone = "1.0.14"
erased-serde = { version = "0.3.31", optional = true }
flexible-string = { version = "0.1.0", optional = true }
if_chain = "1.0.2"
is-terminal = "0.4"
log = { version = "0.4.8", optional = true }
once_cell = "1.16.0"
serde = { version = "1.0.163", optional = true }
spdlog-internal = { version = "=0.1.0", path = "../spdlog-internal", optional = true }
spdlog-macros = { version = "0.1.0", path = "../spdlog-macros" }
spin = "0.9.8"
Expand Down Expand Up @@ -81,6 +84,7 @@ tracing = "=0.1.37"
tracing-subscriber = "=0.3.16"
tracing-appender = "=0.2.2"
paste = "1.0.14"
toml = "0.8.6"

[build-dependencies]
rustc_version = "0.4.0"
Expand Down
24 changes: 24 additions & 0 deletions spdlog/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
mod registry;
mod source;

pub(crate) mod parse;

pub use registry::*;
use serde::{de::DeserializeOwned, Deserialize};
pub use source::*;

use crate::{sync::*, Result};

// TODO: Force `'static` on name?
// Builder?
#[derive(PartialEq, Eq, Hash)]
pub struct ComponentMetadata<'a> {
pub(crate) name: &'a str,
}

pub trait Configurable: Sized {
type Params: DeserializeOwned + Default + Send;

fn metadata() -> ComponentMetadata<'static>;
fn build(params: Self::Params) -> Result<Self>;
}
53 changes: 53 additions & 0 deletions spdlog/src/config/parse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use erased_serde::Deserializer as ErasedDeserializer;
use serde::{
de::{
value::{MapAccessDeserializer, UnitDeserializer},
Error as SerdeDeError, MapAccess, Visitor,
},
Deserializer,
};

use crate::{config, formatter::*};

pub fn formatter_deser<'de, D>(de: D) -> Result<Option<Box<dyn Formatter>>, D::Error>
where
D: Deserializer<'de>,
{
struct ParseVisitor;

impl<'de> Visitor<'de> for ParseVisitor {
type Value = Box<dyn Formatter>;

fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a spdlog-rs component")
}

fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: MapAccess<'de>,
{
let name = map
.next_entry::<String, String>()?
.filter(|(key, _)| key == "name")
.map(|(_, value)| value)
.ok_or_else(|| SerdeDeError::missing_field("name"))?;

let remaining_args = map.size_hint().unwrap(); // I don't know what situation it will be `None``

let formatter = if remaining_args == 0 {
let mut erased_de =
<dyn ErasedDeserializer>::erase(UnitDeserializer::<A::Error>::new());
config::registry().build_formatter(&name, &mut erased_de)
} else {
let mut erased_de =
<dyn ErasedDeserializer>::erase(MapAccessDeserializer::new(map));
config::registry().build_formatter(&name, &mut erased_de)
}
.map_err(|err| SerdeDeError::custom(err))?;

Ok(formatter)
}
}

Ok(Some(de.deserialize_map(ParseVisitor)?))
}
276 changes: 276 additions & 0 deletions spdlog/src/config/registry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
use std::collections::HashMap;

use erased_serde::Deserializer as ErasedDeserializer;

use super::ComponentMetadata;
use crate::{
config::Configurable,
error::ConfigError,
formatter::{Formatter, FullFormatter, PatternFormatter, RuntimePattern},
sink::*,
sync::*,
Error, Result, Sink,
};

type StdResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;

// https://github.com/dtolnay/erased-serde/issues/97
mod erased_serde_ext {
use erased_serde::Result;
use serde::de::Deserialize;

use super::*;

pub trait ErasedDeserialize<'a> {
fn erased_deserialize_in_place(
&mut self,
de: &mut dyn ErasedDeserializer<'a>,
) -> Result<()>;
}

pub trait ErasedDeserializeOwned: for<'a> ErasedDeserialize<'a> {}

impl<T: for<'a> ErasedDeserialize<'a>> ErasedDeserializeOwned for T {}

impl<'a, T: Deserialize<'a>> ErasedDeserialize<'a> for T {
fn erased_deserialize_in_place(
&mut self,
de: &mut dyn ErasedDeserializer<'a>,
) -> Result<()> {
Deserialize::deserialize_in_place(de, self)
}
}
}
use erased_serde_ext::*;

type ComponentDeser<C: Configurable> = fn(de: &mut dyn ErasedDeserializer) -> Result<C>;

type RegisteredComponents<C: Configurable> = HashMap<&'static str, ComponentDeser<C>>;

pub struct Registry {
sink: Mutex<RegisteredComponents<Box<dyn Sink>>>,
formatter: Mutex<RegisteredComponents<Box<dyn Formatter>>>,

// TODO: Consider make them compile-time
builtin_sink: Mutex<RegisteredComponents<Box<dyn Sink>>>,
builtin_formatter: Mutex<RegisteredComponents<Box<dyn Formatter>>>,
}

impl Registry {
pub fn register_sink<S>(&mut self) -> Result<()>
where
S: Sink + Configurable + 'static,
{
self.register_sink_inner::<S>()
}

pub fn register_formatter<F>(&mut self) -> Result<()>
where
F: Formatter + Configurable + 'static,
{
self.register_formatter_inner::<F>()
}
}

impl Registry {
pub(crate) fn with_builtin() -> Self {
let mut registry = Self {
sink: Mutex::new(HashMap::new()),
formatter: Mutex::new(HashMap::new()),
builtin_sink: Mutex::new(HashMap::new()),
builtin_formatter: Mutex::new(HashMap::new()),
};
registry.register_builtin().unwrap(); // Builtin components should not fail to register
registry
}

fn register_builtin(&mut self) -> Result<()> {
self.register_builtin_sink::<FileSink>()?;
self.register_builtin_formatter::<FullFormatter>()?;
self.register_builtin_formatter::<PatternFormatter<RuntimePattern>>()?;
Ok(())
}

pub(crate) fn build_sink(
&self,
name: &str,
de: &mut dyn ErasedDeserializer,
) -> Result<Box<dyn Sink>> {
self.builtin_sink
.lock_expect()
.get(name)
.ok_or_else(|| Error::Config(ConfigError::UnknownComponent(name.to_string())))
.and_then(|f| f(de))
}

pub(crate) fn build_formatter(
&self,
name: &str,
de: &mut dyn ErasedDeserializer,
) -> Result<Box<dyn Formatter>> {
self.builtin_formatter
.lock_expect()
.get(name)
.ok_or_else(|| Error::Config(ConfigError::UnknownComponent(name.to_string())))
.and_then(|f| f(de))
}

impl_registers! {
fn register_sink_inner => sink, Sink,
fn register_formatter_inner => formatter, Formatter,
pub(crate) fn register_builtin_sink => builtin_sink, Sink,
pub(crate) fn register_builtin_formatter => builtin_formatter, Formatter,
}
}

// TODO: Append prefix '$' for custom components
macro_rules! impl_registers {
( $($vis:vis fn $fn_name:ident => $var:ident, $trait:ident),+ $(,)? ) => {
$($vis fn $fn_name<C>(&mut self) -> Result<()>
where
C: $trait + Configurable + 'static,
{
let f = |de: &mut dyn ErasedDeserializer| -> Result<Box<dyn $trait>> {
let mut params = C::Params::default();
params.erased_deserialize_in_place(de).unwrap(); // TODO: Wrong input will trigger a Err, handle it!
Ok(Box::new(C::build(params)?))
};

self.$var
.lock_expect()
.insert(C::metadata().name, f)
.map_or(Ok(()), |_| Err(Error::Config(ConfigError::MultipleRegistration)))
})+
};
}
use impl_registers;

// TODO: Consider removing the `'static` lifetime. Maybe using `Arc<>`?
pub(crate) fn registry() -> &'static Registry {
static REGISTRY: Lazy<Registry> = Lazy::new(Registry::with_builtin);
&REGISTRY
}

#[cfg(test)]
mod tests {
use std::fmt::Write;

use serde::Deserializer;

use super::*;
use crate::{formatter::FmtExtraInfo, prelude::*, ErrorHandler, Record, StringBuf};

pub struct MockSink(i32);

impl Sink for MockSink {
fn log(&self, _record: &Record) -> Result<()> {
unimplemented!()
}

fn flush(&self) -> Result<()> {
unimplemented!()
}

fn level_filter(&self) -> LevelFilter {
unimplemented!()
}

fn set_level_filter(&self, _level_filter: LevelFilter) {
unimplemented!()
}

fn set_formatter(&self, _formatter: Box<dyn Formatter>) {
unimplemented!()
}

fn set_error_handler(&self, handler: Option<ErrorHandler>) {
handler.unwrap()(Error::__ForInternalTestsUseOnly(self.0))
}
}

#[derive(Default, serde::Deserialize)]
pub struct MockParams {
arg: i32,
}

impl Configurable for MockSink {
type Params = MockParams;

fn metadata() -> ComponentMetadata<'static> {
ComponentMetadata { name: "MockSink" }
}

fn build(params: Self::Params) -> Result<Self> {
Ok(Self(params.arg))
}
}

#[derive(Clone)]
pub struct MockFormatter(i32);

impl Formatter for MockFormatter {
fn format(&self, _record: &Record, dest: &mut StringBuf) -> Result<FmtExtraInfo> {
write!(dest, "{}", self.0).unwrap();
Ok(FmtExtraInfo::new())
}

fn clone_box(&self) -> Box<dyn Formatter> {
Box::new(self.clone())
}
}

impl Configurable for MockFormatter {
type Params = MockParams;

fn metadata() -> ComponentMetadata<'static> {
ComponentMetadata {
name: "MockFormatter",
}
}

fn build(params: Self::Params) -> Result<Self> {
Ok(Self(params.arg))
}
}

fn registry_for_test() -> Registry {
let mut registry = Registry::with_builtin();
registry.register_sink::<MockSink>().unwrap();
registry.register_formatter::<MockFormatter>().unwrap();
registry
}

#[test]
fn build_sink_from_params() {
let registry = registry_for_test();

let mut erased_de =
<dyn ErasedDeserializer>::erase(toml::Deserializer::new("arg = 114514"));
let sink = registry.build_sink("MockSink", &mut erased_de).unwrap();
sink.set_error_handler(Some(|err| {
assert!(matches!(err, Error::__ForInternalTestsUseOnly(114514)))
}));

// TODO: test wrong kind
}

#[test]
fn build_formatter_from_params() {
let registry = registry_for_test();

let mut erased_de =
<dyn ErasedDeserializer>::erase(toml::Deserializer::new("arg = 1919810"));
let formatter = registry
.build_formatter("MockFormatter", &mut erased_de)
.unwrap();
let mut dest = StringBuf::new();
formatter
.format(&Record::new(Level::Info, ""), &mut dest)
.unwrap();
assert_eq!(dest, "1919810")

// TODO: test wrong kind
}

// TODO: Test custom components
}
Loading

0 comments on commit 94fb844

Please sign in to comment.