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