Skip to main content

cherenkov/model/index/
resolve.rs

1//! A single resolver registers explicit sources and reuses pinned index entries.
2
3use super::{
4    ModelDetails, ModelIndex, disk, reference,
5    selector::{self, Selector},
6};
7use anyhow::{Context, Result, ensure};
8use std::path::Path;
9
10/// Optional registration settings shared by prepare and explicit model registration.
11#[derive(Default)]
12pub struct ResolveOptions<'a> {
13    /// Optional alias; an existing alias is never silently replaced.
14    pub name: Option<&'a str>,
15    /// HF revision override; mutually exclusive with an @revision suffix.
16    pub revision: Option<&'a str>,
17    /// Optional credential used only if remote metadata must be fetched.
18    pub token: Option<&'a str>,
19    /// Optional inspection event consumer, called serially on this calling thread
20    /// without a catalog lock. Keep handlers short; they can delay the operation.
21    /// Cached lookups emit no inspection events. Use the returned `Result` for
22    /// success or failure. Events are observational and do not control cancellation.
23    pub events: Option<&'a mut dyn FnMut(super::ModelEvent)>,
24}
25
26impl ResolveOptions<'_> {
27    /// Validate source-specific options before catalog lookup or remote access.
28    fn apply(&self, selector: &mut Selector) -> Result<()> {
29        match selector {
30            Selector::Hub { revision, .. } => {
31                ensure!(
32                    revision.is_none() || self.revision.is_none(),
33                    "specify the revision with @revision or --revision, not both"
34                );
35
36                if let Some(value) = self.revision {
37                    selector::validate_revision(value)?;
38
39                    *revision = Some(value.to_owned());
40                }
41            }
42            Selector::Path(_) | Selector::Disk { .. } => {
43                ensure!(
44                    self.revision.is_none() && self.token.is_none(),
45                    "revision and token require an HF source"
46                );
47            }
48            Selector::Registered(_) | Selector::LocalRevision { .. } => {
49                ensure!(self.revision.is_none(), "revision requires a source URI");
50            }
51        }
52
53        Ok(())
54    }
55}
56
57impl ModelIndex {
58    /// Resolve an alias, ID, URI, or explicit path; register unknown sources.
59    /// Unqualified known HF URIs reuse their pinned entry without contacting HF.
60    pub fn select(&self, input: &Path, options: ResolveOptions<'_>) -> Result<ModelDetails> {
61        super::validate_name(options.name)?;
62
63        let mut selector = selector::parse(input)?;
64
65        options.apply(&mut selector)?;
66
67        // Explicit paths must be inspected again to detect changed local sources.
68        if let Selector::Path(path) = &selector {
69            return self.add_local(path, options.name);
70        }
71
72        let input = input.to_str().context("invalid model selector")?;
73        let reference = match options.revision {
74            Some(revision) => format!("{input}@{revision}"),
75            None => input.to_owned(),
76        };
77        let existing = reference::find(&self.lock()?.value, &selector, &reference)?.cloned();
78
79        if let Some(model) = existing {
80            return self.name_existing(model, options.name);
81        }
82
83        match selector {
84            Selector::Hub { repo, revision } => {
85                let (source, description) = super::hub::inspect(
86                    &repo,
87                    revision.as_deref().unwrap_or("main"),
88                    options.token,
89                    options.events,
90                )?;
91                let id = self.register(source, description, options.name, None)?;
92
93                self.show(&id)
94            }
95            Selector::Disk {
96                store,
97                repo,
98                revision,
99            } => self.select_disk(&store, &repo, revision.as_deref(), options.name),
100            _ => anyhow::bail!("model {reference:?} is not registered"),
101        }
102    }
103
104    /// Locate disk files only after lookup has ruled out an existing indexed identity.
105    fn select_disk(
106        &self,
107        name: &str,
108        repo: &str,
109        revision: Option<&str>,
110        alias: Option<&str>,
111    ) -> Result<ModelDetails> {
112        let store = disk::registered(&self.lock()?.value, name)?.clone();
113
114        let path = store.locate(repo, revision)?;
115
116        if matches!(store.layout, super::DiskLayout::Directory)
117            && let Some(revision) = revision
118        {
119            ensure!(
120                reference::is_revision(revision)
121                    && super::local::fingerprint(&path)?.starts_with(revision),
122                "disk model does not match the requested revision"
123            );
124        }
125
126        self.add_local(&path, alias)
127    }
128
129    /// Assign an optional name through the normal collision and ownership checks.
130    fn name_existing(&self, model: super::ModelEntry, name: Option<&str>) -> Result<ModelDetails> {
131        if name.is_none() {
132            return self.show(&model.id);
133        }
134
135        let id = self.register(model.source, model.description, name, None)?;
136
137        self.show(&id)
138    }
139}