1use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticLabel, Severity};
4use crate::interpolation::DocumentInterpolation;
5use crate::loader::{LoadedProject, ProjectInterpolation};
6use crate::model::{Located, ShortPort, ShortVolumeMount};
7use crate::source::{SourceId, SourceSpan};
8use crate::syntax::{MergeScalarKind, MergeSyntaxEntry, MergeSyntaxScalar, MergeSyntaxValue};
9use std::fmt;
10use std::path::{Path, PathBuf};
11
12pub const INTERPOLATION_PROJECT_MISMATCH: DiagnosticCode =
14 DiagnosticCode::new("compose.merge.interpolation-project-mismatch");
15
16pub const INVALID_DOCUMENT_ROOT: DiagnosticCode = DiagnosticCode::new("compose.merge.invalid-document-root");
18
19pub const UNRESOLVED_ALIAS: DiagnosticCode = DiagnosticCode::new("compose.merge.unresolved-alias");
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum MergeOperation {
25 Authored,
27 Added,
29 Replaced,
31 Merged,
33 Appended,
35 Reset,
37 Override,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct MergeProvenance {
44 operation: MergeOperation,
45 sources: Vec<SourceSpan>,
46}
47
48impl MergeProvenance {
49 #[must_use]
51 pub const fn operation(&self) -> MergeOperation {
52 self.operation
53 }
54
55 #[must_use]
57 pub fn sources(&self) -> &[SourceSpan] {
58 &self.sources
59 }
60
61 #[must_use]
63 pub fn effective_source(&self) -> Option<SourceSpan> {
64 self.sources.last().copied()
65 }
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub enum MergedScalarKind {
71 String,
73 Boolean,
75 Number,
77}
78
79#[derive(Clone, PartialEq, Eq)]
81pub struct MergedScalar {
82 raw: String,
83 value: String,
84 kind: MergedScalarKind,
85 sensitive: bool,
86}
87
88impl MergedScalar {
89 #[must_use]
91 pub fn raw(&self) -> &str {
92 &self.raw
93 }
94
95 #[must_use]
97 pub fn value(&self) -> &str {
98 &self.value
99 }
100
101 #[must_use]
103 pub const fn kind(&self) -> MergedScalarKind {
104 self.kind
105 }
106
107 #[must_use]
109 pub const fn is_sensitive(&self) -> bool {
110 self.sensitive
111 }
112}
113
114impl fmt::Debug for MergedScalar {
115 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
116 formatter
117 .debug_struct("MergedScalar")
118 .field("raw", &self.raw)
119 .field("value", &if self.sensitive { "<redacted>" } else { &self.value })
120 .field("kind", &self.kind)
121 .field("sensitive", &self.sensitive)
122 .finish()
123 }
124}
125
126#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
128pub enum NullStyle {
129 Empty,
131 Explicit,
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
137pub enum EntrySyntax {
138 Mapping,
140 ListKeyValue,
142 ListKeyOnly,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq)]
148pub struct MergedEntry {
149 key: String,
150 key_sources: Vec<SourceSpan>,
151 syntax: EntrySyntax,
152 value: MergedValue,
153}
154
155impl MergedEntry {
156 #[must_use]
158 pub fn key(&self) -> &str {
159 &self.key
160 }
161
162 #[must_use]
164 pub fn key_sources(&self) -> &[SourceSpan] {
165 &self.key_sources
166 }
167
168 #[must_use]
170 pub const fn syntax(&self) -> EntrySyntax {
171 self.syntax
172 }
173
174 #[must_use]
176 pub const fn value(&self) -> &MergedValue {
177 &self.value
178 }
179}
180
181#[derive(Debug, Clone, PartialEq, Eq)]
183pub enum MergedValueKind {
184 Null(NullStyle),
186 Scalar(MergedScalar),
188 Mapping(Vec<MergedEntry>),
190 Sequence(Vec<MergedValue>),
192 Alias(String),
194 Tagged {
196 tag: String,
198 value: Box<MergedValue>,
200 },
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct MergedValue {
206 kind: MergedValueKind,
207 provenance: MergeProvenance,
208}
209
210impl MergedValue {
211 #[must_use]
213 pub const fn kind(&self) -> &MergedValueKind {
214 &self.kind
215 }
216
217 #[must_use]
219 pub const fn provenance(&self) -> &MergeProvenance {
220 &self.provenance
221 }
222
223 #[must_use]
225 pub const fn as_scalar(&self) -> Option<&MergedScalar> {
226 match &self.kind {
227 MergedValueKind::Scalar(value) => Some(value),
228 _ => None,
229 }
230 }
231
232 #[must_use]
234 pub fn as_mapping(&self) -> Option<&[MergedEntry]> {
235 match &self.kind {
236 MergedValueKind::Mapping(entries) => Some(entries),
237 _ => None,
238 }
239 }
240
241 #[must_use]
243 pub fn as_sequence(&self) -> Option<&[MergedValue]> {
244 match &self.kind {
245 MergedValueKind::Sequence(values) => Some(values),
246 _ => None,
247 }
248 }
249
250 #[must_use]
252 pub fn get(&self, key: &str) -> Option<&MergedValue> {
253 self.as_mapping()?
254 .iter()
255 .find(|entry| entry.key == key)
256 .map(|entry| &entry.value)
257 }
258
259 #[must_use]
261 pub fn is_sensitive(&self) -> bool {
262 match &self.kind {
263 MergedValueKind::Scalar(value) => value.sensitive,
264 MergedValueKind::Mapping(entries) => entries.iter().any(|entry| entry.value.is_sensitive()),
265 MergedValueKind::Sequence(values) => values.iter().any(Self::is_sensitive),
266 MergedValueKind::Tagged { value, .. } => value.is_sensitive(),
267 MergedValueKind::Null(_) | MergedValueKind::Alias(_) => false,
268 }
269 }
270}
271
272#[derive(Debug, Clone, PartialEq, Eq)]
274pub struct MergedProject {
275 root: MergedValue,
276 base_directory: PathBuf,
277 source_ids: Vec<SourceId>,
278}
279
280impl MergedProject {
281 #[must_use]
283 pub const fn root(&self) -> &MergedValue {
284 &self.root
285 }
286
287 #[must_use]
289 pub fn base_directory(&self) -> &Path {
290 &self.base_directory
291 }
292
293 #[must_use]
295 pub fn source_ids(&self) -> &[SourceId] {
296 &self.source_ids
297 }
298
299 #[must_use]
301 pub fn value(&self, path: &[&str]) -> Option<&MergedValue> {
302 path.iter().try_fold(&self.root, |value, key| value.get(key))
303 }
304}
305
306#[derive(Debug, Clone, PartialEq, Eq)]
308pub struct MergeResult {
309 project: Option<MergedProject>,
310 diagnostics: Vec<Diagnostic>,
311}
312
313impl MergeResult {
314 #[must_use]
316 pub const fn project(&self) -> Option<&MergedProject> {
317 self.project.as_ref()
318 }
319
320 #[must_use]
322 pub fn diagnostics(&self) -> &[Diagnostic] {
323 &self.diagnostics
324 }
325
326 #[must_use]
328 pub fn is_valid(&self) -> bool {
329 self.project.is_some()
330 && self
331 .diagnostics
332 .iter()
333 .all(|diagnostic| diagnostic.severity() != Severity::Error)
334 }
335}
336
337#[must_use]
342pub fn merge_project(project: &LoadedProject, interpolation: Option<&ProjectInterpolation>) -> MergeResult {
343 let mut diagnostics = project.diagnostics().to_vec();
344 if let Some(interpolation) = interpolation {
345 diagnostics.extend(interpolation.diagnostics().iter().cloned());
346 }
347
348 let mut root = None;
349 let mut source_ids = Vec::new();
350 for document in project.documents() {
351 let overlay = interpolation.and_then(|values| values.document(document.source_id()));
352 if interpolation.is_some() && overlay.is_none() {
353 diagnostics.push(
354 Diagnostic::new(
355 INTERPOLATION_PROJECT_MISMATCH,
356 Severity::Error,
357 "loaded document has no matching interpolation overlay",
358 )
359 .with_label(DiagnosticLabel::primary(
360 document.syntax().source_span(),
361 "missing per-file overlay",
362 )),
363 );
364 }
365
366 let Some(syntax_root) = document.syntax().merge_root() else {
367 continue;
368 };
369 if !matches!(syntax_root, MergeSyntaxValue::Mapping { .. }) {
370 diagnostics.push(
371 Diagnostic::new(
372 INVALID_DOCUMENT_ROOT,
373 Severity::Error,
374 "Compose merge inputs must have mapping roots",
375 )
376 .with_label(DiagnosticLabel::primary(
377 document.syntax().source_span(),
378 "document is not a mapping",
379 )),
380 );
381 continue;
382 }
383
384 let value = convert_value(syntax_root, overlay, &mut diagnostics);
385 source_ids.push(document.source_id());
386 root = Some(match root {
387 Some(current) => merge_value(current, value, &[], &mut diagnostics),
388 None => activate_tags(value, &[], &mut diagnostics),
389 });
390 }
391
392 if let Some(interpolation) = interpolation {
393 for overlay in interpolation.documents() {
394 if project.document(overlay.source_id()).is_none() {
395 diagnostics.push(Diagnostic::new(
396 INTERPOLATION_PROJECT_MISMATCH,
397 Severity::Error,
398 "interpolation overlay does not belong to the loaded project",
399 ));
400 }
401 }
402 }
403
404 MergeResult {
405 project: root.map(|root| MergedProject {
406 root,
407 base_directory: project.base_directory().to_path_buf(),
408 source_ids,
409 }),
410 diagnostics,
411 }
412}
413
414fn convert_value(
415 value: MergeSyntaxValue,
416 interpolation: Option<&DocumentInterpolation>,
417 diagnostics: &mut Vec<Diagnostic>,
418) -> MergedValue {
419 match value {
420 MergeSyntaxValue::Empty(span) => authored(MergedValueKind::Null(NullStyle::Empty), span),
421 MergeSyntaxValue::Scalar(value) if value.kind == MergeScalarKind::Null => {
422 authored(MergedValueKind::Null(NullStyle::Explicit), value.span)
423 }
424 MergeSyntaxValue::Scalar(value) => convert_scalar(value, interpolation),
425 MergeSyntaxValue::Mapping { entries, span } => {
426 let entries = entries
427 .into_iter()
428 .map(|entry| convert_entry(entry, interpolation, diagnostics))
429 .collect();
430 authored(MergedValueKind::Mapping(entries), span)
431 }
432 MergeSyntaxValue::Sequence { values, span } => {
433 let values = values
434 .into_iter()
435 .map(|value| convert_value(value, interpolation, diagnostics))
436 .collect();
437 authored(MergedValueKind::Sequence(values), span)
438 }
439 MergeSyntaxValue::Alias { name, span } => {
440 diagnostics.push(
441 Diagnostic::new(UNRESOLVED_ALIAS, Severity::Warning, "YAML alias could not be resolved")
442 .with_label(DiagnosticLabel::primary(span, "unresolved alias")),
443 );
444 authored(MergedValueKind::Alias(name), span)
445 }
446 MergeSyntaxValue::Tagged { tag, value, span } => {
447 let value = convert_value(*value, interpolation, diagnostics);
448 authored(
449 MergedValueKind::Tagged {
450 tag,
451 value: Box::new(value),
452 },
453 span,
454 )
455 }
456 }
457}
458
459fn convert_entry(
460 entry: MergeSyntaxEntry,
461 interpolation: Option<&DocumentInterpolation>,
462 diagnostics: &mut Vec<Diagnostic>,
463) -> MergedEntry {
464 MergedEntry {
465 key: entry.key.value,
466 key_sources: vec![entry.key.span],
467 syntax: EntrySyntax::Mapping,
468 value: convert_value(entry.value, interpolation, diagnostics),
469 }
470}
471
472fn convert_scalar(value: MergeSyntaxScalar, interpolation: Option<&DocumentInterpolation>) -> MergedValue {
473 let resolved = interpolation.and_then(|overlay| overlay.value(value.span));
474 let semantic = resolved.map_or_else(|| value.value.clone(), |result| result.resolved().to_owned());
475 let sensitive = resolved.is_some_and(crate::interpolation::InterpolationResult::is_sensitive);
476 let kind = match value.kind {
477 MergeScalarKind::String => MergedScalarKind::String,
478 MergeScalarKind::Boolean => MergedScalarKind::Boolean,
479 MergeScalarKind::Number => MergedScalarKind::Number,
480 MergeScalarKind::Null => unreachable!("null scalars are converted before convert_scalar"),
481 };
482 authored(
483 MergedValueKind::Scalar(MergedScalar {
484 raw: value.raw,
485 value: semantic,
486 kind,
487 sensitive,
488 }),
489 value.span,
490 )
491}
492
493fn authored(kind: MergedValueKind, span: SourceSpan) -> MergedValue {
494 MergedValue {
495 kind,
496 provenance: MergeProvenance {
497 operation: MergeOperation::Authored,
498 sources: vec![span],
499 },
500 }
501}
502
503fn activate_tags(mut value: MergedValue, path: &[String], diagnostics: &mut Vec<Diagnostic>) -> MergedValue {
504 value = match value.kind {
505 MergedValueKind::Tagged { tag, value: inner } if tag == "!reset" => reset_value(None, &inner, value.provenance),
506 MergedValueKind::Tagged { tag, value: inner } if tag == "!override" => {
507 override_value(None, *inner, value.provenance)
508 }
509 kind => MergedValue {
510 kind,
511 provenance: value.provenance,
512 },
513 };
514
515 match &mut value.kind {
516 MergedValueKind::Mapping(entries) => {
517 for entry in entries {
518 let mut child_path = path.to_vec();
519 child_path.push(entry.key.clone());
520 entry.value = activate_tags(entry.value.clone(), &child_path, diagnostics);
521 }
522 }
523 MergedValueKind::Sequence(values) => {
524 for item in values {
525 *item = activate_tags(item.clone(), path, diagnostics);
526 }
527 }
528 MergedValueKind::Tagged { value: inner, .. } => {
529 **inner = activate_tags((**inner).clone(), path, diagnostics);
530 }
531 MergedValueKind::Null(_) | MergedValueKind::Scalar(_) | MergedValueKind::Alias(_) => {}
532 }
533 let _ = diagnostics;
534 value
535}
536
537fn merge_value(
538 base: MergedValue,
539 incoming: MergedValue,
540 path: &[String],
541 diagnostics: &mut Vec<Diagnostic>,
542) -> MergedValue {
543 if let MergedValueKind::Tagged { tag, value } = incoming.kind {
544 if tag == "!reset" {
545 return reset_value(Some(&base), &value, incoming.provenance);
546 }
547 if tag == "!override" {
548 return override_value(Some(&base), *value, incoming.provenance);
549 }
550 return replace_value(
551 base,
552 MergedValue {
553 kind: MergedValueKind::Tagged { tag, value },
554 provenance: incoming.provenance,
555 },
556 MergeOperation::Replaced,
557 );
558 }
559
560 if is_shell_command(path) {
561 return replace_value(base, incoming, MergeOperation::Replaced);
562 }
563
564 if is_keyed_mapping(path) {
565 if let (Some(base), Some(incoming)) = (normalize_keyed(base.clone()), normalize_keyed(incoming.clone())) {
566 return merge_mappings(base, incoming, path, diagnostics);
567 }
568 }
569
570 match (&base.kind, &incoming.kind) {
571 (MergedValueKind::Mapping(_), MergedValueKind::Mapping(_)) => merge_mappings(base, incoming, path, diagnostics),
572 (MergedValueKind::Sequence(_), MergedValueKind::Sequence(_)) if unique_field(path).is_some() => {
573 merge_unique_sequences(base, incoming, path, diagnostics)
574 }
575 (MergedValueKind::Sequence(_), MergedValueKind::Sequence(_)) => append_sequences(base, incoming),
576 _ => replace_value(
577 base,
578 activate_tags(incoming, path, diagnostics),
579 MergeOperation::Replaced,
580 ),
581 }
582}
583
584fn merge_mappings(
585 base: MergedValue,
586 incoming: MergedValue,
587 path: &[String],
588 diagnostics: &mut Vec<Diagnostic>,
589) -> MergedValue {
590 let MergedValueKind::Mapping(mut base_entries) = base.kind else {
591 return base;
592 };
593 let MergedValueKind::Mapping(incoming_entries) = incoming.kind else {
594 return MergedValue {
595 kind: MergedValueKind::Mapping(base_entries),
596 provenance: base.provenance,
597 };
598 };
599
600 for incoming_entry in incoming_entries {
601 if let Some(index) = base_entries.iter().position(|entry| entry.key == incoming_entry.key) {
602 let mut child_path = path.to_vec();
603 child_path.push(incoming_entry.key.clone());
604 let existing = &mut base_entries[index];
605 existing.value = merge_value(existing.value.clone(), incoming_entry.value, &child_path, diagnostics);
606 extend_sources(&mut existing.key_sources, &incoming_entry.key_sources);
607 existing.syntax = incoming_entry.syntax;
608 } else {
609 let mut incoming_entry = incoming_entry;
610 incoming_entry.value = activate_tags(incoming_entry.value, path, diagnostics);
611 mark_added(&mut incoming_entry.value);
612 base_entries.push(incoming_entry);
613 }
614 }
615
616 MergedValue {
617 kind: MergedValueKind::Mapping(base_entries),
618 provenance: combined_provenance(base.provenance, &incoming.provenance, MergeOperation::Merged),
619 }
620}
621
622fn append_sequences(base: MergedValue, incoming: MergedValue) -> MergedValue {
623 let MergedValueKind::Sequence(mut base_values) = base.kind else {
624 return base;
625 };
626 let MergedValueKind::Sequence(mut incoming_values) = incoming.kind else {
627 return MergedValue {
628 kind: MergedValueKind::Sequence(base_values),
629 provenance: base.provenance,
630 };
631 };
632 for value in &mut incoming_values {
633 mark_added(value);
634 }
635 base_values.append(&mut incoming_values);
636 MergedValue {
637 kind: MergedValueKind::Sequence(base_values),
638 provenance: combined_provenance(base.provenance, &incoming.provenance, MergeOperation::Appended),
639 }
640}
641
642fn merge_unique_sequences(
643 base: MergedValue,
644 incoming: MergedValue,
645 path: &[String],
646 diagnostics: &mut Vec<Diagnostic>,
647) -> MergedValue {
648 let Some(field) = unique_field(path) else {
649 return append_sequences(base, incoming);
650 };
651 let MergedValueKind::Sequence(mut base_values) = base.kind else {
652 return base;
653 };
654 let MergedValueKind::Sequence(incoming_values) = incoming.kind else {
655 return MergedValue {
656 kind: MergedValueKind::Sequence(base_values),
657 provenance: base.provenance,
658 };
659 };
660
661 for mut incoming_value in incoming_values {
662 let key = unique_key(&incoming_value, field);
663 let existing = key.as_ref().and_then(|key| {
664 base_values
665 .iter()
666 .position(|value| unique_key(value, field).as_ref() == Some(key))
667 });
668 if let Some(index) = existing {
669 base_values[index] = merge_value(base_values[index].clone(), incoming_value, path, diagnostics);
670 } else {
671 incoming_value = activate_tags(incoming_value, path, diagnostics);
672 mark_added(&mut incoming_value);
673 base_values.push(incoming_value);
674 }
675 }
676
677 MergedValue {
678 kind: MergedValueKind::Sequence(base_values),
679 provenance: combined_provenance(base.provenance, &incoming.provenance, MergeOperation::Merged),
680 }
681}
682
683fn replace_value(base: MergedValue, mut incoming: MergedValue, operation: MergeOperation) -> MergedValue {
684 incoming.provenance = combined_provenance(base.provenance, &incoming.provenance, operation);
685 incoming
686}
687
688fn reset_value(base: Option<&MergedValue>, tagged: &MergedValue, tag: MergeProvenance) -> MergedValue {
689 let kind = match &tagged.kind {
690 MergedValueKind::Mapping(_) => MergedValueKind::Mapping(Vec::new()),
691 MergedValueKind::Sequence(_) => MergedValueKind::Sequence(Vec::new()),
692 MergedValueKind::Null(_) => match base.map(|value| &value.kind) {
693 Some(MergedValueKind::Mapping(_)) => MergedValueKind::Mapping(Vec::new()),
694 Some(MergedValueKind::Sequence(_)) => MergedValueKind::Sequence(Vec::new()),
695 _ => MergedValueKind::Null(NullStyle::Empty),
696 },
697 _ => MergedValueKind::Null(NullStyle::Empty),
698 };
699 let provenance = match base {
700 Some(base) => combined_provenance(base.provenance.clone(), &tag, MergeOperation::Reset),
701 None => MergeProvenance {
702 operation: MergeOperation::Reset,
703 sources: tag.sources,
704 },
705 };
706 MergedValue { kind, provenance }
707}
708
709fn override_value(base: Option<&MergedValue>, mut tagged: MergedValue, tag: MergeProvenance) -> MergedValue {
710 tagged.provenance = match base {
711 Some(base) => {
712 let prior = combined_provenance(base.provenance.clone(), &tag, MergeOperation::Override);
713 combined_provenance(prior, &tagged.provenance, MergeOperation::Override)
714 }
715 None => combined_provenance(tag, &tagged.provenance, MergeOperation::Override),
716 };
717 tagged
718}
719
720fn combined_provenance(
721 mut base: MergeProvenance,
722 incoming: &MergeProvenance,
723 operation: MergeOperation,
724) -> MergeProvenance {
725 extend_sources(&mut base.sources, &incoming.sources);
726 base.operation = operation;
727 base
728}
729
730fn extend_sources(target: &mut Vec<SourceSpan>, incoming: &[SourceSpan]) {
731 for span in incoming {
732 if !target.contains(span) {
733 target.push(*span);
734 }
735 }
736}
737
738fn mark_added(value: &mut MergedValue) {
739 if value.provenance.operation == MergeOperation::Authored {
740 value.provenance.operation = MergeOperation::Added;
741 }
742}
743
744fn is_shell_command(path: &[String]) -> bool {
745 matches!(path, [services, _, field] if services == "services" && (field == "command" || field == "entrypoint"))
746 || matches!(path, [services, _, healthcheck, test] if services == "services" && healthcheck == "healthcheck" && test == "test")
747}
748
749fn is_keyed_mapping(path: &[String]) -> bool {
750 matches!(path, [services, _, field] if services == "services" && (field == "environment" || field == "labels"))
751}
752
753fn normalize_keyed(value: MergedValue) -> Option<MergedValue> {
754 match value.kind {
755 MergedValueKind::Mapping(_) => Some(value),
756 MergedValueKind::Sequence(values) => {
757 let mut entries = Vec::with_capacity(values.len());
758 for value in values {
759 let scalar = value.as_scalar()?;
760 let (key, entry_value, syntax) = if let Some((key, entry_value)) = scalar.value.split_once('=') {
761 let scalar_value = MergedValue {
762 kind: MergedValueKind::Scalar(MergedScalar {
763 raw: entry_value.to_owned(),
764 value: entry_value.to_owned(),
765 kind: MergedScalarKind::String,
766 sensitive: scalar.sensitive,
767 }),
768 provenance: value.provenance.clone(),
769 };
770 (key.to_owned(), scalar_value, EntrySyntax::ListKeyValue)
771 } else {
772 let null = MergedValue {
773 kind: MergedValueKind::Null(NullStyle::Empty),
774 provenance: value.provenance.clone(),
775 };
776 (scalar.value.clone(), null, EntrySyntax::ListKeyOnly)
777 };
778 let key_source = value.provenance.effective_source()?;
779 entries.push(MergedEntry {
780 key,
781 key_sources: vec![key_source],
782 syntax,
783 value: entry_value,
784 });
785 }
786 Some(MergedValue {
787 kind: MergedValueKind::Mapping(entries),
788 provenance: value.provenance,
789 })
790 }
791 _ => None,
792 }
793}
794
795#[derive(Debug, Clone, Copy, PartialEq, Eq)]
796enum UniqueField {
797 Volume,
798 Device,
799 Config,
800 Secret,
801 Port,
802}
803
804fn unique_field(path: &[String]) -> Option<UniqueField> {
805 let [services, _, field] = path else {
806 return None;
807 };
808 if services != "services" {
809 return None;
810 }
811 match field.as_str() {
812 "volumes" => Some(UniqueField::Volume),
813 "devices" => Some(UniqueField::Device),
814 "configs" => Some(UniqueField::Config),
815 "secrets" => Some(UniqueField::Secret),
816 "ports" => Some(UniqueField::Port),
817 _ => None,
818 }
819}
820
821#[derive(Debug, Clone, PartialEq, Eq)]
822enum UniqueKey {
823 Target(String),
824 Port {
825 ip: String,
826 target: String,
827 published: String,
828 protocol: String,
829 },
830}
831
832fn unique_key(value: &MergedValue, field: UniqueField) -> Option<UniqueKey> {
833 match field {
834 UniqueField::Volume | UniqueField::Device => target_key(value, true).map(UniqueKey::Target),
835 UniqueField::Config | UniqueField::Secret => target_key(value, false).map(UniqueKey::Target),
836 UniqueField::Port => port_key(value),
837 }
838}
839
840fn target_key(value: &MergedValue, colon_syntax: bool) -> Option<String> {
841 if let Some(scalar) = value.as_scalar() {
842 if colon_syntax {
843 let span = value.provenance.effective_source()?;
844 return ShortVolumeMount::new(Located::new(scalar.value.clone(), span))
845 .target()
846 .map(str::to_owned);
847 }
848 return Some(scalar.value.clone());
849 }
850 mapping_scalar(value, "target")
851 .or_else(|| (!colon_syntax).then(|| mapping_scalar(value, "source")).flatten())
852 .map(str::to_owned)
853}
854
855fn port_key(value: &MergedValue) -> Option<UniqueKey> {
856 if let Some(scalar) = value.as_scalar() {
857 let span = value.provenance.effective_source()?;
858 let port = ShortPort::parse(Located::new(scalar.value.clone(), span));
859 return Some(UniqueKey::Port {
860 ip: port.host_ip().unwrap_or_default().to_owned(),
861 target: port.target().to_owned(),
862 published: port.published().unwrap_or_default().to_owned(),
863 protocol: port.protocol().unwrap_or("tcp").to_owned(),
864 });
865 }
866 Some(UniqueKey::Port {
867 ip: mapping_scalar(value, "host_ip").unwrap_or_default().to_owned(),
868 target: mapping_scalar(value, "target")?.to_owned(),
869 published: mapping_scalar(value, "published").unwrap_or_default().to_owned(),
870 protocol: mapping_scalar(value, "protocol").unwrap_or("tcp").to_owned(),
871 })
872}
873
874fn mapping_scalar<'a>(value: &'a MergedValue, key: &str) -> Option<&'a str> {
875 value.get(key)?.as_scalar().map(MergedScalar::value)
876}