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    ///
29    /// The file name can be used to identify the version of the API being
30    /// validated.
31    pub fn file_name(&self) -> &ApiSpecFileName {
32        self.backend.file_name()
33    }
34
35    /// Returns true if this is the latest version of a versioned API, or if the
36    /// API is lockstep.
37    ///
38    /// This is particularly useful for extra files which might not themselves
39    /// be versioned. In that case, you may wish to only generate the extra file
40    /// for the latest version.
41    pub fn is_latest(&self) -> bool {
42        self.backend.is_latest()
43    }
44
45    /// Returns whether this version is blessed, or None if this is not a
46    /// versioned API.
47    pub fn is_blessed(&self) -> Option<bool> {
48        self.backend.is_blessed()
49    }
50
51    /// Retrieves the versioning strategy for this API.
52    pub fn versions(&self) -> &Versions {
53        self.backend.versions()
54    }
55
56    /// Retrieves the title of the API being validated.
57    pub fn title(&self) -> &str {
58        self.backend.title()
59    }
60
61    /// Retrieves optional metadata for the API being validated.
62    pub fn metadata(&self) -> &ManagedApiMetadata {
63        self.backend.metadata()
64    }
65
66    /// Reports a validation error.
67    pub fn report_error(&mut self, error: anyhow::Error) {
68        self.backend.report_error(error);
69    }
70
71    /// Records that the file has the given contents.
72    ///
73    /// In check mode, if the files differ, an error is logged.
74    ///
75    /// In generate mode, the file is overwritten with the given contents.
76    ///
77    /// The path is treated as relative to the root of the repository.
78    pub fn record_file_contents(
79        &mut self,
80        path: impl Into<Utf8PathBuf>,
81        contents: Vec<u8>,
82    ) {
83        self.backend.record_file_contents(path.into(), contents);
84    }
85}
86
87/// The backend for validation.
88///
89/// Not part of the public API -- only implemented by the OpenAPI manager.
90#[doc(hidden)]
91pub trait ValidationBackend {
92    fn ident(&self) -> &ApiIdent;
93    fn file_name(&self) -> &ApiSpecFileName;
94    fn versions(&self) -> &Versions;
95    fn is_latest(&self) -> bool;
96    fn is_blessed(&self) -> Option<bool>;
97    fn title(&self) -> &str;
98    fn metadata(&self) -> &ManagedApiMetadata;
99    fn report_error(&mut self, error: anyhow::Error);
100    fn record_file_contents(&mut self, path: Utf8PathBuf, contents: Vec<u8>);
101}
102
103/// Describes the path to an OpenAPI document file, relative to some root where
104/// similar documents are found
105#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
106pub struct ApiSpecFileName {
107    ident: ApiIdent,
108    kind: ApiSpecFileNameKind,
109}
110
111impl fmt::Display for ApiSpecFileName {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        f.write_str(self.path().as_str())
114    }
115}
116
117impl ApiSpecFileName {
118    // Only used by the OpenAPI manager -- not part of the public API.
119    #[doc(hidden)]
120    pub fn new(ident: ApiIdent, kind: ApiSpecFileNameKind) -> ApiSpecFileName {
121        ApiSpecFileName { ident, kind }
122    }
123
124    pub fn ident(&self) -> &ApiIdent {
125        &self.ident
126    }
127
128    pub fn kind(&self) -> &ApiSpecFileNameKind {
129        &self.kind
130    }
131
132    /// Returns the path of this file relative to the root of the OpenAPI
133    /// documents
134    pub fn path(&self) -> Utf8PathBuf {
135        match &self.kind {
136            ApiSpecFileNameKind::Lockstep => {
137                Utf8PathBuf::from_iter([self.basename()])
138            }
139            ApiSpecFileNameKind::Versioned { .. } => Utf8PathBuf::from_iter([
140                self.ident.deref().clone(),
141                self.basename(),
142            ]),
143        }
144    }
145
146    /// Returns the base name of this file path
147    pub fn basename(&self) -> String {
148        match &self.kind {
149            ApiSpecFileNameKind::Lockstep => format!("{}.json", self.ident),
150            ApiSpecFileNameKind::Versioned { version, hash } => {
151                format!("{}-{}-{}.json", self.ident, version, hash)
152            }
153        }
154    }
155
156    /// For versioned APIs, returns the version part of the filename
157    pub fn version(&self) -> Option<&semver::Version> {
158        match &self.kind {
159            ApiSpecFileNameKind::Lockstep => None,
160            ApiSpecFileNameKind::Versioned { version, .. } => Some(version),
161        }
162    }
163
164    /// For versioned APIs, returns the hash part of the filename
165    pub fn hash(&self) -> Option<&str> {
166        match &self.kind {
167            ApiSpecFileNameKind::Lockstep => None,
168            ApiSpecFileNameKind::Versioned { hash, .. } => Some(hash),
169        }
170    }
171}
172
173/// Describes how a particular OpenAPI document is named.
174#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
175pub enum ApiSpecFileNameKind {
176    /// The file's path implies a lockstep API.
177    Lockstep,
178    /// The file's path implies a versioned API.
179    Versioned {
180        /// The version of the API this document describes.
181        version: semver::Version,
182        /// The hash of the file contents.
183        hash: String,
184    },
185}
186
187/// Newtype for API identifiers
188#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)]
189pub struct ApiIdent(String);
190
191impl fmt::Debug for ApiIdent {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        self.0.fmt(f)
194    }
195}
196
197impl Deref for ApiIdent {
198    type Target = String;
199
200    fn deref(&self) -> &Self::Target {
201        &self.0
202    }
203}
204
205impl fmt::Display for ApiIdent {
206    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
207        self.0.fmt(f)
208    }
209}
210
211impl<S: Into<String>> From<S> for ApiIdent {
212    fn from(value: S) -> Self {
213        Self(value.into())
214    }
215}
216
217impl ApiIdent {
218    /// Given an API identifier, return the basename of its "latest" symlink
219    pub fn versioned_api_latest_symlink(&self) -> String {
220        format!("{self}-latest.json")
221    }
222
223    /// Given an API identifier and a file name, determine if we're looking at
224    /// this API's "latest" symlink
225    pub fn versioned_api_is_latest_symlink(&self, base_name: &str) -> bool {
226        base_name == self.versioned_api_latest_symlink()
227    }
228}