1use globset::{Glob, GlobSet, GlobSetBuilder};
7use serde::Deserialize;
8use thiserror::Error;
9
10pub const SUPPORTED_VERSION: u32 = 1;
11
12const KNOWN_KEYS: &[&str] = &[
14 "version",
15 "schema_epoch",
16 "canonical",
17 "authoring",
18 "contracts",
19 "name",
20 "source",
21 "emit",
22 "mappings",
23 "glob",
24 "require",
25 "gates",
26 "suppression_comments",
27 "protected_paths",
28 "read_only_paths",
29 "db",
30 "direction",
31 "provider",
32 "rls_tests",
33];
34
35#[derive(Debug, Error)]
36pub enum ManifestError {
37 #[error("manifest is not valid TOML or violates the schema: {message}")]
38 Invalid { message: String },
39 #[error("manifest version {found} is unsupported (this binary supports {supported})")]
40 UnsupportedVersion { found: u32, supported: u32 },
41 #[error(
42 "mapping references undeclared contract '{reference}'; declared contracts: {candidates}"
43 )]
44 UnknownContract {
45 reference: String,
46 candidates: String,
47 },
48 #[error("glob '{glob}' is invalid: {message}")]
49 BadGlob { glob: String, message: String },
50 #[error(
51 "schema_epoch must be a positive integer (a human increments it on \
52 epoch-sensitive change, R9); found {found}"
53 )]
54 NonPositiveEpoch { found: u32 },
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
58#[serde(transparent)]
59pub struct ContractName(String);
60
61impl ContractName {
62 #[must_use]
63 pub fn as_str(&self) -> &str {
64 &self.0
65 }
66}
67
68#[derive(Debug, Deserialize)]
69#[serde(deny_unknown_fields)]
70pub struct Contract {
71 pub name: ContractName,
72 pub source: String,
73 pub emit: Vec<String>,
74}
75
76#[derive(Debug, Deserialize)]
77#[serde(deny_unknown_fields)]
78pub struct Mapping {
79 pub glob: String,
80 pub contracts: Vec<ContractName>,
81 pub require: Option<String>,
82}
83
84#[derive(Debug, Deserialize)]
85#[serde(deny_unknown_fields)]
86pub struct Gates {
87 pub suppression_comments: Option<String>,
88 #[serde(default)]
89 pub protected_paths: Vec<String>,
90 #[serde(default)]
95 pub read_only_paths: Vec<String>,
96}
97
98#[derive(Debug, Deserialize)]
102#[serde(deny_unknown_fields)]
103pub struct Db {
104 pub direction: DbDirection,
105 pub provider: Option<String>,
106 pub rls_tests: Option<String>,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
110#[serde(rename_all = "lowercase")]
111pub enum DbDirection {
112 Contract,
113 Database,
114}
115
116#[derive(Debug, Deserialize)]
117#[serde(deny_unknown_fields)]
118struct RawManifest {
119 version: u32,
120 schema_epoch: Option<u32>,
121 canonical: String,
122 authoring: String,
123 #[serde(default)]
124 contracts: Vec<Contract>,
125 #[serde(default)]
126 mappings: Vec<Mapping>,
127 gates: Gates,
128 db: Option<Db>,
129}
130
131pub struct Manifest {
133 pub version: u32,
134 pub schema_epoch: u32,
139 pub canonical: String,
140 pub authoring: String,
141 pub contracts: Vec<Contract>,
142 pub mappings: Vec<Mapping>,
143 pub gates: Gates,
144 pub db: Option<Db>,
145 mapping_globs: GlobSet,
146 protected_globs: GlobSet,
147 read_only_globs: GlobSet,
148}
149
150impl std::fmt::Debug for Manifest {
151 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152 f.debug_struct("Manifest")
154 .field("version", &self.version)
155 .field("schema_epoch", &self.schema_epoch)
156 .field("canonical", &self.canonical)
157 .field("authoring", &self.authoring)
158 .field("contracts", &self.contracts)
159 .field("mappings", &self.mappings)
160 .field("gates", &self.gates)
161 .field("db", &self.db)
162 .finish_non_exhaustive()
163 }
164}
165
166impl Manifest {
167 pub fn parse(text: &str) -> Result<Self, ManifestError> {
174 let raw: RawManifest = toml::from_str(text).map_err(|e| enrich_unknown_key(&e))?;
175
176 if raw.version != SUPPORTED_VERSION {
177 return Err(ManifestError::UnsupportedVersion {
178 found: raw.version,
179 supported: SUPPORTED_VERSION,
180 });
181 }
182 if let Some(0) = raw.schema_epoch {
183 return Err(ManifestError::NonPositiveEpoch { found: 0 });
184 }
185 resolve_contract_references(&raw)?;
186
187 let mapping_globs = build_globset(raw.mappings.iter().map(|m| m.glob.as_str()))?;
188 let protected_globs = build_globset(raw.gates.protected_paths.iter().map(String::as_str))?;
189 let read_only_globs = build_globset(raw.gates.read_only_paths.iter().map(String::as_str))?;
190
191 Ok(Self {
192 version: raw.version,
193 schema_epoch: raw.schema_epoch.unwrap_or(1),
194 canonical: raw.canonical,
195 authoring: raw.authoring,
196 contracts: raw.contracts,
197 mappings: raw.mappings,
198 gates: raw.gates,
199 db: raw.db,
200 mapping_globs,
201 protected_globs,
202 read_only_globs,
203 })
204 }
205
206 #[must_use]
208 pub fn mapping_for(&self, path: &str) -> Option<&Mapping> {
209 self.mapping_globs
210 .matches(path)
211 .first()
212 .map(|&index| &self.mappings[index])
213 }
214
215 #[must_use]
216 pub fn is_protected(&self, path: &str) -> bool {
217 self.protected_globs.is_match(path)
218 }
219
220 #[must_use]
223 pub fn is_read_only(&self, path: &str) -> bool {
224 self.read_only_globs.is_match(path)
225 }
226}
227
228fn resolve_contract_references(raw: &RawManifest) -> Result<(), ManifestError> {
229 let declared: Vec<&str> = raw.contracts.iter().map(|c| c.name.as_str()).collect();
230 for mapping in &raw.mappings {
231 for reference in &mapping.contracts {
232 if !declared.contains(&reference.as_str()) {
233 return Err(ManifestError::UnknownContract {
234 reference: reference.as_str().to_owned(),
235 candidates: declared.join(", "),
236 });
237 }
238 }
239 }
240 Ok(())
241}
242
243fn build_globset<'a>(globs: impl Iterator<Item = &'a str>) -> Result<GlobSet, ManifestError> {
244 let mut builder = GlobSetBuilder::new();
245 for glob in globs {
246 let compiled = Glob::new(glob).map_err(|error| ManifestError::BadGlob {
247 glob: glob.to_owned(),
248 message: error.to_string(),
249 })?;
250 builder.add(compiled);
251 }
252 builder.build().map_err(|error| ManifestError::BadGlob {
253 glob: "<combined>".to_owned(),
254 message: error.to_string(),
255 })
256}
257
258fn enrich_unknown_key(error: &toml::de::Error) -> ManifestError {
261 let message = error.to_string();
262 let Some(unknown) = extract_unknown_field(&message) else {
263 return ManifestError::Invalid { message };
264 };
265 let candidates = nearest_keys(&unknown);
266 if candidates.is_empty() {
267 return ManifestError::Invalid { message };
268 }
269 ManifestError::Invalid {
270 message: format!("{message}; did you mean: {}?", candidates.join(", ")),
271 }
272}
273
274fn extract_unknown_field(message: &str) -> Option<String> {
275 let marker = "unknown field `";
276 let start = message.find(marker)? + marker.len();
277 let rest = &message[start..];
278 let end = rest.find('`')?;
279 Some(rest[..end].to_owned())
280}
281
282fn nearest_keys(unknown: &str) -> Vec<&'static str> {
283 let mut scored: Vec<(usize, &'static str)> = KNOWN_KEYS
284 .iter()
285 .map(|&key| (levenshtein(unknown, key), key))
286 .filter(|&(distance, _)| distance <= 3)
287 .collect();
288 scored.sort_unstable();
289 scored.into_iter().take(3).map(|(_, key)| key).collect()
290}
291
292pub(crate) fn levenshtein(a: &str, b: &str) -> usize {
293 let a_chars: Vec<char> = a.chars().collect();
294 let b_chars: Vec<char> = b.chars().collect();
295 let mut previous: Vec<usize> = (0..=b_chars.len()).collect();
296 let mut current = vec![0usize; b_chars.len() + 1];
297
298 for (i, &a_char) in a_chars.iter().enumerate() {
299 current[0] = i + 1;
300 for (j, &b_char) in b_chars.iter().enumerate() {
301 let substitution = usize::from(a_char != b_char);
302 current[j + 1] = (previous[j] + substitution)
303 .min(previous[j + 1] + 1)
304 .min(current[j] + 1);
305 }
306 std::mem::swap(&mut previous, &mut current);
307 }
308 previous[b_chars.len()]
309}