use std::fmt;
pub use gts_id::{
DEFAULT_GTS_ID_PREFIX, GTS_ID_MAX_LENGTH, GTS_ID_PREFIX, GTS_ID_PREFIX_ENV, GtsId, GtsIdError,
GtsIdPattern, GtsIdPatternSegment, GtsIdSegment, GtsIdSegmentParts, GtsUuidTail,
};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct GtsEntityId(String);
impl GtsEntityId {
fn new(id: &str) -> Self {
Self(id.to_owned())
}
fn into_string(self) -> String {
self.0
}
}
impl fmt::Display for GtsEntityId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for GtsEntityId {
fn as_ref(&self) -> &str {
&self.0
}
}
pub const GTS_ID_URI_PREFIX: &str = "gts://";
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GtsInstanceId(GtsEntityId);
impl serde::Serialize for GtsInstanceId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
impl<'de> serde::Deserialize<'de> for GtsInstanceId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
GtsInstanceId::try_new(&s).map_err(serde::de::Error::custom)
}
}
impl schemars::JsonSchema for GtsInstanceId {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("GtsInstanceId")
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
let json = Self::json_schema_value();
let mut schema_json = serde_json::json!({
"type": "string"
});
if let Some(format) = json.get("format") {
schema_json["format"] = format.clone();
}
if let Some(title) = json.get("title") {
schema_json["title"] = title.clone();
}
if let Some(description) = json.get("description") {
schema_json["description"] = description.clone();
}
if let Some(gts_ref) = json.get("x-gts-ref") {
schema_json["x-gts-ref"] = gts_ref.clone();
}
schema_json.try_into().unwrap_or_default()
}
}
impl GtsInstanceId {
#[must_use]
pub fn json_schema_value() -> serde_json::Value {
serde_json::json!({
"type": "string",
"format": "gts-instance-id",
"title": "GTS Instance ID",
"description": "GTS instance identifier",
"x-gts-ref": format!("{GTS_ID_PREFIX}*")
})
}
#[must_use]
pub fn new(schema_id: &str, segment: &str) -> Self {
Self(GtsEntityId::new(&format!("{schema_id}{segment}")))
}
pub fn try_new(instance_id: &str) -> Result<Self, GtsIdError> {
let parsed = GtsId::try_new(instance_id)?;
if parsed.is_type() {
return Err(GtsIdError::new(
instance_id,
"GTS instance IDs must not end with '~' (a trailing '~' denotes a type id)",
));
}
Ok(Self(GtsEntityId::new(parsed.as_ref())))
}
#[must_use]
pub fn into_string(self) -> String {
self.0.into_string()
}
}
impl fmt::Display for GtsInstanceId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for GtsInstanceId {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl From<GtsInstanceId> for String {
fn from(id: GtsInstanceId) -> Self {
id.0.into_string()
}
}
impl std::ops::Deref for GtsInstanceId {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0.as_ref()
}
}
impl PartialEq<str> for GtsInstanceId {
fn eq(&self, other: &str) -> bool {
self.0.as_ref() == other
}
}
impl PartialEq<&str> for GtsInstanceId {
fn eq(&self, other: &&str) -> bool {
self.0.as_ref() == *other
}
}
impl PartialEq<String> for GtsInstanceId {
fn eq(&self, other: &String) -> bool {
self.0.as_ref() == other
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct GtsTypeId(GtsEntityId);
#[deprecated(since = "0.10.0", note = "renamed to `GtsTypeId`")]
pub type GtsSchemaId = GtsTypeId;
impl serde::Serialize for GtsTypeId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(self.as_ref())
}
}
impl<'de> serde::Deserialize<'de> for GtsTypeId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
GtsTypeId::try_new(&s).map_err(serde::de::Error::custom)
}
}
impl schemars::JsonSchema for GtsTypeId {
fn schema_name() -> std::borrow::Cow<'static, str> {
std::borrow::Cow::Borrowed("GtsTypeId")
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
let json = Self::json_schema_value();
let mut schema_json = serde_json::json!({
"type": "string"
});
if let Some(format) = json.get("format") {
schema_json["format"] = format.clone();
}
if let Some(title) = json.get("title") {
schema_json["title"] = title.clone();
}
if let Some(description) = json.get("description") {
schema_json["description"] = description.clone();
}
if let Some(gts_ref) = json.get("x-gts-ref") {
schema_json["x-gts-ref"] = gts_ref.clone();
}
schema_json.try_into().unwrap_or_default()
}
}
impl GtsTypeId {
#[must_use]
pub fn json_schema_value() -> serde_json::Value {
serde_json::json!({
"type": "string",
"format": "gts-type-id",
"title": "GTS Type ID",
"description": "GTS type identifier",
"x-gts-ref": format!("{GTS_ID_PREFIX}*")
})
}
#[must_use]
pub fn new(type_id: &str) -> Self {
Self(GtsEntityId::new(type_id))
}
pub fn try_new(type_id: &str) -> Result<Self, GtsIdError> {
let parsed = GtsId::try_new(type_id)?;
if !parsed.is_type() {
return Err(GtsIdError::new(type_id, "GTS type IDs must end with '~'"));
}
Ok(Self(GtsEntityId::new(parsed.as_ref())))
}
#[must_use]
pub fn into_string(self) -> String {
self.0.into_string()
}
}
impl fmt::Display for GtsTypeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for GtsTypeId {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl From<GtsTypeId> for String {
fn from(id: GtsTypeId) -> Self {
id.0.into_string()
}
}
impl std::ops::Deref for GtsTypeId {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0.as_ref()
}
}
impl PartialEq<str> for GtsTypeId {
fn eq(&self, other: &str) -> bool {
self.0.as_ref() == other
}
}
impl PartialEq<&str> for GtsTypeId {
fn eq(&self, other: &&str) -> bool {
self.0.as_ref() == *other
}
}
impl PartialEq<String> for GtsTypeId {
fn eq(&self, other: &String) -> bool {
self.0.as_ref() == other
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_type_id_try_new_accepts_type_rejects_instance() {
let id = GtsTypeId::try_new("gts.x.core.events.event.v1~").expect("test");
assert_eq!(id.as_ref(), "gts.x.core.events.event.v1~");
let err = GtsTypeId::try_new("gts.x.core.events.event.v1~a.b.c.d.v1.0")
.expect_err("must reject instance id");
assert!(err.to_string().contains("must end with '~'"));
assert!(GtsTypeId::try_new("not a valid cti~").is_err());
}
#[test]
fn test_instance_id_try_new_accepts_instance_rejects_type() {
let id = GtsInstanceId::try_new("gts.x.core.events.event.v1~a.b.c.d.v1.0").expect("test");
assert_eq!(id.as_ref(), "gts.x.core.events.event.v1~a.b.c.d.v1.0");
let err =
GtsInstanceId::try_new("gts.x.core.events.event.v1~").expect_err("must reject type id");
assert!(err.to_string().contains("must not end with '~'"));
assert!(GtsInstanceId::try_new("not a valid cti").is_err());
}
#[test]
fn test_deserialize_routes_through_validated_constructors() {
let instance: GtsInstanceId =
serde_json::from_str("\"gts.x.core.events.event.v1~a.b.c.d.v1.0\"").expect("valid");
assert_eq!(instance.as_ref(), "gts.x.core.events.event.v1~a.b.c.d.v1.0");
let type_id: GtsTypeId =
serde_json::from_str("\"gts.x.core.events.event.v1~\"").expect("valid");
assert_eq!(type_id.as_ref(), "gts.x.core.events.event.v1~");
assert!(serde_json::from_str::<GtsInstanceId>("\"not a valid cti\"").is_err());
assert!(serde_json::from_str::<GtsTypeId>("\"not a valid cti~\"").is_err());
assert!(
serde_json::from_str::<GtsInstanceId>("\"gts.x.core.events.event.v1~\"").is_err(),
"a trailing-'~' type id must not deserialize as an instance id"
);
assert!(
serde_json::from_str::<GtsTypeId>("\"gts.x.core.events.event.v1~a.b.c.d.v1.0\"")
.is_err(),
"a non-'~' instance id must not deserialize as a type id"
);
}
#[test]
fn test_json_schema_values_use_configured_id_prefix() {
let expected = format!("{GTS_ID_PREFIX}*");
assert_eq!(GtsInstanceId::json_schema_value()["x-gts-ref"], expected);
assert_eq!(GtsTypeId::json_schema_value()["x-gts-ref"], expected);
}
}