unlab-gpu 0.1.0

Micro scripting language for neural networks that uses unmtx-gpu.
Documentation
//
// Copyright (c) 2026 Ɓukasz Szpakowski
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
//! A module of [Bitbucket](https://bitbucket.org) source.
use std::str;
use crate::serde_json;
use super::*;

/// A service domain.
pub const SERVICE_DOMAIN: &'static str = "bitbucket.org";
/// A service API domain.
pub const SERVICE_API_DOMAIN: &'static str = "api.bitbucket.org";

#[derive(Clone, Debug, Deserialize)]
struct Tag
{
    name: String,
}

#[derive(Clone, Debug, Deserialize)]
struct Tags
{
    values: Vec<Tag>,
}

/// A structure of [Bitbucket](https://bitbucket.org) source.
#[derive(Clone)]
pub struct BitbucketSrc
{
    name: PkgName,
    old_name: Option<PkgName>,
    home_dir: PathBuf,
    work_dir: PathBuf,
    printer: Arc<dyn Print + Send + Sync>,
    versions: Option<BTreeSet<Version>>,
    current_version: Option<Version>,
    dir: Option<PathBuf>,
}

impl BitbucketSrc
{
    /// Creates a [Bitbucket](https://bitbucket.org) source.
    pub fn new(name: PkgName, old_name: Option<PkgName>, home_dir: PathBuf, work_dir: PathBuf, printer: Arc<dyn Print + Send + Sync>) -> Option<Self>
    {
        let original_name = old_name.as_ref().unwrap_or(&name);
        if original_name.name().split('/').count() != 3 {
            return None;
        }
        match original_name.name().split_once('/') {
            Some((domain, _)) if domain == SERVICE_DOMAIN => {
                Some(BitbucketSrc {
                        name,
                        old_name,
                        home_dir,
                        work_dir,
                        printer,
                        versions: None,
                        current_version: None,
                        dir: None,
                })
            },
            _ => None,
        }
    }

    /// Returns the package name.
    pub fn name(&self) -> &PkgName
    { &self.name }

    /// Returns the old package name if the source has the old package name, otherwise `None`.
    pub fn old_name(&self) -> Option<&PkgName>
    { 
        match &self.old_name {
            Some(old_name) => Some(old_name),
            None => None,
        }
    }

    /// Returns the path to the Unlab-gpu home directory.
    pub fn home_dir(&self) -> &Path
    { self.home_dir.as_path() }

    /// Returns the path to the work directory of current package.
    pub fn work_dir(&self) -> &Path
    { self.work_dir.as_path() }

    /// Returns the printer.
    pub fn printer(&self) -> &Arc<dyn Print + Send + Sync>
    { &self.printer }

    /// Returns the current package version.
    pub fn current_version(&self) -> Option<&Version>
    { 
        match &self.current_version {
            Some(current_version) => Some(current_version),
            None => None,
        }
    }

    fn update_versions(&self, is_update: bool) -> Result<BTreeSet<Version>>
    {
        let original_name = self.old_name.as_ref().unwrap_or(&self.name);
        let repo_path = match original_name.name().split_once('/') {
            Some((_, tmp_repo_path)) => tmp_repo_path,
            None => return Err(Error::PkgName(self.name.clone(), String::from("no package repository path"))),
        };
        update_pkg_versions(&self.name, &self.old_name, self.home_dir.as_path(), is_update, &self.printer, || {
                let mut easy = curl::easy::Easy::new();
                easy.url(format!("https://{}/2.0/repositories/{}/refs/tags", SERVICE_API_DOMAIN, str_to_url_name(repo_path, true)).as_str())?;
                let mut http_headers = List::new();
                http_headers.append(USER_AGENT_HTTP_HEADER)?;
                easy.http_headers(http_headers)?;
                easy.follow_location(true)?;
                Ok(easy)
        }, |data| {
                let s = match str::from_utf8(data) {
                    Ok(tmp_s) => tmp_s,
                    Err(_) => return Err(Error::PkgName(self.name.clone(), String::from("data contains invalid UTF-8 character"))),
                };
                let tags: Tags = match serde_json::from_str(s) {
                    Ok(tmp_refs) => tmp_refs,
                    Err(err) => return Err(Error::SerdeJson(err)),
                };
                let mut versions: BTreeSet<Version> = BTreeSet::new();
                for tag in &tags.values {
                    match tag_name_to_version(tag.name.as_str()) {
                        Some(version) => {
                            versions.insert(version);
                        },
                        None => (),
                    }
                }
                Ok(versions)
        })
    }
}

impl Source for BitbucketSrc
{
    fn update(&mut self) -> Result<()>
    {
        self.versions = Some(self.update_versions(true)?);
        Ok(())
    }
    
    fn versions(&mut self) -> Result<&BTreeSet<Version>>
    {
        if self.versions.is_none() {
            self.versions = Some(self.update_versions(false)?);
        }
        match &self.versions {
            Some(versions) => Ok(versions),
            None => return Err(Error::PkgName(self.name.clone(), String::from("no package versions"))),
        }
    }
    
    fn set_current_version(&mut self, version: Version)
    { self.current_version = Some(version); }
    
    fn dir(&mut self) -> Result<&Path>
    {
        if self.dir.is_none() {
            match &self.current_version {
                Some(current_version) => {
                    self.dir = Some(extract_pkg_file(&self.name, current_version, &self.work_dir, &self.printer, || {
                            let original_name = self.old_name.as_ref().unwrap_or(&self.name);
                            let tag_name = version_to_tag_name(current_version);
                            let url = format!("https://{}/get/{}.zip", str_to_url_name(original_name.name(), true), str_to_url_name(tag_name.as_str(), false));
                            download_pkg_file(&self.name, &self.old_name, current_version, url.as_str(), &self.home_dir, &self.printer)
                    })?)
                },
                None => return Err(Error::PkgName(self.name.clone(), String::from("no current package version"))),
            }
        }
        match &self.dir {
            Some(versions) => Ok(versions),
            None => return Err(Error::PkgName(self.name.clone(), String::from("no package directory"))),
        }
    }
}

/// A structure of factory of [Bitbucket](https://bitbucket.org) source.
#[derive(Copy, Clone, Debug)]
pub struct BitbucketSrcFactory;

impl BitbucketSrcFactory
{
    /// Creates a factory of [Bitbucket](https://bitbucket.org) source.
    pub fn new() -> Self
    { BitbucketSrcFactory }
}

impl SourceCreate for BitbucketSrcFactory
{
    fn create(&self, name: PkgName, old_name: Option<PkgName>, home_dir: PathBuf, work_dir: PathBuf, printer: Arc<dyn Print + Send + Sync>) -> Option<Box<dyn Source + Send + Sync>>
    { 
        match BitbucketSrc::new(name, old_name, home_dir, work_dir, printer) {
            Some(src) => Some(Box::new(src)),
            None => None,
        }
    }
}