1use std::fmt;
2
3use serde::{Deserialize, Deserializer, Serialize};
4
5use crate::OperationName;
6
7const MAX_SCHEMA_ID_CHARS: usize = 256;
8const MAX_DIAGNOSTIC_CODE_CHARS: usize = 128;
9
10#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
12#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
13#[serde(transparent)]
14pub struct SchemaId(String);
15
16impl SchemaId {
17 pub fn new(value: impl Into<String>) -> Result<Self, SchemaIdError> {
19 let value = value.into();
20 validate_schema_id(&value)?;
21 Ok(Self(value))
22 }
23
24 pub fn parameters(operation: &OperationName, version: u32) -> Result<Self, SchemaIdError> {
26 Self::for_kind(operation, "parameters", version)
27 }
28
29 pub fn result(operation: &OperationName, version: u32) -> Result<Self, SchemaIdError> {
31 Self::for_kind(operation, "result", version)
32 }
33
34 fn for_kind(
35 operation: &OperationName,
36 kind: &'static str,
37 version: u32,
38 ) -> Result<Self, SchemaIdError> {
39 if version == 0 {
40 return Err(SchemaIdError::ZeroVersion);
41 }
42 Self::new(format!(
43 "schema.operations.{}.{kind}.v{version}",
44 operation.as_str()
45 ))
46 }
47
48 #[must_use]
50 pub fn as_str(&self) -> &str {
51 &self.0
52 }
53}
54
55impl fmt::Display for SchemaId {
56 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
57 formatter.write_str(&self.0)
58 }
59}
60
61impl<'de> Deserialize<'de> for SchemaId {
62 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
63 where
64 D: Deserializer<'de>,
65 {
66 let value = String::deserialize(deserializer)?;
67 Self::new(value).map_err(serde::de::Error::custom)
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
73#[non_exhaustive]
74pub enum SchemaIdError {
75 #[error("operation schema version must be greater than zero")]
77 ZeroVersion,
78 #[error("invalid operation schema id: {0}")]
80 Invalid(String),
81}
82
83fn validate_schema_id(value: &str) -> Result<(), SchemaIdError> {
84 if value.chars().count() > MAX_SCHEMA_ID_CHARS {
85 return Err(SchemaIdError::Invalid(value.to_owned()));
86 }
87 let segments = value.split('.').collect::<Vec<_>>();
88 if segments.len() < 6 || segments[0..2] != ["schema", "operations"] {
89 return Err(SchemaIdError::Invalid(value.to_owned()));
90 }
91 let kind = segments[segments.len() - 2];
92 if !matches!(kind, "parameters" | "result") {
93 return Err(SchemaIdError::Invalid(value.to_owned()));
94 }
95 let version = segments[segments.len() - 1];
96 let Some(version) = version.strip_prefix('v') else {
97 return Err(SchemaIdError::Invalid(value.to_owned()));
98 };
99 if version
100 .parse::<u32>()
101 .ok()
102 .filter(|value| *value > 0)
103 .is_none()
104 {
105 return Err(SchemaIdError::Invalid(value.to_owned()));
106 }
107 let operation = segments[2..segments.len() - 2].join(".");
108 OperationName::new(operation).map_err(|_| SchemaIdError::Invalid(value.to_owned()))?;
109 Ok(())
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
114#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
115#[serde(transparent)]
116pub struct DiagnosticCode(String);
117
118impl DiagnosticCode {
119 pub fn new(value: impl Into<String>) -> Result<Self, DiagnosticCodeError> {
121 let value = value.into();
122 if valid_diagnostic_code(&value) {
123 Ok(Self(value))
124 } else {
125 Err(DiagnosticCodeError(value))
126 }
127 }
128
129 #[must_use]
131 pub fn as_str(&self) -> &str {
132 &self.0
133 }
134}
135
136impl fmt::Display for DiagnosticCode {
137 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
138 formatter.write_str(&self.0)
139 }
140}
141
142impl<'de> Deserialize<'de> for DiagnosticCode {
143 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
144 where
145 D: Deserializer<'de>,
146 {
147 let value = String::deserialize(deserializer)?;
148 Self::new(value).map_err(serde::de::Error::custom)
149 }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
154#[error("invalid diagnostic code: {0}")]
155pub struct DiagnosticCodeError(String);
156
157fn valid_diagnostic_code(value: &str) -> bool {
158 let count = value.chars().count();
159 if !(3..=MAX_DIAGNOSTIC_CODE_CHARS).contains(&count) || !value.contains('.') {
160 return false;
161 }
162 value.split('.').all(valid_code_segment)
163}
164
165fn valid_code_segment(segment: &str) -> bool {
166 let mut characters = segment.chars();
167 matches!(characters.next(), Some('a'..='z'))
168 && characters.all(|character| {
169 character.is_ascii_lowercase()
170 || character.is_ascii_digit()
171 || matches!(character, '-' | '_')
172 })
173 && !segment.ends_with(['-', '_'])
174}
175
176#[cfg(test)]
177#[path = "contract_id_tests.rs"]
178mod tests;