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