dropshot_api_manager_types/
validation.rs

1// Copyright 2025 Oxide Computer Company
2
3use crate::{ManagedApiMetadata, Versions};
4use camino::Utf8PathBuf;
5use std::{fmt, ops::Deref};
6
7/// Context for validation of OpenAPI specifications.
8pub struct ValidationContext<'a> {
9    backend: &'a mut dyn ValidationBackend,
10}
11
12impl<'a> ValidationContext<'a> {
13    /// Not part of the public API -- only called by the OpenAPI manager.
14    #[doc(hidden)]
15    pub fn new(backend: &'a mut dyn ValidationBackend) -> Self {
16        Self { backend }
17    }
18
19    /// Retrieves the identifier of the API being validated.
20    ///
21    /// This identifier is set via the OpenAPI manager's `ManagedApiConfig`
22    /// type.
23    pub fn ident(&self) -> &ApiIdent {
24        self.backend.ident()
25    }
26
27    /// Returns a descriptor for the API's file name.
28    pub fn file_name(&self) -> &ApiSpecFileName {
29        self.backend.file_name()
30    }
31
32    /// Retrieves the versioning strategy for this API.
33    pub fn versions(&self) -> &Versions {
34        self.backend.versions()
35    }
36
37    /// Retrieves the title of the API being validated.
38    pub fn title(&self) -> &str {
39        self.backend.title()
40    }
41
42    /// Retrieves optional metadata for the API being validated.
43    pub fn metadata(&self) -> &ManagedApiMetadata {
44        self.backend.metadata()
45    }
46
47    /// Reports a validation error.
48    pub fn report_error(&mut self, error: anyhow::Error) {
49        self.backend.report_error(error);
50    }
51
52    /// Records that the file has the given contents.
53    ///
54    /// In check mode, if the files differ, an error is logged.
55    ///
56    /// In generate mode, the file is overwritten with the given contents.
57    ///
58    /// The path is treated as relative to the root of the repository.
59    pub fn record_file_contents(
60        &mut self,
61        path: impl Into<Utf8PathBuf>,
62        contents: Vec<u8>,
63    ) {
64        self.backend.record_file_contents(path.into(), contents);
65    }
66}
67
68/// The backend for validation.
69///
70/// Not part of the public API -- only implemented by the OpenAPI manager.
71#[doc(hidden)]
72pub trait ValidationBackend {
73    fn ident(&self) -> &ApiIdent;
74    fn file_name(&self) -> &ApiSpecFileName;
75    fn versions(&self) -> &Versions;
76    fn title(&self) -> &str;
77    fn metadata(&self) -> &ManagedApiMetadata;
78    fn report_error(&mut self, error: anyhow::Error);
79    fn record_file_contents(&mut self, path: Utf8PathBuf, contents: Vec<u8>);
80}
81
82/// Describes the path to an OpenAPI document file, relative to some root where
83/// similar documents are found
84#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
85pub struct ApiSpecFileName {
86    ident: ApiIdent,
87    kind: ApiSpecFileNameKind,
88}
89
90impl fmt::Display for ApiSpecFileName {
91    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
92        f.write_str(self.path().as_str())
93    }
94}
95
96impl ApiSpecFileName {
97    // Only used by the OpenAPI manager -- not part of the public API.
98    #[doc(hidden)]
99    pub fn new(ident: ApiIdent, kind: ApiSpecFileNameKind) -> ApiSpecFileName {
100        ApiSpecFileName { ident, kind }
101    }
102
103    pub fn ident(&self) -> &ApiIdent {
104        &self.ident
105    }
106
107    pub fn kind(&self) -> &ApiSpecFileNameKind {
108        &self.kind
109    }
110
111    /// Returns the path of this file relative to the root of the OpenAPI
112    /// documents
113    pub fn path(&self) -> Utf8PathBuf {
114        match &self.kind {
115            ApiSpecFileNameKind::Lockstep => {
116                Utf8PathBuf::from_iter([self.basename()])
117            }
118            ApiSpecFileNameKind::Versioned { .. } => Utf8PathBuf::from_iter([
119                self.ident.deref().clone(),
120                self.basename(),
121            ]),
122        }
123    }
124
125    /// Returns the base name of this file path
126    pub fn basename(&self) -> String {
127        match &self.kind {
128            ApiSpecFileNameKind::Lockstep => format!("{}.json", self.ident),
129            ApiSpecFileNameKind::Versioned { version, hash } => {
130                format!("{}-{}-{}.json", self.ident, version, hash)
131            }
132        }
133    }
134
135    /// For versioned APIs, returns the hash part of the filename
136    pub fn hash(&self) -> Option<&str> {
137        match &self.kind {
138            ApiSpecFileNameKind::Lockstep => None,
139            ApiSpecFileNameKind::Versioned { hash, .. } => Some(hash),
140        }
141    }
142}
143
144/// Describes how a particular OpenAPI document is named.
145#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
146pub enum ApiSpecFileNameKind {
147    /// The file's path implies a lockstep API.
148    Lockstep,
149    /// The file's path implies a versioned API.
150    Versioned {
151        /// The version of the API this document describes.
152        version: semver::Version,
153        /// The hash of the file contents.
154        hash: String,
155    },
156}
157
158/// Newtype for API identifiers
159#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)]
160pub struct ApiIdent(String);
161
162impl fmt::Debug for ApiIdent {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        self.0.fmt(f)
165    }
166}
167
168impl Deref for ApiIdent {
169    type Target = String;
170
171    fn deref(&self) -> &Self::Target {
172        &self.0
173    }
174}
175
176impl fmt::Display for ApiIdent {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        self.0.fmt(f)
179    }
180}
181
182impl From<String> for ApiIdent {
183    fn from(value: String) -> Self {
184        Self(value)
185    }
186}
187
188impl ApiIdent {
189    /// Given an API identifier, return the basename of its "latest" symlink
190    pub fn versioned_api_latest_symlink(&self) -> String {
191        format!("{self}-latest.json")
192    }
193
194    /// Given an API identifier and a file name, determine if we're looking at
195    /// this API's "latest" symlink
196    pub fn versioned_api_is_latest_symlink(&self, base_name: &str) -> bool {
197        base_name == self.versioned_api_latest_symlink()
198    }
199}