use crate::dir_context::{PackDir, RepoKind};
use crate::pack::PackIdentity;
use crate::{Error, Result};
use simple_fs::SPath;
use std::str::FromStr;
#[derive(Debug, Clone, Default, PartialEq)]
pub enum PackRefSubPathScope {
#[default]
PackDir,
BaseSupport,
WorkspaceSupport,
}
#[derive(Debug, Clone, PartialEq)]
pub struct PackRef {
pub namespace: String,
pub name: String,
pub sub_path_scope: PackRefSubPathScope,
pub sub_path: Option<String>,
}
impl PackRef {
pub fn identity_as_path(&self) -> SPath {
SPath::new(format!("{}/{}", self.namespace, self.name))
}
}
pub fn looks_like_pack_ref(path: &SPath) -> bool {
let path_str = path.as_str();
path_str.contains('@') && !path_str.starts_with(".") && !path.is_absolute()
}
impl FromStr for PackRef {
type Err = Error;
fn from_str(full_ref: &str) -> Result<Self> {
let parts: Vec<&str> = full_ref.split('@').collect();
let (namespace_str, name_and_path_str) = match parts.len() {
1 => {
return Err(Error::InvalidPackIdentity {
origin_path: full_ref.to_string(),
cause: "No '@' sign".to_string(),
});
}
2 => {
let ns = parts[0];
let rest = parts[1];
if ns.is_empty() {
return Err(Error::custom(format!(
"Invalid pack reference format: '{}'. Namespace cannot be empty when '@' is present.",
full_ref
)));
}
if rest.is_empty() {
return Err(Error::custom(format!(
"Invalid pack reference format: '{}'. Pack name/path part cannot be empty after '@'.",
full_ref
)));
}
PackIdentity::validate_namespace(ns)?;
(ns, rest)
}
_ => {
return Err(Error::custom(format!(
"Invalid pack reference format: '{}'. Too many '@' symbols.",
full_ref
)));
}
};
let (name_and_scope_part, sub_path) = match name_and_path_str.split_once('/') {
Some((start, path)) => (start, if path.is_empty() { None } else { Some(path.to_string()) }),
None => (name_and_path_str, None),
};
let (name_str, sub_path_scope) = match name_and_scope_part.split_once('$') {
Some((name_part, scope_part)) => {
if name_part.is_empty() {
return Err(Error::custom(format!(
"Invalid pack reference format: '{}'. Pack name cannot be empty before '$'.",
full_ref
)));
}
let scope = match scope_part {
"base" => PackRefSubPathScope::BaseSupport,
"workspace" => PackRefSubPathScope::WorkspaceSupport,
"" => {
return Err(Error::custom(format!(
"Invalid pack reference scope in '{}'. Scope cannot be empty after '$'. Expected '$base' or '$workspace'",
full_ref
)));
}
_ => {
return Err(Error::custom(format!(
"Invalid pack reference scope in '{}'. Expected '$base' or '$workspace', found '${}'.",
full_ref, scope_part
)));
}
};
PackIdentity::validate_name(name_part)?;
(name_part, scope)
}
None => {
PackIdentity::validate_name(name_and_scope_part)?;
(name_and_scope_part, PackRefSubPathScope::PackDir)
}
};
if let Some(ref sp) = sub_path {
if sp.contains('$') {
return Err(Error::custom(format!(
"Invalid pack reference format: '{}'. Character '$' is not allowed in the sub-path.",
full_ref
)));
}
if sp.split('/').any(|part| part == "..") {
return Err(Error::custom(format!(
"Invalid pack reference format: '{}'. Sub-path cannot contain '..'.",
full_ref
)));
}
}
if name_str.is_empty() {
return Err(Error::custom(format!(
"Invalid pack reference format: '{}'. Pack name part cannot be empty.",
full_ref
)));
}
Ok(PackRef {
namespace: namespace_str.to_string(),
name: name_str.to_string(),
sub_path_scope,
sub_path,
})
}
}
impl std::fmt::Display for PackRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}@{}", self.namespace, self.name)?;
match self.sub_path_scope {
PackRefSubPathScope::BaseSupport => write!(f, "$base")?,
PackRefSubPathScope::WorkspaceSupport => write!(f, "$workspace")?,
PackRefSubPathScope::PackDir => {} }
if let Some(sub_path) = &self.sub_path {
write!(f, "/{}", sub_path)?;
}
Ok(())
}
}
#[allow(unused)]
#[derive(Debug, Clone)]
pub struct LocalPackRef {
pub identity: PackIdentity,
pub sub_path: Option<String>,
pub pack_dir: SPath,
pub repo_kind: RepoKind,
}
impl LocalPackRef {
pub fn from_partial(pack_dir: PackDir, partial: PackRef) -> Self {
let repo_kind = pack_dir.repo_kind;
let namespace = partial.namespace;
let pack_dir_path = pack_dir.path;
let identity = PackIdentity {
namespace,
name: partial.name,
};
Self {
identity,
sub_path: partial.sub_path,
pack_dir: pack_dir_path,
repo_kind,
}
}
}
#[allow(unused)]
impl LocalPackRef {
pub fn identity(&self) -> &PackIdentity {
&self.identity
}
pub fn namespace(&self) -> &str {
&self.identity.namespace
}
pub fn name(&self) -> &str {
&self.identity.name
}
pub fn sub_path(&self) -> Option<&str> {
self.sub_path.as_deref()
}
pub fn pack_dir(&self) -> &SPath {
&self.pack_dir
}
pub fn repo_kind(&self) -> RepoKind {
self.repo_kind
}
}
impl std::fmt::Display for LocalPackRef {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}@{}", self.namespace(), self.name())?;
if let Some(sub_path) = &self.sub_path {
write!(f, "/{}", sub_path)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
type Result<T> = core::result::Result<T, Box<dyn std::error::Error>>;
use crate::_test_support::assert_contains;
#[test]
fn test_pack_pack_ref_from_str_simple() -> Result<()> {
let data = [
("pro@coder", "pro", "coder", PackRefSubPathScope::PackDir, None),
(
"pro@coder/agent.yaml",
"pro",
"coder",
PackRefSubPathScope::PackDir,
Some("agent.yaml"),
),
(
"pro@coder/", "pro",
"coder",
PackRefSubPathScope::PackDir,
None,
),
(
"my-ns@pack_name-123",
"my-ns",
"pack_name-123",
PackRefSubPathScope::PackDir,
None,
),
(
"_ns@_name/_sub-path",
"_ns",
"_name",
PackRefSubPathScope::PackDir,
Some("_sub-path"),
),
(
"_ns@_name$workspace/_sub-path",
"_ns",
"_name",
PackRefSubPathScope::WorkspaceSupport,
Some("_sub-path"),
),
];
for (input, ns, name, scope, sub) in data {
let pref = PackRef::from_str(input)?;
assert_eq!(pref.namespace, ns, "Input: {}", input);
assert_eq!(pref.name, name, "Input: {}", input);
assert_eq!(pref.sub_path_scope, scope, "Input: {}", input);
assert_eq!(pref.sub_path.as_deref(), sub, "Input: {}", input);
}
Ok(())
}
#[test]
fn test_pack_pack_ref_from_str_with_scope() -> Result<()> {
let data = [
("pro@coder$base", "pro", "coder", PackRefSubPathScope::BaseSupport, None),
(
"pro@coder$base/data.json",
"pro",
"coder",
PackRefSubPathScope::BaseSupport,
Some("data.json"),
),
(
"pro@coder$workspace",
"pro",
"coder",
PackRefSubPathScope::WorkspaceSupport,
None,
),
(
"pro@coder$workspace/data.json",
"pro",
"coder",
PackRefSubPathScope::WorkspaceSupport,
Some("data.json"),
),
(
"pro@coder$base/", "pro",
"coder",
PackRefSubPathScope::BaseSupport,
None,
),
(
"my-ns@pack_name$base/file-1",
"my-ns",
"pack_name",
PackRefSubPathScope::BaseSupport,
Some("file-1"),
),
];
for (input, ns, name, scope, sub) in data {
let pref = PackRef::from_str(input)?;
assert_eq!(pref.namespace, ns, "Input: {}", input);
assert_eq!(pref.name, name, "Input: {}", input);
assert_eq!(pref.sub_path_scope, scope, "Input: {}", input);
assert_eq!(pref.sub_path.as_deref(), sub, "Input: {}", input);
assert_eq!(
pref.to_string(),
input.trim_end_matches('/'),
"Display mismatch for: {}",
input
);
}
Ok(())
}
#[test]
fn test_pack_pack_ref_from_str_invalids() -> Result<()> {
let data = &[
("", "No '@' sign"), ("@", "Namespace cannot be empty"), ("ns@", "Pack name/path part cannot be empty after '@'."), ("@name", "Namespace cannot be empty"), ("pro@coder$invalid/data.json", "Invalid pack reference scope"), ("pro@coder$baseExtra/data.json", "Invalid pack reference scope"), ("pro@coder$base/data$json", "'$' is not allowed in the sub-path"), ("pro@coder@", "Too many '@' symbols."), ("pro@@coder", "Too many '@' symbols."), ("pro@coder$/sub", "Scope cannot be empty after '$'"), ("pro@coder$ /sub", "Invalid pack reference scope"), ("ns@$base", "Pack name cannot be empty before '$'."), ("$base", "No '@' sign"), ("/", "No '@' sign"), ("ns@/", "Pack name cannot be empty"), ("ns@$base/", "Pack name cannot be empty before '$'."), ("n space@coder", "namespace can only contain"),
("ns@co der", "name can only contain"),
("n+s@coder", "namespace can only contain"),
("ns@co+der", "name can only contain"),
("n$s@coder", "namespace can only contain"), ("ns@co=der", "name can only contain"),
("1ns@coder", "namespace can only contain"), ("ns@1coder", "name can only contain"), ("ns@coder/sub/../path", "Sub-path cannot contain '..'"), ("ns@coder/../sub/path", "Sub-path cannot contain '..'"), ("ns@coder/sub/path/..", "Sub-path cannot contain '..'"), ("ns@coder/..", "Sub-path cannot contain '..'"), ];
for (invalid_input, expected_error) in data {
let result = PackRef::from_str(invalid_input);
assert!(result.is_err(), "Should fail for invalid input: '{}'", invalid_input);
if let Err(err) = result {
assert_contains(&err.to_string(), expected_error);
} else {
panic!("Input '{}' should have failed but succeeded.", invalid_input);
}
}
Ok(())
}
}