Skip to content

Commit 0eaab70

Browse files
committed
This is the basis for substantial changes to make it usable for the `gix-*` crates. The wonderful aspect about this is not only a great reduction in dependencies, but it also removes most proc-macro invocations.
1 parent 5ab19a7 commit 0eaab70

16 files changed

+846
-0
lines changed

gix-error/src/exn/Cargo.toml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Copyright 2025 FastLabs Developers
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
[package]
16+
name = "exn"
17+
version = "0.2.1"
18+
19+
description = "A context-aware concrete Error type built on `std::error::Error`."
20+
21+
edition.workspace = true
22+
homepage.workspace = true
23+
license.workspace = true
24+
readme.workspace = true
25+
repository.workspace = true
26+
27+
[package.metadata.docs.rs]
28+
all-features = true
29+
rustdoc-args = ["--cfg", "docsrs"]
30+
31+
[dependencies]
32+
33+
[dev-dependencies]
34+
insta = { workspace = true }
35+
36+
[lints]
37+
workspace = true

gix-error/src/exn/src/debug.rs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
// Copyright 2025 FastLabs Developers
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use std::fmt;
16+
use std::fmt::Formatter;
17+
18+
use crate::Error;
19+
use crate::Exn;
20+
use crate::Frame;
21+
22+
impl<E: Error> fmt::Debug for Exn<E> {
23+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
24+
write_exn(f, self.as_frame(), 0, "")
25+
}
26+
}
27+
28+
impl fmt::Debug for Frame {
29+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
30+
write_exn(f, self, 0, "")
31+
}
32+
}
33+
34+
fn write_exn(f: &mut Formatter<'_>, frame: &Frame, level: usize, prefix: &str) -> fmt::Result {
35+
write!(f, "{}", frame.as_error())?;
36+
write_location(f, frame)?;
37+
38+
let children = frame.children();
39+
let children_len = children.len();
40+
41+
for (i, child) in children.iter().enumerate() {
42+
write!(f, "\n{}|", prefix)?;
43+
write!(f, "\n{}|-> ", prefix)?;
44+
45+
let child_child_len = child.children().len();
46+
if level == 0 && children_len == 1 && child_child_len == 1 {
47+
write_exn(f, child, 0, prefix)?;
48+
} else if i < children_len - 1 {
49+
write_exn(f, child, level + 1, &format!("{}| ", prefix))?;
50+
} else {
51+
write_exn(f, child, level + 1, &format!("{} ", prefix))?;
52+
}
53+
}
54+
55+
Ok(())
56+
}
57+
58+
#[cfg(not(windows_test))]
59+
fn write_location(f: &mut Formatter<'_>, exn: &Frame) -> fmt::Result {
60+
let location = exn.location();
61+
write!(
62+
f,
63+
", at {}:{}:{}",
64+
location.file(),
65+
location.line(),
66+
location.column()
67+
)
68+
}
69+
70+
#[cfg(windows_test)]
71+
fn write_location(f: &mut Formatter<'_>, exn: &Frame) -> fmt::Result {
72+
let location = exn.location();
73+
use std::os::windows::ffi::OsStrExt;
74+
use std::path::Component;
75+
use std::path::MAIN_SEPARATOR;
76+
use std::path::Path;
77+
78+
let file = location.file();
79+
let path = Path::new(file);
80+
81+
let mut resolved = String::new();
82+
83+
for c in path.components() {
84+
match c {
85+
Component::RootDir => {}
86+
Component::CurDir => resolved.push('.'),
87+
Component::ParentDir => resolved.push_str(".."),
88+
Component::Prefix(prefix) => {
89+
resolved.push_str(&prefix.as_os_str().to_string_lossy());
90+
continue;
91+
}
92+
Component::Normal(s) => resolved.push_str(&s.to_string_lossy()),
93+
}
94+
resolved.push('/');
95+
}
96+
97+
if path.as_os_str().encode_wide().last() != Some(MAIN_SEPARATOR as u16)
98+
&& resolved != "/"
99+
&& resolved.ends_with('/')
100+
{
101+
resolved.pop(); // Pop last '/'
102+
}
103+
104+
let line = location.line();
105+
let column = location.column();
106+
107+
write!(f, ", at {resolved}:{line}:{column}")
108+
}

gix-error/src/exn/src/display.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright 2025 FastLabs Developers
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use std::fmt;
16+
17+
use crate::Error;
18+
use crate::Exn;
19+
20+
impl<E: Error> fmt::Display for Exn<E> {
21+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22+
write!(f, "{}", self.as_error())
23+
}
24+
}

