Skip to content

Commit

Permalink
feat: initial full ArtifactId impl
Browse files Browse the repository at this point in the history
This commit completes the first implementation of the
ArtifactId type, which is really just a wrapper
around GitOid.

This includes all methods delegating to GitOid
under the hood, plus examples run as doc tests which
all pass!

Signed-off-by: Andrew Lilley Brinker <[email protected]>
  • Loading branch information
alilleybrinker committed Feb 20, 2024
1 parent 69f097e commit 02a4458
Show file tree
Hide file tree
Showing 7 changed files with 404 additions and 9 deletions.
4 changes: 1 addition & 3 deletions gitoid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,9 @@
//!
//! ```rust
//! # use gitoid::{Sha256, Blob};
//!
//! type GitOid = gitoid::GitOid<Sha256, Blob>;
//!
//! let gitoid: GitOid = gitoid::GitOid::from_str("hello, world");
//!
//! let gitoid = GitOid::from_str("hello, world");
//! println!("gitoid: {}", gitoid);
//! ```
//!
Expand Down
8 changes: 7 additions & 1 deletion omnibor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,10 @@ repository = "https://github.com/omnibor/omnibor-rs"
version = "0.1.7"

[dependencies]
gitoid = "0.4.0"
gitoid = "0.5.0"
tokio = { version = "1.36.0", features = ["io-util"] }
url = "2.5.0"

[dev-dependencies]
tokio = { version = "1.36.0", features = ["io-util", "fs"] }
tokio-test = "0.4.3"
324 changes: 324 additions & 0 deletions omnibor/src/artifact_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
use crate::Error;
use crate::Result;
use crate::SupportedHash;
use gitoid::Blob;
use gitoid::GitOid;
use std::cmp::Ordering;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use std::hash::Hash;
use std::hash::Hasher;
use std::io::Read;
use std::io::Seek;
use std::str::FromStr;
use tokio::io::AsyncRead;
use tokio::io::AsyncSeek;
use url::Url;

/// An OmniBOR Artifact Identifier.
///
/// This is a content-based unique identifier for any software artifact.
///
/// It is built around, per the specification, any supported hash algorithm.
/// Currently, only SHA-256 is supported, but others may be added in the future.
pub struct ArtifactId<H: SupportedHash> {
#[doc(hidden)]
gitoid: GitOid<H::HashAlgorithm, Blob>,
}

