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