gix-error/src/exn/src/impls.rs

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Copyright 2025 FastLabs Developers
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use std::fmt;
16+
use std::marker::PhantomData;
17+
use std::panic::Location;
18+
19+
use crate::Error;
20+
21+
/// An exception type that can hold an error tree and additional context.
22+
pub struct Exn<E: Error> {
23+
// trade one more indirection for less stack size
24+
frame: Box<Frame>,
25+
phantom: PhantomData<E>,
26+
}
27+
28+
impl<E: Error> From<E> for Exn<E> {
29+
#[track_caller]
30+
fn from(error: E) -> Self {
31+
Exn::new(error)
32+
}
33+
}
34+
35+
impl<E: Error> Exn<E> {
36+
/// Create a new exception with the given error.
37+
///
38+
/// This will automatically walk the [source chain of the error] and add them as children
39+
/// frames.
40+
///
41+
/// See also [`Error::raise`] for a fluent way to convert an error into an `Exn` instance.
42+
///
43+
/// [source chain of the error]: std::error::Error::source
44+
#[track_caller]
45+
pub fn new(error: E) -> Self {
46+
struct SourceError(String);
47+
48+
impl fmt::Debug for SourceError {
49+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50+
fmt::Debug::fmt(&self.0, f)
51+
}
52+
}
53+
54+
impl fmt::Display for SourceError {
55+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56+
fmt::Display::fmt(&self.0, f)
57+
}
58+
}
59+
60+
impl std::error::Error for SourceError {}
61+
62+
fn walk(error: &dyn std::error::Error, location: &'static Location<'static>) -> Vec<Frame> {
63+
if let Some(source) = error.source() {
64+
let children = vec![Frame {
65+
error: Box::new(SourceError(source.to_string())),
66+
location,
67+
children: walk(source, location),
68+
}];
69+
children
70+
} else {
71+
vec![]
72+
}
73+
}
74+
75+
let location = Location::caller();
76+
let children = walk(&error, location);
77+
let frame = Frame {
78+
error: Box::new(error),
79+
location,
80+
children,
81+
};
82+
83+
Self {
84+
frame: Box::new(frame),
85+
phantom: PhantomData,
86+
}
87+
}
88+
89+
/// Create a new exception with the given error and children.
90+
#[track_caller]
91+
pub fn from_iter<T, I>(children: I, err: E) -> Self
92+
where
93+
T: Error,
94+
I: IntoIterator,
95+
I::Item: Into<Exn<T>>,
96+
{
97+
let mut new_exn = Exn::new(err);
98+
for exn in children {
99+
let exn = exn.into();
100+
new_exn.frame.children.push(*exn.frame);
101+
}
102+
new_exn
103+
}
104+
105+
/// Raise a new exception; this will make the current exception a child of the new one.
106+
#[track_caller]
107+
pub fn raise<T: Error>(self, err: T) -> Exn<T> {
108+
let mut new_exn = Exn::new(err);
109+
new_exn.frame.children.push(*self.frame);
110+
new_exn
111+
}
112+
113+
/// Return the current exception.
114+
pub fn as_error(&self) -> &E {
115+
self.frame
116+
.as_any()
117+
.downcast_ref()
118+
.expect("error type must match")
119+
}
120+
121+
/// Return the underlying exception frame.
122+
pub fn as_frame(&self) -> &Frame {
123+
&self.frame
124+
}
125+
}
126+
127+
/// A frame in the exception tree.
128+
pub struct Frame {
129+
/// The error that occurred at this frame.
130+
error: Box<dyn Error>,
131+
/// The source code location where this exception frame was created.
132+
location: &'static Location<'static>,
133+
/// Child exception frames that provide additional context or source errors.
134+
children: Vec<Frame>,
135+
}
136+
137+
impl Frame {
138+
/// Return the error as a reference to [`std::any::Any`].
139+
pub fn as_any(&self) -> &dyn std::any::Any {
140+
&*self.error
141+
}
142+
143+
/// Return the error as a reference to [`std::error::Error`].
144+
pub fn as_error(&self) -> &dyn std::error::Error {
145+
&*self.error
146+
}
147+
148+
/// Return the source code location where this exception frame was created.
149+
pub fn location(&self) -> &'static Location<'static> {
150+
self.location
151+
}
152+
153+
/// Return a slice of the children of the exception.
154+
pub fn children(&self) -> &[Frame] {
155+
&self.children
156+
}
157+
}

0 commit comments

Comments
 (0)