impl<H: SupportedHash> ArtifactId<H> {
/// Construct an [`ArtifactId`] from an existing [`GitOid`].
///
/// # Example
///
/// ```rust
/// # use omnibor::ArtifactId;
/// # use omnibor::Sha256;
/// # use gitoid::GitOid;
/// let gitoid = GitOid::from_str("hello, world");
/// let id: ArtifactId<Sha256> = ArtifactId::from_gitoid(gitoid);
/// println!("Artifact ID: {}", id);
/// ```
pub fn from_gitoid(gitoid: GitOid<H::HashAlgorithm, Blob>) -> ArtifactId<H> {
ArtifactId { gitoid }
}

/// Construct an [`ArtifactId`] from raw bytes.
///
/// # Example
///
/// ```rust
/// # use omnibor::ArtifactId;
/// # use omnibor::Sha256;
/// let id: ArtifactId<Sha256> = ArtifactId::from_bytes(&[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
/// println!("Artifact ID: {}", id);
/// ```
pub fn from_bytes<B: AsRef<[u8]>>(content: B) -> ArtifactId<H> {
ArtifactId::from_gitoid(GitOid::from_bytes(content))
}

/// Construct an [`ArtifactId`] from a string.
///
/// # Example
///
/// ```rust
/// # use omnibor::ArtifactId;
/// # use omnibor::Sha256;
/// let id: ArtifactId<Sha256> = ArtifactId::from_str("hello, world");
/// println!("Artifact ID: {}", id);
/// ```
#[allow(clippy::should_implement_trait)]
pub fn from_str<S: AsRef<str>>(s: S) -> ArtifactId<H> {
ArtifactId::from_gitoid(GitOid::from_str(s))
}

/// Construct an [`ArtifactId`] from a synchronous reader.
///
/// # Example
///
/// ```rust
/// # use omnibor::ArtifactId;
/// # use omnibor::Sha256;
/// # use std::fs::File;
/// let file = File::open("test/data/hello_world.txt").unwrap();
/// let id: ArtifactId<Sha256> = ArtifactId::from_reader(&file).unwrap();
/// println!("Artifact ID: {}", id);
/// ```
pub fn from_reader<R: Read + Seek>(reader: R) -> Result<ArtifactId<H>> {
let gitoid = GitOid::from_reader(reader)?;
Ok(ArtifactId::from_gitoid(gitoid))
}

/// Construct an [`ArtifactId`] from a synchronous reader with an expected length.
///
/// # Example
///
/// ```rust
/// # use omnibor::ArtifactId;
/// # use omnibor::Sha256;
/// # use std::fs::File;
/// let file = File::open("test/data/hello_world.txt").unwrap();
/// let id: ArtifactId<Sha256> = ArtifactId::from_reader_with_length(&file, 11).unwrap();
/// println!("Artifact ID: {}", id);
/// ```
pub fn from_reader_with_length<R: Read>(
reader: R,
expected_length: usize,
) -> Result<ArtifactId<H>> {
let gitoid = GitOid::from_reader_with_length(reader, expected_length)?;
Ok(ArtifactId::from_gitoid(gitoid))
}

/// Construct an [`ArtifactId`] from an asynchronous reader.
///
/// # Example
///
/// ```rust
/// # use omnibor::ArtifactId;
/// # use omnibor::Sha256;
/// # use tokio::fs::File;
/// # tokio_test::block_on(async {
/// let mut file = File::open("test/data/hello_world.txt").await.unwrap();
/// let id: ArtifactId<Sha256> = ArtifactId::from_async_reader(&mut file).await.unwrap();
/// println!("Artifact ID: {}", id);
/// # })
/// ```
pub async fn from_async_reader<R: AsyncRead + AsyncSeek + Unpin>(
reader: R,
) -> Result<ArtifactId<H>> {
let gitoid = GitOid::from_async_reader(reader).await?;
Ok(ArtifactId::from_gitoid(gitoid))
}

/// Construct an [`ArtifactId`] from an asynchronous reader with an expected length.
///
/// # Example
///
/// ```rust
/// # use omnibor::ArtifactId;
/// # use omnibor::Sha256;
/// # use tokio::fs::File;
/// # tokio_test::block_on(async {
/// let mut file = File::open("test/data/hello_world.txt").await.unwrap();
/// let id: ArtifactId<Sha256> = ArtifactId::from_async_reader_with_length(&mut file, 11).await.unwrap();
/// println!("Artifact ID: {}", id);
/// # })
/// ```
pub async fn from_async_reader_with_length<R: AsyncRead + Unpin>(
reader: R,
expected_length: usize,
) -> Result<ArtifactId<H>> {
let gitoid = GitOid::from_async_reader_with_length(reader, expected_length).await?;
Ok(ArtifactId::from_gitoid(gitoid))
}

/// Construct an [`ArtifactId`] from a [`Url`].
///
/// # Example
///
/// ```rust
/// # use omnibor::ArtifactId;
/// # use omnibor::Sha256;
/// # use url::Url;
/// let url = Url::parse("gitoid:blob:sha256:fee53a18d32820613c0527aa79be5cb30173c823a9b448fa4817767cc84c6f03").unwrap();
/// let id: ArtifactId<Sha256> = ArtifactId::from_url(url).unwrap();
/// println!("Artifact ID: {}", id);
/// ```
pub fn from_url(url: Url) -> Result<ArtifactId<H>> {
ArtifactId::try_from(url)
}

/// Get the [`Url`] representation of the [`ArtifactId`].
///
/// # Example
///
/// ```rust
/// # use omnibor::ArtifactId;
/// # use omnibor::Sha256;
/// let id: ArtifactId<Sha256> = ArtifactId::from_str("hello, world");
/// println!("Artifact ID URL: {}", id.url());
/// ```
pub fn url(&self) -> Url {
self.gitoid.url()
}

/// Get the underlying bytes of the [`ArtifactId`] hash.
///
/// # Example
///
/// ```rust
/// # use omnibor::ArtifactId;
/// # use omnibor::Sha256;
/// let id: ArtifactId<Sha256> = ArtifactId::from_str("hello, world");
/// println!("Artifact ID bytes: {:?}", id.as_bytes());
/// ```
pub fn as_bytes(&self) -> &[u8] {
self.gitoid.as_bytes()
}

/// Get the bytes of the [`ArtifactId`] hash as a hexadecimal string.
///
/// # Example
///
/// ```rust
/// # use omnibor::ArtifactId;
/// # use omnibor::Sha256;
/// let id: ArtifactId<Sha256> = ArtifactId::from_str("hello, world");
/// println!("Artifact ID bytes as hex: {}", id.as_hex());
/// ```
pub fn as_hex(&self) -> String {
self.gitoid.as_hex()
}

/// Get the name of the hash algorithm used in the [`ArtifactId`] as a string.
///
/// # Example
///
/// ```rust
/// # use omnibor::ArtifactId;
/// # use omnibor::Sha256;
/// let id: ArtifactId<Sha256> = ArtifactId::from_str("hello, world");
/// println!("Artifact ID hash algorithm: {}", id.hash_algorithm());
/// ```
pub const fn hash_algorithm(&self) -> &'static str {
self.gitoid.hash_algorithm()
}

