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