Skip to main content

compose_lens/model/
mod.rs

1//! Source-aware native Compose document types.
2
3mod command;
4mod dependency;
5mod environment;
6mod host;
7mod identity;
8mod image;
9mod network;
10mod port;
11mod resource;
12mod sections;
13mod ulimit;
14mod value;
15mod volume;
16
17pub use command::Command;
18pub use dependency::{
19    DependencyCondition, DependsOn, Healthcheck, HealthcheckDuration, HealthcheckRetries, HealthcheckTest,
20    HealthcheckTestKind, ServiceDependency,
21};
22pub use environment::{Environment, EnvironmentListEntry, EnvironmentMapEntry};
23pub use host::{ExtraHostSeparator, ExtraHosts, HostAddress, HostAddressKind, LongExtraHost, ShortExtraHost};
24pub use identity::{IdentityComponent, UserNamespaceMode, UserNamespaceModeKind, UserSpec};
25pub use image::{ImageDigest, ImageReference};
26pub use network::{Ipam, IpamConfig, NetworkDefinition, ServiceNetwork, ServiceNetworks};
27pub use port::{LongPort, Port, ShortPort};
28pub use resource::{ConfigDefinition, ConfigGrant, LongGrant, SecretDefinition, SecretGrant, VolumeDefinition};
29pub use sections::{
30    Build, BuildDefinition, BuildField, BuildFieldKind, DeployDefinition, DeployField, DeployFieldKind,
31};
32pub use ulimit::{LimitValue, Ulimit, UlimitRange, UlimitValue, Ulimits};
33pub use value::{BooleanValue, ComposeScalar, KeyValueEntry, Labels};
34pub use volume::{
35    BindOptions, ContainerPath, ContainerPathKind, LongVolumeMount, MountType, SelinuxRelabel, ShortVolumeMount,
36    VolumeMount, VolumeSyntax,
37};
38
39use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticLabel, Severity};
40use crate::source::{SourceId, SourceSpan};
41use crate::syntax::{SyntaxDocument, scalar_string_from_source};
42use std::collections::{BTreeMap, BTreeSet};
43use yaml_edit::{AnchorRegistry, AsYaml, Mapping, ScalarType, ScalarValue, YamlNode};
44
45/// A Compose document root must be a mapping.
46pub const DOCUMENT_ROOT_TYPE: DiagnosticCode = DiagnosticCode::new("compose.document.expected-mapping");
47
48/// `ComposeLens` currently types the first document in a multi-document YAML stream.
49pub const MULTIPLE_DOCUMENTS: DiagnosticCode = DiagnosticCode::new("compose.document.multiple-documents");
50
51/// A mapping contains a duplicate field.
52pub const DUPLICATE_FIELD: DiagnosticCode = DiagnosticCode::new("compose.model.duplicate-field");
53
54/// A Compose value has to be a mapping at this location.
55pub const EXPECTED_MAPPING: DiagnosticCode = DiagnosticCode::new("compose.model.expected-mapping");
56
57/// A Compose value has to be a sequence at this location.
58pub const EXPECTED_SEQUENCE: DiagnosticCode = DiagnosticCode::new("compose.model.expected-sequence");
59
60/// A Compose value has to be a scalar at this location.
61pub const EXPECTED_SCALAR: DiagnosticCode = DiagnosticCode::new("compose.model.expected-scalar");
62
63/// A Compose value has to be a boolean at this location.
64pub const EXPECTED_BOOLEAN: DiagnosticCode = DiagnosticCode::new("compose.model.expected-boolean");
65
66/// A field supports multiple Compose syntax forms, but the authored form is invalid here.
67pub const EXPECTED_FIELD_FORM: DiagnosticCode = DiagnosticCode::new("compose.model.expected-field-form");
68
69/// A service port is neither scalar short syntax nor mapping long syntax.
70pub const PORT_EXPECTED_FORM: DiagnosticCode = DiagnosticCode::new("compose.port.expected-short-or-long");
71
72/// A long-syntax service port is missing `target`.
73pub const PORT_MISSING_TARGET: DiagnosticCode = DiagnosticCode::new("compose.port.long.missing-target");
74
75/// A service config or secret grant is neither scalar short syntax nor mapping long syntax.
76pub const GRANT_EXPECTED_FORM: DiagnosticCode = DiagnosticCode::new("compose.grant.expected-short-or-long");
77
78/// A long-syntax service config or secret grant is missing `source`.
79pub const GRANT_MISSING_SOURCE: DiagnosticCode = DiagnosticCode::new("compose.grant.long.missing-source");
80
81/// A top-level resource definition must be a mapping or an explicit null.
82pub const RESOURCE_EXPECTED_FORM: DiagnosticCode = DiagnosticCode::new("compose.resource.expected-mapping-or-null");
83
84/// A service-volume item is neither short nor long syntax.
85pub const VOLUME_EXPECTED_FORM: DiagnosticCode = DiagnosticCode::new("compose.volume.expected-short-or-long");
86
87/// A long-syntax service volume is missing `type`.
88pub const VOLUME_MISSING_TYPE: DiagnosticCode = DiagnosticCode::new("compose.volume.long.missing-type");
89
90/// A long-syntax service volume is missing `target`.
91pub const VOLUME_MISSING_TARGET: DiagnosticCode = DiagnosticCode::new("compose.volume.long.missing-target");
92
93/// A long-syntax bind mount has an invalid `SELinux` value.
94pub const VOLUME_INVALID_SELINUX: DiagnosticCode = DiagnosticCode::new("compose.volume.bind.invalid-selinux");
95
96/// A short `extra_hosts` entry does not contain a hostname/address separator.
97pub const EXTRA_HOST_INVALID_ENTRY: DiagnosticCode = DiagnosticCode::new("compose.extra-hosts.invalid-entry");
98
99/// A service limit is neither unlimited, a non-negative integer, nor deferred.
100pub const ULIMIT_INVALID_VALUE: DiagnosticCode = DiagnosticCode::new("compose.ulimits.invalid-value");
101
102/// A health-check list has no valid command-mode token.
103pub const HEALTHCHECK_INVALID_TEST: DiagnosticCode = DiagnosticCode::new("compose.healthcheck.invalid-test");
104
105/// A health-check duration does not follow Compose duration syntax.
106pub const HEALTHCHECK_INVALID_DURATION: DiagnosticCode = DiagnosticCode::new("compose.healthcheck.invalid-duration");
107
108/// A health-check retry count is not a non-negative integer or deferred expression.
109pub const HEALTHCHECK_INVALID_RETRIES: DiagnosticCode = DiagnosticCode::new("compose.healthcheck.invalid-retries");
110
111/// A long dependency uses an unrecognized condition.
112pub const DEPENDENCY_INVALID_CONDITION: DiagnosticCode = DiagnosticCode::new("compose.dependencies.invalid-condition");
113
114/// A typed dependency names a service missing from the same document.
115pub const DEPENDENCY_MISSING_SERVICE: DiagnosticCode = DiagnosticCode::new("compose.dependencies.missing-service");
116
117/// A `service_healthy` dependency has no enabled health check.
118pub const DEPENDENCY_MISSING_HEALTHCHECK: DiagnosticCode =
119    DiagnosticCode::new("compose.dependencies.missing-healthcheck");
120
121/// A `service_healthy` dependency may rely on health metadata from its image.
122pub const DEPENDENCY_HEALTHCHECK_UNVERIFIED: DiagnosticCode =
123    DiagnosticCode::new("compose.dependencies.healthcheck-unverified");
124
125/// A typed value and the exact source span from which it was read.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct Located<T> {
128    value: T,
129    span: SourceSpan,
130}
131
132impl<T> Located<T> {
133    pub(crate) const fn new(value: T, span: SourceSpan) -> Self {
134        Self { value, span }
135    }
136
137    /// Returns the typed value.
138    #[must_use]
139    pub const fn value(&self) -> &T {
140        &self.value
141    }
142
143    /// Returns the value's source span.
144    #[must_use]
145    pub const fn span(&self) -> SourceSpan {
146        self.span
147    }
148
149    /// Removes the source wrapper and returns the typed value.
150    #[must_use]
151    pub fn into_value(self) -> T {
152        self.value
153    }
154}
155
156/// Source provenance for an extension or not-yet-typed field.
157///
158/// The loss-aware [`SyntaxDocument`] retains the actual value and spelling. This reference lets
159/// typed callers locate it without exposing the private YAML implementation.
160#[derive(Debug, Clone, PartialEq, Eq)]
161pub struct FieldReference {
162    name: Located<String>,
163    span: SourceSpan,
164    value_span: Option<SourceSpan>,
165}
166
167impl FieldReference {
168    /// Returns the semantic field name and its source span.
169    #[must_use]
170    pub const fn name(&self) -> &Located<String> {
171        &self.name
172    }
173
174    /// Returns the span covering the key and value when both are available.
175    #[must_use]
176    pub const fn span(&self) -> SourceSpan {
177        self.span
178    }
179
180    /// Returns the value span when the YAML node exposes one.
181    #[must_use]
182    pub const fn value_span(&self) -> Option<SourceSpan> {
183        self.value_span
184    }
185}
186
187/// A source-aware typed Compose service.
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct Service {
190    name: Located<String>,
191    span: SourceSpan,
192    container_name: Option<Located<String>>,
193    image: Option<Located<ImageReference>>,
194    command: Option<Command>,
195    environment: Option<Environment>,
196    labels: Option<Labels>,
197    extra_hosts: Option<ExtraHosts>,
198    user: Option<UserSpec>,
199    userns_mode: Option<UserNamespaceMode>,
200    group_add: Vec<Located<String>>,
201    working_dir: Option<Located<String>>,
202    read_only: Option<Located<BooleanValue>>,
203    ulimits: Option<Ulimits>,
204    depends_on: Option<DependsOn>,
205    healthcheck: Option<Healthcheck>,
206    build: Option<Build>,
207    deploy: Option<DeployDefinition>,
208    ports: Vec<Port>,
209    volumes: Vec<VolumeMount>,
210    networks: Option<ServiceNetworks>,
211    profiles: Vec<Located<String>>,
212    configs: Vec<ConfigGrant>,
213    secrets: Vec<SecretGrant>,
214    extension_fields: Vec<FieldReference>,
215    unknown_fields: Vec<FieldReference>,
216}
217
218impl Service {
219    fn new(name: Located<String>, span: SourceSpan) -> Self {
220        Self {
221            name,
222            span,
223            container_name: None,
224            image: None,
225            command: None,
226            environment: None,
227            labels: None,
228            extra_hosts: None,
229            user: None,
230            userns_mode: None,
231            group_add: Vec::new(),
232            working_dir: None,
233            read_only: None,
234            ulimits: None,
235            depends_on: None,
236            healthcheck: None,
237            build: None,
238            deploy: None,
239            ports: Vec::new(),
240            volumes: Vec::new(),
241            networks: None,
242            profiles: Vec::new(),
243            configs: Vec::new(),
244            secrets: Vec::new(),
245            extension_fields: Vec::new(),
246            unknown_fields: Vec::new(),
247        }
248    }
249
250    /// Returns the service name.
251    #[must_use]
252    pub const fn name(&self) -> &Located<String> {
253        &self.name
254    }
255
256    /// Returns the complete service definition span.
257    #[must_use]
258    pub const fn span(&self) -> SourceSpan {
259        self.span
260    }
261
262    /// Returns the explicitly authored runtime container name.
263    #[must_use]
264    pub const fn container_name(&self) -> Option<&Located<String>> {
265        self.container_name.as_ref()
266    }
267
268    /// Returns the explicitly authored image reference.
269    #[must_use]
270    pub const fn image(&self) -> Option<&Located<ImageReference>> {
271        self.image.as_ref()
272    }
273
274    /// Returns the command without normalizing its authored form.
275    #[must_use]
276    pub const fn command(&self) -> Option<&Command> {
277        self.command.as_ref()
278    }
279
280    /// Returns environment variables with list and mapping forms kept distinct.
281    #[must_use]
282    pub const fn environment(&self) -> Option<&Environment> {
283        self.environment.as_ref()
284    }
285
286    /// Returns service metadata labels with list and mapping forms kept distinct.
287    #[must_use]
288    pub const fn labels(&self) -> Option<&Labels> {
289        self.labels.as_ref()
290    }
291
292    /// Returns additional host mappings with short and long forms retained.
293    #[must_use]
294    pub const fn extra_hosts(&self) -> Option<&ExtraHosts> {
295        self.extra_hosts.as_ref()
296    }
297
298    /// Returns the raw-preserving container user/group value.
299    #[must_use]
300    pub const fn user(&self) -> Option<&UserSpec> {
301        self.user.as_ref()
302    }
303
304    /// Returns the raw-preserving user-namespace mode.
305    #[must_use]
306    pub const fn userns_mode(&self) -> Option<&UserNamespaceMode> {
307        self.userns_mode.as_ref()
308    }
309
310    /// Returns supplementary groups in authored order without resolving names or IDs.
311    #[must_use]
312    pub fn group_add(&self) -> &[Located<String>] {
313        &self.group_add
314    }
315
316    /// Returns the container working-directory override.
317    #[must_use]
318    pub const fn working_dir(&self) -> Option<&Located<String>> {
319        self.working_dir.as_ref()
320    }
321
322    /// Returns the explicit read-only root-filesystem choice.
323    #[must_use]
324    pub const fn read_only(&self) -> Option<&Located<BooleanValue>> {
325        self.read_only.as_ref()
326    }
327
328    /// Returns explicitly authored service resource limits.
329    #[must_use]
330    pub const fn ulimits(&self) -> Option<&Ulimits> {
331        self.ulimits.as_ref()
332    }
333
334    /// Returns service dependencies with short and long forms retained.
335    #[must_use]
336    pub const fn depends_on(&self) -> Option<&DependsOn> {
337        self.depends_on.as_ref()
338    }
339
340    /// Returns the service health-check definition.
341    #[must_use]
342    pub const fn healthcheck(&self) -> Option<&Healthcheck> {
343        self.healthcheck.as_ref()
344    }
345
346    /// Returns the build declaration with short and long forms retained.
347    #[must_use]
348    pub const fn build(&self) -> Option<&Build> {
349        self.build.as_ref()
350    }
351
352    /// Returns independently classified deploy subfields.
353    #[must_use]
354    pub const fn deploy(&self) -> Option<&DeployDefinition> {
355        self.deploy.as_ref()
356    }
357
358    /// Returns published ports in authored order.
359    #[must_use]
360    pub fn ports(&self) -> &[Port] {
361        &self.ports
362    }
363
364    /// Returns service-volume mounts in authored order.
365    #[must_use]
366    pub fn volumes(&self) -> &[VolumeMount] {
367        &self.volumes
368    }
369
370    /// Returns service network attachments with short and long forms kept distinct.
371    #[must_use]
372    pub const fn networks(&self) -> Option<&ServiceNetworks> {
373        self.networks.as_ref()
374    }
375
376    /// Returns explicitly authored profile names.
377    #[must_use]
378    pub fn profiles(&self) -> &[Located<String>] {
379        &self.profiles
380    }
381
382    /// Returns service config grants in authored order.
383    #[must_use]
384    pub fn configs(&self) -> &[ConfigGrant] {
385        &self.configs
386    }
387
388    /// Returns service secret grants in authored order.
389    #[must_use]
390    pub fn secrets(&self) -> &[SecretGrant] {
391        &self.secrets
392    }
393
394    /// Returns retained service `x-` extension fields.
395    #[must_use]
396    pub fn extension_fields(&self) -> &[FieldReference] {
397        &self.extension_fields
398    }
399
400    /// Returns service fields not yet represented by the typed subset.
401    #[must_use]
402    pub fn unknown_fields(&self) -> &[FieldReference] {
403        &self.unknown_fields
404    }
405}
406
407/// A source-aware native Compose document.
408#[derive(Debug, Clone, PartialEq, Eq)]
409pub struct ComposeDocument {
410    source_id: SourceId,
411    span: SourceSpan,
412    name: Option<Located<String>>,
413    services: Vec<Service>,
414    networks: Vec<NetworkDefinition>,
415    volumes: Vec<VolumeDefinition>,
416    configs: Vec<ConfigDefinition>,
417    secrets: Vec<SecretDefinition>,
418    extension_fields: Vec<FieldReference>,
419    unknown_fields: Vec<FieldReference>,
420}
421
422impl ComposeDocument {
423    /// Extracts the initial typed Compose subset from a loss-aware syntax document.
424    ///
425    /// Parsing does not interpolate values, apply defaults, normalize short and long forms, or
426    /// access the environment. Structural problems produce diagnostics and as much typed data as
427    /// can be recovered.
428    #[must_use]
429    pub fn parse(syntax: &SyntaxDocument) -> ModelParse {
430        Parser::new(syntax).parse()
431    }
432
433    /// Returns the source identifier.
434    #[must_use]
435    pub const fn source_id(&self) -> SourceId {
436        self.source_id
437    }
438
439    /// Returns the typed root mapping span.
440    #[must_use]
441    pub const fn span(&self) -> SourceSpan {
442        self.span
443    }
444
445    /// Returns the explicitly authored project name.
446    #[must_use]
447    pub const fn name(&self) -> Option<&Located<String>> {
448        self.name.as_ref()
449    }
450
451    /// Returns services in authored order.
452    #[must_use]
453    pub fn services(&self) -> &[Service] {
454        &self.services
455    }
456
457    /// Finds the first service with the requested name.
458    #[must_use]
459    pub fn service(&self, name: &str) -> Option<&Service> {
460        self.services.iter().find(|service| service.name.value == name)
461    }
462
463    /// Validates dependency targets and `service_healthy` health-check requirements in this document.
464    ///
465    /// Multi-file callers should validate the merged project view through
466    /// [`crate::resolution::validate_references`] instead.
467    #[must_use]
468    pub fn validate_dependencies(&self) -> Vec<Diagnostic> {
469        let mut diagnostics = Vec::new();
470        for service in &self.services {
471            let Some(depends_on) = service.depends_on() else {
472                continue;
473            };
474            match depends_on {
475                DependsOn::Short { services, .. } => {
476                    for target in services {
477                        if self.service(target.value()).is_none() {
478                            diagnostics.push(missing_dependency_diagnostic(target.span(), false, true));
479                        }
480                    }
481                }
482                DependsOn::Long { services, .. } => {
483                    for dependency in services {
484                        let required = !matches!(
485                            dependency.required().map(Located::value),
486                            Some(BooleanValue::Literal(false))
487                        );
488                        let Some(target) = self.service(dependency.service().value()) else {
489                            diagnostics.push(missing_dependency_diagnostic(
490                                dependency.service().span(),
491                                false,
492                                required,
493                            ));
494                            continue;
495                        };
496                        let needs_healthcheck = matches!(
497                            dependency.condition().map(Located::value),
498                            Some(DependencyCondition::ServiceHealthy)
499                        );
500                        if needs_healthcheck && target.healthcheck().is_none() {
501                            let span = dependency
502                                .condition()
503                                .map_or_else(|| dependency.service().span(), Located::span);
504                            diagnostics.push(unverified_healthcheck_diagnostic(span));
505                        } else if needs_healthcheck && target.healthcheck().is_some_and(Healthcheck::is_disabled) {
506                            let span = dependency
507                                .condition()
508                                .map_or_else(|| dependency.service().span(), Located::span);
509                            diagnostics.push(missing_dependency_diagnostic(span, true, required));
510                        }
511                    }
512                }
513            }
514        }
515        diagnostics
516    }
517
518    /// Returns top-level network definitions in authored order.
519    #[must_use]
520    pub fn networks(&self) -> &[NetworkDefinition] {
521        &self.networks
522    }
523
524    /// Returns top-level volume definitions in authored order.
525    #[must_use]
526    pub fn volumes(&self) -> &[VolumeDefinition] {
527        &self.volumes
528    }
529
530    /// Returns top-level config definitions in authored order.
531    #[must_use]
532    pub fn configs(&self) -> &[ConfigDefinition] {
533        &self.configs
534    }
535
536    /// Returns top-level secret definitions in authored order.
537    #[must_use]
538    pub fn secrets(&self) -> &[SecretDefinition] {
539        &self.secrets
540    }
541
542    /// Returns retained top-level `x-` extension fields.
543    #[must_use]
544    pub fn extension_fields(&self) -> &[FieldReference] {
545        &self.extension_fields
546    }
547
548    /// Returns top-level fields not yet represented by the typed subset.
549    #[must_use]
550    pub fn unknown_fields(&self) -> &[FieldReference] {
551        &self.unknown_fields
552    }
553}
554
555/// A recoverable typed-model parse result.
556#[derive(Debug, Clone, PartialEq, Eq)]
557pub struct ModelParse {
558    document: Option<ComposeDocument>,
559    diagnostics: Vec<Diagnostic>,
560}
561
562impl ModelParse {
563    /// Returns the typed document when the root could be interpreted.
564    #[must_use]
565    pub const fn document(&self) -> Option<&ComposeDocument> {
566        self.document.as_ref()
567    }
568
569    /// Returns structural typed-model diagnostics in source order.
570    #[must_use]
571    pub fn diagnostics(&self) -> &[Diagnostic] {
572        &self.diagnostics
573    }
574
575    /// Reports whether no error diagnostics were emitted.
576    #[must_use]
577    pub fn is_valid(&self) -> bool {
578        !self
579            .diagnostics
580            .iter()
581            .any(|diagnostic| diagnostic.severity() == Severity::Error)
582    }
583
584    /// Separates the recovered document and diagnostics.
585    #[must_use]
586    pub fn into_parts(self) -> (Option<ComposeDocument>, Vec<Diagnostic>) {
587        (self.document, self.diagnostics)
588    }
589}
590
591fn missing_dependency_diagnostic(span: SourceSpan, healthcheck: bool, required: bool) -> Diagnostic {
592    let severity = if required { Severity::Error } else { Severity::Warning };
593    if healthcheck {
594        Diagnostic::new(
595            DEPENDENCY_MISSING_HEALTHCHECK,
596            severity,
597            if required {
598                "service_healthy dependency requires an enabled health check"
599            } else {
600                "optional service_healthy dependency has no enabled health check"
601            },
602        )
603        .with_label(DiagnosticLabel::primary(span, "dependency cannot become healthy"))
604    } else {
605        Diagnostic::new(
606            DEPENDENCY_MISSING_SERVICE,
607            severity,
608            if required {
609                "service dependency is not declared in this Compose document"
610            } else {
611                "optional service dependency is not declared in this Compose document"
612            },
613        )
614        .with_label(DiagnosticLabel::primary(span, "missing dependency service"))
615    }
616}
617
618fn unverified_healthcheck_diagnostic(span: SourceSpan) -> Diagnostic {
619    Diagnostic::new(
620        DEPENDENCY_HEALTHCHECK_UNVERIFIED,
621        Severity::Warning,
622        "service_healthy dependency has no Compose healthcheck to validate",
623    )
624    .with_label(DiagnosticLabel::primary(span, "image health metadata is not available"))
625    .with_note("the dependency image may still define a health check; verify it at build or runtime")
626}
627
628#[derive(Debug)]
629struct Parser {
630    source_id: SourceId,
631    source_span: SourceSpan,
632    source: String,
633    tree: yaml_edit::YamlFile,
634    anchors: AnchorRegistry,
635    diagnostics: Vec<Diagnostic>,
636}
637
638impl Parser {
639    fn new(syntax: &SyntaxDocument) -> Self {
640        let tree = syntax.yaml_file();
641        let anchors = tree
642            .document()
643            .map_or_else(AnchorRegistry::new, |document| AnchorRegistry::from_document(&document));
644        Self {
645            source_id: syntax.source_id(),
646            source_span: syntax.source_span(),
647            source: syntax.source_text().to_owned(),
648            tree,
649            anchors,
650            diagnostics: Vec::new(),
651        }
652    }
653
654    fn parse(mut self) -> ModelParse {
655        if self.tree.documents().count() > 1 {
656            self.diagnostics.push(
657                Diagnostic::new(
658                    MULTIPLE_DOCUMENTS,
659                    Severity::Error,
660                    "Compose input must contain one YAML document",
661                )
662                .with_label(DiagnosticLabel::primary(self.source_span, "multiple YAML documents")),
663            );
664        }
665
666        let Some(root) = self.tree.document() else {
667            self.diagnostics.push(
668                Diagnostic::new(
669                    DOCUMENT_ROOT_TYPE,
670                    Severity::Error,
671                    "Compose document root must be a mapping",
672                )
673                .with_label(DiagnosticLabel::primary(self.source_span, "empty document")),
674            );
675            return ModelParse {
676                document: None,
677                diagnostics: self.diagnostics,
678            };
679        };
680        let root_span = span_from_position(self.source_id, root.byte_range());
681        let Some(mapping) = root.as_mapping() else {
682            self.diagnostics.push(
683                Diagnostic::new(
684                    DOCUMENT_ROOT_TYPE,
685                    Severity::Error,
686                    "Compose document root must be a mapping",
687                )
688                .with_label(DiagnosticLabel::primary(root_span, "not a mapping")),
689            );
690            return ModelParse {
691                document: None,
692                diagnostics: self.diagnostics,
693            };
694        };
695
696        let document = self.parse_root(&mapping, root_span);
697        ModelParse {
698            document: Some(document),
699            diagnostics: self.diagnostics,
700        }
701    }
702
703    fn parse_root(&mut self, mapping: &Mapping, span: SourceSpan) -> ComposeDocument {
704        let mut document = ComposeDocument {
705            source_id: self.source_id,
706            span,
707            name: None,
708            services: Vec::new(),
709            networks: Vec::new(),
710            volumes: Vec::new(),
711            configs: Vec::new(),
712            secrets: Vec::new(),
713            extension_fields: Vec::new(),
714            unknown_fields: Vec::new(),
715        };
716        let mut seen = BTreeMap::new();
717
718        for field in self.fields(mapping) {
719            let duplicate = self.record_duplicate(&mut seen, &field);
720            match field.name.value.as_str() {
721                "name" if !duplicate => {
722                    document.name = self.parse_string(&field, "project name");
723                }
724                "services" if !duplicate => {
725                    document.services = self.parse_services(&field);
726                }
727                "networks" if !duplicate => {
728                    document.networks = self.parse_network_definitions(&field);
729                }
730                "volumes" if !duplicate => {
731                    document.volumes = self.parse_volume_definitions(&field);
732                }
733                "configs" if !duplicate => {
734                    document.configs = self.parse_config_definitions(&field);
735                }
736                "secrets" if !duplicate => {
737                    document.secrets = self.parse_secret_definitions(&field);
738                }
739                name if name.starts_with("x-") => {
740                    document.extension_fields.push(field.reference());
741                }
742                _ if duplicate => {}
743                _ => document.unknown_fields.push(field.reference()),
744            }
745        }
746        document
747    }
748
749    fn parse_services(&mut self, field: &ParsedField) -> Vec<Service> {
750        let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
751            self.expected(EXPECTED_MAPPING, field, "services must be a mapping");
752            return Vec::new();
753        };
754        let mut services = Vec::new();
755        let mut seen = BTreeMap::new();
756        for service_field in self.fields(mapping) {
757            self.record_duplicate(&mut seen, &service_field);
758            let Some(service_mapping) = service_field.value.as_ref().and_then(YamlNode::as_mapping) else {
759                self.expected(EXPECTED_MAPPING, &service_field, "service definition must be a mapping");
760                continue;
761            };
762            services.push(self.parse_service(&service_field, service_mapping));
763        }
764        services
765    }
766
767    fn parse_service(&mut self, field: &ParsedField, mapping: &Mapping) -> Service {
768        let mut service = Service::new(field.name.clone(), field.span);
769        let mut seen = BTreeMap::new();
770        for service_field in self.fields(mapping) {
771            let duplicate = self.record_duplicate(&mut seen, &service_field);
772            match service_field.name.value.as_str() {
773                "container_name" if !duplicate => {
774                    service.container_name = self.parse_string(&service_field, "service container name");
775                }
776                "image" if !duplicate => {
777                    service.image = self
778                        .parse_string(&service_field, "service image")
779                        .map(|value| Located::new(ImageReference::parse(value.value), value.span));
780                }
781                "command" if !duplicate => {
782                    service.command = self.parse_command(&service_field);
783                }
784                "environment" if !duplicate => {
785                    service.environment = self.parse_environment(&service_field);
786                }
787                "labels" if !duplicate => {
788                    service.labels = self.parse_labels(&service_field);
789                }
790                "extra_hosts" if !duplicate => {
791                    service.extra_hosts = self.parse_extra_hosts(&service_field);
792                }
793                "user" if !duplicate => {
794                    service.user = self.parse_string(&service_field, "service user").map(UserSpec::parse);
795                }
796                "userns_mode" if !duplicate => {
797                    service.userns_mode = self
798                        .parse_string(&service_field, "service user namespace mode")
799                        .map(UserNamespaceMode::parse);
800                }
801                "group_add" if !duplicate => {
802                    service.group_add = self.parse_string_sequence(&service_field, "service supplementary groups");
803                }
804                "working_dir" if !duplicate => {
805                    service.working_dir = self.parse_string(&service_field, "service working directory");
806                }
807                "read_only" if !duplicate => {
808                    service.read_only = self.parse_boolean(&service_field, "service read_only");
809                }
810                "ulimits" if !duplicate => {
811                    service.ulimits = self.parse_ulimits(&service_field);
812                }
813                "depends_on" if !duplicate => {
814                    service.depends_on = self.parse_depends_on(&service_field);
815                }
816                "healthcheck" if !duplicate => {
817                    service.healthcheck = self.parse_healthcheck(&service_field);
818                }
819                "build" if !duplicate => {
820                    service.build = self.parse_build(&service_field);
821                }
822                "deploy" if !duplicate => {
823                    service.deploy = self.parse_deploy(&service_field);
824                }
825                "ports" if !duplicate => {
826                    service.ports = self.parse_service_ports(&service_field);
827                }
828                "volumes" if !duplicate => {
829                    service.volumes = self.parse_service_volumes(&service_field);
830                }
831                "networks" if !duplicate => {
832                    service.networks = self.parse_service_networks(&service_field);
833                }
834                "profiles" if !duplicate => {
835                    service.profiles = self.parse_string_sequence(&service_field, "service profiles");
836                }
837                "configs" if !duplicate => {
838                    service.configs = self.parse_config_grants(&service_field);
839                }
840                "secrets" if !duplicate => {
841                    service.secrets = self.parse_secret_grants(&service_field);
842                }
843                name if name.starts_with("x-") => {
844                    service.extension_fields.push(service_field.reference());
845                }
846                _ if duplicate => {}
847                _ => service.unknown_fields.push(service_field.reference()),
848            }
849        }
850        service
851    }
852
853    fn parse_command(&mut self, field: &ParsedField) -> Option<Command> {
854        match field.value.as_ref() {
855            Some(YamlNode::Scalar(scalar)) => {
856                let span = span_from_position(self.source_id, scalar.byte_range());
857                if ScalarValue::from_scalar(scalar).scalar_type() == ScalarType::Null {
858                    Some(Command::Null(span))
859                } else {
860                    Some(Command::String(Located::new(
861                        scalar_string_from_source(&self.source, scalar),
862                        span,
863                    )))
864                }
865            }
866            Some(YamlNode::Sequence(sequence)) => {
867                let span = span_from_position(self.source_id, sequence.byte_range());
868                let values =
869                    self.parse_scalar_nodes(sequence.values(), field.span, "command list items must be scalars");
870                Some(Command::List { span, values })
871            }
872            _ => {
873                self.expected(
874                    EXPECTED_FIELD_FORM,
875                    field,
876                    "command must be null, a scalar, or a sequence",
877                );
878                None
879            }
880        }
881    }
882
883    fn parse_environment(&mut self, field: &ParsedField) -> Option<Environment> {
884        match field.value.as_ref() {
885            Some(YamlNode::Sequence(sequence)) => {
886                let span = span_from_position(self.source_id, sequence.byte_range());
887                let entries = self
888                    .parse_scalar_nodes(sequence.values(), field.span, "environment list items must be scalars")
889                    .into_iter()
890                    .map(EnvironmentListEntry::parse)
891                    .collect();
892                Some(Environment::List { span, entries })
893            }
894            Some(YamlNode::Mapping(mapping)) => {
895                let span = span_from_position(self.source_id, mapping.byte_range());
896                let entries = self.parse_environment_map(mapping);
897                Some(Environment::Map { span, entries })
898            }
899            _ => {
900                self.expected(EXPECTED_FIELD_FORM, field, "environment must be a sequence or mapping");
901                None
902            }
903        }
904    }
905
906    fn parse_environment_map(&mut self, mapping: &Mapping) -> Vec<EnvironmentMapEntry> {
907        let mut entries = Vec::new();
908        let mut seen = BTreeMap::new();
909        for field in self.fields(mapping) {
910            if self.record_duplicate(&mut seen, &field) {
911                continue;
912            }
913            let value = self.parse_compose_scalar(&field, "environment values must be scalars");
914            if let Some(value) = value {
915                entries.push(EnvironmentMapEntry::new(field.name, value, field.span));
916            }
917        }
918        entries
919    }
920
921    fn parse_extra_hosts(&mut self, field: &ParsedField) -> Option<ExtraHosts> {
922        match field.value.as_ref() {
923            Some(YamlNode::Sequence(sequence)) => {
924                let span = span_from_position(self.source_id, sequence.byte_range());
925                let entries = self
926                    .parse_scalar_nodes(sequence.values(), field.span, "extra_hosts entries must be scalars")
927                    .into_iter()
928                    .map(|raw| {
929                        let entry = ShortExtraHost::parse(raw);
930                        if !entry.is_complete() {
931                            self.diagnostics.push(
932                                Diagnostic::new(
933                                    EXTRA_HOST_INVALID_ENTRY,
934                                    Severity::Error,
935                                    "short extra_hosts entry must contain a hostname and address",
936                                )
937                                .with_label(DiagnosticLabel::primary(
938                                    entry.raw().span(),
939                                    "missing separator or value",
940                                )),
941                            );
942                        }
943                        entry
944                    })
945                    .collect();
946                Some(ExtraHosts::Short { span, entries })
947            }
948            Some(YamlNode::Mapping(mapping)) => {
949                let span = span_from_position(self.source_id, mapping.byte_range());
950                let mut entries = Vec::new();
951                let mut seen = BTreeMap::new();
952                for host in self.fields(mapping) {
953                    if self.record_duplicate(&mut seen, &host) {
954                        continue;
955                    }
956                    if let Some(address) = self.parse_string(&host, "extra host address") {
957                        let address = Located::new(HostAddress::parse(address.value), address.span);
958                        entries.push(LongExtraHost::new(host.name, address, host.span));
959                    }
960                }
961                Some(ExtraHosts::Long { span, entries })
962            }
963            _ => {
964                self.expected(EXPECTED_FIELD_FORM, field, "extra_hosts must be a sequence or mapping");
965                None
966            }
967        }
968    }
969
970    fn parse_ulimits(&mut self, field: &ParsedField) -> Option<Ulimits> {
971        let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
972            self.expected(EXPECTED_MAPPING, field, "ulimits must be a mapping");
973            return None;
974        };
975        let span = span_from_position(self.source_id, mapping.byte_range());
976        let mut entries = Vec::new();
977        let mut seen = BTreeMap::new();
978        for limit in self.fields(mapping) {
979            if self.record_duplicate(&mut seen, &limit) {
980                continue;
981            }
982            let value = match limit.value.as_ref() {
983                Some(YamlNode::Scalar(_)) => self.parse_limit_value(&limit, "ulimit value").map(UlimitValue::Single),
984                Some(YamlNode::Mapping(range)) => Some(UlimitValue::Range(self.parse_ulimit_range(range))),
985                _ => {
986                    self.expected(
987                        EXPECTED_FIELD_FORM,
988                        &limit,
989                        "ulimit must be a scalar or soft/hard mapping",
990                    );
991                    None
992                }
993            };
994            if let Some(value) = value {
995                entries.push(Ulimit::new(limit.name, limit.span, value));
996            }
997        }
998        Some(Ulimits::new(span, entries))
999    }
1000
1001    fn parse_ulimit_range(&mut self, mapping: &Mapping) -> UlimitRange {
1002        let span = span_from_position(self.source_id, mapping.byte_range());
1003        let mut range = UlimitRange::new(span);
1004        let mut seen = BTreeMap::new();
1005        for field in self.fields(mapping) {
1006            let duplicate = self.record_duplicate(&mut seen, &field);
1007            match field.name.value.as_str() {
1008                "soft" if !duplicate => self
1009                    .parse_limit_value(&field, "ulimit soft value")
1010                    .into_iter()
1011                    .for_each(|value| range.set_soft(value)),
1012                "hard" if !duplicate => self
1013                    .parse_limit_value(&field, "ulimit hard value")
1014                    .into_iter()
1015                    .for_each(|value| range.set_hard(value)),
1016                name if name.starts_with("x-") => range.push_extension(field.reference()),
1017                _ if duplicate => {}
1018                _ => range.push_unknown(field.reference()),
1019            }
1020        }
1021        range
1022    }
1023
1024    fn parse_limit_value(&mut self, field: &ParsedField, description: &str) -> Option<Located<LimitValue>> {
1025        let value = self.parse_string(field, description)?;
1026        let parsed = LimitValue::parse(value.value);
1027        if !parsed.is_valid() {
1028            self.diagnostics.push(
1029                Diagnostic::new(
1030                    ULIMIT_INVALID_VALUE,
1031                    Severity::Error,
1032                    "ulimit must be -1, a non-negative integer, or an interpolation expression",
1033                )
1034                .with_label(DiagnosticLabel::primary(value.span, "invalid ulimit value")),
1035            );
1036        }
1037        Some(Located::new(parsed, value.span))
1038    }
1039
1040    fn parse_depends_on(&mut self, field: &ParsedField) -> Option<DependsOn> {
1041        match field.value.as_ref() {
1042            Some(YamlNode::Sequence(sequence)) => {
1043                let span = span_from_position(self.source_id, sequence.byte_range());
1044                let services = self.parse_scalar_nodes(
1045                    sequence.values(),
1046                    field.span,
1047                    "dependency service names must be scalars",
1048                );
1049                Some(DependsOn::Short { span, services })
1050            }
1051            Some(YamlNode::Mapping(mapping)) => {
1052                let span = span_from_position(self.source_id, mapping.byte_range());
1053                let mut services = Vec::new();
1054                let mut seen = BTreeMap::new();
1055                for dependency in self.fields(mapping) {
1056                    if self.record_duplicate(&mut seen, &dependency) {
1057                        continue;
1058                    }
1059                    let mut parsed = ServiceDependency::new(dependency.name.clone(), dependency.span);
1060                    if Self::field_is_null(&dependency) {
1061                        services.push(parsed);
1062                        continue;
1063                    }
1064                    let Some(options) = dependency.value.as_ref().and_then(YamlNode::as_mapping) else {
1065                        self.expected(
1066                            EXPECTED_MAPPING,
1067                            &dependency,
1068                            "long dependency options must be a mapping or null",
1069                        );
1070                        continue;
1071                    };
1072                    let mut option_seen = BTreeMap::new();
1073                    for option in self.fields(options) {
1074                        let duplicate = self.record_duplicate(&mut option_seen, &option);
1075                        match option.name.value.as_str() {
1076                            "condition" if !duplicate => {
1077                                if let Some(value) = self.parse_string(&option, "dependency condition") {
1078                                    let condition = DependencyCondition::parse(value.value);
1079                                    if !condition.is_known() {
1080                                        self.diagnostics.push(
1081                                            Diagnostic::new(
1082                                                DEPENDENCY_INVALID_CONDITION,
1083                                                Severity::Error,
1084                                                "dependency condition is not defined by Compose",
1085                                            )
1086                                            .with_label(
1087                                                DiagnosticLabel::primary(value.span, "unknown dependency condition"),
1088                                            ),
1089                                        );
1090                                    }
1091                                    parsed.set_condition(Located::new(condition, value.span));
1092                                }
1093                            }
1094                            "restart" if !duplicate => self
1095                                .parse_boolean(&option, "dependency restart")
1096                                .into_iter()
1097                                .for_each(|value| parsed.set_restart(value)),
1098                            "required" if !duplicate => self
1099                                .parse_boolean(&option, "dependency required")
1100                                .into_iter()
1101                                .for_each(|value| parsed.set_required(value)),
1102                            name if name.starts_with("x-") => parsed.push_extension(option.reference()),
1103                            _ if duplicate => {}
1104                            _ => parsed.push_unknown(option.reference()),
1105                        }
1106                    }
1107                    services.push(parsed);
1108                }
1109                Some(DependsOn::Long { span, services })
1110            }
1111            _ => {
1112                self.expected(EXPECTED_FIELD_FORM, field, "depends_on must be a sequence or mapping");
1113                None
1114            }
1115        }
1116    }
1117
1118    fn parse_healthcheck(&mut self, field: &ParsedField) -> Option<Healthcheck> {
1119        let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
1120            self.expected(EXPECTED_MAPPING, field, "healthcheck must be a mapping");
1121            return None;
1122        };
1123        let span = span_from_position(self.source_id, mapping.byte_range());
1124        let mut healthcheck = Healthcheck::new(span);
1125        let mut seen = BTreeMap::new();
1126        for option in self.fields(mapping) {
1127            let duplicate = self.record_duplicate(&mut seen, &option);
1128            match option.name.value.as_str() {
1129                "test" if !duplicate => self
1130                    .parse_healthcheck_test(&option)
1131                    .into_iter()
1132                    .for_each(|value| healthcheck.set_test(value)),
1133                "interval" if !duplicate => self
1134                    .parse_healthcheck_duration(&option, "healthcheck interval")
1135                    .into_iter()
1136                    .for_each(|value| healthcheck.set_interval(value)),
1137                "timeout" if !duplicate => self
1138                    .parse_healthcheck_duration(&option, "healthcheck timeout")
1139                    .into_iter()
1140                    .for_each(|value| healthcheck.set_timeout(value)),
1141                "retries" if !duplicate => self
1142                    .parse_healthcheck_retries(&option)
1143                    .into_iter()
1144                    .for_each(|value| healthcheck.set_retries(value)),
1145                "start_period" if !duplicate => self
1146                    .parse_healthcheck_duration(&option, "healthcheck start period")
1147                    .into_iter()
1148                    .for_each(|value| healthcheck.set_start_period(value)),
1149                "start_interval" if !duplicate => self
1150                    .parse_healthcheck_duration(&option, "healthcheck start interval")
1151                    .into_iter()
1152                    .for_each(|value| healthcheck.set_start_interval(value)),
1153                "disable" if !duplicate => self
1154                    .parse_boolean(&option, "healthcheck disable")
1155                    .into_iter()
1156                    .for_each(|value| healthcheck.set_disable(value)),
1157                name if name.starts_with("x-") => healthcheck.push_extension(option.reference()),
1158                _ if duplicate => {}
1159                _ => healthcheck.push_unknown(option.reference()),
1160            }
1161        }
1162        Some(healthcheck)
1163    }
1164
1165    fn parse_healthcheck_duration(
1166        &mut self,
1167        field: &ParsedField,
1168        description: &str,
1169    ) -> Option<Located<HealthcheckDuration>> {
1170        let value = self.parse_string(field, description)?;
1171        let duration = HealthcheckDuration::parse(value.value);
1172        if !duration.is_valid() {
1173            self.diagnostics.push(
1174                Diagnostic::new(
1175                    HEALTHCHECK_INVALID_DURATION,
1176                    Severity::Error,
1177                    "healthcheck duration must use Compose duration syntax or interpolation",
1178                )
1179                .with_label(DiagnosticLabel::primary(value.span, "invalid healthcheck duration")),
1180            );
1181        }
1182        Some(Located::new(duration, value.span))
1183    }
1184
1185    fn parse_healthcheck_retries(&mut self, field: &ParsedField) -> Option<Located<HealthcheckRetries>> {
1186        let value = self.parse_string(field, "healthcheck retries")?;
1187        let retries = HealthcheckRetries::parse(value.value);
1188        if !retries.is_valid() {
1189            self.diagnostics.push(
1190                Diagnostic::new(
1191                    HEALTHCHECK_INVALID_RETRIES,
1192                    Severity::Error,
1193                    "healthcheck retries must be a non-negative integer or interpolation expression",
1194                )
1195                .with_label(DiagnosticLabel::primary(value.span, "invalid healthcheck retry count")),
1196            );
1197        }
1198        Some(Located::new(retries, value.span))
1199    }
1200
1201    fn parse_healthcheck_test(&mut self, field: &ParsedField) -> Option<HealthcheckTest> {
1202        match field.value.as_ref() {
1203            Some(YamlNode::Scalar(_)) => self
1204                .parse_string(field, "healthcheck test")
1205                .map(HealthcheckTest::String),
1206            Some(YamlNode::Sequence(sequence)) => {
1207                let span = span_from_position(self.source_id, sequence.byte_range());
1208                let values =
1209                    self.parse_scalar_nodes(sequence.values(), field.span, "healthcheck test items must be scalars");
1210                let kind = values.first().map(|value| HealthcheckTestKind::parse(value.value()));
1211                if kind.is_none()
1212                    || kind == Some(HealthcheckTestKind::Other)
1213                    || (kind == Some(HealthcheckTestKind::None) && values.len() != 1)
1214                {
1215                    self.diagnostics.push(
1216                        Diagnostic::new(
1217                            HEALTHCHECK_INVALID_TEST,
1218                            Severity::Error,
1219                            "healthcheck list must begin with NONE, CMD, or CMD-SHELL",
1220                        )
1221                        .with_label(DiagnosticLabel::primary(span, "invalid healthcheck command mode")),
1222                    );
1223                }
1224                Some(HealthcheckTest::List { span, kind, values })
1225            }
1226            _ => {
1227                self.expected(
1228                    EXPECTED_FIELD_FORM,
1229                    field,
1230                    "healthcheck test must be a scalar or sequence",
1231                );
1232                None
1233            }
1234        }
1235    }
1236
1237    fn parse_build(&mut self, field: &ParsedField) -> Option<Build> {
1238        match field.value.as_ref() {
1239            Some(YamlNode::Scalar(_)) => self.parse_string(field, "build context").map(Build::Context),
1240            Some(YamlNode::Mapping(mapping)) => {
1241                let span = span_from_position(self.source_id, mapping.byte_range());
1242                let mut definition = BuildDefinition::new(span);
1243                let mut seen = BTreeMap::new();
1244                for option in self.fields(mapping) {
1245                    let duplicate = self.record_duplicate(&mut seen, &option);
1246                    if duplicate {
1247                        continue;
1248                    }
1249                    if let Some(kind) = BuildFieldKind::from_name(option.name.value()) {
1250                        definition.push_field(BuildField::new(kind, option.reference()));
1251                    } else if option.name.value().starts_with("x-") {
1252                        definition.push_extension(option.reference());
1253                    } else {
1254                        definition.push_unknown(option.reference());
1255                    }
1256                }
1257                Some(Build::Definition(definition))
1258            }
1259            _ => {
1260                self.expected(EXPECTED_FIELD_FORM, field, "build must be a scalar context or mapping");
1261                None
1262            }
1263        }
1264    }
1265
1266    fn parse_deploy(&mut self, field: &ParsedField) -> Option<DeployDefinition> {
1267        let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
1268            self.expected(EXPECTED_MAPPING, field, "deploy must be a mapping");
1269            return None;
1270        };
1271        let span = span_from_position(self.source_id, mapping.byte_range());
1272        let mut definition = DeployDefinition::new(span);
1273        let mut seen = BTreeMap::new();
1274        for option in self.fields(mapping) {
1275            let duplicate = self.record_duplicate(&mut seen, &option);
1276            if duplicate {
1277                continue;
1278            }
1279            if let Some(kind) = DeployFieldKind::from_name(option.name.value()) {
1280                definition.push_field(DeployField::new(kind, option.reference()));
1281            } else if option.name.value().starts_with("x-") {
1282                definition.push_extension(option.reference());
1283            } else {
1284                definition.push_unknown(option.reference());
1285            }
1286        }
1287        Some(definition)
1288    }
1289
1290    fn source_column(&self, offset: usize) -> usize {
1291        let prefix = self.source.get(..offset).unwrap_or_default();
1292        let line_start = prefix.rfind('\n').map_or(0, |index| index + 1);
1293        self.source[line_start..offset].chars().count()
1294    }
1295
1296    fn parse_service_ports(&mut self, field: &ParsedField) -> Vec<Port> {
1297        let Some(sequence) = field.value.as_ref().and_then(YamlNode::as_sequence) else {
1298            self.expected(EXPECTED_SEQUENCE, field, "service ports must be a sequence");
1299            return Vec::new();
1300        };
1301
1302        let mut ports = Vec::new();
1303        for value in sequence.values() {
1304            match value {
1305                YamlNode::Scalar(scalar) => {
1306                    let span = span_from_position(self.source_id, scalar.byte_range());
1307                    ports.push(Port::Short(ShortPort::parse(Located::new(
1308                        scalar_string_from_source(&self.source, &scalar),
1309                        span,
1310                    ))));
1311                }
1312                YamlNode::Mapping(mapping) => {
1313                    ports.push(Port::Long(Box::new(self.parse_long_port(&mapping))));
1314                }
1315                other => self.unsupported_sequence_item(
1316                    PORT_EXPECTED_FORM,
1317                    &other,
1318                    field.span,
1319                    "service port must use scalar short syntax or mapping long syntax",
1320                ),
1321            }
1322        }
1323        ports
1324    }
1325
1326    fn parse_long_port(&mut self, mapping: &Mapping) -> LongPort {
1327        let span = span_from_position(self.source_id, mapping.byte_range());
1328        let mut port = LongPort::new(span);
1329        let mut seen = BTreeMap::new();
1330        for field in self.fields(mapping) {
1331            let duplicate = self.record_duplicate(&mut seen, &field);
1332            match field.name.value.as_str() {
1333                "target" if !duplicate => self
1334                    .parse_string(&field, "port target")
1335                    .into_iter()
1336                    .for_each(|value| port.set_target(value)),
1337                "published" if !duplicate => self
1338                    .parse_string(&field, "published port")
1339                    .into_iter()
1340                    .for_each(|value| port.set_published(value)),
1341                "host_ip" if !duplicate => self
1342                    .parse_string(&field, "port host IP")
1343                    .into_iter()
1344                    .for_each(|value| port.set_host_ip(value)),
1345                "protocol" if !duplicate => self
1346                    .parse_string(&field, "port protocol")
1347                    .into_iter()
1348                    .for_each(|value| port.set_protocol(value)),
1349                "app_protocol" if !duplicate => self
1350                    .parse_string(&field, "port application protocol")
1351                    .into_iter()
1352                    .for_each(|value| port.set_app_protocol(value)),
1353                "mode" if !duplicate => self
1354                    .parse_string(&field, "port mode")
1355                    .into_iter()
1356                    .for_each(|value| port.set_mode(value)),
1357                "name" if !duplicate => self
1358                    .parse_string(&field, "port name")
1359                    .into_iter()
1360                    .for_each(|value| port.set_name(value)),
1361                name if name.starts_with("x-") => port.push_extension(field.reference()),
1362                _ if duplicate => {}
1363                _ => port.push_unknown(field.reference()),
1364            }
1365        }
1366        if port.target().is_none() {
1367            self.missing(PORT_MISSING_TARGET, span, "long port is missing `target`");
1368        }
1369        port
1370    }
1371
1372    fn parse_service_networks(&mut self, field: &ParsedField) -> Option<ServiceNetworks> {
1373        match field.value.as_ref() {
1374            Some(YamlNode::Sequence(sequence)) => {
1375                let span = span_from_position(self.source_id, sequence.byte_range());
1376                let names =
1377                    self.parse_scalar_nodes(sequence.values(), field.span, "service network names must be scalars");
1378                Some(ServiceNetworks::Short { span, names })
1379            }
1380            Some(YamlNode::Mapping(mapping)) => {
1381                let span = span_from_position(self.source_id, mapping.byte_range());
1382                let networks = self.parse_service_network_map(mapping);
1383                Some(ServiceNetworks::Long { span, networks })
1384            }
1385            _ => {
1386                self.expected(
1387                    EXPECTED_FIELD_FORM,
1388                    field,
1389                    "service networks must be a sequence or mapping",
1390                );
1391                None
1392            }
1393        }
1394    }
1395
1396    fn parse_service_network_map(&mut self, mapping: &Mapping) -> Vec<ServiceNetwork> {
1397        let mut networks = Vec::new();
1398        let mut seen = BTreeMap::new();
1399        for field in self.fields(mapping) {
1400            if self.record_duplicate(&mut seen, &field) {
1401                continue;
1402            }
1403            if Self::field_is_null(&field) {
1404                networks.push(ServiceNetwork::new(field.name, field.span));
1405                continue;
1406            }
1407            let Some(options) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
1408                self.expected(
1409                    EXPECTED_MAPPING,
1410                    &field,
1411                    "service network options must be a mapping or null",
1412                );
1413                continue;
1414            };
1415            networks.push(self.parse_service_network(&field, options));
1416        }
1417        networks
1418    }
1419
1420    fn parse_service_network(&mut self, field: &ParsedField, mapping: &Mapping) -> ServiceNetwork {
1421        let mut network = ServiceNetwork::new(field.name.clone(), field.span);
1422        let mut seen = BTreeMap::new();
1423        for option in self.fields(mapping) {
1424            let duplicate = self.record_duplicate(&mut seen, &option);
1425            match option.name.value.as_str() {
1426                "aliases" if !duplicate => network.set_aliases(self.parse_string_sequence(&option, "network aliases")),
1427                "interface_name" if !duplicate => self
1428                    .parse_string(&option, "network interface name")
1429                    .into_iter()
1430                    .for_each(|value| network.set_interface_name(value)),
1431                "ipv4_address" if !duplicate => self
1432                    .parse_string(&option, "network IPv4 address")
1433                    .into_iter()
1434                    .for_each(|value| network.set_ipv4_address(value)),
1435                "ipv6_address" if !duplicate => self
1436                    .parse_string(&option, "network IPv6 address")
1437                    .into_iter()
1438                    .for_each(|value| network.set_ipv6_address(value)),
1439                "link_local_ips" if !duplicate => {
1440                    network.set_link_local_ips(self.parse_string_sequence(&option, "link-local IP addresses"));
1441                }
1442                "mac_address" if !duplicate => self
1443                    .parse_string(&option, "network MAC address")
1444                    .into_iter()
1445                    .for_each(|value| network.set_mac_address(value)),
1446                "driver_opts" if !duplicate => {
1447                    network.set_driver_opts(self.parse_scalar_mapping(&option, "network driver options"));
1448                }
1449                "gw_priority" if !duplicate => self
1450                    .parse_string(&option, "network gateway priority")
1451                    .into_iter()
1452                    .for_each(|value| network.set_gw_priority(value)),
1453                "priority" if !duplicate => self
1454                    .parse_string(&option, "network priority")
1455                    .into_iter()
1456                    .for_each(|value| network.set_priority(value)),
1457                name if name.starts_with("x-") => network.push_extension(option.reference()),
1458                _ if duplicate => {}
1459                _ => network.push_unknown(option.reference()),
1460            }
1461        }
1462        network
1463    }
1464
1465    fn parse_config_grants(&mut self, field: &ParsedField) -> Vec<ConfigGrant> {
1466        self.parse_grants(field)
1467            .into_iter()
1468            .map(|grant| match grant {
1469                ParsedGrant::Short(value) => ConfigGrant::Short(value),
1470                ParsedGrant::Long(value) => ConfigGrant::Long(value),
1471            })
1472            .collect()
1473    }
1474
1475    fn parse_secret_grants(&mut self, field: &ParsedField) -> Vec<SecretGrant> {
1476        self.parse_grants(field)
1477            .into_iter()
1478            .map(|grant| match grant {
1479                ParsedGrant::Short(value) => SecretGrant::Short(value),
1480                ParsedGrant::Long(value) => SecretGrant::Long(value),
1481            })
1482            .collect()
1483    }
1484
1485    fn parse_grants(&mut self, field: &ParsedField) -> Vec<ParsedGrant> {
1486        let Some(sequence) = field.value.as_ref().and_then(YamlNode::as_sequence) else {
1487            self.expected(EXPECTED_SEQUENCE, field, "service grants must be a sequence");
1488            return Vec::new();
1489        };
1490        let mut grants = Vec::new();
1491        for value in sequence.values() {
1492            match value {
1493                YamlNode::Scalar(scalar) => {
1494                    let span = span_from_position(self.source_id, scalar.byte_range());
1495                    grants.push(ParsedGrant::Short(Located::new(
1496                        scalar_string_from_source(&self.source, &scalar),
1497                        span,
1498                    )));
1499                }
1500                YamlNode::Mapping(mapping) => {
1501                    grants.push(ParsedGrant::Long(Box::new(self.parse_long_grant(&mapping))));
1502                }
1503                other => self.unsupported_sequence_item(
1504                    GRANT_EXPECTED_FORM,
1505                    &other,
1506                    field.span,
1507                    "grant must use scalar short syntax or mapping long syntax",
1508                ),
1509            }
1510        }
1511        grants
1512    }
1513
1514    fn parse_long_grant(&mut self, mapping: &Mapping) -> LongGrant {
1515        let span = span_from_position(self.source_id, mapping.byte_range());
1516        let mut grant = LongGrant::new(span);
1517        let mut seen = BTreeMap::new();
1518        for field in self.fields(mapping) {
1519            let duplicate = self.record_duplicate(&mut seen, &field);
1520            match field.name.value.as_str() {
1521                "source" if !duplicate => self
1522                    .parse_string(&field, "grant source")
1523                    .into_iter()
1524                    .for_each(|value| grant.set_source(value)),
1525                "target" if !duplicate => self
1526                    .parse_string(&field, "grant target")
1527                    .into_iter()
1528                    .for_each(|value| grant.set_target(value)),
1529                "uid" if !duplicate => self
1530                    .parse_string(&field, "grant user ID")
1531                    .into_iter()
1532                    .for_each(|value| grant.set_uid(value)),
1533                "gid" if !duplicate => self
1534                    .parse_string(&field, "grant group ID")
1535                    .into_iter()
1536                    .for_each(|value| grant.set_gid(value)),
1537                "mode" if !duplicate => self
1538                    .parse_string(&field, "grant mode")
1539                    .into_iter()
1540                    .for_each(|value| grant.set_mode(value)),
1541                name if name.starts_with("x-") => grant.push_extension(field.reference()),
1542                _ if duplicate => {}
1543                _ => grant.push_unknown(field.reference()),
1544            }
1545        }
1546        if grant.source().is_none() {
1547            self.missing(GRANT_MISSING_SOURCE, span, "long grant is missing `source`");
1548        }
1549        grant
1550    }
1551
1552    fn parse_service_volumes(&mut self, field: &ParsedField) -> Vec<VolumeMount> {
1553        let Some(sequence) = field.value.as_ref().and_then(YamlNode::as_sequence) else {
1554            self.expected(EXPECTED_SEQUENCE, field, "service volumes must be a sequence");
1555            return Vec::new();
1556        };
1557
1558        sequence
1559            .values()
1560            .filter_map(|value| match value {
1561                YamlNode::Scalar(scalar) => {
1562                    let span = span_from_position(self.source_id, scalar.byte_range());
1563                    let raw = Located::new(scalar_string_from_source(&self.source, &scalar), span);
1564                    Some(VolumeMount::Short(ShortVolumeMount::new(raw)))
1565                }
1566                YamlNode::Mapping(mapping) => Some(VolumeMount::Long(Box::new(self.parse_long_volume(&mapping)))),
1567                other => {
1568                    let span = node_span(self.source_id, &other).unwrap_or(field.span);
1569                    self.diagnostics.push(
1570                        Diagnostic::new(
1571                            VOLUME_EXPECTED_FORM,
1572                            Severity::Error,
1573                            "service volume must use scalar short syntax or mapping long syntax",
1574                        )
1575                        .with_label(DiagnosticLabel::primary(span, "unsupported volume form")),
1576                    );
1577                    None
1578                }
1579            })
1580            .collect()
1581    }
1582
1583    fn parse_long_volume(&mut self, mapping: &Mapping) -> LongVolumeMount {
1584        let span = span_from_position(self.source_id, mapping.byte_range());
1585        let mut mount = LongVolumeMount::new(span);
1586        let mut seen = BTreeMap::new();
1587        for field in self.fields(mapping) {
1588            let duplicate = self.record_duplicate(&mut seen, &field);
1589            match field.name.value.as_str() {
1590                "type" if !duplicate => {
1591                    if let Some(value) = self.parse_string(&field, "volume type") {
1592                        mount.set_mount_type(Located::new(MountType::from_text(value.value), value.span));
1593                    }
1594                }
1595                "source" if !duplicate => {
1596                    if let Some(value) = self.parse_string(&field, "volume source") {
1597                        mount.set_source(value);
1598                    }
1599                }
1600                "target" if !duplicate => {
1601                    if let Some(value) = self.parse_string(&field, "volume target") {
1602                        mount.set_target(value);
1603                    }
1604                }
1605                "read_only" if !duplicate => {
1606                    if let Some(value) = self.parse_boolean(&field, "read_only") {
1607                        mount.set_read_only(value);
1608                    }
1609                }
1610                "bind" if !duplicate => {
1611                    if let Some(value) = self.parse_bind_options(&field) {
1612                        mount.set_bind(value);
1613                    }
1614                }
1615                name if name.starts_with("x-") => mount.push_extension(field.reference()),
1616                _ if duplicate => {}
1617                _ => mount.push_unknown(field.reference()),
1618            }
1619        }
1620
1621        if mount.mount_type().is_none() {
1622            self.missing(VOLUME_MISSING_TYPE, span, "long volume is missing `type`");
1623        }
1624        if mount.target().is_none() {
1625            self.missing(VOLUME_MISSING_TARGET, span, "long volume is missing `target`");
1626        }
1627        mount
1628    }
1629
1630    fn parse_bind_options(&mut self, field: &ParsedField) -> Option<BindOptions> {
1631        let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
1632            self.expected(EXPECTED_MAPPING, field, "bind options must be a mapping");
1633            return None;
1634        };
1635        let span = span_from_position(self.source_id, mapping.byte_range());
1636        let mut bind = BindOptions::new(span);
1637        let mut seen = BTreeMap::new();
1638        for bind_field in self.fields(mapping) {
1639            let duplicate = self.record_duplicate(&mut seen, &bind_field);
1640            match bind_field.name.value.as_str() {
1641                "propagation" if !duplicate => {
1642                    if let Some(value) = self.parse_string(&bind_field, "bind propagation") {
1643                        bind.set_propagation(value);
1644                    }
1645                }
1646                "create_host_path" if !duplicate => {
1647                    if let Some(value) = self.parse_boolean(&bind_field, "create_host_path") {
1648                        bind.set_create_host_path(value);
1649                    }
1650                }
1651                "selinux" if !duplicate => {
1652                    if let Some(value) = self.parse_string(&bind_field, "SELinux relabel mode") {
1653                        let mode = match value.value.as_str() {
1654                            "z" => Some(SelinuxRelabel::Shared),
1655                            "Z" => Some(SelinuxRelabel::Private),
1656                            _ => None,
1657                        };
1658                        if let Some(mode) = mode {
1659                            bind.set_selinux(Located::new(mode, value.span));
1660                        } else {
1661                            self.diagnostics.push(
1662                                Diagnostic::new(
1663                                    VOLUME_INVALID_SELINUX,
1664                                    Severity::Error,
1665                                    "SELinux relabel mode must be `z` or `Z`",
1666                                )
1667                                .with_label(DiagnosticLabel::primary(value.span, "invalid SELinux mode")),
1668                            );
1669                        }
1670                    }
1671                }
1672                name if name.starts_with("x-") => bind.push_extension(bind_field.reference()),
1673                _ if duplicate => {}
1674                _ => bind.push_unknown(bind_field.reference()),
1675            }
1676        }
1677        Some(bind)
1678    }
1679
1680    fn parse_network_definitions(&mut self, field: &ParsedField) -> Vec<NetworkDefinition> {
1681        let Some(mapping) = self.resource_collection(field, "networks") else {
1682            return Vec::new();
1683        };
1684        let mut definitions = Vec::new();
1685        let mut seen = BTreeMap::new();
1686        for resource in self.fields(&mapping) {
1687            if self.record_duplicate(&mut seen, &resource) {
1688                continue;
1689            }
1690            if Self::field_is_null(&resource) {
1691                definitions.push(NetworkDefinition::new(resource.name, resource.span));
1692                continue;
1693            }
1694            let Some(definition) = resource.value.as_ref().and_then(YamlNode::as_mapping) else {
1695                self.expected(
1696                    RESOURCE_EXPECTED_FORM,
1697                    &resource,
1698                    "network definition must be a mapping or null",
1699                );
1700                continue;
1701            };
1702            definitions.push(self.parse_network_definition(&resource, definition));
1703        }
1704        definitions
1705    }
1706
1707    fn parse_network_definition(&mut self, field: &ParsedField, mapping: &Mapping) -> NetworkDefinition {
1708        let mut network = NetworkDefinition::new(field.name.clone(), field.span);
1709        let mut seen = BTreeMap::new();
1710        for option in self.fields(mapping) {
1711            let duplicate = self.record_duplicate(&mut seen, &option);
1712            match option.name.value.as_str() {
1713                "driver" if !duplicate => self
1714                    .parse_string(&option, "network driver")
1715                    .into_iter()
1716                    .for_each(|value| network.set_driver(value)),
1717                "driver_opts" if !duplicate => {
1718                    network.set_driver_opts(self.parse_scalar_mapping(&option, "network driver options"));
1719                }
1720                "attachable" if !duplicate => self
1721                    .parse_boolean(&option, "network attachable")
1722                    .into_iter()
1723                    .for_each(|value| network.set_attachable(value)),
1724                "enable_ipv4" if !duplicate => self
1725                    .parse_boolean(&option, "network enable_ipv4")
1726                    .into_iter()
1727                    .for_each(|value| network.set_enable_ipv4(value)),
1728                "enable_ipv6" if !duplicate => self
1729                    .parse_boolean(&option, "network enable_ipv6")
1730                    .into_iter()
1731                    .for_each(|value| network.set_enable_ipv6(value)),
1732                "external" if !duplicate => self
1733                    .parse_boolean(&option, "network external")
1734                    .into_iter()
1735                    .for_each(|value| network.set_external(value)),
1736                "internal" if !duplicate => self
1737                    .parse_boolean(&option, "network internal")
1738                    .into_iter()
1739                    .for_each(|value| network.set_internal(value)),
1740                "ipam" if !duplicate => self
1741                    .parse_ipam(&option)
1742                    .into_iter()
1743                    .for_each(|value| network.set_ipam(value)),
1744                "labels" if !duplicate => self
1745                    .parse_labels(&option)
1746                    .into_iter()
1747                    .for_each(|value| network.set_labels(value)),
1748                "name" if !duplicate => self
1749                    .parse_string(&option, "network custom name")
1750                    .into_iter()
1751                    .for_each(|value| network.set_custom_name(value)),
1752                name if name.starts_with("x-") => network.push_extension(option.reference()),
1753                _ if duplicate => {}
1754                _ => network.push_unknown(option.reference()),
1755            }
1756        }
1757        network
1758    }
1759
1760    fn parse_ipam(&mut self, field: &ParsedField) -> Option<Ipam> {
1761        let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
1762            self.expected(EXPECTED_MAPPING, field, "network IPAM must be a mapping");
1763            return None;
1764        };
1765        let span = span_from_position(self.source_id, mapping.byte_range());
1766        let mut ipam = Ipam::new(span);
1767        let mut seen = BTreeMap::new();
1768        for option in self.fields(mapping) {
1769            let duplicate = self.record_duplicate(&mut seen, &option);
1770            match option.name.value.as_str() {
1771                "driver" if !duplicate => self
1772                    .parse_string(&option, "IPAM driver")
1773                    .into_iter()
1774                    .for_each(|value| ipam.set_driver(value)),
1775                "config" if !duplicate => ipam.set_config(self.parse_ipam_configs(&option)),
1776                "options" if !duplicate => {
1777                    ipam.set_options(self.parse_scalar_mapping(&option, "IPAM options"));
1778                }
1779                name if name.starts_with("x-") => ipam.push_extension(option.reference()),
1780                _ if duplicate => {}
1781                _ => ipam.push_unknown(option.reference()),
1782            }
1783        }
1784        Some(ipam)
1785    }
1786
1787    fn parse_ipam_configs(&mut self, field: &ParsedField) -> Vec<IpamConfig> {
1788        let Some(sequence) = field.value.as_ref().and_then(YamlNode::as_sequence) else {
1789            self.expected(EXPECTED_SEQUENCE, field, "IPAM config must be a sequence");
1790            return Vec::new();
1791        };
1792        let mut configs = Vec::new();
1793        for value in sequence.values() {
1794            let YamlNode::Mapping(mapping) = value else {
1795                self.unsupported_sequence_item(
1796                    EXPECTED_MAPPING,
1797                    &value,
1798                    field.span,
1799                    "IPAM config entries must be mappings",
1800                );
1801                continue;
1802            };
1803            configs.push(self.parse_ipam_config(&mapping));
1804        }
1805        configs
1806    }
1807
1808    fn parse_ipam_config(&mut self, mapping: &Mapping) -> IpamConfig {
1809        let span = span_from_position(self.source_id, mapping.byte_range());
1810        let mut config = IpamConfig::new(span);
1811        let mut seen = BTreeMap::new();
1812        for field in self.fields(mapping) {
1813            let duplicate = self.record_duplicate(&mut seen, &field);
1814            match field.name.value.as_str() {
1815                "subnet" if !duplicate => self
1816                    .parse_string(&field, "IPAM subnet")
1817                    .into_iter()
1818                    .for_each(|value| config.set_subnet(value)),
1819                "ip_range" if !duplicate => self
1820                    .parse_string(&field, "IPAM allocation range")
1821                    .into_iter()
1822                    .for_each(|value| config.set_ip_range(value)),
1823                "gateway" if !duplicate => self
1824                    .parse_string(&field, "IPAM gateway")
1825                    .into_iter()
1826                    .for_each(|value| config.set_gateway(value)),
1827                "aux_addresses" if !duplicate => {
1828                    config.set_aux_addresses(self.parse_scalar_mapping(&field, "IPAM auxiliary addresses"));
1829                }
1830                name if name.starts_with("x-") => config.push_extension(field.reference()),
1831                _ if duplicate => {}
1832                _ => config.push_unknown(field.reference()),
1833            }
1834        }
1835        config
1836    }
1837
1838    fn parse_volume_definitions(&mut self, field: &ParsedField) -> Vec<VolumeDefinition> {
1839        let Some(mapping) = self.resource_collection(field, "volumes") else {
1840            return Vec::new();
1841        };
1842        let mut definitions = Vec::new();
1843        let mut seen = BTreeMap::new();
1844        for resource in self.fields(&mapping) {
1845            if self.record_duplicate(&mut seen, &resource) {
1846                continue;
1847            }
1848            let mut volume = VolumeDefinition::new(resource.name.clone(), resource.span);
1849            if Self::field_is_null(&resource) {
1850                definitions.push(volume);
1851                continue;
1852            }
1853            let Some(definition) = resource.value.as_ref().and_then(YamlNode::as_mapping) else {
1854                self.expected(
1855                    RESOURCE_EXPECTED_FORM,
1856                    &resource,
1857                    "volume definition must be a mapping or null",
1858                );
1859                continue;
1860            };
1861            let mut nested_seen = BTreeMap::new();
1862            for option in self.fields(definition) {
1863                let duplicate = self.record_duplicate(&mut nested_seen, &option);
1864                match option.name.value.as_str() {
1865                    "driver" if !duplicate => self
1866                        .parse_string(&option, "volume driver")
1867                        .into_iter()
1868                        .for_each(|value| volume.set_driver(value)),
1869                    "driver_opts" if !duplicate => {
1870                        volume.set_driver_opts(self.parse_scalar_mapping(&option, "volume driver options"));
1871                    }
1872                    "external" if !duplicate => self
1873                        .parse_boolean(&option, "volume external")
1874                        .into_iter()
1875                        .for_each(|value| volume.set_external(value)),
1876                    "labels" if !duplicate => self
1877                        .parse_labels(&option)
1878                        .into_iter()
1879                        .for_each(|value| volume.set_labels(value)),
1880                    "name" if !duplicate => self
1881                        .parse_string(&option, "volume custom name")
1882                        .into_iter()
1883                        .for_each(|value| volume.set_custom_name(value)),
1884                    name if name.starts_with("x-") => volume.push_extension(option.reference()),
1885                    _ if duplicate => {}
1886                    _ => volume.push_unknown(option.reference()),
1887                }
1888            }
1889            definitions.push(volume);
1890        }
1891        definitions
1892    }
1893
1894    fn parse_config_definitions(&mut self, field: &ParsedField) -> Vec<ConfigDefinition> {
1895        let Some(mapping) = self.resource_collection(field, "configs") else {
1896            return Vec::new();
1897        };
1898        let mut definitions = Vec::new();
1899        let mut seen = BTreeMap::new();
1900        for resource in self.fields(&mapping) {
1901            if self.record_duplicate(&mut seen, &resource) {
1902                continue;
1903            }
1904            let mut config = ConfigDefinition::new(resource.name.clone(), resource.span);
1905            if Self::field_is_null(&resource) {
1906                definitions.push(config);
1907                continue;
1908            }
1909            let Some(definition) = resource.value.as_ref().and_then(YamlNode::as_mapping) else {
1910                self.expected(
1911                    RESOURCE_EXPECTED_FORM,
1912                    &resource,
1913                    "config definition must be a mapping or null",
1914                );
1915                continue;
1916            };
1917            let mut nested_seen = BTreeMap::new();
1918            for option in self.fields(definition) {
1919                let duplicate = self.record_duplicate(&mut nested_seen, &option);
1920                match option.name.value.as_str() {
1921                    "file" if !duplicate => self
1922                        .parse_string(&option, "config file")
1923                        .into_iter()
1924                        .for_each(|value| config.set_file(value)),
1925                    "environment" if !duplicate => self
1926                        .parse_string(&option, "config environment source")
1927                        .into_iter()
1928                        .for_each(|value| config.set_environment(value)),
1929                    "content" if !duplicate => self
1930                        .parse_string(&option, "config content")
1931                        .into_iter()
1932                        .for_each(|value| config.set_content(value)),
1933                    "external" if !duplicate => self
1934                        .parse_boolean(&option, "config external")
1935                        .into_iter()
1936                        .for_each(|value| config.set_external(value)),
1937                    "name" if !duplicate => self
1938                        .parse_string(&option, "config custom name")
1939                        .into_iter()
1940                        .for_each(|value| config.set_custom_name(value)),
1941                    name if name.starts_with("x-") => config.push_extension(option.reference()),
1942                    _ if duplicate => {}
1943                    _ => config.push_unknown(option.reference()),
1944                }
1945            }
1946            definitions.push(config);
1947        }
1948        definitions
1949    }
1950
1951    fn parse_secret_definitions(&mut self, field: &ParsedField) -> Vec<SecretDefinition> {
1952        let Some(mapping) = self.resource_collection(field, "secrets") else {
1953            return Vec::new();
1954        };
1955        let mut definitions = Vec::new();
1956        let mut seen = BTreeMap::new();
1957        for resource in self.fields(&mapping) {
1958            if self.record_duplicate(&mut seen, &resource) {
1959                continue;
1960            }
1961            let mut secret = SecretDefinition::new(resource.name.clone(), resource.span);
1962            if Self::field_is_null(&resource) {
1963                definitions.push(secret);
1964                continue;
1965            }
1966            let Some(definition) = resource.value.as_ref().and_then(YamlNode::as_mapping) else {
1967                self.expected(
1968                    RESOURCE_EXPECTED_FORM,
1969                    &resource,
1970                    "secret definition must be a mapping or null",
1971                );
1972                continue;
1973            };
1974            let mut nested_seen = BTreeMap::new();
1975            for option in self.fields(definition) {
1976                let duplicate = self.record_duplicate(&mut nested_seen, &option);
1977                match option.name.value.as_str() {
1978                    "file" if !duplicate => self
1979                        .parse_string(&option, "secret file")
1980                        .into_iter()
1981                        .for_each(|value| secret.set_file(value)),
1982                    "environment" if !duplicate => self
1983                        .parse_string(&option, "secret environment source")
1984                        .into_iter()
1985                        .for_each(|value| secret.set_environment(value)),
1986                    "external" if !duplicate => self
1987                        .parse_boolean(&option, "secret external")
1988                        .into_iter()
1989                        .for_each(|value| secret.set_external(value)),
1990                    "name" if !duplicate => self
1991                        .parse_string(&option, "secret custom name")
1992                        .into_iter()
1993                        .for_each(|value| secret.set_custom_name(value)),
1994                    name if name.starts_with("x-") => secret.push_extension(option.reference()),
1995                    _ if duplicate => {}
1996                    _ => secret.push_unknown(option.reference()),
1997                }
1998            }
1999            definitions.push(secret);
2000        }
2001        definitions
2002    }
2003
2004    fn resource_collection(&mut self, field: &ParsedField, kind: &str) -> Option<Mapping> {
2005        let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
2006            self.expected(EXPECTED_MAPPING, field, format!("top-level {kind} must be a mapping"));
2007            return None;
2008        };
2009        Some(mapping.clone())
2010    }
2011
2012    fn parse_string(&mut self, field: &ParsedField, description: &str) -> Option<Located<String>> {
2013        let Some(scalar) = field.value.as_ref().and_then(YamlNode::as_scalar) else {
2014            self.expected(EXPECTED_SCALAR, field, format!("{description} must be a scalar"));
2015            return None;
2016        };
2017        if ScalarValue::from_scalar(scalar).scalar_type() == ScalarType::Null {
2018            self.expected(
2019                EXPECTED_SCALAR,
2020                field,
2021                format!("{description} must be a non-null scalar"),
2022            );
2023            return None;
2024        }
2025        Some(Located::new(
2026            scalar_string_from_source(&self.source, scalar),
2027            span_from_position(self.source_id, scalar.byte_range()),
2028        ))
2029    }
2030
2031    fn parse_boolean(&mut self, field: &ParsedField, description: &str) -> Option<Located<BooleanValue>> {
2032        let Some(scalar) = field.value.as_ref().and_then(YamlNode::as_scalar) else {
2033            self.expected(EXPECTED_BOOLEAN, field, format!("{description} must be a boolean"));
2034            return None;
2035        };
2036        let span = span_from_position(self.source_id, scalar.byte_range());
2037        let scalar_value = ScalarValue::from_scalar(scalar);
2038        if let Some(value) = scalar_value.to_bool() {
2039            return Some(Located::new(BooleanValue::Literal(value), span));
2040        }
2041        let value = scalar_string_from_source(&self.source, scalar);
2042        if value.contains('$') {
2043            return Some(Located::new(BooleanValue::Expression(value), span));
2044        }
2045        self.diagnostics.push(
2046            Diagnostic::new(
2047                EXPECTED_BOOLEAN,
2048                Severity::Error,
2049                format!("{description} must be a boolean or interpolation expression"),
2050            )
2051            .with_label(DiagnosticLabel::primary(span, "not a boolean expression")),
2052        );
2053        None
2054    }
2055
2056    fn parse_string_sequence(&mut self, field: &ParsedField, description: &str) -> Vec<Located<String>> {
2057        let Some(sequence) = field.value.as_ref().and_then(YamlNode::as_sequence) else {
2058            self.expected(EXPECTED_SEQUENCE, field, format!("{description} must be a sequence"));
2059            return Vec::new();
2060        };
2061        self.parse_scalar_nodes(
2062            sequence.values(),
2063            field.span,
2064            format!("{description} entries must be scalars"),
2065        )
2066    }
2067
2068    fn parse_scalar_nodes(
2069        &mut self,
2070        nodes: impl Iterator<Item = YamlNode>,
2071        fallback_span: SourceSpan,
2072        message: impl Into<String>,
2073    ) -> Vec<Located<String>> {
2074        let message = message.into();
2075        let mut values = Vec::new();
2076        for node in nodes {
2077            let YamlNode::Scalar(scalar) = node else {
2078                self.unsupported_sequence_item(EXPECTED_SCALAR, &node, fallback_span, &message);
2079                continue;
2080            };
2081            let scalar_value = ScalarValue::from_scalar(&scalar);
2082            if scalar_value.scalar_type() == ScalarType::Null {
2083                self.unsupported_sequence_item(EXPECTED_SCALAR, &YamlNode::Scalar(scalar), fallback_span, &message);
2084                continue;
2085            }
2086            let span = span_from_position(self.source_id, scalar.byte_range());
2087            values.push(Located::new(scalar_string_from_source(&self.source, &scalar), span));
2088        }
2089        values
2090    }
2091
2092    fn parse_scalar_mapping(&mut self, field: &ParsedField, description: &str) -> Vec<KeyValueEntry> {
2093        let Some(mapping) = field.value.as_ref().and_then(YamlNode::as_mapping) else {
2094            self.expected(EXPECTED_MAPPING, field, format!("{description} must be a mapping"));
2095            return Vec::new();
2096        };
2097        let mut entries = Vec::new();
2098        let mut seen = BTreeMap::new();
2099        for entry in self.fields(mapping) {
2100            if self.record_duplicate(&mut seen, &entry) {
2101                continue;
2102            }
2103            if let Some(value) = self.parse_compose_scalar(&entry, format!("{description} values must be scalars")) {
2104                entries.push(KeyValueEntry::new(entry.name, value, entry.span));
2105            }
2106        }
2107        entries
2108    }
2109
2110    fn parse_compose_scalar(
2111        &mut self,
2112        field: &ParsedField,
2113        message: impl Into<String>,
2114    ) -> Option<Located<ComposeScalar>> {
2115        let Some(node) = field.value.as_ref() else {
2116            return Some(Located::new(ComposeScalar::Null, field.name.span));
2117        };
2118        let Some(scalar) = node.as_scalar() else {
2119            self.expected(EXPECTED_SCALAR, field, message);
2120            return None;
2121        };
2122        let span = span_from_position(self.source_id, scalar.byte_range());
2123        let value = ScalarValue::from_scalar(scalar);
2124        let typed = match value.scalar_type() {
2125            ScalarType::Null => ComposeScalar::Null,
2126            ScalarType::Boolean => ComposeScalar::Boolean(value.to_bool().unwrap_or(false)),
2127            ScalarType::Integer | ScalarType::Float => {
2128                ComposeScalar::Number(scalar_string_from_source(&self.source, scalar))
2129            }
2130            ScalarType::String | ScalarType::Timestamp | ScalarType::Regex => {
2131                ComposeScalar::String(scalar_string_from_source(&self.source, scalar))
2132            }
2133        };
2134        Some(Located::new(typed, span))
2135    }
2136
2137    fn parse_labels(&mut self, field: &ParsedField) -> Option<Labels> {
2138        match field.value.as_ref() {
2139            Some(YamlNode::Sequence(sequence)) => {
2140                let span = span_from_position(self.source_id, sequence.byte_range());
2141                let values =
2142                    self.parse_scalar_nodes(sequence.values(), field.span, "label list entries must be scalars");
2143                Some(Labels::List { span, values })
2144            }
2145            Some(YamlNode::Mapping(mapping)) => {
2146                let span = span_from_position(self.source_id, mapping.byte_range());
2147                let entries = self.parse_scalar_mapping(field, "labels");
2148                Some(Labels::Map { span, entries })
2149            }
2150            _ => {
2151                self.expected(EXPECTED_FIELD_FORM, field, "labels must be a sequence or mapping");
2152                None
2153            }
2154        }
2155    }
2156
2157    fn field_is_null(field: &ParsedField) -> bool {
2158        field.value.as_ref().is_none_or(|node| {
2159            node.as_scalar()
2160                .is_some_and(|scalar| ScalarValue::from_scalar(scalar).scalar_type() == ScalarType::Null)
2161        })
2162    }
2163
2164    fn unsupported_sequence_item(
2165        &mut self,
2166        code: DiagnosticCode,
2167        node: &YamlNode,
2168        fallback_span: SourceSpan,
2169        message: impl Into<String>,
2170    ) {
2171        let span = node_span(self.source_id, node).unwrap_or(fallback_span);
2172        self.diagnostics.push(
2173            Diagnostic::new(code, Severity::Error, message)
2174                .with_label(DiagnosticLabel::primary(span, "unsupported value form")),
2175        );
2176    }
2177
2178    fn fields(&mut self, mapping: &Mapping) -> Vec<ParsedField> {
2179        let fields = self.raw_fields(mapping);
2180        let mut fields = self.flatten_empty_value_continuations(fields);
2181        for field in &mut fields {
2182            field.value = field.value.take().map(|value| self.resolve_alias(value));
2183        }
2184        fields
2185    }
2186
2187    fn raw_fields(&mut self, mapping: &Mapping) -> Vec<ParsedField> {
2188        mapping
2189            .entries()
2190            .filter_map(|entry| {
2191                let key = entry.key_node()?;
2192                let Some(scalar) = key.as_scalar() else {
2193                    let span = node_span(self.source_id, &key)
2194                        .unwrap_or_else(|| span_from_position(self.source_id, mapping.byte_range()));
2195                    self.diagnostics.push(
2196                        Diagnostic::new(EXPECTED_SCALAR, Severity::Error, "Compose mapping keys must be scalars")
2197                            .with_label(DiagnosticLabel::primary(span, "non-scalar key")),
2198                    );
2199                    return None;
2200                };
2201                let name_span = span_from_position(self.source_id, scalar.byte_range());
2202                let authored_value = entry.value_node();
2203                let value_span = authored_value
2204                    .as_ref()
2205                    .and_then(|value| node_span(self.source_id, value));
2206                let value = authored_value.map(unwrap_processing_tag);
2207                let span = value_span.map_or(name_span, |value_span| union(name_span, value_span));
2208                Some(ParsedField {
2209                    name: Located::new(scalar_string_from_source(&self.source, scalar), name_span),
2210                    value,
2211                    value_span,
2212                    span,
2213                })
2214            })
2215            .collect()
2216    }
2217
2218    fn resolve_alias(&self, node: YamlNode) -> YamlNode {
2219        let mut node = node;
2220        let mut visited = BTreeSet::new();
2221        for _ in 0..64 {
2222            let YamlNode::Alias(alias) = &node else {
2223                return node;
2224            };
2225            if !visited.insert(alias.name()) {
2226                return node;
2227            }
2228            let Some(target) = self.anchors.resolve(&alias.name()).and_then(|target| {
2229                YamlNode::from_syntax(target.clone()).or_else(|| target.children().find_map(YamlNode::from_syntax))
2230            }) else {
2231                return node;
2232            };
2233            node = target;
2234        }
2235        node
2236    }
2237
2238    fn flatten_empty_value_continuations(&mut self, fields: Vec<ParsedField>) -> Vec<ParsedField> {
2239        let Some(target_column) = fields.first().map(|field| self.source_column(field.name.span.start())) else {
2240            return fields;
2241        };
2242        self.recover_fields(fields, target_column)
2243    }
2244
2245    fn recover_fields(&mut self, fields: Vec<ParsedField>, target_column: usize) -> Vec<ParsedField> {
2246        let mut flattened = Vec::new();
2247        for mut field in fields {
2248            let field_column = self.source_column(field.name.span.start());
2249            let nested_mapping = field.value.as_ref().and_then(YamlNode::as_mapping).cloned();
2250            let continuation = nested_mapping.as_ref().is_some_and(|mapping| {
2251                !self.is_flow_mapping(mapping)
2252                    && mapping
2253                        .entries()
2254                        .find_map(|entry| {
2255                            let key = entry.key_node()?;
2256                            let scalar = key.as_scalar()?;
2257                            Some(scalar.byte_range().start as usize)
2258                        })
2259                        .is_some_and(|key_start| self.source_column(key_start) <= field_column)
2260            });
2261
2262            if continuation {
2263                field.value = None;
2264                field.value_span = None;
2265                field.span = field.name.span;
2266            }
2267            if field_column == target_column {
2268                flattened.push(field);
2269            }
2270            if let Some(mapping) = nested_mapping.filter(|mapping| !self.is_flow_mapping(mapping)) {
2271                let nested = self.raw_fields(&mapping);
2272                flattened.extend(self.recover_fields(nested, target_column));
2273            }
2274        }
2275        flattened
2276    }
2277
2278    fn is_flow_mapping(&self, mapping: &Mapping) -> bool {
2279        let position = mapping.byte_range();
2280        self.source
2281            .get(position.start as usize..position.end as usize)
2282            .is_some_and(|text| text.trim_start().starts_with('{'))
2283    }
2284
2285    fn record_duplicate(&mut self, seen: &mut BTreeMap<String, SourceSpan>, field: &ParsedField) -> bool {
2286        if let Some(first) = seen.get(field.name.value()) {
2287            self.diagnostics.push(
2288                Diagnostic::new(
2289                    DUPLICATE_FIELD,
2290                    Severity::Error,
2291                    "Compose mapping fields must be unique",
2292                )
2293                .with_label(DiagnosticLabel::primary(field.name.span, "duplicate field"))
2294                .with_label(DiagnosticLabel::secondary(*first, "first field")),
2295            );
2296            true
2297        } else {
2298            seen.insert(field.name.value.clone(), field.name.span);
2299            false
2300        }
2301    }
2302
2303    fn expected(&mut self, code: DiagnosticCode, field: &ParsedField, message: impl Into<String>) {
2304        self.diagnostics.push(
2305            Diagnostic::new(code, Severity::Error, message)
2306                .with_label(DiagnosticLabel::primary(field.span, "unexpected value form")),
2307        );
2308    }
2309
2310    fn missing(&mut self, code: DiagnosticCode, span: SourceSpan, message: &'static str) {
2311        self.diagnostics.push(
2312            Diagnostic::new(code, Severity::Error, message)
2313                .with_label(DiagnosticLabel::primary(span, "incomplete long syntax")),
2314        );
2315    }
2316}
2317
2318fn unwrap_processing_tag(node: YamlNode) -> YamlNode {
2319    let YamlNode::TaggedNode(tagged) = &node else {
2320        return node;
2321    };
2322    if !matches!(tagged.tag().as_deref(), Some("!reset" | "!override")) {
2323        return node;
2324    }
2325    tagged
2326        .as_node()
2327        .and_then(|syntax| syntax.children().find_map(YamlNode::from_syntax))
2328        .unwrap_or(node)
2329}
2330
2331#[derive(Debug, Clone)]
2332enum ParsedGrant {
2333    Short(Located<String>),
2334    Long(Box<LongGrant>),
2335}
2336
2337#[derive(Debug, Clone)]
2338struct ParsedField {
2339    name: Located<String>,
2340    value: Option<YamlNode>,
2341    value_span: Option<SourceSpan>,
2342    span: SourceSpan,
2343}
2344
2345impl ParsedField {
2346    fn reference(&self) -> FieldReference {
2347        FieldReference {
2348            name: self.name.clone(),
2349            span: self.span,
2350            value_span: self.value_span,
2351        }
2352    }
2353}
2354
2355fn node_span(source_id: SourceId, node: &YamlNode) -> Option<SourceSpan> {
2356    let position = match node {
2357        YamlNode::Scalar(value) => value.byte_range(),
2358        YamlNode::Mapping(value) => value.byte_range(),
2359        YamlNode::Sequence(value) => value.byte_range(),
2360        YamlNode::Alias(_) | YamlNode::TaggedNode(_) => {
2361            let range = node.as_node()?.text_range();
2362            return Some(SourceSpan::from_valid_offsets(
2363                source_id,
2364                u32::from(range.start()) as usize,
2365                u32::from(range.end()) as usize,
2366            ));
2367        }
2368    };
2369    Some(span_from_position(source_id, position))
2370}
2371
2372fn span_from_position(source_id: SourceId, position: yaml_edit::TextPosition) -> SourceSpan {
2373    SourceSpan::from_valid_offsets(source_id, position.start as usize, position.end as usize)
2374}
2375
2376fn union(left: SourceSpan, right: SourceSpan) -> SourceSpan {
2377    SourceSpan::from_valid_offsets(
2378        left.source_id(),
2379        left.start().min(right.start()),
2380        left.end().max(right.end()),
2381    )
2382}