Skip to content

Commit

Permalink
use slugs rather than UUIDs for package IDs
Browse files Browse the repository at this point in the history
Closes #30
  • Loading branch information
bates64 committed May 1, 2023
1 parent 99d077b commit 40f5f68
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 39 deletions.
14 changes: 11 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "merlon"
version = "1.2.1"
version = "1.3.0"
edition = "2021"
authors = ["Alex Bates <alex@nanaian.town>"]
description = "Mod package manager for the Paper Mario (N64) decompilation"
Expand All @@ -13,6 +13,7 @@ crate-type = ["cdylib", "rlib"] # bin for executable, cdylib for Python, rlib fo

[dependencies]
anyhow = { version = "1.0.70", features = ["std"] }
arrayvec = { version = "0.7.2", features = ["serde"] }
chrono = "0.4.24"
clap = { version = "4.2.4", features = ["derive"] }
fs_extra = "1.3.0"
Expand All @@ -28,7 +29,6 @@ sha1 = "0.10.5"
temp-dir = "0.1.11"
thiserror = "1.0.40"
toml = "0.7.3"
uuid = { version = "1.3.1", features = ["v4", "serde"] }

[dev-dependencies]
rand = "0.8.5"
Expand Down
11 changes: 11 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Changelog

Merlon adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). This means that if a change is made that
breaks backward-compatibility, the major version number will be incremented. This applies for both the Merlon
application and the Rust / Python APIs.

## 2.0.0

- Package IDs have been changed to be kebab-case strings rather than UUIDs. Package IDs must now be in kebab-case, no
less than 3 characters, and no more than 64 characters. This is to allow for more human-readable package IDs, especially
as Git branch names and directory names. **Rust API breaking change.**
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ getting_started
star_rod
emulators
glossary
changelog
```

```{toctree}
Expand Down
2 changes: 1 addition & 1 deletion src/package/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ impl Manifest {
pub fn new(name: Name) -> Result<Self> {
Ok(Self {
metadata: Metadata {
id: Id::new(),
id: Id::generate_for_package_name(&name),
name,
version: "0.1.0".parse()?,
authors: vec![get_author()?],
Expand Down
95 changes: 62 additions & 33 deletions src/package/manifest/id.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,76 @@
use std::{ops::Deref, str::FromStr};
use std::str::FromStr;
use std::fmt;
use uuid::Uuid;
use pyo3::{prelude::*, exceptions::PyValueError};
use pyo3::prelude::*;
use serde::{Deserialize, Serialize};
use heck::AsKebabCase;
use thiserror::Error;
use arrayvec::ArrayString;

/// Package ID. This is a UUID.
use super::Name;

pub const MAX_LEN: usize = 64; // 64 characters should be enough; see https://github.com/rust-lang/crates.io/issues/696

/// Package ID.
/// It is used to uniquely identify a package, no two packages in the same registry can have the same ID.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Id(Uuid);
#[serde(transparent)]
pub struct Id(ArrayString<MAX_LEN>);

impl Id {
/// Generates a new unique package ID.
pub fn new() -> Self {
Self(Uuid::new_v4())
}
/// Errors that can occur when validating a package name.
#[derive(Error, Debug)]
pub enum Error {
/// ID is not in lower kebab-case.
#[error("package ID must be in lower kebab-case")]
NotKebabCase,

/// ID is longer than 64 characters.
#[error("package ID must not be longer than 64 characters")]
TooLong,

/// ID is too short, or empty. IDs must be at least 3 characters long.
#[error("package ID must have at least 3 characters")]
TooShort,
}

mod python_exception {
#![allow(missing_docs)]
pyo3::create_exception!(merlon, Error, pyo3::exceptions::PyValueError);
}

impl From<Uuid> for Id {
fn from(uuid: Uuid) -> Self {
Self(uuid)
impl Id {
/// Creates a new ID from a package name.
pub fn generate_for_package_name(name: &Name) -> Self {
let mut s = name.as_kebab_case();
if s.len() > 3 {
s.truncate(64);
s
} else {
const PREFIX: &'static str = "merlon-";
s.truncate(MAX_LEN - PREFIX.len());
format!("{}-{}", PREFIX, s)
}.parse().expect("ID is valid")
}
}

impl From<Id> for Uuid {
fn from(id: Id) -> Self {
id.0
impl FromStr for Id {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
// Validate the ID.
if s.len() < 3 {
return Err(Error::TooShort);
}
if s != &format!("{}", AsKebabCase(&s)) {
return Err(Error::NotKebabCase);
}
// Convert it
let s = ArrayString::from(&s).map_err(|_| Error::TooLong)?;
Ok(Self(s))
}
}

impl Deref for Id {
type Target = Uuid;
fn deref(&self) -> &Self::Target {
&self.0
impl AsRef<str> for Id {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}

Expand All @@ -40,21 +80,10 @@ impl fmt::Display for Id {
}
}

impl FromStr for Id {
type Err = uuid::Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(Uuid::parse_str(s)?))
}
}

impl FromPyObject<'_> for Id {
fn extract(ob: &PyAny) -> PyResult<Self> {
let string: String = ob.extract()?;
let uuid = Uuid::parse_str(&string).map_err(|e| {
PyValueError::new_err(format!("Invalid UUID: {}", e))
})?;
Ok(Self(uuid))
let s: String = ob.extract()?;
Self::from_str(&s).map_err(|e| python_exception::Error::new_err(e.to_string()))
}
}

Expand Down

0 comments on commit 40f5f68

Please sign in to comment.