1use std::{
2 fmt,
3 path::{Path, PathBuf},
4 sync::Arc,
5};
6
7use anyhow::Context;
8use serde::{Deserialize, Serialize};
9
10use crate::{
11 glob::{Glob, IgnorableGlob},
12 path_serializer,
13 project::ProjectNode,
14 snapshot_middleware::{emit_legacy_scripts_default, Middleware},
15 RojoRef,
16};
17
18#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
21pub struct InstanceMetadata {
22 pub ignore_unknown_instances: bool,
26
27 #[serde(skip_serializing_if = "Option::is_none")]
30 pub instigating_source: Option<InstigatingSource>,
31
32 #[serde(serialize_with = "path_serializer::serialize_vec_absolute")]
53 pub relevant_paths: Vec<PathBuf>,
54
55 pub context: InstanceContext,
62
63 pub specified_id: Option<RojoRef>,
65
66 pub middleware: Option<Middleware>,
69
70 pub schema: Option<String>,
73}
74
75impl InstanceMetadata {
76 pub fn new() -> Self {
77 Self {
78 ignore_unknown_instances: false,
79 instigating_source: None,
80 relevant_paths: Vec::new(),
81 context: InstanceContext::default(),
82 specified_id: None,
83 middleware: None,
84 schema: None,
85 }
86 }
87
88 pub fn ignore_unknown_instances(self, ignore_unknown_instances: bool) -> Self {
89 Self {
90 ignore_unknown_instances,
91 ..self
92 }
93 }
94
95 pub fn instigating_source(self, instigating_source: impl Into<InstigatingSource>) -> Self {
96 Self {
97 instigating_source: Some(instigating_source.into()),
98 ..self
99 }
100 }
101
102 pub fn relevant_paths(self, relevant_paths: Vec<PathBuf>) -> Self {
103 Self {
104 relevant_paths,
105 ..self
106 }
107 }
108
109 pub fn context(self, context: &InstanceContext) -> Self {
110 Self {
111 context: context.clone(),
112 ..self
113 }
114 }
115
116 pub fn specified_id(self, id: Option<RojoRef>) -> Self {
117 Self {
118 specified_id: id,
119 ..self
120 }
121 }
122
123 pub fn middleware(self, middleware: Middleware) -> Self {
124 Self {
125 middleware: Some(middleware),
126 ..self
127 }
128 }
129
130 pub fn schema(self, schema: Option<String>) -> Self {
131 Self { schema, ..self }
132 }
133}
134
135impl Default for InstanceMetadata {
136 fn default() -> Self {
137 Self::new()
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
142pub struct InstanceContext {
143 #[serde(skip_serializing_if = "Vec::is_empty")]
144 pub path_ignore_rules: Arc<Vec<PathIgnoreRule>>,
145 pub emit_legacy_scripts: bool,
146 #[serde(skip_serializing_if = "Vec::is_empty")]
147 pub sync_rules: Vec<SyncRule>,
148}
149
150impl InstanceContext {
151 pub fn new() -> Self {
152 Self {
153 path_ignore_rules: Arc::new(Vec::new()),
154 emit_legacy_scripts: emit_legacy_scripts_default().unwrap(),
155 sync_rules: Vec::new(),
156 }
157 }
158
159 pub fn with_emit_legacy_scripts(emit_legacy_scripts: Option<bool>) -> Self {
160 Self {
161 emit_legacy_scripts: emit_legacy_scripts
162 .or_else(emit_legacy_scripts_default)
163 .unwrap(),
164 ..Self::new()
165 }
166 }
167
168 pub fn add_path_ignore_rules<I>(&mut self, new_rules: I)
170 where
171 I: IntoIterator<Item = PathIgnoreRule>,
172 I::IntoIter: ExactSizeIterator,
173 {
174 let new_rules = new_rules.into_iter();
175
176 if new_rules.len() == 0 {
179 return;
180 }
181
182 let rules = Arc::make_mut(&mut self.path_ignore_rules);
183 rules.extend(new_rules);
184 }
185
186 pub fn add_sync_rules<I>(&mut self, new_rules: I)
188 where
189 I: IntoIterator<Item = SyncRule>,
190 {
191 self.sync_rules.extend(new_rules);
192 }
193
194 pub fn clear_sync_rules(&mut self) {
196 self.sync_rules.clear();
197 }
198
199 pub fn set_emit_legacy_scripts(&mut self, emit_legacy_scripts: bool) {
200 self.emit_legacy_scripts = emit_legacy_scripts;
201 }
202
203 pub fn get_user_sync_rule(&self, path: &Path) -> Option<&SyncRule> {
206 self.sync_rules.iter().find(|&rule| rule.matches(path))
207 }
208}
209
210impl Default for InstanceContext {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
217pub struct PathIgnoreRule {
218 #[serde(serialize_with = "path_serializer::serialize_absolute")]
222 pub base_path: PathBuf,
223
224 pub glob: IgnorableGlob,
226}
227
228impl PathIgnoreRule {
229 pub fn matches<P: AsRef<Path>>(&self, path: P) -> bool {
230 let path = path.as_ref();
231
232 match path.strip_prefix(&self.base_path) {
233 Ok(suffix) => self.glob.is_match(suffix),
234 Err(_) => false,
235 }
236 }
237
238 pub fn is_negation(&self) -> bool {
239 self.glob.is_negation()
240 }
241}
242
243pub fn is_path_ignored<P: AsRef<Path>>(rules: &[PathIgnoreRule], path: P) -> bool {
248 let path = path.as_ref();
249 let mut ignored = false;
250 for rule in rules {
251 if rule.matches(path) {
252 ignored = !rule.is_negation();
253 }
254 }
255 ignored
256}
257
258#[derive(Clone, PartialEq, Serialize, Deserialize)]
260pub enum InstigatingSource {
261 Path(#[serde(serialize_with = "path_serializer::serialize_absolute")] PathBuf),
263 ProjectNode {
265 #[serde(serialize_with = "path_serializer::serialize_absolute")]
266 path: PathBuf,
267 name: String,
268 node: ProjectNode,
269 parent_class: Option<String>,
270 },
271}
272
273impl InstigatingSource {
274 pub fn path(&self) -> &Path {
275 match self {
276 Self::Path(path) => path.as_path(),
277 Self::ProjectNode { path, .. } => path.as_path(),
278 }
279 }
280}
281
282impl fmt::Debug for InstigatingSource {
283 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
284 match self {
285 InstigatingSource::Path(path) => write!(formatter, "Path({})", path.display()),
286 InstigatingSource::ProjectNode {
287 name,
288 node,
289 path,
290 parent_class,
291 } => write!(
292 formatter,
293 "ProjectNode({}: {:?}) from path {} and parent class {:?}",
294 name,
295 node,
296 path.display(),
297 parent_class,
298 ),
299 }
300 }
301}
302
303impl From<PathBuf> for InstigatingSource {
304 fn from(path: PathBuf) -> Self {
305 InstigatingSource::Path(path)
306 }
307}
308
309impl From<&Path> for InstigatingSource {
310 fn from(path: &Path) -> Self {
311 InstigatingSource::Path(path.to_path_buf())
312 }
313}
314
315#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
318pub struct SyncRule {
319 #[serde(rename = "pattern")]
321 pub include: Glob,
322 #[serde(skip_serializing_if = "Option::is_none")]
324 pub exclude: Option<Glob>,
325 #[serde(rename = "use")]
327 pub middleware: Middleware,
328 #[serde(skip_serializing_if = "Option::is_none")]
331 pub suffix: Option<String>,
332 #[serde(skip)]
335 pub base_path: PathBuf,
336}
337
338impl SyncRule {
339 pub fn matches(&self, path: &Path) -> bool {
341 match path.strip_prefix(&self.base_path) {
342 Ok(suffix) => {
343 if let Some(pattern) = &self.exclude {
344 if pattern.is_match(suffix) {
345 return false;
346 }
347 }
348 self.include.is_match(suffix)
349 }
350 Err(_) => false,
351 }
352 }
353
354 pub fn file_name_for_path<'a>(&self, path: &'a Path) -> anyhow::Result<&'a str> {
355 if let Some(suffix) = &self.suffix {
356 let file_name = path
357 .file_name()
358 .and_then(|s| s.to_str())
359 .with_context(|| format!("file name of {} is invalid", path.display()))?;
360 if file_name.ends_with(suffix) {
361 let end = file_name.len().saturating_sub(suffix.len());
362 Ok(&file_name[..end])
363 } else {
364 Ok(file_name)
365 }
366 } else {
367 path.file_stem()
370 .and_then(|s| s.to_str())
371 .with_context(|| format!("file name of {} is invalid", path.display()))
372 }
373 }
374}