Skip to main content

dissolve_python/
domain_types.rs

1// Copyright (C) 2024 Jelmer Vernooij <jelmer@samba.org>
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//! Domain-specific types for better type safety and clarity
16
17use serde::{Deserialize, Serialize};
18use std::fmt::{Display, Formatter, Result as FmtResult};
19use std::str::FromStr;
20
21/// Represents a Python module name (e.g., "mypackage.submodule")
22#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct ModuleName(String);
24
25impl ModuleName {
26    pub fn new(name: impl Into<String>) -> Self {
27        Self(name.into())
28    }
29
30    pub fn as_str(&self) -> &str {
31        &self.0
32    }
33
34    pub fn into_string(self) -> String {
35        self.0
36    }
37
38    /// Check if this module is a parent of another module
39    pub fn is_parent_of(&self, other: &ModuleName) -> bool {
40        other.0.starts_with(&self.0)
41            && other.0.len() > self.0.len()
42            && other.0.chars().nth(self.0.len()) == Some('.')
43    }
44
45    /// Get the parent module name (returns None for root modules)
46    pub fn parent(&self) -> Option<ModuleName> {
47        self.0
48            .rfind('.')
49            .map(|pos| ModuleName(self.0[..pos].to_string()))
50    }
51}
52
53impl Display for ModuleName {
54    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
55        write!(f, "{}", self.0)
56    }
57}
58
59impl FromStr for ModuleName {
60    type Err = &'static str;
61
62    fn from_str(s: &str) -> Result<Self, Self::Err> {
63        if s.is_empty() {
64            Err("Module name cannot be empty")
65        } else {
66            Ok(ModuleName(s.to_string()))
67        }
68    }
69}
70
71/// Represents a Python function name (e.g., "calculate_total")
72#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
73pub struct FunctionName(String);
74
75impl FunctionName {
76    pub fn new(name: impl Into<String>) -> Self {
77        Self(name.into())
78    }
79
80    pub fn as_str(&self) -> &str {
81        &self.0
82    }
83
84    pub fn into_string(self) -> String {
85        self.0
86    }
87}
88
89impl Display for FunctionName {
90    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
91        write!(f, "{}", self.0)
92    }
93}
94
95impl FromStr for FunctionName {
96    type Err = &'static str;
97
98    fn from_str(s: &str) -> Result<Self, Self::Err> {
99        if s.is_empty() {
100            Err("Function name cannot be empty")
101        } else {
102            Ok(FunctionName(s.to_string()))
103        }
104    }
105}
106
107/// Represents a fully qualified function name (e.g., "mymodule.MyClass.method")
108#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
109pub struct QualifiedName {
110    pub module: ModuleName,
111    pub function: FunctionName,
112}
113
114impl QualifiedName {
115    pub fn new(module: ModuleName, function: FunctionName) -> Self {
116        Self { module, function }
117    }
118
119    pub fn from_string(qualified_name: &str) -> Result<Self, &'static str> {
120        if let Some(last_dot) = qualified_name.rfind('.') {
121            let module_part = &qualified_name[..last_dot];
122            let function_part = &qualified_name[last_dot + 1..];
123
124            Ok(QualifiedName {
125                module: ModuleName::new(module_part),
126                function: FunctionName::new(function_part),
127            })
128        } else {
129            Err("Qualified name must contain at least one dot")
130        }
131    }
132
133    pub fn to_string(&self) -> String {
134        format!("{}.{}", self.module, self.function)
135    }
136}
137
138impl Display for QualifiedName {
139    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
140        write!(f, "{}.{}", self.module, self.function)
141    }
142}
143
144/// Represents a source file path in a type-safe way
145#[derive(Debug, Clone, PartialEq, Eq, Hash)]
146pub struct SourcePath(std::path::PathBuf);
147
148impl SourcePath {
149    pub fn new(path: impl Into<std::path::PathBuf>) -> Self {
150        Self(path.into())
151    }
152
153    pub fn as_path(&self) -> &std::path::Path {
154        &self.0
155    }
156
157    pub fn into_path_buf(self) -> std::path::PathBuf {
158        self.0
159    }
160
161    pub fn to_string_lossy(&self) -> std::borrow::Cow<str> {
162        self.0.to_string_lossy()
163    }
164}
165
166impl Display for SourcePath {
167    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
168        write!(f, "{}", self.0.display())
169    }
170}
171
172// Re-export Version from core::types to avoid duplication
173pub use crate::core::types::Version;
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_module_name_parent() {
181        let module = ModuleName::new("parent.child.grandchild");
182        let parent = module.parent().unwrap();
183        assert_eq!(parent.as_str(), "parent.child");
184
185        let grandparent = parent.parent().unwrap();
186        assert_eq!(grandparent.as_str(), "parent");
187
188        assert!(grandparent.parent().is_none());
189    }
190
191    #[test]
192    fn test_module_is_parent_of() {
193        let parent = ModuleName::new("parent");
194        let child = ModuleName::new("parent.child");
195        let unrelated = ModuleName::new("other");
196
197        assert!(parent.is_parent_of(&child));
198        assert!(!parent.is_parent_of(&unrelated));
199        assert!(!child.is_parent_of(&parent));
200    }
201
202    #[test]
203    fn test_qualified_name_from_string() {
204        let qname = QualifiedName::from_string("module.submodule.function").unwrap();
205        assert_eq!(qname.module.as_str(), "module.submodule");
206        assert_eq!(qname.function.as_str(), "function");
207
208        assert!(QualifiedName::from_string("nomodule").is_err());
209    }
210}