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