code-moniker-core 0.2.0

Core symbol-graph types and per-language extractors for code-moniker (pure Rust, no pgrx). Consumed by the CLI and the PostgreSQL extension.
Documentation
use super::{UriConfig, UriError};
use crate::core::moniker::Moniker;

pub fn to_uri(moniker: &Moniker, config: &UriConfig<'_>) -> Result<String, UriError> {
	let view = moniker.as_view();
	let mut out = String::with_capacity(config.scheme.len() + view.as_bytes().len() + 16);
	out.push_str(config.scheme);
	write_name(&mut out, view.project());

	for seg in view.segments() {
		out.push('/');
		let kind = std::str::from_utf8(seg.kind).map_err(|_| UriError::NonUtf8Segment)?;
		out.push_str(kind);
		out.push(':');
		write_name(&mut out, seg.name);
	}

	Ok(out)
}

fn name_needs_escaping(bytes: &[u8]) -> bool {
	bytes.is_empty()
		|| bytes
			.iter()
			.any(|b| *b == b'/' || *b == b'`' || b.is_ascii_whitespace())
}

fn write_name(out: &mut String, bytes: &[u8]) {
	let s = std::str::from_utf8(bytes).expect("segment names must be UTF-8");
	if !name_needs_escaping(bytes) {
		out.push_str(s);
		return;
	}
	out.push('`');
	for c in s.chars() {
		if c == '`' {
			out.push_str("``");
		} else {
			out.push(c);
		}
	}
	out.push('`');
}

#[cfg(test)]
mod tests {
	use super::super::test_helpers::*;
	use super::*;
	use crate::core::moniker::MonikerBuilder;

	#[test]
	fn to_uri_project_only() {
		let m = MonikerBuilder::new().project(b"my-app").build();
		assert_eq!(
			to_uri(&m, &default_config()).unwrap(),
			"esac+moniker://my-app"
		);
	}

	#[test]
	fn to_uri_path_chain() {
		let m = MonikerBuilder::new()
			.project(b"my-app")
			.segment(b"path", b"main")
			.segment(b"path", b"com")
			.segment(b"path", b"acme")
			.segment(b"class", b"Foo")
			.build();
		assert_eq!(
			to_uri(&m, &default_config()).unwrap(),
			"esac+moniker://my-app/path:main/path:com/path:acme/class:Foo"
		);
	}

	#[test]
	fn to_uri_method_no_arity_in_name() {
		let m = MonikerBuilder::new()
			.project(b"my-app")
			.segment(b"path", b"main")
			.segment(b"class", b"Foo")
			.segment(b"method", b"bar()")
			.build();
		assert_eq!(
			to_uri(&m, &default_config()).unwrap(),
			"esac+moniker://my-app/path:main/class:Foo/method:bar()"
		);
	}

	#[test]
	fn to_uri_method_with_arity_in_name() {
		let m = MonikerBuilder::new()
			.project(b"app")
			.segment(b"class", b"Foo")
			.segment(b"method", b"bar(2)")
			.build();
		assert_eq!(
			to_uri(&m, &default_config()).unwrap(),
			"esac+moniker://app/class:Foo/method:bar(2)"
		);
	}

	#[test]
	fn to_uri_escapes_slash_in_name() {
		let m = MonikerBuilder::new()
			.project(b"app")
			.segment(b"path", b"util/test.ts")
			.build();
		assert_eq!(
			to_uri(&m, &default_config()).unwrap(),
			"esac+moniker://app/path:`util/test.ts`"
		);
	}

	#[test]
	fn to_uri_escapes_backtick() {
		let m = MonikerBuilder::new()
			.project(b"app")
			.segment(b"class", b"weird`name")
			.build();
		assert_eq!(
			to_uri(&m, &default_config()).unwrap(),
			"esac+moniker://app/class:`weird``name`"
		);
	}

	use proptest::prelude::*;

	fn arb_moniker() -> impl Strategy<Value = crate::core::moniker::Moniker> {
		use crate::core::moniker::MonikerBuilder;
		(
			"[a-zA-Z][a-zA-Z0-9_-]{0,15}",
			proptest::collection::vec(("[a-zA-Z][a-zA-Z0-9_]{0,7}", "\\PC{0,32}"), 0..6),
		)
			.prop_map(|(project, segs)| {
				let mut b = MonikerBuilder::new();
				b.project(project.as_bytes());
				for (kind, name) in &segs {
					b.segment(kind.as_bytes(), name.as_bytes());
				}
				b.build()
			})
	}

	proptest! {
		#![proptest_config(ProptestConfig {
			cases: 256,
			..ProptestConfig::default()
		})]

		#[test]
		fn to_uri_from_uri_roundtrip(m in arb_moniker()) {
			let s = to_uri(&m, &default_config()).expect("to_uri must succeed on builder output");
			let m2 = super::super::parse::from_uri(&s, &default_config())
				.expect("from_uri must accept what to_uri produced");
			prop_assert_eq!(m, m2);
		}
	}
}