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