Skip to main content

rigspark_core/
catalog_request.rs

1use crate::sizing::ValidationError;
2use serde_json::Value;
3use url::Url;
4
5const MAX_EVENT_BYTES: usize = 64 * 1024;
6const MODEL_URL_HEADING: &str = "### Hugging Face model URL";
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub struct Repository(String);
10
11impl Repository {
12    pub fn parse(raw: &str) -> Result<Self, ValidationError> {
13        let raw = raw.trim();
14        let authority = raw
15            .strip_prefix("https://")
16            .and_then(|rest| rest.split('/').next())
17            .ok_or_else(|| invalid("canonical Hugging Face URL required"))?;
18        let url = Url::parse(raw).map_err(|_| invalid("invalid Hugging Face URL"))?;
19        if url.scheme() != "https"
20            || authority != "huggingface.co"
21            || url.host_str() != Some("huggingface.co")
22            || !url.username().is_empty()
23            || url.password().is_some()
24            || url.port().is_some()
25            || url.query().is_some()
26            || url.fragment().is_some()
27        {
28            return Err(invalid("canonical Hugging Face URL required"));
29        }
30        let parts: Vec<_> = url
31            .path_segments()
32            .ok_or_else(|| invalid("repository path required"))?
33            .collect();
34        if parts.len() != 2 || !parts.iter().all(|part| valid_part(part)) {
35            return Err(invalid("Hugging Face owner/repository path required"));
36        }
37        Ok(Self(format!("{}/{}", parts[0], parts[1])))
38    }
39
40    pub fn as_str(&self) -> &str {
41        &self.0
42    }
43
44    pub fn url(&self) -> String {
45        format!("https://huggingface.co/{}", self.0)
46    }
47}
48
49fn valid_part(part: &str) -> bool {
50    !part.is_empty()
51        && part.len() <= 96
52        && part.as_bytes()[0].is_ascii_alphanumeric()
53        && part
54            .bytes()
55            .all(|byte| byte.is_ascii_alphanumeric() || b"._-".contains(&byte))
56}
57
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct ModelRequest {
60    pub issue_number: u64,
61    pub issue_url: String,
62    pub repository: Repository,
63}
64
65impl ModelRequest {
66    pub fn from_event(raw: &str) -> Result<Self, ValidationError> {
67        if raw.len() > MAX_EVENT_BYTES {
68            return Err(invalid("GitHub event exceeds 64 KiB"));
69        }
70        let event: Value =
71            serde_json::from_str(raw).map_err(|_| invalid("invalid GitHub event"))?;
72        let issue = event
73            .get("issue")
74            .and_then(Value::as_object)
75            .ok_or_else(|| invalid("GitHub issue missing"))?;
76        let issue_number = issue
77            .get("number")
78            .and_then(Value::as_u64)
79            .filter(|number| *number > 0)
80            .ok_or_else(|| invalid("GitHub issue number missing"))?;
81        let issue_url = issue
82            .get("html_url")
83            .and_then(Value::as_str)
84            .ok_or_else(|| invalid("GitHub issue URL missing"))?;
85        validate_issue_url(issue_url, issue_number)?;
86        let body = issue
87            .get("body")
88            .and_then(Value::as_str)
89            .ok_or_else(|| invalid("GitHub issue body missing"))?;
90        let repository = Repository::parse(extract_field(body)?)?;
91        Ok(Self {
92            issue_number,
93            issue_url: issue_url.to_owned(),
94            repository,
95        })
96    }
97}
98
99fn extract_field(body: &str) -> Result<&str, ValidationError> {
100    let (_, after) = body
101        .split_once(MODEL_URL_HEADING)
102        .ok_or_else(|| invalid("model URL field missing"))?;
103    let value = after.split("\n### ").next().unwrap_or(after).trim();
104    if value.is_empty() || value.lines().count() != 1 {
105        return Err(invalid("model URL field must contain one URL"));
106    }
107    Ok(value)
108}
109
110fn validate_issue_url(raw: &str, issue_number: u64) -> Result<(), ValidationError> {
111    let url = Url::parse(raw).map_err(|_| invalid("invalid GitHub issue URL"))?;
112    let expected = format!("/shashankswe2020-ux/rigspark/issues/{issue_number}");
113    if url.scheme() != "https"
114        || url.host_str() != Some("github.com")
115        || url.path() != expected
116        || url.query().is_some()
117        || url.fragment().is_some()
118    {
119        return Err(invalid("unexpected GitHub issue URL"));
120    }
121    Ok(())
122}
123
124fn invalid(message: &str) -> ValidationError {
125    ValidationError(message.into())
126}