1#[cfg(not(feature = "std"))]
4extern crate alloc;
5
6#[cfg(not(feature = "std"))]
7use alloc::{borrow::Cow, string::String, vec::Vec};
8
9#[cfg(feature = "std")]
10use std::borrow::Cow;
11
12use crate::utils::urn::{UrnComponents, UrnValidationError};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Pattern {
17 Alpha,
19 Numeric,
21 AlphaNumeric,
23 AlphaNumericHyphen,
25}
26
27impl Pattern {
28 pub fn matches(&self, value: &str) -> bool {
30 match self {
31 Pattern::Alpha => value.chars().all(|c| c.is_ascii_alphabetic()),
32 Pattern::Numeric => value.chars().all(|c| c.is_ascii_digit()),
33 Pattern::AlphaNumeric => value.chars().all(|c| c.is_ascii_alphanumeric()),
34 Pattern::AlphaNumericHyphen => value.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'),
35 }
36 }
37
38 pub fn pattern_str(&self) -> &'static str {
40 match self {
41 Pattern::Alpha => "^[a-zA-Z]+$",
42 Pattern::Numeric => "^[0-9]+$",
43 Pattern::AlphaNumeric => "^[a-zA-Z0-9]+$",
44 Pattern::AlphaNumericHyphen => "^[a-z0-9-]+$",
45 }
46 }
47}
48
49#[derive(Debug, Clone)]
51pub enum Constraint {
52 Const(&'static str),
54 OneOf(Cow<'static, [&'static str]>),
56}
57
58impl Constraint {
59 pub fn matches(&self, value: &str) -> bool {
61 match self {
62 Constraint::Const(expected) => value == *expected,
63 Constraint::OneOf(options) => options.contains(&value),
64 }
65 }
66
67 pub fn pattern_str(&self) -> &'static str {
69 match self {
70 Constraint::Const(_) => "const(\"value\")",
71 Constraint::OneOf(_) => "oneof(...)",
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct FieldConfig {
79 pub name: &'static str,
81 pub required: bool,
83 pub constraints: Vec<Constraint>,
85 pub pattern: Option<Pattern>,
87 pub nss_separator: Option<&'static str>,
89}
90
91impl FieldConfig {
92 fn new(name: &'static str, required: bool) -> Self {
93 Self { name, required, constraints: Vec::new(), pattern: None, nss_separator: None }
94 }
95}
96
97#[derive(Debug, Clone)]
99pub enum NssFormat {
100 Join(&'static str),
102 Custom(String),
104}
105
106#[derive(Debug, Clone)]
108pub struct UrnSpecBuilder {
109 nid: &'static str,
111 fields: Vec<FieldConfig>,
113 nss_format: NssFormat,
115}
116
117impl UrnSpecBuilder {
118 fn get_or_create_field_index(&mut self, name: &'static str, required: bool) -> usize {
120 if let Some(index) = self.fields.iter().position(|f| f.name == name) {
121 self.fields[index].required = required;
122 index
123 } else {
124 self.fields.push(FieldConfig::new(name, required));
125 self.fields.len() - 1
126 }
127 }
128
129 pub fn field_required(mut self, name: &'static str) -> Self {
131 self.get_or_create_field_index(name, true);
132 self
133 }
134
135 pub fn field_optional(mut self, name: &'static str) -> Self {
137 self.get_or_create_field_index(name, false);
138 self
139 }
140
141 pub fn field_const(mut self, name: &'static str, value: &'static str) -> Self {
143 let index = self.get_or_create_field_index(name, true);
144 self.fields[index].constraints.push(Constraint::Const(value));
145 self
146 }
147
148 pub fn field_oneof(mut self, name: &'static str, options: &'static [&'static str]) -> Self {
150 let index = self.get_or_create_field_index(name, true);
151 self.fields[index].constraints.push(Constraint::OneOf(Cow::Borrowed(options)));
152 self
153 }
154
155 pub fn field_pattern(mut self, name: &'static str, pattern: Pattern) -> Self {
157 let index = self.get_or_create_field_index(name, true);
158 self.fields[index].pattern = Some(pattern);
159 self
160 }
161
162 pub fn field_nss_separator(mut self, name: &'static str, separator: &'static str) -> Self {
164 if let Some(field) = self.fields.iter_mut().find(|f| f.name == name) {
165 field.nss_separator = Some(separator);
166 }
167
168 self
169 }
170
171 pub fn nss_format(mut self, format: &str) -> Self {
173 self.nss_format = NssFormat::Custom(format.to_string());
174 self
175 }
176
177 pub fn validate<'a>(&self, components: &dyn UrnComponents<'a>) -> Result<(), UrnValidationError> {
179 for field in &self.fields {
180 if field.required && components.get_component(field.name).is_none() {
182 return Err(UrnValidationError::RequiredFieldMissing(field.name));
183 }
184
185 if let Some(value) = components.get_component(field.name) {
187 let value_str = value.as_ref();
188
189 for constraint in &field.constraints {
191 if !constraint.matches(value_str) {
192 return Err(UrnValidationError::InvalidFormat { field: field.name, pattern: None });
193 }
194 }
195
196 if let Some(pattern) = field.pattern {
198 if !pattern.matches(value_str) {
199 return Err(UrnValidationError::InvalidFormat { field: field.name, pattern: Some(pattern) });
200 }
201 }
202 }
203 }
204
205 Ok(())
206 }
207
208 pub fn build_nss<'a>(&self, components: &dyn UrnComponents<'a>) -> Result<String, UrnValidationError> {
210 match &self.nss_format {
211 NssFormat::Join(default_separator) => {
212 let mut nss_parts = Vec::new();
213 for field in &self.fields {
214 if let Some(value) = components.get_component(field.name) {
215 if !nss_parts.is_empty() {
217 let separator = field.nss_separator.unwrap_or(default_separator);
219 nss_parts.push(separator);
220 }
221
222 nss_parts.push(value.as_ref());
223 }
224 }
225
226 if nss_parts.is_empty() {
227 return Err(UrnValidationError::RequiredFieldMissing("nss components"));
228 }
229
230 Ok(nss_parts.join(""))
231 }
232 NssFormat::Custom(format_str) => {
233 let mut field_values = Vec::new();
235 for field in &self.fields {
236 if field.required {
237 let value = components
238 .get_component(field.name)
239 .ok_or(UrnValidationError::RequiredFieldMissing(field.name))?;
240 field_values.push(value.as_ref());
241 } else if let Some(value) = components.get_component(field.name) {
242 field_values.push(value.as_ref());
243 }
244 }
245
246 let placeholder_count = format_str.matches("{}").count();
248 if placeholder_count != field_values.len() {
249 return Err(UrnValidationError::RequiredFieldMissing("nss components"));
250 }
251
252 let mut result = format_str.to_string();
254 for value in field_values {
255 if let Some(pos) = result.find("{}") {
256 result.replace_range(pos..pos + 2, value);
257 }
258 }
259
260 let nss = result;
261
262 Ok(nss)
263 }
264 }
265 }
266
267 pub fn nid(&self) -> &'static str {
269 self.nid
270 }
271
272 pub fn fields(&self) -> &[FieldConfig] {
274 &self.fields
275 }
276}
277
278impl From<&'static str> for UrnSpecBuilder {
279 fn from(nid: &'static str) -> Self {
280 Self { nid, fields: Vec::new(), nss_format: NssFormat::Join(":") }
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use crate::utils::urn::UrnBuilder;
287
288 use super::*;
289
290 #[test]
291 fn test_pattern_matches() {
292 let test_cases: &[(Pattern, &[&str], &[&str])] = &[
293 (
294 Pattern::Alpha,
295 &["abc", "XYZ", "Hello", "WORLD"],
296 &["abc123", "123", "abc-xyz", "a1b", "space here"],
297 ),
298 (Pattern::Numeric, &["123", "0", "999", "42"], &["abc", "12a", "1-2", "12.5"]),
299 (
300 Pattern::AlphaNumeric,
301 &["abc123", "XYZ999", "Hello42", "WORLD0", "abc", "123"],
302 &["abc-xyz", "space here", "12.5", "a_b"],
303 ),
304 (
305 Pattern::AlphaNumericHyphen,
306 &["abc-123", "xyz-y", "test-case", "kebab-case-id", "abc", "123"],
307 &["abc_xyz", "space here", "12.5"],
308 ),
309 ];
310
311 for (pattern, valid_values, invalid_values) in test_cases {
312 for value in *valid_values {
313 assert!(pattern.matches(value), "Pattern {pattern:?} should match '{value}'");
314 }
315 for value in *invalid_values {
316 assert!(!pattern.matches(value), "Pattern {pattern:?} should not match '{value}'");
317 }
318 }
319 }
320
321 #[test]
322 fn test_pattern_str() {
323 let test_cases: &[(Pattern, &str)] = &[
325 (Pattern::Alpha, "^[a-zA-Z]+$"),
326 (Pattern::Numeric, "^[0-9]+$"),
327 (Pattern::AlphaNumeric, "^[a-zA-Z0-9]+$"),
328 (Pattern::AlphaNumericHyphen, "^[a-z0-9-]+$"),
329 ];
330
331 for (pattern, expected) in test_cases {
332 assert_eq!(
333 pattern.pattern_str(),
334 *expected,
335 "Pattern {pattern:?} should have pattern_str '{expected}'"
336 );
337 }
338 }
339
340 #[test]
341 fn test_constraint_matches() {
342 let const_test_cases: &[(&str, &[&str], &[&str])] =
343 &[("expected", &["expected"], &["unexpected", "Expected", "EXPECTED", ""])];
344
345 for (const_value, valid_values, invalid_values) in const_test_cases {
346 let constraint = Constraint::Const(const_value);
347 for value in *valid_values {
348 assert!(constraint.matches(value), "Const({const_value:?}) should match '{value}'");
349 }
350 for value in *invalid_values {
351 assert!(!constraint.matches(value), "Const({const_value:?}) should not match '{value}'");
352 }
353 }
354
355 let oneof_test_cases: &[(&[&str], &[&str], &[&str])] = &[
357 (
358 &["option1", "option2", "option3"],
359 &["option1", "option2", "option3"],
360 &["option4", "Option1", "OPTION1", ""],
361 ),
362 (&["a", "b", "c"], &["a", "b", "c"], &["d", "A", "ab", ""]),
363 ];
364
365 for (options, valid_values, invalid_values) in oneof_test_cases {
366 let constraint = Constraint::OneOf(Cow::Borrowed(*options));
367 for value in *valid_values {
368 assert!(constraint.matches(value), "OneOf({options:?}) should match '{value}'");
369 }
370 for value in *invalid_values {
371 assert!(!constraint.matches(value), "OneOf({options:?}) should not match '{value}'");
372 }
373 }
374
375 let tested_const = !const_test_cases.is_empty();
377 let tested_oneof = !oneof_test_cases.is_empty();
378 assert!(tested_const, "Const variant must be tested");
379 assert!(tested_oneof, "OneOf variant must be tested");
380 }
381
382 #[test]
383 fn test_constraint_pattern_str() {
384 assert_eq!(Constraint::Const("value").pattern_str(), "const(\"value\")");
385 assert_eq!(Constraint::OneOf(Cow::Borrowed(&["a", "b"])).pattern_str(), "oneof(...)");
386 }
387
388 #[test]
389 fn test_urn_spec_builder_validation() {
390 macro_rules! validate_pass {
392 (spec: [$($spec_op:ident($($arg:expr),*)),+], values: [$($k:literal = $v:literal),*]) => {{
393 let spec = UrnSpecBuilder::from("test")$(.$spec_op($($arg),*))+;
394 let builder = UrnBuilder::default()$(.set($k, $v))*;
395 assert!(spec.validate(&builder).is_ok());
396 }};
397 }
398
399 macro_rules! validate_fail_missing {
400 (spec: [$($spec_op:ident($($arg:expr),*)),+], values: [$($k:literal = $v:literal),*], field: $expected:literal) => {{
401 let spec = UrnSpecBuilder::from("test")$(.$spec_op($($arg),*))+;
402 let builder = UrnBuilder::default()$(.set($k, $v))*;
403 assert!(matches!(
404 spec.validate(&builder),
405 Err(UrnValidationError::RequiredFieldMissing(field)) if field == $expected
406 ));
407 }};
408 }
409
410 macro_rules! validate_fail_format {
411 (spec: [$($spec_op:ident($($arg:expr),*)),+], values: [$($k:literal = $v:literal),*], field: $expected:literal) => {{
412 let spec = UrnSpecBuilder::from("test")$(.$spec_op($($arg),*))+;
413 let builder = UrnBuilder::default()$(.set($k, $v))*;
414 assert!(matches!(
415 spec.validate(&builder),
416 Err(UrnValidationError::InvalidFormat { field, .. }) if field == $expected
417 ));
418 }};
419 }
420
421 validate_fail_missing!(
423 spec: [field_required("field1"), field_pattern("field1", Pattern::Alpha)],
424 values: [],
425 field: "field1"
426 );
427
428 validate_pass!(spec: [field_optional("field1")], values: []);
430
431 validate_pass!(
433 spec: [field_required("field1"), field_pattern("field1", Pattern::Alpha)],
434 values: ["field1" = "abc"]
435 );
436
437 validate_fail_format!(
439 spec: [field_required("field1"), field_pattern("field1", Pattern::Alpha)],
440 values: ["field1" = "abc123"],
441 field: "field1"
442 );
443
444 validate_pass!(
446 spec: [field_required("field1"), field_const("field1", "expected")],
447 values: ["field1" = "expected"]
448 );
449
450 validate_fail_format!(
452 spec: [field_required("field1"), field_const("field1", "expected")],
453 values: ["field1" = "unexpected"],
454 field: "field1"
455 );
456
457 validate_pass!(
459 spec: [field_required("field1"), field_oneof("field1", &["a", "b", "c"])],
460 values: ["field1" = "b"]
461 );
462
463 validate_fail_format!(
465 spec: [field_required("field1"), field_oneof("field1", &["a", "b", "c"])],
466 values: ["field1" = "d"],
467 field: "field1"
468 );
469 }
470
471 #[test]
472 fn test_urn_spec_builder_build_nss() -> Result<(), UrnValidationError> {
473 macro_rules! build_nss_pass {
475 (spec: [$($spec_op:ident($($arg:expr),*)),+], values: [$($k:literal = $v:literal),+], expected: $exp:literal) => {{
476 let spec = UrnSpecBuilder::from("test")$(.$spec_op($($arg),*))+;
477 let builder = UrnBuilder::default()$(.set($k, $v))+;
478 assert_eq!(spec.build_nss(&builder)?, $exp);
479 }};
480 }
481
482 macro_rules! build_nss_fail {
483 (spec: [$($spec_op:ident($($arg:expr),*)),*], values: [$($k:literal = $v:literal),*], error: $err_msg:literal) => {{
484 #![allow(unused_mut)]
485 let mut spec = UrnSpecBuilder::from("test");
486 $(spec = spec.$spec_op($($arg),*);)*
487 let mut builder = UrnBuilder::default();
488 $(builder = builder.set($k, $v);)*
489 assert!(matches!(
490 spec.build_nss(&builder),
491 Err(UrnValidationError::RequiredFieldMissing(msg)) if msg == $err_msg
492 ));
493 }};
494 }
495
496 build_nss_pass!(
498 spec: [
499 field_required("field1"),
500 field_required("field2"),
501 field_nss_separator("field2", "/"),
502 field_optional("field3")
503 ],
504 values: ["field1" = "value1", "field2" = "value2"],
505 expected: "value1/value2"
506 );
507
508 build_nss_pass!(
510 spec: [
511 field_required("field1"),
512 field_required("field2"),
513 field_optional("field3"),
514 nss_format("{}:{}/{}")
515 ],
516 values: ["field1" = "value1", "field2" = "value2", "field3" = "value3"],
517 expected: "value1:value2/value3"
518 );
519
520 build_nss_fail!(
522 spec: [],
523 values: [],
524 error: "nss components"
525 );
526
527 build_nss_fail!(
529 spec: [field_required("field1"), field_required("field2"), nss_format("{}")],
530 values: ["field1" = "value1", "field2" = "value2"],
531 error: "nss components"
532 );
533
534 Ok(())
535 }
536
537 #[test]
538 fn test_urn_spec_builder_field_operations() {
539 let spec = UrnSpecBuilder::from("test")
540 .field_required("field1")
541 .field_optional("field2")
542 .field_const("field1", "const_value")
543 .field_oneof("field2", &["opt1", "opt2"])
544 .field_pattern("field1", Pattern::Alpha)
545 .field_nss_separator("field1", ":")
546 .field_nss_separator("field2", "/");
547
548 assert_eq!(spec.nid(), "test");
549 assert_eq!(spec.fields().len(), 2);
550 assert_eq!(spec.fields()[0].name, "field1");
551 assert!(spec.fields()[0].required);
552 assert_eq!(spec.fields()[0].constraints.len(), 1);
553 assert_eq!(spec.fields()[0].pattern, Some(Pattern::Alpha));
554 assert_eq!(spec.fields()[0].nss_separator, Some(":"));
555 assert_eq!(spec.fields()[1].name, "field2");
556 assert!(spec.fields()[1].required);
558 assert_eq!(spec.fields()[1].constraints.len(), 1);
559 assert_eq!(spec.fields()[1].nss_separator, Some("/"));
560 }
561}