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 plain: bool,
86 strict_yaml_string: bool,
87 sensitive: bool,
88}
89
90impl MergedScalar {
91 #[must_use]
93 pub fn raw(&self) -> &str {
94 &self.raw
95 }
96
97 #[must_use]
99 pub fn value(&self) -> &str {
100 &self.value
101 }
102
103 #[must_use]
105 pub const fn kind(&self) -> MergedScalarKind {
106 self.kind
107 }
108
109 #[must_use]
111 pub(crate) const fn is_strict_yaml_string(&self) -> bool {
112 self.strict_yaml_string
113 }
114
115 #[must_use]
117 pub(crate) const fn is_plain_style(&self) -> bool {
118 self.plain
119 }
120
121 #[must_use]
123 pub const fn is_sensitive(&self) -> bool {
124 self.sensitive
125 }
126}
127
128impl fmt::Debug for MergedScalar {
129 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130 formatter
131 .debug_struct("MergedScalar")
132 .field("raw", &self.raw)
133 .field("value", &if self.sensitive { "<redacted>" } else { &self.value })
134 .field("kind", &self.kind)
135 .field("plain", &self.plain)
136 .field("strict_yaml_string", &self.strict_yaml_string)
137 .field("sensitive", &self.sensitive)
138 .finish()
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144pub enum NullStyle {
145 Empty,
147 Explicit,
149}
150
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
153pub enum EntrySyntax {
154 Mapping,
156 ListKeyValue,
158 ListKeyOnly,
160}
161
162#[derive(Clone, PartialEq, Eq)]
164pub struct MergedEntry {
165 key: String,
166 key_sources: Vec<SourceSpan>,
167 key_sensitive: bool,
168 strict_yaml_string: bool,
169 syntax: EntrySyntax,
170 raw_list_item: Option<MergedScalar>,
171 value: MergedValue,
172}
173
174impl fmt::Debug for MergedEntry {
175 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176 formatter
177 .debug_struct("MergedEntry")
178 .field("key", &if self.key_sensitive { "<redacted>" } else { &self.key })
179 .field("key_sources", &self.key_sources)
180 .field("key_sensitive", &self.key_sensitive)
181 .field("strict_yaml_string", &self.strict_yaml_string)
182 .field("syntax", &self.syntax)
183 .field("raw_list_item", &self.raw_list_item)
184 .field("value", &self.value)
185 .finish()
186 }
187}
188
189impl MergedEntry {
190 #[must_use]
192 pub fn key(&self) -> &str {
193 &self.key
194 }
195
196 #[must_use]
198 pub fn key_sources(&self) -> &[SourceSpan] {
199 &self.key_sources
200 }
201
202 #[must_use]
204 pub const fn is_key_sensitive(&self) -> bool {
205 self.key_sensitive
206 }
207
208 #[must_use]
210 pub(crate) const fn is_strict_yaml_string(&self) -> bool {
211 self.strict_yaml_string
212 }
213
214 #[must_use]
216 pub const fn syntax(&self) -> EntrySyntax {
217 self.syntax
218 }
219
220 #[must_use]
222 pub const fn raw_list_item(&self) -> Option<&MergedScalar> {
223 self.raw_list_item.as_ref()
224 }
225
226 #[must_use]
228 pub const fn value(&self) -> &MergedValue {
229 &self.value
230 }
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
235pub enum MergedValueKind {
236 Null(NullStyle),
238 Scalar(MergedScalar),
240 Mapping(Vec<MergedEntry>),
242 Sequence(Vec<MergedValue>),
244 Alias(String),
246 Tagged {
248 tag: String,
250 value: Box<MergedValue>,
252 },
253}
254
255#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct MergedValue {
258 kind: MergedValueKind,
259 provenance: MergeProvenance,
260}
261
262impl MergedValue {
263 #[must_use]
265 pub const fn kind(&self) -> &MergedValueKind {
266 &self.kind
267 }
268
269 #[must_use]
271 pub const fn provenance(&self) -> &MergeProvenance {
272 &self.provenance
273 }
274
275 #[must_use]
277 pub const fn as_scalar(&self) -> Option<&MergedScalar> {
278 match &self.kind {
279 MergedValueKind::Scalar(value) => Some(value),
280 _ => None,
281 }
282 }
283
284 #[must_use]
286 pub fn as_mapping(&self) -> Option<&[MergedEntry]> {
287 match &self.kind {
288 MergedValueKind::Mapping(entries) => Some(entries),
289 _ => None,
290 }
291 }
292
293 #[must_use]
295 pub fn as_sequence(&self) -> Option<&[MergedValue]> {
296 match &self.kind {
297 MergedValueKind::Sequence(values) => Some(values),
298 _ => None,
299 }
300 }
301
302 #[must_use]
304 pub fn get(&self, key: &str) -> Option<&MergedValue> {
305 self.as_mapping()?
306 .iter()
307 .find(|entry| entry.key == key)
308 .map(|entry| &entry.value)
309 }
310
311 #[must_use]
313 pub fn is_sensitive(&self) -> bool {
314 match &self.kind {
315 MergedValueKind::Scalar(value) => value.sensitive,
316 MergedValueKind::Mapping(entries) => entries.iter().any(|entry| entry.value.is_sensitive()),
317 MergedValueKind::Sequence(values) => values.iter().any(Self::is_sensitive),
318 MergedValueKind::Tagged { value, .. } => value.is_sensitive(),
319 MergedValueKind::Null(_) | MergedValueKind::Alias(_) => false,
320 }
321 }
322}
323
324#[derive(Debug, Clone, PartialEq, Eq)]
326pub struct MergedProject {
327 root: MergedValue,
328 base_directory: PathBuf,
329 source_ids: Vec<SourceId>,
330}
331
332impl MergedProject {
333 #[must_use]
335 pub const fn root(&self) -> &MergedValue {
336 &self.root
337 }
338
339 #[must_use]
341 pub fn base_directory(&self) -> &Path {
342 &self.base_directory
343 }
344
345 #[must_use]
347 pub fn source_ids(&self) -> &[SourceId] {
348 &self.source_ids
349 }
350
351 #[must_use]
353 pub fn value(&self, path: &[&str]) -> Option<&MergedValue> {
354 path.iter().try_fold(&self.root, |value, key| value.get(key))
355 }
356}
357
358#[derive(Debug, Clone, PartialEq, Eq)]
360pub struct MergeResult {
361 project: Option<MergedProject>,
362 diagnostics: Vec<Diagnostic>,
363}
364
365impl MergeResult {
366 #[must_use]
368 pub const fn project(&self) -> Option<&MergedProject> {
369 self.project.as_ref()
370 }
371
372 #[must_use]
374 pub fn diagnostics(&self) -> &[Diagnostic] {
375 &self.diagnostics
376 }
377
378 #[must_use]
380 pub fn is_valid(&self) -> bool {
381 self.project.is_some()
382 && self
383 .diagnostics
384 .iter()
385 .all(|diagnostic| diagnostic.severity() != Severity::Error)
386 }
387}
388
389#[must_use]
394pub fn merge_project(project: &LoadedProject, interpolation: Option<&ProjectInterpolation>) -> MergeResult {
395 let mut diagnostics = project.diagnostics().to_vec();
396 if let Some(interpolation) = interpolation {
397 diagnostics.extend(interpolation.diagnostics().iter().cloned());
398 }
399
400 let mut root = None;
401 let mut source_ids = Vec::new();
402 for document in project.documents() {
403 let overlay = interpolation.and_then(|values| values.document(document.source_id()));
404 if interpolation.is_some() && overlay.is_none() {
405 diagnostics.push(
406 Diagnostic::new(
407 INTERPOLATION_PROJECT_MISMATCH,
408 Severity::Error,
409 "loaded document has no matching interpolation overlay",
410 )
411 .with_label(DiagnosticLabel::primary(
412 document.syntax().source_span(),
413 "missing per-file overlay",
414 )),
415 );
416 }
417
418 let Some(syntax_root) = document.syntax().merge_root() else {
419 continue;
420 };
421 if !matches!(syntax_root, MergeSyntaxValue::Mapping { .. }) {
422 diagnostics.push(
423 Diagnostic::new(
424 INVALID_DOCUMENT_ROOT,
425 Severity::Error,
426 "Compose merge inputs must have mapping roots",
427 )
428 .with_label(DiagnosticLabel::primary(
429 document.syntax().source_span(),
430 "document is not a mapping",
431 )),
432 );
433 continue;
434 }
435
436 let value = convert_value(syntax_root, overlay, &mut diagnostics);
437 source_ids.push(document.source_id());
438 root = Some(match root {
439 Some(current) => merge_value(current, value, &[], &mut diagnostics),
440 None => activate_tags(value, &[], &mut diagnostics),
441 });
442 }
443
444 if let Some(interpolation) = interpolation {
445 for overlay in interpolation.documents() {
446 if project.document(overlay.source_id()).is_none() {
447 diagnostics.push(Diagnostic::new(
448 INTERPOLATION_PROJECT_MISMATCH,
449 Severity::Error,
450 "interpolation overlay does not belong to the loaded project",
451 ));
452 }
453 }
454 }
455
456 MergeResult {
457 project: root.map(|root| MergedProject {
458 root,
459 base_directory: project.base_directory().to_path_buf(),
460 source_ids,
461 }),
462 diagnostics,
463 }
464}
465
466fn convert_value(
467 value: MergeSyntaxValue,
468 interpolation: Option<&DocumentInterpolation>,
469 diagnostics: &mut Vec<Diagnostic>,
470) -> MergedValue {
471 match value {
472 MergeSyntaxValue::Empty(span) => authored(MergedValueKind::Null(NullStyle::Empty), span),
473 MergeSyntaxValue::Scalar(value) if value.kind == MergeScalarKind::Null => {
474 authored(MergedValueKind::Null(NullStyle::Explicit), value.span)
475 }
476 MergeSyntaxValue::Scalar(value) => convert_scalar(value, interpolation),
477 MergeSyntaxValue::Mapping { entries, span } => {
478 let entries = entries
479 .into_iter()
480 .map(|entry| convert_entry(entry, interpolation, diagnostics))
481 .collect();
482 authored(MergedValueKind::Mapping(entries), span)
483 }
484 MergeSyntaxValue::Sequence { values, span } => {
485 let values = values
486 .into_iter()
487 .map(|value| convert_value(value, interpolation, diagnostics))
488 .collect();
489 authored(MergedValueKind::Sequence(values), span)
490 }
491 MergeSyntaxValue::Alias { name, span } => {
492 diagnostics.push(
493 Diagnostic::new(UNRESOLVED_ALIAS, Severity::Warning, "YAML alias could not be resolved")
494 .with_label(DiagnosticLabel::primary(span, "unresolved alias")),
495 );
496 authored(MergedValueKind::Alias(name), span)
497 }
498 MergeSyntaxValue::Tagged { tag, value, span } => {
499 let value = convert_value(*value, interpolation, diagnostics);
500 authored(
501 MergedValueKind::Tagged {
502 tag,
503 value: Box::new(value),
504 },
505 span,
506 )
507 }
508 }
509}
510
511fn convert_entry(
512 entry: MergeSyntaxEntry,
513 interpolation: Option<&DocumentInterpolation>,
514 diagnostics: &mut Vec<Diagnostic>,
515) -> MergedEntry {
516 MergedEntry {
517 key: entry.key.value,
518 key_sources: vec![entry.key.span],
519 key_sensitive: false,
520 strict_yaml_string: entry.key.strict_yaml_string,
521 syntax: EntrySyntax::Mapping,
522 raw_list_item: None,
523 value: convert_value(entry.value, interpolation, diagnostics),
524 }
525}
526
527fn convert_scalar(value: MergeSyntaxScalar, interpolation: Option<&DocumentInterpolation>) -> MergedValue {
528 let resolved = interpolation.and_then(|overlay| overlay.value(value.span));
529 let semantic = resolved.map_or_else(|| value.value.clone(), |result| result.resolved().to_owned());
530 let sensitive = resolved.is_some_and(crate::interpolation::InterpolationResult::is_sensitive);
531 let kind = match value.kind {
532 MergeScalarKind::String => MergedScalarKind::String,
533 MergeScalarKind::Boolean => MergedScalarKind::Boolean,
534 MergeScalarKind::Number => MergedScalarKind::Number,
535 MergeScalarKind::Null => unreachable!("null scalars are converted before convert_scalar"),
536 };
537 authored(
538 MergedValueKind::Scalar(MergedScalar {
539 raw: value.raw,
540 value: semantic,
541 kind,
542 plain: value.plain,
543 strict_yaml_string: value.strict_yaml_string,
544 sensitive,
545 }),
546 value.span,
547 )
548}
549
550fn authored(kind: MergedValueKind, span: SourceSpan) -> MergedValue {
551 MergedValue {
552 kind,
553 provenance: MergeProvenance {
554 operation: MergeOperation::Authored,
555 sources: vec![span],
556 },
557 }
558}
559
560fn activate_tags(mut value: MergedValue, path: &[String], diagnostics: &mut Vec<Diagnostic>) -> MergedValue {
561 value = match value.kind {
562 MergedValueKind::Tagged { tag, value: inner } if tag == "!reset" => reset_value(None, &inner, value.provenance),
563 MergedValueKind::Tagged { tag, value: inner } if tag == "!override" => {
564 override_value(None, *inner, value.provenance)
565 }
566 kind => MergedValue {
567 kind,
568 provenance: value.provenance,
569 },
570 };
571
572 match &mut value.kind {
573 MergedValueKind::Mapping(entries) => {
574 for entry in entries {
575 let mut child_path = path.to_vec();
576 child_path.push(entry.key.clone());
577 entry.value = activate_tags(entry.value.clone(), &child_path, diagnostics);
578 }
579 }
580 MergedValueKind::Sequence(values) => {
581 for item in values {
582 *item = activate_tags(item.clone(), path, diagnostics);
583 }
584 }
585 MergedValueKind::Tagged { value: inner, .. } => {
586 **inner = activate_tags((**inner).clone(), path, diagnostics);
587 }
588 MergedValueKind::Null(_) | MergedValueKind::Scalar(_) | MergedValueKind::Alias(_) => {}
589 }
590 let _ = diagnostics;
591 value
592}
593
594fn merge_value(
595 base: MergedValue,
596 incoming: MergedValue,
597 path: &[String],
598 diagnostics: &mut Vec<Diagnostic>,
599) -> MergedValue {
600 if let MergedValueKind::Tagged { tag, value } = incoming.kind {
601 if tag == "!reset" {
602 return reset_value(Some(&base), &value, incoming.provenance);
603 }
604 if tag == "!override" {
605 return override_value(Some(&base), *value, incoming.provenance);
606 }
607 return replace_value(
608 base,
609 MergedValue {
610 kind: MergedValueKind::Tagged { tag, value },
611 provenance: incoming.provenance,
612 },
613 MergeOperation::Replaced,
614 );
615 }
616
617 if is_shell_command(path) {
618 return replace_value(base, incoming, MergeOperation::Replaced);
619 }
620
621 if is_whole_sequence_replacement(path) {
622 return replace_value(
623 base,
624 activate_tags(incoming, path, diagnostics),
625 MergeOperation::Replaced,
626 );
627 }
628
629 if is_keyed_mapping(path) {
630 let normalize = if is_annotations(path) {
631 normalize_annotations
632 } else {
633 normalize_keyed
634 };
635 if let (Some(base), Some(incoming)) = (normalize(base.clone()), normalize(incoming.clone())) {
636 return merge_mappings(base, incoming, path, diagnostics);
637 }
638 }
639
640 match (&base.kind, &incoming.kind) {
641 (MergedValueKind::Mapping(_), MergedValueKind::Mapping(_)) => merge_mappings(base, incoming, path, diagnostics),
642 (MergedValueKind::Sequence(_), MergedValueKind::Sequence(_)) if unique_field(path).is_some() => {
643 merge_unique_sequences(base, incoming, path, diagnostics)
644 }
645 (MergedValueKind::Sequence(_), MergedValueKind::Sequence(_)) => append_sequences(base, incoming),
646 _ => replace_value(
647 base,
648 activate_tags(incoming, path, diagnostics),
649 MergeOperation::Replaced,
650 ),
651 }
652}
653
654fn merge_mappings(
655 base: MergedValue,
656 incoming: MergedValue,
657 path: &[String],
658 diagnostics: &mut Vec<Diagnostic>,
659) -> MergedValue {
660 let MergedValueKind::Mapping(mut base_entries) = base.kind else {
661 return base;
662 };
663 let MergedValueKind::Mapping(incoming_entries) = incoming.kind else {
664 return MergedValue {
665 kind: MergedValueKind::Mapping(base_entries),
666 provenance: base.provenance,
667 };
668 };
669
670 for incoming_entry in incoming_entries {
671 if let Some(index) = base_entries.iter().position(|entry| entry.key == incoming_entry.key) {
672 let mut child_path = path.to_vec();
673 child_path.push(incoming_entry.key.clone());
674 let existing = &mut base_entries[index];
675 existing.value = merge_value(existing.value.clone(), incoming_entry.value, &child_path, diagnostics);
676 extend_sources(&mut existing.key_sources, &incoming_entry.key_sources);
677 existing.key_sensitive |= incoming_entry.key_sensitive;
678 existing.strict_yaml_string = incoming_entry.strict_yaml_string;
679 existing.syntax = incoming_entry.syntax;
680 existing.raw_list_item = incoming_entry.raw_list_item;
681 } else {
682 let mut incoming_entry = incoming_entry;
683 incoming_entry.value = activate_tags(incoming_entry.value, path, diagnostics);
684 mark_added(&mut incoming_entry.value);
685 base_entries.push(incoming_entry);
686 }
687 }
688
689 MergedValue {
690 kind: MergedValueKind::Mapping(base_entries),
691 provenance: combined_provenance(base.provenance, &incoming.provenance, MergeOperation::Merged),
692 }
693}
694
695fn append_sequences(base: MergedValue, incoming: MergedValue) -> MergedValue {
696 let MergedValueKind::Sequence(mut base_values) = base.kind else {
697 return base;
698 };
699 let MergedValueKind::Sequence(mut incoming_values) = incoming.kind else {
700 return MergedValue {
701 kind: MergedValueKind::Sequence(base_values),
702 provenance: base.provenance,
703 };
704 };
705 for value in &mut incoming_values {
706 mark_added(value);
707 }
708 base_values.append(&mut incoming_values);
709 MergedValue {
710 kind: MergedValueKind::Sequence(base_values),
711 provenance: combined_provenance(base.provenance, &incoming.provenance, MergeOperation::Appended),
712 }
713}
714
715fn merge_unique_sequences(
716 base: MergedValue,
717 incoming: MergedValue,
718 path: &[String],
719 diagnostics: &mut Vec<Diagnostic>,
720) -> MergedValue {
721 let Some(field) = unique_field(path) else {
722 return append_sequences(base, incoming);
723 };
724 if matches!(field, UniqueField::ExactStringScalar | UniqueField::ExactScalar) {
725 return merge_exact_scalar_sequences(base, incoming, field == UniqueField::ExactScalar);
726 }
727 let MergedValueKind::Sequence(mut base_values) = base.kind else {
728 return base;
729 };
730 let MergedValueKind::Sequence(incoming_values) = incoming.kind else {
731 return MergedValue {
732 kind: MergedValueKind::Sequence(base_values),
733 provenance: base.provenance,
734 };
735 };
736
737 for mut incoming_value in incoming_values {
738 let key = unique_key(&incoming_value, field);
739 let existing = key.as_ref().and_then(|key| {
740 base_values
741 .iter()
742 .position(|value| unique_key(value, field).as_ref() == Some(key))
743 });
744 if let Some(index) = existing {
745 base_values[index] = merge_value(base_values[index].clone(), incoming_value, path, diagnostics);
746 } else {
747 incoming_value = activate_tags(incoming_value, path, diagnostics);
748 mark_added(&mut incoming_value);
749 base_values.push(incoming_value);
750 }
751 }
752
753 MergedValue {
754 kind: MergedValueKind::Sequence(base_values),
755 provenance: combined_provenance(base.provenance, &incoming.provenance, MergeOperation::Merged),
756 }
757}
758
759fn merge_exact_scalar_sequences(
760 base: MergedValue,
761 incoming: MergedValue,
762 include_all_scalar_kinds: bool,
763) -> MergedValue {
764 let MergedValueKind::Sequence(mut values) = base.kind else {
765 return base;
766 };
767 let MergedValueKind::Sequence(mut incoming_values) = incoming.kind else {
768 return MergedValue {
769 kind: MergedValueKind::Sequence(values),
770 provenance: base.provenance,
771 };
772 };
773 for value in &mut incoming_values {
774 mark_added(value);
775 }
776 values.append(&mut incoming_values);
777
778 let mut deduplicated: Vec<MergedValue> = Vec::with_capacity(values.len());
779 for value in values {
780 let exact = value.as_scalar().and_then(|scalar| {
781 (include_all_scalar_kinds || scalar.kind == MergedScalarKind::String)
782 .then(|| (scalar.kind, scalar.value.clone()))
783 });
784 let existing = exact.as_ref().and_then(|(kind, exact)| {
785 deduplicated.iter().position(|candidate| {
786 candidate
787 .as_scalar()
788 .is_some_and(|scalar| scalar.kind == *kind && scalar.value == *exact)
789 })
790 });
791 if let Some(index) = existing {
792 let prior = &deduplicated[index];
793 let sensitive = prior.is_sensitive() || value.is_sensitive();
794 let provenance = combined_provenance(prior.provenance.clone(), &value.provenance, MergeOperation::Merged);
795 let mut retained = prior.clone();
796 retained.provenance = provenance;
797 if let MergedValueKind::Scalar(scalar) = &mut retained.kind {
798 scalar.sensitive = sensitive;
799 }
800 deduplicated[index] = retained;
801 } else {
802 deduplicated.push(value);
803 }
804 }
805
806 MergedValue {
807 kind: MergedValueKind::Sequence(deduplicated),
808 provenance: combined_provenance(base.provenance, &incoming.provenance, MergeOperation::Merged),
809 }
810}
811
812fn replace_value(base: MergedValue, mut incoming: MergedValue, operation: MergeOperation) -> MergedValue {
813 incoming.provenance = combined_provenance(base.provenance, &incoming.provenance, operation);
814 incoming
815}
816
817fn reset_value(base: Option<&MergedValue>, tagged: &MergedValue, tag: MergeProvenance) -> MergedValue {
818 let kind = match &tagged.kind {
819 MergedValueKind::Mapping(_) => MergedValueKind::Mapping(Vec::new()),
820 MergedValueKind::Sequence(_) => MergedValueKind::Sequence(Vec::new()),
821 MergedValueKind::Null(_) => match base.map(|value| &value.kind) {
822 Some(MergedValueKind::Mapping(_)) => MergedValueKind::Mapping(Vec::new()),
823 Some(MergedValueKind::Sequence(_)) => MergedValueKind::Sequence(Vec::new()),
824 _ => MergedValueKind::Null(NullStyle::Empty),
825 },
826 _ => MergedValueKind::Null(NullStyle::Empty),
827 };
828 let provenance = match base {
829 Some(base) => combined_provenance(base.provenance.clone(), &tag, MergeOperation::Reset),
830 None => MergeProvenance {
831 operation: MergeOperation::Reset,
832 sources: tag.sources,
833 },
834 };
835 MergedValue { kind, provenance }
836}
837
838fn override_value(base: Option<&MergedValue>, mut tagged: MergedValue, tag: MergeProvenance) -> MergedValue {
839 tagged.provenance = match base {
840 Some(base) => {
841 let prior = combined_provenance(base.provenance.clone(), &tag, MergeOperation::Override);
842 combined_provenance(prior, &tagged.provenance, MergeOperation::Override)
843 }
844 None => combined_provenance(tag, &tagged.provenance, MergeOperation::Override),
845 };
846 tagged
847}
848
849fn combined_provenance(
850 mut base: MergeProvenance,
851 incoming: &MergeProvenance,
852 operation: MergeOperation,
853) -> MergeProvenance {
854 extend_sources(&mut base.sources, &incoming.sources);
855 base.operation = operation;
856 base
857}
858
859fn extend_sources(target: &mut Vec<SourceSpan>, incoming: &[SourceSpan]) {
860 for span in incoming {
861 if !target.contains(span) {
862 target.push(*span);
863 }
864 }
865}
866
867fn mark_added(value: &mut MergedValue) {
868 if value.provenance.operation == MergeOperation::Authored {
869 value.provenance.operation = MergeOperation::Added;
870 }
871}
872
873fn is_shell_command(path: &[String]) -> bool {
874 matches!(path, [services, _, field] if services == "services" && (field == "command" || field == "entrypoint"))
875 || matches!(path, [services, _, healthcheck, test] if services == "services" && healthcheck == "healthcheck" && test == "test")
876}
877
878fn is_whole_sequence_replacement(path: &[String]) -> bool {
879 matches!(path, [services, _, field] if services == "services" && field == "dns_opt")
880}
881
882fn is_keyed_mapping(path: &[String]) -> bool {
883 matches!(path, [services, _, field] if services == "services" && (field == "environment" || field == "labels" || field == "annotations"))
884}
885
886fn is_annotations(path: &[String]) -> bool {
887 matches!(path, [services, _, field] if services == "services" && field == "annotations")
888}
889
890fn normalize_annotations(value: MergedValue) -> Option<MergedValue> {
891 if let MergedValueKind::Sequence(values) = &value.kind {
892 for item in values {
893 let scalar = item.as_scalar()?;
894 if scalar.kind != MergedScalarKind::String {
895 return None;
896 }
897 let (name, _) = scalar.value.split_once('=')?;
898 if name.is_empty() {
899 return None;
900 }
901 }
902 }
903 normalize_keyed(value)
904}
905
906fn normalize_keyed(value: MergedValue) -> Option<MergedValue> {
907 match value.kind {
908 MergedValueKind::Mapping(_) => Some(value),
909 MergedValueKind::Sequence(values) => {
910 let mut entries = Vec::with_capacity(values.len());
911 for value in values {
912 let scalar = value.as_scalar()?;
913 let (key, entry_value, syntax) = if let Some((key, entry_value)) = scalar.value.split_once('=') {
914 let scalar_value = MergedValue {
915 kind: MergedValueKind::Scalar(MergedScalar {
916 raw: entry_value.to_owned(),
917 value: entry_value.to_owned(),
918 kind: MergedScalarKind::String,
919 plain: true,
920 strict_yaml_string: true,
921 sensitive: scalar.sensitive,
922 }),
923 provenance: value.provenance.clone(),
924 };
925 (key.to_owned(), scalar_value, EntrySyntax::ListKeyValue)
926 } else {
927 let null = MergedValue {
928 kind: MergedValueKind::Null(NullStyle::Empty),
929 provenance: value.provenance.clone(),
930 };
931 (scalar.value.clone(), null, EntrySyntax::ListKeyOnly)
932 };
933 let key_source = value.provenance.effective_source()?;
934 entries.push(MergedEntry {
935 key,
936 key_sources: vec![key_source],
937 key_sensitive: scalar.sensitive,
938 strict_yaml_string: true,
939 syntax,
940 raw_list_item: Some(scalar.clone()),
941 value: entry_value,
942 });
943 }
944 Some(MergedValue {
945 kind: MergedValueKind::Mapping(entries),
946 provenance: value.provenance,
947 })
948 }
949 _ => None,
950 }
951}
952
953#[derive(Debug, Clone, Copy, PartialEq, Eq)]
954enum UniqueField {
955 ExactStringScalar,
956 ExactScalar,
957 Volume,
958 Device,
959 Config,
960 Secret,
961 Port,
962}
963
964fn unique_field(path: &[String]) -> Option<UniqueField> {
965 let [services, _, field] = path else {
966 return None;
967 };
968 if services != "services" {
969 return None;
970 }
971 match field.as_str() {
972 "volumes" => Some(UniqueField::Volume),
973 "devices" => Some(UniqueField::Device),
974 "configs" => Some(UniqueField::Config),
975 "secrets" => Some(UniqueField::Secret),
976 "ports" => Some(UniqueField::Port),
977 "cap_add" | "cap_drop" => Some(UniqueField::ExactStringScalar),
978 "expose" => Some(UniqueField::ExactScalar),
979 _ => None,
980 }
981}
982
983#[derive(Debug, Clone, PartialEq, Eq)]
984enum UniqueKey {
985 Scalar(MergedScalarKind, String),
986 Target(String),
987 Port {
988 ip: String,
989 target: String,
990 published: String,
991 protocol: String,
992 },
993}
994
995fn unique_key(value: &MergedValue, field: UniqueField) -> Option<UniqueKey> {
996 match field {
997 UniqueField::ExactStringScalar => value.as_scalar().and_then(|scalar| {
998 (scalar.kind == MergedScalarKind::String).then(|| UniqueKey::Target(scalar.value.clone()))
999 }),
1000 UniqueField::ExactScalar => value
1001 .as_scalar()
1002 .map(|scalar| UniqueKey::Scalar(scalar.kind, scalar.value.clone())),
1003 UniqueField::Volume | UniqueField::Device => target_key(value, true).map(UniqueKey::Target),
1004 UniqueField::Config | UniqueField::Secret => target_key(value, false).map(UniqueKey::Target),
1005 UniqueField::Port => port_key(value),
1006 }
1007}
1008
1009fn target_key(value: &MergedValue, colon_syntax: bool) -> Option<String> {
1010 if let Some(scalar) = value.as_scalar() {
1011 if colon_syntax {
1012 let span = value.provenance.effective_source()?;
1013 return ShortVolumeMount::new(Located::new(scalar.value.clone(), span))
1014 .target()
1015 .map(str::to_owned);
1016 }
1017 return Some(scalar.value.clone());
1018 }
1019 mapping_scalar(value, "target")
1020 .or_else(|| (!colon_syntax).then(|| mapping_scalar(value, "source")).flatten())
1021 .map(str::to_owned)
1022}
1023
1024fn port_key(value: &MergedValue) -> Option<UniqueKey> {
1025 if let Some(scalar) = value.as_scalar() {
1026 let span = value.provenance.effective_source()?;
1027 let port = ShortPort::parse(Located::new(scalar.value.clone(), span));
1028 return Some(UniqueKey::Port {
1029 ip: port.host_ip().unwrap_or_default().to_owned(),
1030 target: port.target().to_owned(),
1031 published: port.published().unwrap_or_default().to_owned(),
1032 protocol: port.protocol().unwrap_or("tcp").to_owned(),
1033 });
1034 }
1035 Some(UniqueKey::Port {
1036 ip: mapping_scalar(value, "host_ip").unwrap_or_default().to_owned(),
1037 target: mapping_scalar(value, "target")?.to_owned(),
1038 published: mapping_scalar(value, "published").unwrap_or_default().to_owned(),
1039 protocol: mapping_scalar(value, "protocol").unwrap_or("tcp").to_owned(),
1040 })
1041}
1042
1043fn mapping_scalar<'a>(value: &'a MergedValue, key: &str) -> Option<&'a str> {
1044 value.get(key)?.as_scalar().map(MergedScalar::value)
1045}