uv_migrator/utils/
author.rs

1use crate::error::{Error, Result};
2use crate::migrators::setup_py::SetupPyMigrationSource;
3use std::path::Path;
4use toml_edit::DocumentMut;
5
6#[derive(Debug)]
7pub struct Author {
8    pub name: String,
9    pub email: Option<String>,
10}
11
12pub fn extract_authors_from_setup_py(project_dir: &Path) -> Result<Vec<Author>> {
13    let setup_py_path = project_dir.join("setup.py");
14    if !setup_py_path.exists() {
15        return Ok(vec![]);
16    }
17
18    let content = std::fs::read_to_string(&setup_py_path).map_err(|e| Error::FileOperation {
19        path: setup_py_path.clone(),
20        message: format!("Failed to read setup.py: {}", e),
21    })?;
22
23    let mut authors = Vec::new();
24
25    // Extract author and author_email from setup()
26    if let Some(start_idx) = content.find("setup(") {
27        let bracket_content = SetupPyMigrationSource::extract_setup_content(&content[start_idx..])?;
28
29        if let Some(name) = SetupPyMigrationSource::extract_parameter(&bracket_content, "author") {
30            let email = SetupPyMigrationSource::extract_parameter(&bracket_content, "author_email");
31            authors.push(Author { name, email });
32        }
33    }
34
35    Ok(authors)
36}
37
38pub fn extract_authors_from_poetry(project_dir: &Path) -> Result<Vec<Author>> {
39    let old_pyproject_path = project_dir.join("old.pyproject.toml");
40    if !old_pyproject_path.exists() {
41        return Ok(vec![]);
42    }
43
44    let content =
45        std::fs::read_to_string(&old_pyproject_path).map_err(|e| Error::FileOperation {
46            path: old_pyproject_path.clone(),
47            message: format!("Failed to read old.pyproject.toml: {}", e),
48        })?;
49
50    let doc = content.parse::<DocumentMut>().map_err(Error::Toml)?;
51
52    // Extract authors from project section (Poetry 2.0 style)
53    if let Some(project) = doc.get("project") {
54        if let Some(authors_array) = project.get("authors").and_then(|a| a.as_array()) {
55            let mut results = Vec::new();
56            for author_value in authors_array.iter() {
57                if let Some(author_str) = author_value.as_str() {
58                    results.push(parse_author_string(author_str));
59                } else if let Some(author_table) = author_value.as_inline_table() {
60                    // Poetry 2.0 style inline table
61                    let name = author_table
62                        .get("name")
63                        .and_then(|n| n.as_str())
64                        .unwrap_or("Unknown")
65                        .to_string();
66
67                    let email = author_table
68                        .get("email")
69                        .and_then(|e| e.as_str())
70                        .map(|s| s.to_string());
71
72                    results.push(Author { name, email });
73                }
74            }
75            return Ok(results);
76        }
77    }
78
79    // Fallback to traditional Poetry section
80    let authors = match doc
81        .get("tool")
82        .and_then(|t| t.get("poetry"))
83        .and_then(|poetry| poetry.get("authors"))
84    {
85        Some(array) => {
86            let mut result = Vec::new();
87            if let Some(arr) = array.as_array() {
88                for value in arr.iter() {
89                    if let Some(author_str) = value.as_str() {
90                        result.push(parse_author_string(author_str));
91                    }
92                }
93            }
94            result
95        }
96        None => vec![],
97    };
98
99    Ok(authors)
100}
101
102fn parse_author_string(author_str: &str) -> Author {
103    let author_str = author_str.trim();
104
105    // First, check for Poetry 2.0 style inline table author format
106    if author_str.starts_with('{') && author_str.ends_with('}') {
107        // Remove {} and split by commas
108        let content = &author_str[1..author_str.len() - 1];
109        let mut name = String::new();
110        let mut email = None;
111
112        for part in content.split(',') {
113            let part = part.trim();
114            if let Some(name_part) = part
115                .strip_prefix("name = ")
116                .or_else(|| part.strip_prefix("name="))
117            {
118                name = name_part.trim_matches(&['"', '\''][..]).to_string();
119            }
120            if let Some(email_part) = part
121                .strip_prefix("email = ")
122                .or_else(|| part.strip_prefix("email="))
123            {
124                email = Some(email_part.trim_matches(&['"', '\''][..]).to_string());
125            }
126        }
127
128        return Author { name, email };
129    }
130
131    // Classic Poetry author format with email in angle brackets
132    let (name, email) = match (author_str.rfind('<'), author_str.rfind('>')) {
133        (Some(start), Some(end)) if start < end => {
134            let name = author_str[..start].trim().to_string();
135            let email = author_str[start + 1..end].trim().to_string();
136            (name, Some(email))
137        }
138        _ => (author_str.to_string(), None),
139    };
140
141    Author { name, email }
142}