pub(crate) use super::rails_inflector::{camelize, pluralize, underscore};
pub(crate) const RUBY_VISIBILITY_REFERENCE_PREFIX: &str = "ruby-visibility:";
pub(crate) const RUBY_CUSTOM_INFLECTIONS_REFERENCE: &str = "ruby-inflections:custom";
pub(crate) const RAILS_ISOLATE_NAMESPACE_PREFIX: &str = "rails-isolate-namespace:";
pub(crate) const RAILS_ROUTES_ENGINE_PREFIX: &str = "rails-routes-engine:";
pub(crate) const RAILS_ROUTES_DRAW_PREFIX: &str = "rails-routes-draw:";
pub(crate) const RAILS_ROUTES_UNSUPPORTED_PREFIX: &str = "rails-routes-unsupported:";
pub(crate) const RAILS_API_ONLY_REFERENCE: &str = "rails-api-only";
pub(crate) fn is_rails_evidence(name: &str) -> bool {
name == RUBY_CUSTOM_INFLECTIONS_REFERENCE
|| name == RAILS_API_ONLY_REFERENCE
|| [
RUBY_VISIBILITY_REFERENCE_PREFIX,
RAILS_ISOLATE_NAMESPACE_PREFIX,
RAILS_ROUTES_ENGINE_PREFIX,
RAILS_ROUTES_DRAW_PREFIX,
RAILS_ROUTES_UNSUPPORTED_PREFIX,
]
.iter()
.any(|prefix| name.starts_with(prefix))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RouteFile<'a> {
Main {
root: &'a str,
},
Drawn {
root: &'a str,
name: &'a str,
},
Inflections,
Application {
root: &'a str,
},
}
fn split<'a>(path: &'a str, marker: &str) -> Option<(&'a str, &'a str)> {
if let Some(rest) = path.strip_prefix(marker) {
return Some(("", rest));
}
let index = path.find(&format!("/{marker}"))?;
Some((&path[..=index], &path[index + 1 + marker.len()..]))
}
pub(crate) fn route_file(path: &str) -> Option<RouteFile<'_>> {
if let Some((root, "")) = split(path, "config/routes.rb") {
return Some(RouteFile::Main { root });
}
if let Some((root, rest)) = split(path, "config/routes/") {
return rest
.strip_suffix(".rb")
.filter(|name| !name.is_empty())
.map(|name| RouteFile::Drawn { root, name });
}
if let Some((_, "")) = split(path, "config/initializers/inflections.rb") {
return Some(RouteFile::Inflections);
}
if let Some((root, "")) = split(path, "config/application.rb") {
return Some(RouteFile::Application { root });
}
None
}
pub(crate) fn route_root(path: &str) -> Option<&str> {
match route_file(path)? {
RouteFile::Main { root } | RouteFile::Drawn { root, .. } => Some(root),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RouteTarget<'a> {
pub unless_api_only: bool,
pub absolute: bool,
pub constant: &'a str,
pub action: &'a str,
}
pub(crate) fn parse_route_target(name: &str) -> Option<RouteTarget<'_>> {
let (unless_api_only, rest) = match name.strip_prefix('~') {
Some(rest) => (true, rest),
None => (false, name),
};
let (absolute, rest) = match rest.strip_prefix("::") {
Some(rest) => (true, rest),
None => (false, rest),
};
let (constant, action) = rest.split_once('#')?;
Some(RouteTarget {
unless_api_only,
absolute,
constant,
action,
})
}
impl RouteTarget<'_> {
pub(crate) fn format(&self) -> String {
format!(
"{}{}{}#{}",
if self.unless_api_only { "~" } else { "" },
if self.absolute { "::" } else { "" },
self.constant,
self.action
)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn classifies_route_files_under_any_application_root() {
assert_eq!(
route_file("config/routes.rb"),
Some(RouteFile::Main { root: "" })
);
assert_eq!(
route_file("engines/blog/config/routes.rb"),
Some(RouteFile::Main {
root: "engines/blog/"
})
);
assert_eq!(
route_file("config/routes/admin/users.rb"),
Some(RouteFile::Drawn {
root: "",
name: "admin/users"
})
);
assert_eq!(
route_file("web/config/initializers/inflections.rb"),
Some(RouteFile::Inflections)
);
assert_eq!(
route_file("web/config/application.rb"),
Some(RouteFile::Application { root: "web/" })
);
for path in [
"config/routes.rb.bak",
"myconfig/routes.rb",
"config/routes/.rb",
"config/routes/notes.erb",
"app/config.rb",
] {
assert_eq!(route_file(path), None, "{path}");
}
}
#[test]
fn route_targets_round_trip() {
for name in ["NotesController#show", "~::Admin::NotesController#edit"] {
assert_eq!(parse_route_target(name).unwrap().format(), name);
}
assert!(parse_route_target("NotesController").is_none());
}
}