Skip to main content

morphir_core/naming/
path.rs

1use super::Name;
2use crate::ir::{Diagnostic, DiagnosticCode, DiagnosticError};
3use schemars::JsonSchema;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, JsonSchema)]
8pub struct Path {
9    pub segments: Vec<Name>,
10}
11
12impl Path {
13    pub fn new(source: &str) -> Self {
14        let segments = if source.is_empty() {
15            Vec::new()
16        } else {
17            source.split('/').map(Name::from).collect()
18        };
19        Path { segments }
20    }
21
22    pub fn from_canonical_string(source: &str) -> Result<Self, String> {
23        if source.is_empty() {
24            return Ok(Self {
25                segments: Vec::new(),
26            });
27        }
28
29        source
30            .split('/')
31            .map(Name::from_canonical_string)
32            .collect::<Result<Vec<_>, _>>()
33            .map(|segments| Self { segments })
34    }
35
36    pub fn to_canonical_string(&self) -> String {
37        self.segments
38            .iter()
39            .map(Name::to_canonical_string)
40            .collect::<Vec<_>>()
41            .join("/")
42    }
43
44    pub fn is_empty(&self) -> bool {
45        self.segments.is_empty()
46    }
47}
48
49impl fmt::Display for Path {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        write!(f, "{}", self.to_canonical_string())
52    }
53}
54
55impl Serialize for Path {
56    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
57    where
58        S: serde::Serializer,
59    {
60        serializer.serialize_str(&self.to_canonical_string())
61    }
62}
63
64/// Reads a path from the canonical slash-separated string or the legacy array of legacy names.
65///
66/// Refusals carry a [`Diagnostic`] through [`DiagnosticError`] for the same reason
67/// [`Name`]'s reader does: a code and a cursor have to survive serde's `Display`-only error
68/// channel, or the caller can only answer `invalid_type`. A segment that is not a name is
69/// refused by [`Name`]'s own reader, whose error already carries its diagnostic.
70impl<'de> Deserialize<'de> for Path {
71    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
72    where
73        D: serde::Deserializer<'de>,
74    {
75        use serde::de;
76
77        // Accept both array format (Classic) and string format (V4)
78        let value = serde_json::Value::deserialize(deserializer)?;
79        match value {
80            // V4 canonical string format: "my-org/my-lib" or "test-package"
81            serde_json::Value::String(s) => Path::from_canonical_string(&s).map_err(|error| {
82                de::Error::custom(DiagnosticError(Diagnostic::normalization(
83                    DiagnosticCode::InvalidName,
84                    "/",
85                    error,
86                )))
87            }),
88            // Classic array format: [["my"], ["org"], ["my"], ["lib"]]
89            serde_json::Value::Array(arr) => {
90                let segments: Result<Vec<Name>, _> = arr
91                    .into_iter()
92                    .map(|v| serde_json::from_value(v).map_err(de::Error::custom))
93                    .collect();
94                Ok(Path {
95                    segments: segments?,
96                })
97            }
98            _ => Err(de::Error::custom(DiagnosticError(
99                Diagnostic::normalization(
100                    DiagnosticCode::InvalidType,
101                    "/",
102                    "a path is a canonical string or a legacy array of names",
103                ),
104            ))),
105        }
106    }
107}