use crate::utils::urn::{Urn, UrnComponents, UrnSpec, UrnValidationError};
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{borrow::Cow, boxed::Box, collections::BTreeMap as HashMap, vec::Vec};
#[cfg(feature = "std")]
use std::{borrow::Cow, boxed::Box, collections::HashMap};
#[cfg(feature = "builder")]
use crate::builder::TypeBuilder;
type SpecBuilderFn<'a> = dyn FnOnce(&mut UrnBuilder<'a>) -> Result<Cow<'static, str>, UrnValidationError> + 'a;
#[derive(Default)]
pub struct UrnBuilder<'a> {
pub(crate) nid: Option<Cow<'a, str>>,
pub(crate) components: HashMap<&'static str, Cow<'a, str>>,
nss_mode: NssMode<'a>,
}
#[derive(Default)]
enum NssMode<'a> {
#[default]
Unset,
Direct(Cow<'a, str>),
Spec(Box<SpecBuilderFn<'a>>),
}
impl<'a> UrnBuilder<'a> {
#[inline]
pub fn get(&self, key: &'static str) -> Option<&Cow<'a, str>> {
self.components.get(key)
}
}
impl<'a> UrnComponents<'a> for UrnBuilder<'a> {
fn get_component(&self, key: &'static str) -> Option<&Cow<'a, str>> {
self.get(key)
}
fn set_component(&mut self, key: &'static str, value: Cow<'a, str>) {
self.components.insert(key, value);
}
fn remove_component(&mut self, key: &'static str) -> Option<Cow<'a, str>> {
self.components.remove(key)
}
#[cfg(feature = "std")]
#[inline]
fn iter(&self) -> Box<dyn Iterator<Item = (&'static str, &Cow<'a, str>)> + '_> {
Box::new(self.components.iter().map(|(k, v)| (*k, v)))
}
#[cfg(not(feature = "std"))]
#[inline]
fn iter(&self) -> Box<dyn Iterator<Item = (&'static str, &Cow<'a, str>)> + '_> {
Box::new(self.components.iter().map(|(k, v)| (*k, v)))
}
#[cfg(feature = "std")]
#[inline]
fn iter_mut(&mut self) -> Box<dyn Iterator<Item = (&'static str, &mut Cow<'a, str>)> + '_> {
Box::new(self.components.iter_mut().map(|(k, v)| (*k, v)))
}
#[cfg(not(feature = "std"))]
#[inline]
fn iter_mut(&mut self) -> Box<dyn Iterator<Item = (&'static str, &mut Cow<'a, str>)> + '_> {
Box::new(self.components.iter_mut().map(|(k, v)| (*k, v)))
}
}
impl<'a> UrnBuilder<'a> {
#[inline]
pub fn set(mut self, key: &'static str, value: impl Into<Cow<'a, str>>) -> Self {
self.components.insert(key, value.into());
self
}
#[inline]
pub fn with_nid(mut self, nid: impl Into<Cow<'a, str>>) -> Self {
self.nid = Some(nid.into());
self
}
pub fn with_nss(mut self, nss: impl Into<Cow<'a, str>>) -> Self {
self.nss_mode = NssMode::Direct(nss.into());
self
}
pub fn with_spec<S: UrnSpec>(mut self) -> Self {
let spec_builder = move |builder: &mut UrnBuilder<'a>| -> Result<Cow<'static, str>, UrnValidationError> {
S::transform(builder as &mut dyn UrnComponents<'a>);
S::validate(builder as &dyn UrnComponents<'a>)?;
S::build_nss(builder as &dyn UrnComponents<'a>)
};
self.nss_mode = NssMode::Spec(Box::new(spec_builder));
self
}
}
impl<'a, S: UrnSpec> From<S> for UrnBuilder<'a> {
#[inline]
fn from(_: S) -> Self {
let mut builder = UrnBuilder::default().with_spec::<S>();
builder.nid = Some(Cow::Borrowed(S::NID));
builder
}
}
impl<'a> UrnBuilder<'a> {
pub fn build(mut self) -> Result<Urn<'a>, UrnValidationError> {
let nid_ref = self.nid.as_ref().ok_or(UrnValidationError::RequiredFieldMissing("nid"))?;
Urn::validate_nid(nid_ref)?;
if matches!(self.nss_mode, NssMode::Spec(_)) {
let builder_fn = match core::mem::replace(&mut self.nss_mode, NssMode::Unset) {
NssMode::Spec(f) => f,
_ => unreachable!(),
};
let nss = builder_fn(&mut self)?;
let nid = self.nid.ok_or(UrnValidationError::RequiredFieldMissing("nid"))?;
return Ok(Urn { nid, nss });
}
let nss_mode = self.nss_mode;
let components = self.components;
let nid = self.nid;
let nss = match nss_mode {
NssMode::Unset => {
let mut keys: Vec<&'static str> = components.keys().copied().collect();
keys.sort();
let mut nss_parts = Vec::new();
for key in keys {
if let Some(value) = components.get(key) {
nss_parts.push(value.as_ref());
}
}
if nss_parts.is_empty() {
return Err(UrnValidationError::RequiredFieldMissing("nss components"));
}
nss_parts.join(":").into()
}
NssMode::Direct(nss) => nss,
NssMode::Spec(_) => unreachable!("Spec case handled above"),
};
let nid = nid.ok_or(UrnValidationError::RequiredFieldMissing("nid"))?;
Ok(Urn { nid, nss })
}
}
#[cfg(feature = "builder")]
impl<'a> TypeBuilder<Urn<'a>> for UrnBuilder<'a> {
type Error = UrnValidationError;
fn build(self) -> Result<Urn<'a>, Self::Error> {
UrnBuilder::build(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::utils::urn::builders::spec::Pattern;
#[cfg(feature = "derive")]
crate::urn_spec! {
TestUrnSpec,
nid: "test",
nss_structure {
category: { value: "instrumentation", sep: ":" },
type: { values: ["trace", "event", "seed", "verdict"], sep: "/" },
id: { pattern: Pattern::AlphaNumericHyphen }
},
nss_format: "{}:{}/{}"
}
#[cfg(not(feature = "derive"))]
use crate::utils::urn::{UrnComponents, UrnSpec, UrnSpecBuilder};
#[cfg(not(feature = "derive"))]
struct TestUrnSpec;
#[cfg(not(feature = "derive"))]
impl TestUrnSpec {
fn spec_builder() -> UrnSpecBuilder {
UrnSpecBuilder::from("test")
.field_required("category")
.field_const("category", "instrumentation")
.field_nss_separator("category", ":")
.field_required("type")
.field_oneof("type", &["trace", "event", "seed", "verdict"])
.field_nss_separator("type", "/")
.field_required("id")
.field_pattern("id", Pattern::AlphaNumericHyphen)
.nss_format("{}:{}/{}")
}
}
#[cfg(not(feature = "derive"))]
impl UrnSpec for TestUrnSpec {
const NID: &'static str = "test";
fn validate<'a>(components: &dyn UrnComponents<'a>) -> Result<(), UrnValidationError> {
Self::spec_builder().validate(components)
}
fn build_nss<'a>(components: &dyn UrnComponents<'a>) -> Result<Cow<'static, str>, UrnValidationError> {
let nss = Self::spec_builder().build_nss(components)?;
Ok(nss.into())
}
}
#[test]
fn test_urn_builder_with_nss() -> Result<(), UrnValidationError> {
let test_cases: &[(&str, &str, &str)] = &[
("tightbeam", "test:resource", "urn:tightbeam:test:resource"),
("example", "path/to/resource", "urn:example:path/to/resource"),
("test", "simple", "urn:test:simple"),
];
for (nid, nss, expected) in test_cases {
let urn = UrnBuilder::default().with_nid(*nid).with_nss(*nss).build()?;
assert_eq!(urn.nid, *nid);
assert_eq!(urn.nss, *nss);
assert_eq!(urn.to_string(), *expected);
}
Ok(())
}
#[test]
fn test_urn_builder_with_components() -> Result<(), UrnValidationError> {
type ComponentsTestCase<'a> = (&'a str, &'a [(&'a str, &'a str)], &'a str);
let test_cases: &[ComponentsTestCase] = &[
("example", &[("type", "book"), ("id", "123")], "urn:example:123:book"),
("test", &[("a", "1"), ("b", "2"), ("c", "3")], "urn:test:1:2:3"),
("ns", &[("z", "last"), ("a", "first")], "urn:ns:first:last"),
];
for (nid, components, expected) in test_cases {
let mut builder = UrnBuilder::default().with_nid(*nid);
for (key, value) in *components {
builder = builder.set(key, *value);
}
let urn = builder.build()?;
assert_eq!(urn.nid, *nid);
assert_eq!(urn.to_string(), *expected);
}
Ok(())
}
#[test]
fn test_urn_builder_missing_nid() {
let test_cases: &[&str] = &["test", "resource:path", ""];
for nss in test_cases {
let result = UrnBuilder::default().with_nss(*nss).build();
assert!(matches!(result, Err(UrnValidationError::RequiredFieldMissing(_))));
}
}
#[test]
fn test_urn_builder_with_spec() -> Result<(), UrnValidationError> {
macro_rules! build_with_spec {
(values: [$($k:literal = $v:literal),+], nss: $expected_nss:literal, urn: $expected_urn:literal) => {{
let urn = UrnBuilder::from(TestUrnSpec)
$(.set($k, $v))+
.build()?;
assert_eq!(urn.nid, TestUrnSpec::NID);
assert_eq!(urn.nss, $expected_nss);
assert_eq!(urn.to_string(), $expected_urn);
}};
}
build_with_spec!(
values: ["category" = "instrumentation", "type" = "trace", "id" = "abc-123"],
nss: "instrumentation:trace/abc-123",
urn: "urn:test:instrumentation:trace/abc-123"
);
build_with_spec!(
values: ["category" = "instrumentation", "type" = "event", "id" = "test-456"],
nss: "instrumentation:event/test-456",
urn: "urn:test:instrumentation:event/test-456"
);
build_with_spec!(
values: ["category" = "instrumentation", "type" = "seed", "id" = "xyz-789"],
nss: "instrumentation:seed/xyz-789",
urn: "urn:test:instrumentation:seed/xyz-789"
);
build_with_spec!(
values: ["category" = "instrumentation", "type" = "verdict", "id" = "result-1"],
nss: "instrumentation:verdict/result-1",
urn: "urn:test:instrumentation:verdict/result-1"
);
Ok(())
}
#[test]
fn test_urn_builder_with_spec_missing_fields() {
macro_rules! missing_field {
(values: [$($k:literal = $v:literal),*], field: $expected:literal) => {{
let mut builder = UrnBuilder::from(TestUrnSpec);
$(builder = builder.set($k, $v);)*
let result = builder.build();
assert!(matches!(result, Err(UrnValidationError::RequiredFieldMissing(field)) if field == $expected));
}};
}
missing_field!(values: ["type" = "trace", "id" = "123"], field: "category");
missing_field!(values: ["category" = "instrumentation", "id" = "123"], field: "type");
missing_field!(values: ["category" = "instrumentation", "type" = "trace"], field: "id");
}
#[test]
fn test_urn_builder_with_spec_invalid_fields() {
macro_rules! invalid_field {
(values: [$($k:literal = $v:literal),+], field: $expected:literal) => {{
let result = UrnBuilder::from(TestUrnSpec)
$(.set($k, $v))+
.build();
assert!(matches!(result, Err(UrnValidationError::InvalidFormat { field, .. }) if field == $expected));
}};
}
invalid_field!(values: ["category" = "invalid", "type" = "trace", "id" = "123"], field: "category");
invalid_field!(values: ["category" = "instrumentation", "type" = "invalid", "id" = "123"], field: "type");
invalid_field!(values: ["category" = "instrumentation", "type" = "trace", "id" = "invalid_chars!"], field: "id");
invalid_field!(values: ["category" = "instrumentation", "type" = "trace", "id" = "space here"], field: "id");
invalid_field!(values: ["category" = "instrumentation", "type" = "trace", "id" = "underscore_here"], field: "id");
}
#[test]
fn test_urn_builder_empty_components() {
let test_cases: &[&str] = &["tightbeam", "test", "example"];
for nid in test_cases {
let result = UrnBuilder::default().with_nid(*nid).build();
assert!(matches!(result, Err(UrnValidationError::RequiredFieldMissing(_))));
}
}
}