/// Get the object type used in the [`ArtifactId`] as a string.
///
/// # Example
///
/// ```rust
/// # use omnibor::ArtifactId;
/// # use omnibor::Sha256;
/// let id: ArtifactId<Sha256> = ArtifactId::from_str("hello, world");
/// println!("Artifact ID object type: {}", id.object_type());
/// ```
pub const fn object_type(&self) -> &'static str {
self.gitoid.object_type()
}

/// Get the length in bytes of the hash used in the [`ArtifactId`].
///
/// # Example
///
/// ```rust
/// # use omnibor::ArtifactId;
/// # use omnibor::Sha256;
/// let id: ArtifactId<Sha256> = ArtifactId::from_str("hello, world");
/// println!("Artifact ID hash length in bytes: {}", id.hash_len());
/// ```
pub fn hash_len(&self) -> usize {
self.gitoid.hash_len()
}
}

impl<H: SupportedHash> FromStr for ArtifactId<H> {
type Err = Error;

fn from_str(s: &str) -> Result<ArtifactId<H>> {
Ok(ArtifactId::from_str(s))
}
}

impl<H: SupportedHash> Clone for ArtifactId<H> {
fn clone(&self) -> Self {
*self
}
}

impl<H: SupportedHash> Copy for ArtifactId<H> {}

impl<H: SupportedHash> PartialEq<ArtifactId<H>> for ArtifactId<H> {
fn eq(&self, other: &Self) -> bool {
self.gitoid == other.gitoid
}
}

impl<H: SupportedHash> Eq for ArtifactId<H> {}

impl<H: SupportedHash> PartialOrd<ArtifactId<H>> for ArtifactId<H> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}

impl<H: SupportedHash> Ord for ArtifactId<H> {
fn cmp(&self, other: &Self) -> Ordering {
self.gitoid.cmp(&other.gitoid)
}
}

impl<H: SupportedHash> Hash for ArtifactId<H> {
fn hash<H2>(&self, state: &mut H2)
where
H2: Hasher,
{
self.gitoid.hash(state);
}
}

impl<H: SupportedHash> Debug for ArtifactId<H> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_struct("ArtifactId")
.field("gitoid", &self.gitoid)
.finish()
}
}

impl<H: SupportedHash> Display for ArtifactId<H> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}", self.gitoid)
}
}

impl<H: SupportedHash> TryFrom<Url> for ArtifactId<H> {
type Error = Error;

fn try_from(url: Url) -> Result<ArtifactId<H>> {
let gitoid = GitOid::from_url(url)?;
Ok(ArtifactId::from_gitoid(gitoid))
}
}
39 changes: 39 additions & 0 deletions omnibor/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#[cfg(doc)]
use crate::ArtifactId;
use gitoid::Error as GitOidError;
use std::error::Error as StdError;
use std::fmt::Display;
use std::fmt::Formatter;
use std::fmt::Result as FmtResult;
use std::result::Result as StdResult;

pub type Result<T> = StdResult<T, Error>;

/// Errors arising from [`ArtifactId`] use.
#[derive(Debug)]
pub enum Error {
/// An error arising from the underlying `gitoid` crate.
GitOid(GitOidError),
}

impl Display for Error {
fn fmt(&self, f: &mut Formatter) -> FmtResult {
match self {
Error::GitOid(inner) => write!(f, "{}", inner),
}
}
}

impl StdError for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Error::GitOid(inner) => Some(inner),
}
}
}

impl From<GitOidError> for Error {
fn from(inner: GitOidError) -> Error {
Error::GitOid(inner)
}
}
Loading

0 comments on commit 02a4458

Please sign in to comment.