use std::path::{Path, PathBuf};
use syn::{Expr, ExprLit, Ident, ItemMod, Lit, Meta};
trait ModPath {
fn is_mod_file(&self) -> bool;
}
impl ModPath for Path {
fn is_mod_file(&self) -> bool {
self.file_name().map(|s| s == "mod.rs").unwrap_or_default()
}
}
#[derive(Debug, Clone, Default)]
pub struct ModContext(Vec<ModSegment>);
impl ModContext {
pub fn push(&mut self, value: ModSegment) {
self.0.push(value);
}
pub fn pop(&mut self) -> Option<ModSegment> {
self.0.pop()
}
pub fn relative_to(&self, base: &Path, root: bool) -> Vec<PathBuf> {
let mut parent = base.to_path_buf();
parent.pop();
if root || base.is_mod_file() {
self.to_path_bufs()
.into_iter()
.map(|end| parent.clone().join(end))
.collect()
} else {
parent = parent.join(base.file_stem().unwrap());
self.to_path_bufs()
.into_iter()
.map(|end| parent.clone().join(end))
.collect()
}
}
fn to_path_bufs(&self) -> Vec<PathBuf> {
let mut buf = PathBuf::new();
for item in &self.0 {
buf.push(PathBuf::from(item.clone()));
}
if !self.is_last_ident() {
return vec![buf];
}
let mut inline = buf.clone();
inline.set_extension("rs");
vec![inline, buf.join("mod.rs")]
}
fn is_last_ident(&self) -> bool {
self.0.last().map(|seg| seg.is_ident()).unwrap_or_default()
}
}
impl From<Vec<ModSegment>> for ModContext {
fn from(segments: Vec<ModSegment>) -> Self {
Self(segments)
}
}
#[derive(Debug, Clone)]
pub enum ModSegment {
Ident(Ident),
Path(PathBuf),
}
impl ModSegment {
pub fn is_ident(&self) -> bool {
match self {
ModSegment::Ident(_) => true,
ModSegment::Path(_) => false,
}
}
pub fn is_path(&self) -> bool {
!self.is_ident()
}
}
#[cfg(test)]
impl ModSegment {
pub(self) fn new_ident(ident: &'static str) -> Self {
ModSegment::Ident(syn::Ident::new(ident, proc_macro2::Span::call_site()))
}
pub(self) fn new_path(path: &'static str) -> Self {
ModSegment::Path(PathBuf::from(path))
}
}
impl From<&ItemMod> for ModSegment {
fn from(v: &ItemMod) -> Self {
for attr in &v.attrs {
if let Meta::NameValue(ref name_value) = attr.meta {
if name_value.path.is_ident("path") {
if let Expr::Lit(ExprLit {
lit: Lit::Str(ref path_value),
..
}) = name_value.value
{
return ModSegment::Path(path_value.value().into());
}
}
}
}
ModSegment::Ident(v.ident.clone())
}
}
impl From<&mut ItemMod> for ModSegment {
fn from(v: &mut ItemMod) -> Self {
ModSegment::from(&*v)
}
}
impl From<ModSegment> for PathBuf {
fn from(seg: ModSegment) -> Self {
match seg {
ModSegment::Path(buf) => buf,
ModSegment::Ident(ident) => ident.to_string().into(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn relative_to_lib() {
let ctx = ModContext::from(vec![
ModSegment::new_ident("threads"),
ModSegment::new_ident("local"),
]);
assert_eq!(
ctx.relative_to(&Path::new("/src/lib.rs"), true),
vec![
Path::new("/src/threads/local.rs"),
Path::new("/src/threads/local/mod.rs"),
]
);
}
#[test]
fn relative_to_mod() {
let ctx = ModContext::from(vec![
ModSegment::new_ident("threads"),
ModSegment::new_ident("local"),
]);
assert_eq!(
ctx.relative_to(&Path::new("/src/runner/mod.rs"), false),
vec![
Path::new("/src/runner/threads/local.rs"),
Path::new("/src/runner/threads/local/mod.rs"),
]
);
}
#[test]
fn relative_to_2018_mod() {
let ctx = ModContext::from(vec![
ModSegment::new_ident("threads"),
ModSegment::new_ident("local"),
]);
assert_eq!(
ctx.relative_to(&Path::new("/src/runner.rs"), false),
vec![
Path::new("/src/runner/threads/local.rs"),
Path::new("/src/runner/threads/local/mod.rs"),
]
);
}
#[test]
fn relative_to_non_standard_root() {
let ctx = ModContext::from(vec![
ModSegment::new_ident("threads"),
ModSegment::new_ident("local"),
]);
assert_eq!(
ctx.relative_to(&Path::new("/src/runner.rs"), true),
vec![
Path::new("/src/threads/local.rs"),
Path::new("/src/threads/local/mod.rs"),
]
);
}
#[test]
fn relative_to_paths() {
let ctx = ModContext::from(vec![
ModSegment::new_path("threads"),
ModSegment::new_path("tls.rs"),
]);
assert_eq!(
ctx.relative_to(&Path::new("/src/lib.rs"), true),
vec![Path::new("/src/threads/tls.rs")]
);
}
#[test]
fn relative_to_path_around_ident() {
let ctx = ModContext::from(vec![
ModSegment::new_path("threads"),
ModSegment::new_ident("tls"),
]);
assert_eq!(
ctx.relative_to(&Path::new("/src/lib.rs"), true),
vec![
Path::new("/src/threads/tls.rs"),
Path::new("/src/threads/tls/mod.rs"),
]
);
}
}