Skip to main content

compose_lens/model/
sections.rs

1//! Field-level build and deploy section models.
2
3use super::{
4    BooleanValue, BuildExtraHosts, BuildNoCache, BuildProvenance, BuildSbom, FieldReference, KeyValueEntry, Labels,
5    Located, SecretGrant, ShmSize, Ulimits,
6};
7use crate::source::SourceSpan;
8use std::fmt;
9use std::sync::Arc;
10
11/// A Compose build declaration with short and long forms retained.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum Build {
14    /// A scalar build context.
15    Context(Located<String>),
16    /// A mapping of independently classified build fields.
17    Definition(BuildDefinition),
18}
19
20/// A long-syntax build definition.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct BuildDefinition {
23    span: SourceSpan,
24    values: Box<BuildValues>,
25    fields: Vec<BuildField>,
26    extension_fields: Vec<FieldReference>,
27    unknown_fields: Arc<Vec<FieldReference>>,
28}
29
30/// Heap-stored optional build values keep the public `Build` enum compact without changing its
31/// short/long syntax variants.
32#[derive(Debug, Clone, PartialEq, Eq)]
33struct BuildValues {
34    additional_contexts: Option<BuildAdditionalContexts>,
35    entitlements: Option<Arc<Vec<Located<String>>>>,
36    extra_hosts: Option<BuildExtraHosts>,
37    context: Option<Located<String>>,
38    args: Option<BuildArgs>,
39    cache_from: Option<Arc<Vec<Located<String>>>>,
40    cache_to: Option<Arc<Vec<Located<String>>>>,
41    dockerfile: Option<Located<String>>,
42    dockerfile_inline: Option<Located<String>>,
43    target: Option<Located<String>>,
44    network: Option<Box<Located<String>>>,
45    isolation: Option<Box<Located<String>>>,
46    platforms: Option<Arc<Vec<Located<String>>>>,
47    no_cache: Option<Box<Located<BuildNoCache>>>,
48    no_cache_filter: Option<BuildNoCacheFilter>,
49    privileged: Option<Box<Located<BooleanValue>>>,
50    sbom: Option<Box<Located<BuildSbom>>>,
51    provenance: Option<Box<Located<BuildProvenance>>>,
52    pull: Option<Box<Located<BooleanValue>>>,
53    shm_size: Option<Box<ShmSize>>,
54    tags: Option<Arc<Vec<Located<String>>>>,
55    labels: Option<Box<Labels>>,
56    secrets: Option<Arc<Vec<SecretGrant>>>,
57    ssh: Option<BuildSsh>,
58    ulimits: Option<Box<Ulimits>>,
59}
60
61impl BuildDefinition {
62    pub(super) fn new(span: SourceSpan) -> Self {
63        Self {
64            span,
65            values: Box::new(BuildValues {
66                additional_contexts: None,
67                entitlements: None,
68                extra_hosts: None,
69                context: None,
70                args: None,
71                cache_from: None,
72                cache_to: None,
73                dockerfile: None,
74                dockerfile_inline: None,
75                target: None,
76                network: None,
77                isolation: None,
78                platforms: None,
79                no_cache: None,
80                no_cache_filter: None,
81                privileged: None,
82                sbom: None,
83                provenance: None,
84                pull: None,
85                shm_size: None,
86                tags: None,
87                labels: None,
88                secrets: None,
89                ssh: None,
90                ulimits: None,
91            }),
92            fields: Vec::new(),
93            extension_fields: Vec::new(),
94            unknown_fields: Arc::new(Vec::new()),
95        }
96    }
97
98    pub(super) fn push_field(&mut self, field: BuildField) {
99        self.fields.push(field);
100    }
101
102    pub(super) fn set_context(&mut self, context: Located<String>) {
103        self.values.context = Some(context);
104    }
105
106    pub(super) fn set_additional_contexts(&mut self, additional_contexts: Option<BuildAdditionalContexts>) {
107        self.values.additional_contexts = additional_contexts;
108    }
109
110    pub(super) fn set_entitlements(&mut self, entitlements: Vec<Located<String>>) {
111        self.values.entitlements = Some(Arc::new(entitlements));
112    }
113
114    pub(super) fn set_extra_hosts(&mut self, extra_hosts: BuildExtraHosts) {
115        self.values.extra_hosts = Some(extra_hosts);
116    }
117
118    pub(super) fn set_args(&mut self, args: BuildArgs) {
119        self.values.args = Some(args);
120    }
121
122    pub(super) fn set_cache_from(&mut self, cache_from: Vec<Located<String>>) {
123        self.values.cache_from = Some(Arc::new(cache_from));
124    }
125
126    pub(super) fn set_cache_to(&mut self, cache_to: Vec<Located<String>>) {
127        self.values.cache_to = Some(Arc::new(cache_to));
128    }
129
130    pub(super) fn set_dockerfile(&mut self, dockerfile: Located<String>) {
131        self.values.dockerfile = Some(dockerfile);
132    }
133
134    pub(super) fn set_dockerfile_inline(&mut self, dockerfile_inline: Located<String>) {
135        self.values.dockerfile_inline = Some(dockerfile_inline);
136    }
137
138    pub(super) fn set_target(&mut self, target: Located<String>) {
139        self.values.target = Some(target);
140    }
141
142    pub(super) fn set_network(&mut self, network: Located<String>) {
143        self.values.network = Some(Box::new(network));
144    }
145
146    pub(super) fn set_isolation(&mut self, isolation: Located<String>) {
147        self.values.isolation = Some(Box::new(isolation));
148    }
149
150    pub(super) fn set_platforms(&mut self, platforms: Vec<Located<String>>) {
151        self.values.platforms = Some(Arc::new(platforms));
152    }
153
154    pub(super) fn set_no_cache(&mut self, no_cache: Located<BuildNoCache>) {
155        self.values.no_cache = Some(Box::new(no_cache));
156    }
157    pub(super) fn set_no_cache_filter(&mut self, value: BuildNoCacheFilter) {
158        self.values.no_cache_filter = Some(value);
159    }
160    pub(super) fn set_privileged(&mut self, value: Located<BooleanValue>) {
161        self.values.privileged = Some(Box::new(value));
162    }
163
164    pub(super) fn set_sbom(&mut self, sbom: Located<BuildSbom>) {
165        self.values.sbom = Some(Box::new(sbom));
166    }
167    pub(super) fn set_provenance(&mut self, value: Located<BuildProvenance>) {
168        self.values.provenance = Some(Box::new(value));
169    }
170
171    pub(super) fn set_pull(&mut self, pull: Located<BooleanValue>) {
172        self.values.pull = Some(Box::new(pull));
173    }
174
175    pub(super) fn set_shm_size(&mut self, shm_size: ShmSize) {
176        self.values.shm_size = Some(Box::new(shm_size));
177    }
178
179    pub(super) fn set_tags(&mut self, tags: Vec<Located<String>>) {
180        self.values.tags = Some(Arc::new(tags));
181    }
182
183    pub(super) fn set_labels(&mut self, labels: Labels) {
184        self.values.labels = Some(Box::new(labels));
185    }
186
187    pub(super) fn set_secrets(&mut self, secrets: Vec<SecretGrant>) {
188        self.values.secrets = Some(Arc::new(secrets));
189    }
190
191    pub(super) fn set_ssh(&mut self, ssh: BuildSsh) {
192        self.values.ssh = Some(ssh);
193    }
194
195    pub(super) fn set_ulimits(&mut self, ulimits: Ulimits) {
196        self.values.ulimits = Some(Box::new(ulimits));
197    }
198
199    pub(super) fn push_extension(&mut self, field: FieldReference) {
200        self.extension_fields.push(field);
201    }
202
203    pub(super) fn push_unknown(&mut self, field: FieldReference) {
204        Arc::make_mut(&mut self.unknown_fields).push(field);
205    }
206
207    /// Returns the complete mapping span.
208    /// Returns the complete update-config mapping span.
209    /// Returns the complete update-config mapping span.
210    /// Returns the complete update-config mapping span.
211    #[must_use]
212    pub const fn span(&self) -> SourceSpan {
213        self.span
214    }
215
216    /// Returns the explicitly authored long-syntax build context when it is a string scalar.
217    ///
218    /// Other build subfields remain source-addressable references and are not semantically
219    /// interpreted by this model.
220    /// Returns the raw update parallelism scalar.
221    #[must_use]
222    pub const fn context(&self) -> Option<&Located<String>> {
223        self.values.context.as_ref()
224    }
225
226    /// Returns authored additional build contexts without normalizing mapping and list syntax.
227    ///
228    /// List entries remain raw ordered strings, including duplicates and `NAME=VALUE` spelling.
229    /// Mapping entries retain scalar kinds and authored order. This model does not interpret
230    /// names, paths, URLs, images, service schemes, or builder behavior.
231    /// Returns the raw update delay string.
232    #[must_use]
233    pub const fn additional_contexts(&self) -> Option<&BuildAdditionalContexts> {
234        self.values.additional_contexts.as_ref()
235    }
236
237    /// Returns authored build entitlements in order as opaque raw strings.
238    ///
239    /// Explicit emptiness remains distinct from omission. This model retains duplicates and does
240    /// not infer entitlement allowlists, privilege state, `BuildKit` or platform support, build
241    /// execution, or runtime effect.
242    /// Returns the raw update monitor string.
243    #[must_use]
244    pub fn entitlements(&self) -> Option<&[Located<String>]> {
245        self.values.entitlements.as_deref().map(Vec::as_slice)
246    }
247
248    /// Returns authored build-time host mappings without using service `extra_hosts` semantics.
249    ///
250    /// List entries retain raw `=`/`:` spelling, IPv4/IPv6 brackets, `host-gateway`, and unknown
251    /// values. Mapping values retain either a scalar string or ordered string list; no address
252    /// normalization, validation, DNS lookup, host inspection, or build behavior is performed.
253    /// Returns the raw update failure-action string.
254    #[must_use]
255    pub const fn extra_hosts(&self) -> Option<&BuildExtraHosts> {
256        self.values.extra_hosts.as_ref()
257    }
258
259    /// Returns explicitly authored build arguments without normalizing mapping and list syntax.
260    ///
261    /// Mapping entries retain their string, number, boolean, or null scalar kinds. List entries
262    /// remain raw ordered strings, including duplicates and bare argument names.
263    /// Returns the raw update maximum-failure-ratio scalar.
264    #[must_use]
265    pub const fn args(&self) -> Option<&BuildArgs> {
266        self.values.args.as_ref()
267    }
268
269    /// Returns authored external build-cache sources in order.
270    ///
271    /// An explicit empty sequence remains distinct from omission. Entries are raw string scalars;
272    /// this model preserves duplicates and does not parse cache type, reference, source, path,
273    /// image, credentials, or builder behavior.
274    /// Returns the update order.
275    #[must_use]
276    pub fn cache_from(&self) -> Option<&[Located<String>]> {
277        self.values.cache_from.as_deref().map(Vec::as_slice)
278    }
279
280    /// Returns authored external build-cache destinations in order.
281    ///
282    /// An explicit empty sequence remains distinct from omission. Entries are raw string scalars;
283    /// this model preserves duplicates and does not parse cache type, reference, destination,
284    /// path, image, credentials, or builder behavior.
285    /// Returns retained update-config extensions.
286    #[must_use]
287    pub fn cache_to(&self) -> Option<&[Located<String>]> {
288        self.values.cache_to.as_deref().map(Vec::as_slice)
289    }
290
291    /// Returns the explicitly authored long-syntax Dockerfile when it is a non-empty scalar.
292    ///
293    /// Other build subfields remain source-addressable references and are not semantically
294    /// interpreted by this model.
295    /// Returns retained update-config malformed or unknown fields.
296    #[must_use]
297    pub const fn dockerfile(&self) -> Option<&Located<String>> {
298        self.values.dockerfile.as_ref()
299    }
300
301    /// Returns the authored inline Dockerfile as an exact string scalar.
302    ///
303    /// Empty and multiline content remains distinct from omission. `ComposeLens` does not parse
304    /// Containerfile syntax, resolve paths or contexts, scan content for secrets, build images,
305    /// or infer Docker, `BuildKit`, or runtime behavior.
306    #[must_use]
307    pub const fn dockerfile_inline(&self) -> Option<&Located<String>> {
308        self.values.dockerfile_inline.as_ref()
309    }
310
311    /// Returns the explicitly authored long-syntax build target as an opaque scalar.
312    ///
313    /// An empty scalar remains an authored target; this model does not infer stage-name grammar.
314    #[must_use]
315    pub const fn target(&self) -> Option<&Located<String>> {
316        self.values.target.as_ref()
317    }
318
319    /// Returns the explicitly authored long-syntax build network as an opaque scalar.
320    ///
321    /// An empty scalar remains authored; this model does not infer network names, defaults, or
322    /// runtime behavior.
323    #[must_use]
324    pub fn network(&self) -> Option<&Located<String>> {
325        self.values.network.as_deref()
326    }
327
328    /// Returns the explicitly authored long-syntax build isolation as an opaque YAML string.
329    ///
330    /// This model preserves only YAML string scalars and does not validate isolation modes,
331    /// platforms, defaults, privileges, or `BUILDAH_ISOLATION` behavior. It is unrelated to the
332    /// service-level `isolation` field.
333    #[must_use]
334    pub fn isolation(&self) -> Option<&Located<String>> {
335        self.values.isolation.as_deref()
336    }
337
338    /// Returns explicitly authored build platforms in order.
339    ///
340    /// An explicit empty sequence remains distinct from omission. Platforms are raw scalar
341    /// values; this model does not parse OCI platform grammar or validate availability.
342    #[must_use]
343    pub fn platforms(&self) -> Option<&[Located<String>]> {
344        self.values.platforms.as_deref().map(Vec::as_slice)
345    }
346
347    /// Returns the explicitly authored build cache-disable choice with YAML scalar type retained.
348    ///
349    /// Omission does not imply a default. String values, including empty and interpolation-shaped
350    /// strings, are not coerced or resolved as booleans; this model does not infer builder or
351    /// cache behavior.
352    #[must_use]
353    pub fn no_cache(&self) -> Option<&Located<BuildNoCache>> {
354        self.values.no_cache.as_deref()
355    }
356    /// Returns raw no-cache filter scalar or list syntax.
357    #[must_use]
358    pub const fn no_cache_filter(&self) -> Option<&BuildNoCacheFilter> {
359        self.values.no_cache_filter.as_ref()
360    }
361    /// Returns the explicit build privileged boolean or deferred expression.
362    #[must_use]
363    pub fn privileged(&self) -> Option<&Located<BooleanValue>> {
364        self.values.privileged.as_deref()
365    }
366
367    /// Returns the explicitly authored build SBOM choice with YAML scalar type retained.
368    ///
369    /// Omission does not imply a default. String values, including empty, generator-shaped, and
370    /// interpolation-shaped strings, are not coerced or interpreted; this model does not generate
371    /// an SBOM or infer builder behavior.
372    #[must_use]
373    pub fn sbom(&self) -> Option<&Located<BuildSbom>> {
374        self.values.sbom.as_deref()
375    }
376    /// Returns authored Build provenance as a boolean or opaque string scalar.
377    #[must_use]
378    pub fn provenance(&self) -> Option<&Located<BuildProvenance>> {
379        self.values.provenance.as_deref()
380    }
381
382    /// Returns whether this build should pull referenced images before building.
383    ///
384    /// A literal boolean and a deferred interpolation expression remain distinct. Omission is not
385    /// treated as an implicit default, and this model does not resolve expressions or infer build
386    /// execution behavior.
387    #[must_use]
388    pub fn pull(&self) -> Option<&Located<BooleanValue>> {
389        self.values.pull.as_deref()
390    }
391
392    /// Returns the explicitly authored build-container shared-memory size.
393    ///
394    /// This retains the same YAML number/string spelling, documented lowercase-unit
395    /// classification, lexical-zero, deferred-expression, and provider-dependent states as
396    /// service `shm_size`. Omission does not infer a builder default, allocation, host setting,
397    /// or runtime behavior.
398    #[must_use]
399    pub fn shm_size(&self) -> Option<&ShmSize> {
400        self.values.shm_size.as_deref()
401    }
402
403    /// Returns explicitly authored additional build tags in order.
404    ///
405    /// An explicit empty sequence remains distinct from omission. Tags are opaque scalar values;
406    /// this model does not apply image-reference grammar or duplicate handling.
407    #[must_use]
408    pub fn tags(&self) -> Option<&[Located<String>]> {
409        self.values.tags.as_deref().map(Vec::as_slice)
410    }
411
412    /// Returns explicitly authored build labels without normalizing mapping and list syntax.
413    ///
414    /// List entries remain ordered raw strings, including duplicates and bare labels. Mapping
415    /// entries retain their scalar kinds and authored order.
416    #[must_use]
417    pub fn labels(&self) -> Option<&Labels> {
418        self.values.labels.as_deref()
419    }
420
421    /// Returns explicitly authored build secret grants in order.
422    ///
423    /// Short resource-name and long mapping syntax remain distinct. An explicit empty sequence,
424    /// duplicate entries, raw scalar spellings, and unknown long-form fields are retained; this
425    /// model neither resolves top-level secrets nor materializes secret contents.
426    #[must_use]
427    pub fn secrets(&self) -> Option<&[SecretGrant]> {
428        self.values.secrets.as_deref().map(Vec::as_slice)
429    }
430
431    /// Returns authored `BuildKit` SSH grants without normalizing mapping and list syntax.
432    ///
433    /// SSH identifiers, paths, agent sockets, and material are opaque sensitive data. This
434    /// model neither parses them nor accesses the host, an agent, a socket, or a build runtime.
435    #[must_use]
436    pub const fn ssh(&self) -> Option<&BuildSsh> {
437        self.values.ssh.as_ref()
438    }
439
440    /// Returns explicitly authored build-container resource limits.
441    ///
442    /// The same single and soft/hard range forms as service `ulimits` are retained. This model
443    /// does not inject defaults, normalize unlimited values, validate host limits, or infer
444    /// builder or runtime behavior.
445    #[must_use]
446    pub fn ulimits(&self) -> Option<&Ulimits> {
447        self.values.ulimits.as_deref()
448    }
449
450    /// Returns recognized build fields in authored order.
451    #[must_use]
452    pub fn fields(&self) -> &[BuildField] {
453        &self.fields
454    }
455
456    /// Finds the first recognized field of the requested kind.
457    #[must_use]
458    pub fn field(&self, kind: BuildFieldKind) -> Option<&BuildField> {
459        self.fields.iter().find(|field| field.kind == kind)
460    }
461
462    /// Returns retained `x-` fields.
463    #[must_use]
464    pub fn extension_fields(&self) -> &[FieldReference] {
465        &self.extension_fields
466    }
467
468    /// Returns fields not recognized by this release.
469    #[must_use]
470    pub fn unknown_fields(&self) -> &[FieldReference] {
471        self.unknown_fields.as_slice()
472    }
473}
474
475/// Authored `build.no_cache_filter` form retaining exact string values.
476#[derive(Debug, Clone, PartialEq, Eq)]
477#[non_exhaustive]
478pub enum BuildNoCacheFilter {
479    /// One exact scalar stage name.
480    Scalar(Located<String>),
481    /// Ordered exact stage names.
482    List(Vec<Located<String>>),
483}
484
485/// Additional build contexts with mapping and list syntax retained.
486#[derive(Debug, Clone, PartialEq, Eq)]
487pub enum BuildAdditionalContexts {
488    /// List syntax such as `name=path`, retained as raw strings.
489    List {
490        /// The complete sequence span.
491        span: SourceSpan,
492        /// Context entries in authored order.
493        values: Vec<Located<String>>,
494    },
495    /// Mapping syntax with scalar context values.
496    Map {
497        /// The complete mapping span.
498        span: SourceSpan,
499        /// Context entries in authored order.
500        entries: Vec<KeyValueEntry>,
501    },
502}
503
504impl BuildAdditionalContexts {
505    /// Returns the authored collection span.
506    #[must_use]
507    pub const fn span(&self) -> SourceSpan {
508        match self {
509            Self::List { span, .. } | Self::Map { span, .. } => *span,
510        }
511    }
512}
513
514/// Compose build arguments with mapping and list syntax retained.
515#[derive(Debug, Clone, PartialEq, Eq)]
516pub enum BuildArgs {
517    /// List syntax such as `HTTP_PROXY=http://proxy` or a bare `HTTP_PROXY`.
518    List {
519        /// The complete sequence span.
520        span: SourceSpan,
521        /// Raw argument strings in authored order.
522        values: Vec<Located<String>>,
523    },
524    /// Mapping syntax with scalar argument values.
525    Map {
526        /// The complete mapping span.
527        span: SourceSpan,
528        /// Argument entries in authored order.
529        entries: Vec<KeyValueEntry>,
530    },
531}
532
533impl BuildArgs {
534    /// Returns the authored collection span.
535    #[must_use]
536    pub const fn span(&self) -> SourceSpan {
537        match self {
538            Self::List { span, .. } | Self::Map { span, .. } => *span,
539        }
540    }
541}
542
543/// Sensitive `BuildKit` SSH grants with mapping and list syntax retained.
544///
545/// List entries remain raw ordered strings, including duplicates. Mapping entries retain scalar
546/// string, number, boolean, or null values in authored order. Neither form parses identifiers,
547/// paths, PEM material, sockets, `SSH_AUTH_SOCK`, Containerfile mounts, or agent behavior.
548///
549/// Sensitive storage cannot be destructured by downstream callers; use [`Self::as_list`] or
550/// [`Self::as_map`] explicitly when raw inspection is required.
551///
552/// ```compile_fail
553/// use compose_lens::model::BuildSsh;
554///
555/// fn forbidden(ssh: BuildSsh) {
556///     let BuildSsh { storage } = ssh;
557/// }
558/// ```
559#[derive(Clone, PartialEq, Eq)]
560pub struct BuildSsh {
561    form: BuildSshForm,
562    span: SourceSpan,
563    storage: BuildSshStorage,
564}
565
566#[derive(Clone, Copy, Debug, PartialEq, Eq)]
567#[non_exhaustive]
568/// The authored `build.ssh` collection form.
569pub enum BuildSshForm {
570    /// List syntax with raw SSH grant strings in authored order.
571    List,
572    /// Mapping syntax with sensitive scalar SSH grant values.
573    Map,
574}
575
576#[derive(Clone, PartialEq, Eq)]
577enum BuildSshStorage {
578    List(Vec<Located<String>>),
579    Map(Vec<KeyValueEntry>),
580}
581
582impl fmt::Debug for BuildSsh {
583    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
584        formatter
585            .debug_struct("BuildSsh")
586            .field("form", &self.form)
587            .field("span", &self.span)
588            .field("storage", &"<redacted>")
589            .finish()
590    }
591}
592
593impl BuildSsh {
594    /// Returns the authored collection span.
595    #[must_use]
596    pub const fn span(&self) -> SourceSpan {
597        self.span
598    }
599
600    /// Returns the authored syntax form without exposing sensitive storage.
601    #[must_use]
602    pub const fn form(&self) -> BuildSshForm {
603        self.form
604    }
605
606    /// Explicitly returns raw ordered list-form grants.
607    #[must_use]
608    pub fn as_list(&self) -> Option<&[Located<String>]> {
609        let BuildSshStorage::List(values) = &self.storage else {
610            return None;
611        };
612        Some(values)
613    }
614
615    /// Explicitly returns raw ordered mapping-form grants.
616    #[must_use]
617    pub fn as_map(&self) -> Option<&[KeyValueEntry]> {
618        let BuildSshStorage::Map(entries) = &self.storage else {
619            return None;
620        };
621        Some(entries)
622    }
623
624    pub(super) fn list(span: SourceSpan, values: Vec<Located<String>>) -> Self {
625        Self {
626            form: BuildSshForm::List,
627            span,
628            storage: BuildSshStorage::List(values),
629        }
630    }
631
632    pub(super) fn map(span: SourceSpan, entries: Vec<KeyValueEntry>) -> Self {
633        Self {
634            form: BuildSshForm::Map,
635            span,
636            storage: BuildSshStorage::Map(entries),
637        }
638    }
639}
640
641/// One recognized build subfield and its source reference.
642#[derive(Debug, Clone, PartialEq, Eq)]
643pub struct BuildField {
644    kind: BuildFieldKind,
645    reference: FieldReference,
646}
647
648impl BuildField {
649    pub(super) const fn new(kind: BuildFieldKind, reference: FieldReference) -> Self {
650        Self { kind, reference }
651    }
652
653    /// Returns the field's specification-level identity.
654    #[must_use]
655    pub const fn kind(&self) -> BuildFieldKind {
656        self.kind
657    }
658
659    /// Returns source spans for reading or editing the retained value.
660    #[must_use]
661    pub const fn reference(&self) -> &FieldReference {
662        &self.reference
663    }
664}
665
666/// Recognized fields from the current Compose Build Specification.
667#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
668#[non_exhaustive]
669pub enum BuildFieldKind {
670    /// Additional named build contexts.
671    AdditionalContexts,
672    /// Dockerfile build arguments.
673    Args,
674    /// External cache sources.
675    CacheFrom,
676    /// External cache destinations.
677    CacheTo,
678    /// Build context.
679    Context,
680    /// Dockerfile path.
681    Dockerfile,
682    /// Inline Dockerfile content.
683    DockerfileInline,
684    /// Build entitlements.
685    Entitlements,
686    /// Build-time host mappings.
687    ExtraHosts,
688    /// Container isolation technology.
689    Isolation,
690    /// Build image labels.
691    Labels,
692    /// Build network mode.
693    Network,
694    /// Disable build cache.
695    NoCache,
696    /// Target platforms.
697    Platforms,
698    /// Privileged build mode.
699    Privileged,
700    /// Supply-chain provenance.
701    Provenance,
702    /// Pull referenced images.
703    Pull,
704    /// Software bill of materials.
705    Sbom,
706    /// Build-time secret grants.
707    Secrets,
708    /// SSH agent/socket grants.
709    Ssh,
710    /// Build shared-memory size.
711    ShmSize,
712    /// Additional output tags.
713    Tags,
714    /// Dockerfile target stage.
715    Target,
716    /// Build-container resource limits.
717    Ulimits,
718    /// Select build stages excluded from cache.
719    NoCacheFilter,
720}
721
722impl BuildFieldKind {
723    pub(super) fn from_name(name: &str) -> Option<Self> {
724        Some(match name {
725            "additional_contexts" => Self::AdditionalContexts,
726            "args" => Self::Args,
727            "cache_from" => Self::CacheFrom,
728            "cache_to" => Self::CacheTo,
729            "context" => Self::Context,
730            "dockerfile" => Self::Dockerfile,
731            "dockerfile_inline" => Self::DockerfileInline,
732            "entitlements" => Self::Entitlements,
733            "extra_hosts" => Self::ExtraHosts,
734            "isolation" => Self::Isolation,
735            "labels" => Self::Labels,
736            "network" => Self::Network,
737            "no_cache" => Self::NoCache,
738            "no_cache_filter" => Self::NoCacheFilter,
739            "platforms" => Self::Platforms,
740            "privileged" => Self::Privileged,
741            "provenance" => Self::Provenance,
742            "pull" => Self::Pull,
743            "sbom" => Self::Sbom,
744            "secrets" => Self::Secrets,
745            "ssh" => Self::Ssh,
746            "shm_size" => Self::ShmSize,
747            "tags" => Self::Tags,
748            "target" => Self::Target,
749            "ulimits" => Self::Ulimits,
750            _ => return None,
751        })
752    }
753}
754
755/// A deploy definition split into independently classifiable fields.
756#[derive(Debug, Clone, PartialEq, Eq)]
757pub struct DeployDefinition {
758    span: SourceSpan,
759    endpoint_mode: Option<Located<DeployEndpointMode>>,
760    labels: Option<Box<Labels>>,
761    mode: Option<Located<DeployMode>>,
762    placement: Option<Box<DeployPlacement>>,
763    replicas: Option<Located<DeployReplicas>>,
764    resources: Option<Box<DeployResources>>,
765    restart_policy: Option<Box<DeployRestartPolicy>>,
766    rollback_config: Option<Box<DeployRollbackConfig>>,
767    update_config: Option<Box<DeployUpdateConfig>>,
768    fields: Vec<DeployField>,
769    extension_fields: Vec<FieldReference>,
770    unknown_fields: Vec<FieldReference>,
771}
772
773impl DeployDefinition {
774    pub(super) const fn new(span: SourceSpan) -> Self {
775        Self {
776            span,
777            endpoint_mode: None,
778            labels: None,
779            mode: None,
780            placement: None,
781            replicas: None,
782            resources: None,
783            restart_policy: None,
784            rollback_config: None,
785            update_config: None,
786            fields: Vec::new(),
787            extension_fields: Vec::new(),
788            unknown_fields: Vec::new(),
789        }
790    }
791
792    pub(super) fn push_field(&mut self, field: DeployField) {
793        self.fields.push(field);
794    }
795
796    pub(super) fn set_endpoint_mode(&mut self, endpoint_mode: Located<DeployEndpointMode>) {
797        self.endpoint_mode = Some(endpoint_mode);
798    }
799
800    pub(super) fn set_labels(&mut self, labels: Labels) {
801        self.labels = Some(Box::new(labels));
802    }
803
804    pub(super) fn set_mode(&mut self, mode: Located<DeployMode>) {
805        self.mode = Some(mode);
806    }
807
808    pub(super) fn set_placement(&mut self, placement: DeployPlacement) {
809        self.placement = Some(Box::new(placement));
810    }
811
812    pub(super) fn set_replicas(&mut self, replicas: Located<DeployReplicas>) {
813        self.replicas = Some(replicas);
814    }
815
816    pub(super) fn set_resources(&mut self, resources: DeployResources) {
817        self.resources = Some(Box::new(resources));
818    }
819
820    pub(super) fn set_restart_policy(&mut self, restart_policy: DeployRestartPolicy) {
821        self.restart_policy = Some(Box::new(restart_policy));
822    }
823    pub(super) fn set_rollback_config(&mut self, rollback_config: DeployRollbackConfig) {
824        self.rollback_config = Some(Box::new(rollback_config));
825    }
826    pub(super) fn set_update_config(&mut self, update_config: DeployUpdateConfig) {
827        self.update_config = Some(Box::new(update_config));
828    }
829
830    pub(super) fn push_extension(&mut self, field: FieldReference) {
831        self.extension_fields.push(field);
832    }
833
834    pub(super) fn push_unknown(&mut self, field: FieldReference) {
835        self.unknown_fields.push(field);
836    }
837
838    /// Returns the complete deploy mapping span.
839    #[must_use]
840    pub const fn span(&self) -> SourceSpan {
841        self.span
842    }
843
844    /// Returns the authored service-discovery endpoint mode.
845    #[must_use]
846    pub const fn endpoint_mode(&self) -> Option<&Located<DeployEndpointMode>> {
847        self.endpoint_mode.as_ref()
848    }
849
850    /// Returns authored deployment labels without conflating them with service container labels.
851    #[must_use]
852    pub fn labels(&self) -> Option<&Labels> {
853        self.labels.as_deref()
854    }
855
856    /// Returns the authored deployment mode.
857    #[must_use]
858    pub const fn mode(&self) -> Option<&Located<DeployMode>> {
859        self.mode.as_ref()
860    }
861
862    /// Returns authored deploy placement without scheduling interpretation.
863    #[must_use]
864    pub fn placement(&self) -> Option<&DeployPlacement> {
865        self.placement.as_deref()
866    }
867
868    /// Returns the authored replica-count spelling and YAML scalar category.
869    #[must_use]
870    pub const fn replicas(&self) -> Option<&Located<DeployReplicas>> {
871        self.replicas.as_ref()
872    }
873
874    /// Returns authored deploy resources without resource-policy interpretation.
875    #[must_use]
876    pub fn resources(&self) -> Option<&DeployResources> {
877        self.resources.as_deref()
878    }
879
880    /// Returns the authored deploy restart policy without using service `restart` semantics.
881    #[must_use]
882    pub fn restart_policy(&self) -> Option<&DeployRestartPolicy> {
883        self.restart_policy.as_deref()
884    }
885    /// Returns the authored rollback configuration without rollout interpretation.
886    #[must_use]
887    pub fn rollback_config(&self) -> Option<&DeployRollbackConfig> {
888        self.rollback_config.as_deref()
889    }
890    /// Returns the authored rolling-update configuration without rollout interpretation.
891    #[must_use]
892    pub fn update_config(&self) -> Option<&DeployUpdateConfig> {
893        self.update_config.as_deref()
894    }
895
896    /// Returns recognized deploy fields in authored order.
897    #[must_use]
898    pub fn fields(&self) -> &[DeployField] {
899        &self.fields
900    }
901
902    /// Finds the first recognized field of the requested kind.
903    #[must_use]
904    pub fn field(&self, kind: DeployFieldKind) -> Option<&DeployField> {
905        self.fields.iter().find(|field| field.kind == kind)
906    }
907
908    /// Returns retained `x-` fields.
909    #[must_use]
910    pub fn extension_fields(&self) -> &[FieldReference] {
911        &self.extension_fields
912    }
913
914    /// Returns fields not recognized by this release.
915    #[must_use]
916    pub fn unknown_fields(&self) -> &[FieldReference] {
917        &self.unknown_fields
918    }
919}
920
921/// A deploy endpoint mode with unknown provider values retained verbatim.
922#[derive(Debug, Clone, PartialEq, Eq)]
923#[non_exhaustive]
924pub enum DeployEndpointMode {
925    /// Use the virtual-IP service-discovery mode.
926    Vip,
927    /// Use DNS round-robin service discovery.
928    Dnsrr,
929    /// A value outside the documented Compose endpoint modes.
930    Other(String),
931}
932
933impl DeployEndpointMode {
934    pub(crate) fn parse(value: String) -> Self {
935        match value.as_str() {
936            "vip" => Self::Vip,
937            "dnsrr" => Self::Dnsrr,
938            _ => Self::Other(value),
939        }
940    }
941
942    /// Returns whether the mode is one of Compose's documented endpoint modes.
943    #[must_use]
944    pub const fn is_documented(&self) -> bool {
945        matches!(self, Self::Vip | Self::Dnsrr)
946    }
947}
948
949/// A deploy mode with unknown provider values retained verbatim.
950#[derive(Debug, Clone, PartialEq, Eq)]
951#[non_exhaustive]
952pub enum DeployMode {
953    /// Run one task on every eligible node.
954    Global,
955    /// Run a caller-specified replica count.
956    Replicated,
957    /// A value outside the documented Compose deploy modes.
958    Other(String),
959}
960
961impl DeployMode {
962    pub(crate) fn parse(value: String) -> Self {
963        match value.as_str() {
964            "global" => Self::Global,
965            "replicated" => Self::Replicated,
966            _ => Self::Other(value),
967        }
968    }
969
970    /// Returns whether the mode is one of Compose's documented deployment modes.
971    #[must_use]
972    pub const fn is_documented(&self) -> bool {
973        matches!(self, Self::Global | Self::Replicated)
974    }
975}
976
977/// A raw deploy replica-count scalar with its YAML category preserved.
978#[derive(Debug, Clone, PartialEq, Eq)]
979#[non_exhaustive]
980pub enum DeployReplicas {
981    /// A YAML numeric scalar, retained without integer validation or normalization.
982    YamlNumber(String),
983    /// A YAML string scalar, including empty and deferred expressions.
984    String(String),
985}
986
987/// Authored deploy resources with source-aware child fields.
988#[derive(Debug, Clone, PartialEq, Eq)]
989pub struct DeployResources {
990    span: SourceSpan,
991    limits: Option<Box<DeployResourceLimits>>,
992    reservations: Option<Box<DeployResourceReservations>>,
993    extension_fields: Vec<FieldReference>,
994    unknown_fields: Vec<FieldReference>,
995}
996
997impl DeployResources {
998    pub(super) const fn new(span: SourceSpan) -> Self {
999        Self {
1000            span,
1001            limits: None,
1002            reservations: None,
1003            extension_fields: Vec::new(),
1004            unknown_fields: Vec::new(),
1005        }
1006    }
1007
1008    pub(super) fn set_limits(&mut self, limits: DeployResourceLimits) {
1009        self.limits = Some(Box::new(limits));
1010    }
1011
1012    pub(super) fn set_reservations(&mut self, reservations: DeployResourceReservations) {
1013        self.reservations = Some(Box::new(reservations));
1014    }
1015
1016    pub(super) fn push_extension(&mut self, value: FieldReference) {
1017        self.extension_fields.push(value);
1018    }
1019
1020    pub(super) fn push_unknown(&mut self, value: FieldReference) {
1021        self.unknown_fields.push(value);
1022    }
1023
1024    /// Returns the complete resources mapping span.
1025    #[must_use]
1026    pub const fn span(&self) -> SourceSpan {
1027        self.span
1028    }
1029
1030    /// Returns authored resource limits without default or runtime interpretation.
1031    #[must_use]
1032    pub fn limits(&self) -> Option<&DeployResourceLimits> {
1033        self.limits.as_deref()
1034    }
1035
1036    /// Returns authored resource reservations without scheduling interpretation.
1037    #[must_use]
1038    pub fn reservations(&self) -> Option<&DeployResourceReservations> {
1039        self.reservations.as_deref()
1040    }
1041
1042    /// Returns retained resources extensions.
1043    #[must_use]
1044    pub fn extension_fields(&self) -> &[FieldReference] {
1045        &self.extension_fields
1046    }
1047
1048    /// Returns retained unknown resources fields.
1049    #[must_use]
1050    pub fn unknown_fields(&self) -> &[FieldReference] {
1051        &self.unknown_fields
1052    }
1053}
1054
1055/// Authored deploy resource reservations with source-aware child fields.
1056#[derive(Debug, Clone, PartialEq, Eq)]
1057pub struct DeployResourceReservations {
1058    span: SourceSpan,
1059    cpus: Option<Located<DeployResourceCpus>>,
1060    memory: Option<Located<DeployResourceMemory>>,
1061    generic_resources: Option<DeployGenericResources>,
1062    devices: Option<DeployReservationDevices>,
1063    extension_fields: Vec<FieldReference>,
1064    unknown_fields: Vec<FieldReference>,
1065}
1066
1067impl DeployResourceReservations {
1068    pub(super) const fn new(span: SourceSpan) -> Self {
1069        Self {
1070            span,
1071            cpus: None,
1072            memory: None,
1073            generic_resources: None,
1074            devices: None,
1075            extension_fields: Vec::new(),
1076            unknown_fields: Vec::new(),
1077        }
1078    }
1079
1080    pub(super) fn set_cpus(&mut self, cpus: Located<DeployResourceCpus>) {
1081        self.cpus = Some(cpus);
1082    }
1083
1084    pub(super) fn set_memory(&mut self, memory: Located<DeployResourceMemory>) {
1085        self.memory = Some(memory);
1086    }
1087
1088    pub(super) fn set_generic_resources(&mut self, generic_resources: DeployGenericResources) {
1089        self.generic_resources = Some(generic_resources);
1090    }
1091
1092    pub(super) fn set_devices(&mut self, devices: DeployReservationDevices) {
1093        self.devices = Some(devices);
1094    }
1095
1096    pub(super) fn push_extension(&mut self, value: FieldReference) {
1097        self.extension_fields.push(value);
1098    }
1099
1100    pub(super) fn push_unknown(&mut self, value: FieldReference) {
1101        self.unknown_fields.push(value);
1102    }
1103
1104    /// Returns the complete resource-reservations mapping span.
1105    #[must_use]
1106    pub const fn span(&self) -> SourceSpan {
1107        self.span
1108    }
1109
1110    /// Returns the authored deploy resource reservation CPU scalar spelling and category.
1111    #[must_use]
1112    pub const fn cpus(&self) -> Option<&Located<DeployResourceCpus>> {
1113        self.cpus.as_ref()
1114    }
1115
1116    /// Returns the authored deploy resource reservation memory value and its source location.
1117    #[must_use]
1118    pub const fn memory(&self) -> Option<&Located<DeployResourceMemory>> {
1119        self.memory.as_ref()
1120    }
1121
1122    /// Returns authored ordered generic-resource reservations, including an explicit empty list.
1123    #[must_use]
1124    pub const fn generic_resources(&self) -> Option<&DeployGenericResources> {
1125        self.generic_resources.as_ref()
1126    }
1127
1128    /// Returns authored reservation devices, including an explicit empty list.
1129    #[must_use]
1130    pub const fn devices(&self) -> Option<&DeployReservationDevices> {
1131        self.devices.as_ref()
1132    }
1133
1134    /// Returns retained resource-reservation extensions.
1135    #[must_use]
1136    pub fn extension_fields(&self) -> &[FieldReference] {
1137        &self.extension_fields
1138    }
1139
1140    /// Returns retained unknown resource-reservation fields.
1141    #[must_use]
1142    pub fn unknown_fields(&self) -> &[FieldReference] {
1143        &self.unknown_fields
1144    }
1145}
1146
1147/// Ordered schema-backed deploy resource-reservation devices.
1148#[derive(Debug, Clone, PartialEq, Eq)]
1149pub struct DeployReservationDevices {
1150    span: SourceSpan,
1151    items: Vec<DeployReservationDevice>,
1152}
1153
1154impl DeployReservationDevices {
1155    pub(super) const fn new(span: SourceSpan, items: Vec<DeployReservationDevice>) -> Self {
1156        Self { span, items }
1157    }
1158
1159    /// Returns the complete devices sequence span.
1160    #[must_use]
1161    pub const fn span(&self) -> SourceSpan {
1162        self.span
1163    }
1164
1165    /// Returns items in authored order, including duplicate and partially recovered entries.
1166    #[must_use]
1167    pub fn items(&self) -> &[DeployReservationDevice] {
1168        &self.items
1169    }
1170}
1171
1172/// One schema-backed deploy resource-reservation device item.
1173#[derive(Debug, Clone, PartialEq, Eq)]
1174pub struct DeployReservationDevice {
1175    span: SourceSpan,
1176    form: DeployReservationDeviceForm,
1177    capabilities: Option<DeployReservationDeviceCapabilities>,
1178    driver: Option<Located<String>>,
1179    count: Option<Located<DeployReservationDeviceCount>>,
1180    device_ids: Option<DeployReservationDeviceIds>,
1181    options: Option<DeployReservationDeviceOptions>,
1182    extension_fields: Vec<FieldReference>,
1183    unknown_fields: Vec<FieldReference>,
1184}
1185
1186impl DeployReservationDevice {
1187    pub(super) fn new(span: SourceSpan) -> Self {
1188        Self {
1189            span,
1190            form: DeployReservationDeviceForm::Mapping,
1191            capabilities: None,
1192            driver: None,
1193            count: None,
1194            device_ids: None,
1195            options: None,
1196            extension_fields: Vec::new(),
1197            unknown_fields: Vec::new(),
1198        }
1199    }
1200
1201    pub(super) fn unmodeled(span: SourceSpan) -> Self {
1202        Self {
1203            span,
1204            form: DeployReservationDeviceForm::Unmodeled,
1205            capabilities: None,
1206            driver: None,
1207            count: None,
1208            device_ids: None,
1209            options: None,
1210            extension_fields: Vec::new(),
1211            unknown_fields: Vec::new(),
1212        }
1213    }
1214
1215    pub(super) fn set_capabilities(&mut self, capabilities: DeployReservationDeviceCapabilities) {
1216        self.capabilities = Some(capabilities);
1217    }
1218
1219    pub(super) fn set_driver(&mut self, driver: Located<String>) {
1220        self.driver = Some(driver);
1221    }
1222
1223    pub(super) fn set_count(&mut self, count: Located<DeployReservationDeviceCount>) {
1224        self.count = Some(count);
1225    }
1226
1227    pub(super) fn set_device_ids(&mut self, device_ids: DeployReservationDeviceIds) {
1228        self.device_ids = Some(device_ids);
1229    }
1230
1231    pub(super) fn set_options(&mut self, options: DeployReservationDeviceOptions) {
1232        self.options = Some(options);
1233    }
1234
1235    pub(super) fn push_extension(&mut self, value: FieldReference) {
1236        self.extension_fields.push(value);
1237    }
1238
1239    pub(super) fn push_unknown(&mut self, value: FieldReference) {
1240        self.unknown_fields.push(value);
1241    }
1242
1243    /// Returns the complete item span.
1244    #[must_use]
1245    pub const fn span(&self) -> SourceSpan {
1246        self.span
1247    }
1248
1249    /// Returns whether the item was a mapping or an unmodeled sequence entry.
1250    #[must_use]
1251    pub const fn form(&self) -> DeployReservationDeviceForm {
1252        self.form
1253    }
1254
1255    /// Returns the required capabilities list when its form was valid.
1256    #[must_use]
1257    pub const fn capabilities(&self) -> Option<&DeployReservationDeviceCapabilities> {
1258        self.capabilities.as_ref()
1259    }
1260
1261    /// Returns the optional raw device driver when its form was valid.
1262    #[must_use]
1263    pub const fn driver(&self) -> Option<&Located<String>> {
1264        self.driver.as_ref()
1265    }
1266
1267    /// Returns the optional raw device allocation count when its scalar form was valid.
1268    #[must_use]
1269    pub const fn count(&self) -> Option<&Located<DeployReservationDeviceCount>> {
1270        self.count.as_ref()
1271    }
1272
1273    /// Returns optional ordered device allocation IDs, including an explicit empty list.
1274    #[must_use]
1275    pub const fn device_ids(&self) -> Option<&DeployReservationDeviceIds> {
1276        self.device_ids.as_ref()
1277    }
1278
1279    /// Returns optional raw device options without provider-specific interpretation.
1280    #[must_use]
1281    pub const fn options(&self) -> Option<&DeployReservationDeviceOptions> {
1282        self.options.as_ref()
1283    }
1284
1285    /// Returns retained extensions.
1286    #[must_use]
1287    pub fn extension_fields(&self) -> &[FieldReference] {
1288        &self.extension_fields
1289    }
1290
1291    /// Returns retained unknown or malformed fields.
1292    #[must_use]
1293    pub fn unknown_fields(&self) -> &[FieldReference] {
1294        &self.unknown_fields
1295    }
1296}
1297
1298/// Authored resource-reservation device item shape retained without coercion.
1299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1300#[non_exhaustive]
1301pub enum DeployReservationDeviceForm {
1302    /// A mapping-form item.
1303    Mapping,
1304    /// A non-mapping sequence item retained as evidence.
1305    Unmodeled,
1306}
1307
1308/// Raw resource-reservation device allocation-count scalar spelling.
1309#[derive(Debug, Clone, PartialEq, Eq)]
1310#[non_exhaustive]
1311pub enum DeployReservationDeviceCount {
1312    /// An exact YAML integer scalar spelling.
1313    YamlInteger(String),
1314    /// An exact YAML string scalar spelling.
1315    String(String),
1316}
1317
1318/// Ordered raw resource-reservation device allocation IDs.
1319#[derive(Debug, Clone, PartialEq, Eq)]
1320pub struct DeployReservationDeviceIds {
1321    span: SourceSpan,
1322    items: Vec<DeployReservationDeviceId>,
1323}
1324
1325impl DeployReservationDeviceIds {
1326    pub(super) const fn new(span: SourceSpan, items: Vec<DeployReservationDeviceId>) -> Self {
1327        Self { span, items }
1328    }
1329
1330    /// Returns the complete device-IDs sequence span.
1331    #[must_use]
1332    pub const fn span(&self) -> SourceSpan {
1333        self.span
1334    }
1335
1336    /// Returns IDs in authored order, including duplicates and unmodeled entries.
1337    #[must_use]
1338    pub fn items(&self) -> &[DeployReservationDeviceId] {
1339        &self.items
1340    }
1341}
1342
1343/// One resource-reservation device allocation ID retained without interpretation.
1344#[derive(Debug, Clone, PartialEq, Eq)]
1345pub struct DeployReservationDeviceId {
1346    span: SourceSpan,
1347    form: DeployReservationDeviceIdForm,
1348    value: Option<Located<String>>,
1349}
1350
1351impl DeployReservationDeviceId {
1352    pub(super) fn string(value: Located<String>) -> Self {
1353        Self {
1354            span: value.span(),
1355            form: DeployReservationDeviceIdForm::String,
1356            value: Some(value),
1357        }
1358    }
1359
1360    pub(super) const fn unmodeled(span: SourceSpan) -> Self {
1361        Self {
1362            span,
1363            form: DeployReservationDeviceIdForm::Unmodeled,
1364            value: None,
1365        }
1366    }
1367
1368    /// Returns the complete item span.
1369    #[must_use]
1370    pub const fn span(&self) -> SourceSpan {
1371        self.span
1372    }
1373
1374    /// Returns whether the item was a YAML string or retained unmodeled value.
1375    #[must_use]
1376    pub const fn form(&self) -> DeployReservationDeviceIdForm {
1377        self.form
1378    }
1379
1380    /// Returns the exact YAML string when the item had string form.
1381    #[must_use]
1382    pub const fn value(&self) -> Option<&Located<String>> {
1383        self.value.as_ref()
1384    }
1385}
1386
1387/// Resource-reservation device allocation-ID item shape retained without coercion.
1388#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1389#[non_exhaustive]
1390pub enum DeployReservationDeviceIdForm {
1391    /// A YAML string item.
1392    String,
1393    /// A non-string sequence item retained as evidence.
1394    Unmodeled,
1395}
1396
1397/// Schema-shaped resource-reservation device options retaining map or list syntax.
1398#[derive(Debug, Clone, PartialEq, Eq)]
1399#[non_exhaustive]
1400pub enum DeployReservationDeviceOptions {
1401    /// Ordered mapping entries plus malformed or duplicate fields retained as evidence.
1402    Map {
1403        /// Complete mapping span.
1404        span: SourceSpan,
1405        /// Valid non-empty strict-string keyed scalar entries.
1406        entries: Vec<KeyValueEntry>,
1407        /// Malformed or duplicate mapping fields.
1408        unmodeled_entries: Vec<FieldReference>,
1409    },
1410    /// Ordered list entries, including malformed entries.
1411    List {
1412        /// Complete sequence span.
1413        span: SourceSpan,
1414        /// Items in authored order.
1415        items: Vec<DeployReservationDeviceOptionItem>,
1416    },
1417}
1418
1419impl DeployReservationDeviceOptions {
1420    /// Returns the complete authored collection span.
1421    #[must_use]
1422    pub const fn span(&self) -> SourceSpan {
1423        match self {
1424            Self::Map { span, .. } | Self::List { span, .. } => *span,
1425        }
1426    }
1427
1428    /// Returns valid map entries when the authored form was a mapping.
1429    #[must_use]
1430    pub fn as_map(&self) -> Option<&[KeyValueEntry]> {
1431        let Self::Map { entries, .. } = self else { return None };
1432        Some(entries)
1433    }
1434
1435    /// Returns malformed or duplicate map fields when the authored form was a mapping.
1436    #[must_use]
1437    pub fn unmodeled_entries(&self) -> Option<&[FieldReference]> {
1438        let Self::Map { unmodeled_entries, .. } = self else {
1439            return None;
1440        };
1441        Some(unmodeled_entries)
1442    }
1443
1444    /// Returns ordered list entries when the authored form was a sequence.
1445    #[must_use]
1446    pub fn as_list(&self) -> Option<&[DeployReservationDeviceOptionItem]> {
1447        let Self::List { items, .. } = self else { return None };
1448        Some(items)
1449    }
1450}
1451
1452/// One device-options list item retained without splitting or coercion.
1453#[derive(Debug, Clone, PartialEq, Eq)]
1454pub struct DeployReservationDeviceOptionItem {
1455    span: SourceSpan,
1456    form: DeployReservationDeviceOptionItemForm,
1457    value: Option<Located<String>>,
1458}
1459
1460impl DeployReservationDeviceOptionItem {
1461    pub(super) fn string(value: Located<String>) -> Self {
1462        Self {
1463            span: value.span(),
1464            form: DeployReservationDeviceOptionItemForm::String,
1465            value: Some(value),
1466        }
1467    }
1468    pub(super) const fn unmodeled(span: SourceSpan) -> Self {
1469        Self {
1470            span,
1471            form: DeployReservationDeviceOptionItemForm::Unmodeled,
1472            value: None,
1473        }
1474    }
1475    /// Returns the complete item span.
1476    #[must_use]
1477    pub const fn span(&self) -> SourceSpan {
1478        self.span
1479    }
1480    /// Returns whether the item was a strict YAML string or retained unmodeled value.
1481    #[must_use]
1482    pub const fn form(&self) -> DeployReservationDeviceOptionItemForm {
1483        self.form
1484    }
1485    /// Returns the exact string when the item had string form.
1486    #[must_use]
1487    pub const fn value(&self) -> Option<&Located<String>> {
1488        self.value.as_ref()
1489    }
1490}
1491
1492/// Resource-reservation device-options list item shape.
1493#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1494#[non_exhaustive]
1495pub enum DeployReservationDeviceOptionItemForm {
1496    /// A strict YAML string list item.
1497    String,
1498    /// A non-string list item retained as evidence.
1499    Unmodeled,
1500}
1501
1502/// Ordered required resource-reservation device capabilities.
1503#[derive(Debug, Clone, PartialEq, Eq)]
1504pub struct DeployReservationDeviceCapabilities {
1505    span: SourceSpan,
1506    items: Vec<DeployReservationDeviceCapability>,
1507}
1508
1509impl DeployReservationDeviceCapabilities {
1510    pub(super) const fn new(span: SourceSpan, items: Vec<DeployReservationDeviceCapability>) -> Self {
1511        Self { span, items }
1512    }
1513
1514    /// Returns the complete capabilities sequence span.
1515    #[must_use]
1516    pub const fn span(&self) -> SourceSpan {
1517        self.span
1518    }
1519
1520    /// Returns items in authored order, including duplicates and unmodeled entries.
1521    #[must_use]
1522    pub fn items(&self) -> &[DeployReservationDeviceCapability] {
1523        &self.items
1524    }
1525}
1526
1527/// One resource-reservation device capability retained without name interpretation.
1528#[derive(Debug, Clone, PartialEq, Eq)]
1529pub struct DeployReservationDeviceCapability {
1530    span: SourceSpan,
1531    form: DeployReservationDeviceCapabilityForm,
1532    value: Option<Located<String>>,
1533}
1534
1535impl DeployReservationDeviceCapability {
1536    pub(super) fn string(value: Located<String>) -> Self {
1537        Self {
1538            span: value.span(),
1539            form: DeployReservationDeviceCapabilityForm::String,
1540            value: Some(value),
1541        }
1542    }
1543
1544    pub(super) const fn unmodeled(span: SourceSpan) -> Self {
1545        Self {
1546            span,
1547            form: DeployReservationDeviceCapabilityForm::Unmodeled,
1548            value: None,
1549        }
1550    }
1551
1552    /// Returns the complete item span.
1553    #[must_use]
1554    pub const fn span(&self) -> SourceSpan {
1555        self.span
1556    }
1557
1558    /// Returns whether the item was a YAML string or retained unmodeled value.
1559    #[must_use]
1560    pub const fn form(&self) -> DeployReservationDeviceCapabilityForm {
1561        self.form
1562    }
1563
1564    /// Returns the exact YAML string when the item had string form.
1565    #[must_use]
1566    pub const fn value(&self) -> Option<&Located<String>> {
1567        self.value.as_ref()
1568    }
1569}
1570
1571/// Resource-reservation device capability item shape retained without coercion.
1572#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1573#[non_exhaustive]
1574pub enum DeployReservationDeviceCapabilityForm {
1575    /// A YAML string item.
1576    String,
1577    /// A non-string sequence item retained as evidence.
1578    Unmodeled,
1579}
1580
1581/// Ordered schema-backed deploy generic-resource reservations.
1582#[derive(Debug, Clone, PartialEq, Eq)]
1583pub struct DeployGenericResources {
1584    span: SourceSpan,
1585    items: Vec<DeployGenericResource>,
1586}
1587
1588impl DeployGenericResources {
1589    pub(super) const fn new(span: SourceSpan, items: Vec<DeployGenericResource>) -> Self {
1590        Self { span, items }
1591    }
1592
1593    /// Returns the complete generic-resources sequence span.
1594    #[must_use]
1595    pub const fn span(&self) -> SourceSpan {
1596        self.span
1597    }
1598
1599    /// Returns items in authored order, including duplicates and partially recovered entries.
1600    #[must_use]
1601    pub fn items(&self) -> &[DeployGenericResource] {
1602        &self.items
1603    }
1604}
1605
1606/// One schema-backed generic-resource reservation item.
1607#[derive(Debug, Clone, PartialEq, Eq)]
1608pub struct DeployGenericResource {
1609    span: SourceSpan,
1610    form: DeployGenericResourceForm,
1611    discrete_resource_spec: Option<DeployDiscreteResourceSpec>,
1612    extension_fields: Vec<FieldReference>,
1613    unknown_fields: Vec<FieldReference>,
1614}
1615
1616impl DeployGenericResource {
1617    pub(super) fn new(span: SourceSpan) -> Self {
1618        Self {
1619            span,
1620            form: DeployGenericResourceForm::Mapping,
1621            discrete_resource_spec: None,
1622            extension_fields: Vec::new(),
1623            unknown_fields: Vec::new(),
1624        }
1625    }
1626    pub(super) fn unmodeled(span: SourceSpan) -> Self {
1627        Self {
1628            span,
1629            form: DeployGenericResourceForm::Unmodeled,
1630            discrete_resource_spec: None,
1631            extension_fields: Vec::new(),
1632            unknown_fields: Vec::new(),
1633        }
1634    }
1635    pub(super) fn set_discrete_resource_spec(&mut self, value: DeployDiscreteResourceSpec) {
1636        self.discrete_resource_spec = Some(value);
1637    }
1638    pub(super) fn push_extension(&mut self, value: FieldReference) {
1639        self.extension_fields.push(value);
1640    }
1641    pub(super) fn push_unknown(&mut self, value: FieldReference) {
1642        self.unknown_fields.push(value);
1643    }
1644    /// Returns this item mapping span.
1645    #[must_use]
1646    pub const fn span(&self) -> SourceSpan {
1647        self.span
1648    }
1649    /// Returns whether the item was a mapping or an unmodeled sequence entry.
1650    #[must_use]
1651    pub const fn form(&self) -> DeployGenericResourceForm {
1652        self.form
1653    }
1654    /// Returns the optional schema-backed discrete resource specification.
1655    #[must_use]
1656    pub const fn discrete_resource_spec(&self) -> Option<&DeployDiscreteResourceSpec> {
1657        self.discrete_resource_spec.as_ref()
1658    }
1659    /// Returns retained extensions.
1660    #[must_use]
1661    pub fn extension_fields(&self) -> &[FieldReference] {
1662        &self.extension_fields
1663    }
1664    /// Returns retained unknown or malformed fields.
1665    #[must_use]
1666    pub fn unknown_fields(&self) -> &[FieldReference] {
1667        &self.unknown_fields
1668    }
1669}
1670
1671/// Authored generic-resource item shape retained without coercion.
1672#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1673#[non_exhaustive]
1674pub enum DeployGenericResourceForm {
1675    /// A mapping-form item.
1676    Mapping,
1677    /// A non-mapping sequence item retained as evidence.
1678    Unmodeled,
1679}
1680
1681/// Schema-backed discrete generic-resource specification.
1682#[derive(Debug, Clone, PartialEq, Eq)]
1683pub struct DeployDiscreteResourceSpec {
1684    span: SourceSpan,
1685    kind: Option<Located<String>>,
1686    value: Option<Located<DeployDiscreteResourceValue>>,
1687    extension_fields: Vec<FieldReference>,
1688    unknown_fields: Vec<FieldReference>,
1689}
1690
1691impl DeployDiscreteResourceSpec {
1692    pub(super) fn new(span: SourceSpan) -> Self {
1693        Self {
1694            span,
1695            kind: None,
1696            value: None,
1697            extension_fields: Vec::new(),
1698            unknown_fields: Vec::new(),
1699        }
1700    }
1701    pub(super) fn set_kind(&mut self, value: Located<String>) {
1702        self.kind = Some(value);
1703    }
1704    pub(super) fn set_value(&mut self, value: Located<DeployDiscreteResourceValue>) {
1705        self.value = Some(value);
1706    }
1707    pub(super) fn push_extension(&mut self, value: FieldReference) {
1708        self.extension_fields.push(value);
1709    }
1710    pub(super) fn push_unknown(&mut self, value: FieldReference) {
1711        self.unknown_fields.push(value);
1712    }
1713    /// Returns the complete discrete-resource-spec mapping span.
1714    #[must_use]
1715    pub const fn span(&self) -> SourceSpan {
1716        self.span
1717    }
1718    /// Returns the optional raw kind scalar.
1719    #[must_use]
1720    pub const fn kind(&self) -> Option<&Located<String>> {
1721        self.kind.as_ref()
1722    }
1723    /// Returns the optional raw value scalar category.
1724    #[must_use]
1725    pub const fn value(&self) -> Option<&Located<DeployDiscreteResourceValue>> {
1726        self.value.as_ref()
1727    }
1728    /// Returns retained extensions.
1729    #[must_use]
1730    pub fn extension_fields(&self) -> &[FieldReference] {
1731        &self.extension_fields
1732    }
1733    /// Returns retained unknown or malformed members.
1734    #[must_use]
1735    pub fn unknown_fields(&self) -> &[FieldReference] {
1736        &self.unknown_fields
1737    }
1738}
1739
1740/// Raw scalar category for a discrete generic-resource value.
1741#[derive(Debug, Clone, PartialEq, Eq)]
1742#[non_exhaustive]
1743pub enum DeployDiscreteResourceValue {
1744    /// A YAML numeric scalar retained without numeric interpretation.
1745    YamlNumber(String),
1746    /// A YAML string scalar retained without schema-specific interpretation.
1747    String(String),
1748}
1749
1750/// Authored deploy resource limits with source-aware child fields.
1751#[derive(Debug, Clone, PartialEq, Eq)]
1752pub struct DeployResourceLimits {
1753    span: SourceSpan,
1754    cpus: Option<Located<DeployResourceCpus>>,
1755    memory: Option<Located<DeployResourceMemory>>,
1756    pids: Option<Located<DeployResourcePids>>,
1757    extension_fields: Vec<FieldReference>,
1758    unknown_fields: Vec<FieldReference>,
1759}
1760
1761impl DeployResourceLimits {
1762    pub(super) const fn new(span: SourceSpan) -> Self {
1763        Self {
1764            span,
1765            cpus: None,
1766            memory: None,
1767            pids: None,
1768            extension_fields: Vec::new(),
1769            unknown_fields: Vec::new(),
1770        }
1771    }
1772
1773    pub(super) fn set_pids(&mut self, pids: Located<DeployResourcePids>) {
1774        self.pids = Some(pids);
1775    }
1776
1777    pub(super) fn set_cpus(&mut self, cpus: Located<DeployResourceCpus>) {
1778        self.cpus = Some(cpus);
1779    }
1780
1781    pub(super) fn set_memory(&mut self, memory: Located<DeployResourceMemory>) {
1782        self.memory = Some(memory);
1783    }
1784
1785    pub(super) fn push_extension(&mut self, value: FieldReference) {
1786        self.extension_fields.push(value);
1787    }
1788
1789    pub(super) fn push_unknown(&mut self, value: FieldReference) {
1790        self.unknown_fields.push(value);
1791    }
1792
1793    /// Returns the complete resource-limits mapping span.
1794    #[must_use]
1795    pub const fn span(&self) -> SourceSpan {
1796        self.span
1797    }
1798
1799    /// Returns the authored deploy resource PID scalar spelling and category.
1800    #[must_use]
1801    pub const fn pids(&self) -> Option<&Located<DeployResourcePids>> {
1802        self.pids.as_ref()
1803    }
1804
1805    /// Returns the authored deploy resource CPU scalar spelling and category.
1806    #[must_use]
1807    pub const fn cpus(&self) -> Option<&Located<DeployResourceCpus>> {
1808        self.cpus.as_ref()
1809    }
1810
1811    /// Returns the authored deploy resource memory value and its source location.
1812    #[must_use]
1813    pub const fn memory(&self) -> Option<&Located<DeployResourceMemory>> {
1814        self.memory.as_ref()
1815    }
1816
1817    /// Returns retained resource-limit extensions.
1818    #[must_use]
1819    pub fn extension_fields(&self) -> &[FieldReference] {
1820        &self.extension_fields
1821    }
1822
1823    /// Returns retained unknown resource-limit fields.
1824    #[must_use]
1825    pub fn unknown_fields(&self) -> &[FieldReference] {
1826        &self.unknown_fields
1827    }
1828}
1829
1830/// Raw deploy resource PID scalar category and spelling.
1831#[derive(Debug, Clone, PartialEq, Eq)]
1832#[non_exhaustive]
1833pub enum DeployResourcePids {
1834    /// A YAML integer scalar without range validation.
1835    YamlInteger(String),
1836    /// A YAML string scalar without numeric validation.
1837    String(String),
1838}
1839
1840/// Raw deploy resource CPU scalar category and spelling.
1841#[derive(Debug, Clone, PartialEq, Eq)]
1842#[non_exhaustive]
1843pub enum DeployResourceCpus {
1844    /// A YAML integer or floating-point scalar without numeric validation or normalization.
1845    YamlNumber(String),
1846    /// A YAML string scalar without numeric validation.
1847    String(String),
1848}
1849
1850/// Raw-preserving deploy resource memory value with deploy-specific classification.
1851#[derive(Debug, Clone, PartialEq, Eq)]
1852pub struct DeployResourceMemory {
1853    raw: String,
1854    kind: DeployResourceMemoryKind,
1855}
1856
1857impl DeployResourceMemory {
1858    pub(crate) fn parse(raw: String) -> Self {
1859        let kind = if raw.contains('$') {
1860            DeployResourceMemoryKind::Expression
1861        } else if let Some((amount_raw, unit)) = split_deploy_resource_memory_unit(&raw) {
1862            if deploy_resource_memory_lexical_zero(amount_raw) {
1863                DeployResourceMemoryKind::Zero {
1864                    amount_raw: amount_raw.to_owned(),
1865                    unit: Some(unit),
1866                }
1867            } else {
1868                DeployResourceMemoryKind::Documented {
1869                    amount_raw: amount_raw.to_owned(),
1870                    unit,
1871                }
1872            }
1873        } else if deploy_resource_memory_lexical_zero(&raw) {
1874            DeployResourceMemoryKind::Zero {
1875                amount_raw: raw.clone(),
1876                unit: None,
1877            }
1878        } else {
1879            DeployResourceMemoryKind::ProviderDependentString
1880        };
1881        Self { raw, kind }
1882    }
1883
1884    /// Returns the exact deploy resource memory scalar text without normalization.
1885    #[must_use]
1886    pub fn raw(&self) -> &str {
1887        &self.raw
1888    }
1889
1890    /// Returns the non-destructive deploy resource memory classification.
1891    #[must_use]
1892    pub const fn kind(&self) -> &DeployResourceMemoryKind {
1893        &self.kind
1894    }
1895}
1896
1897/// Raw-preserving semantic family of a deploy resource memory string.
1898#[derive(Debug, Clone, PartialEq, Eq)]
1899#[non_exhaustive]
1900pub enum DeployResourceMemoryKind {
1901    /// A string ending in one documented lowercase byte suffix.
1902    Documented {
1903        /// Exact text before the suffix; no amount grammar is inferred.
1904        amount_raw: String,
1905        /// Exact documented suffix family.
1906        unit: DeployResourceMemoryUnit,
1907    },
1908    /// An all-zero amount spelling whose runtime meaning is not inferred.
1909    Zero {
1910        /// Exact all-zero amount spelling.
1911        amount_raw: String,
1912        /// Documented suffix when one was present.
1913        unit: Option<DeployResourceMemoryUnit>,
1914    },
1915    /// A dollar-bearing string deferred to Compose interpolation.
1916    Expression,
1917    /// A string outside the documented lowercase-suffix family.
1918    ProviderDependentString,
1919}
1920
1921/// One lowercase byte-unit suffix documented for deploy resource memory.
1922#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1923#[non_exhaustive]
1924pub enum DeployResourceMemoryUnit {
1925    /// Bytes (`b`).
1926    B,
1927    /// Kilobytes (`k`).
1928    K,
1929    /// Kilobytes (`kb`).
1930    Kb,
1931    /// Megabytes (`m`).
1932    M,
1933    /// Megabytes (`mb`).
1934    Mb,
1935    /// Gigabytes (`g`).
1936    G,
1937    /// Gigabytes (`gb`).
1938    Gb,
1939}
1940
1941impl DeployResourceMemoryUnit {
1942    /// Returns the exact lowercase documented suffix.
1943    #[must_use]
1944    pub const fn as_str(self) -> &'static str {
1945        match self {
1946            Self::B => "b",
1947            Self::K => "k",
1948            Self::Kb => "kb",
1949            Self::M => "m",
1950            Self::Mb => "mb",
1951            Self::G => "g",
1952            Self::Gb => "gb",
1953        }
1954    }
1955}
1956
1957fn split_deploy_resource_memory_unit(value: &str) -> Option<(&str, DeployResourceMemoryUnit)> {
1958    for (suffix, unit) in [
1959        ("kb", DeployResourceMemoryUnit::Kb),
1960        ("mb", DeployResourceMemoryUnit::Mb),
1961        ("gb", DeployResourceMemoryUnit::Gb),
1962        ("b", DeployResourceMemoryUnit::B),
1963        ("k", DeployResourceMemoryUnit::K),
1964        ("m", DeployResourceMemoryUnit::M),
1965        ("g", DeployResourceMemoryUnit::G),
1966    ] {
1967        if let Some(amount) = value.strip_suffix(suffix) {
1968            if !amount.is_empty() {
1969                return Some((amount, unit));
1970            }
1971        }
1972    }
1973    None
1974}
1975
1976fn deploy_resource_memory_lexical_zero(value: &str) -> bool {
1977    !value.is_empty() && value.bytes().all(|byte| byte == b'0')
1978}
1979
1980/// A deploy restart-policy mapping with independent raw-preserving members.
1981#[derive(Debug, Clone, PartialEq, Eq)]
1982pub struct DeployRestartPolicy {
1983    span: SourceSpan,
1984    condition: Option<Located<DeployRestartCondition>>,
1985    delay: Option<Located<DeployRestartDuration>>,
1986    max_attempts: Option<Located<DeployRestartMaxAttempts>>,
1987    window: Option<Located<DeployRestartDuration>>,
1988    extension_fields: Vec<FieldReference>,
1989    unknown_fields: Vec<FieldReference>,
1990}
1991
1992impl DeployRestartPolicy {
1993    pub(super) const fn new(span: SourceSpan) -> Self {
1994        Self {
1995            span,
1996            condition: None,
1997            delay: None,
1998            max_attempts: None,
1999            window: None,
2000            extension_fields: Vec::new(),
2001            unknown_fields: Vec::new(),
2002        }
2003    }
2004    pub(super) fn set_condition(&mut self, value: Located<DeployRestartCondition>) {
2005        self.condition = Some(value);
2006    }
2007    pub(super) fn set_delay(&mut self, value: Located<DeployRestartDuration>) {
2008        self.delay = Some(value);
2009    }
2010    pub(super) fn set_max_attempts(&mut self, value: Located<DeployRestartMaxAttempts>) {
2011        self.max_attempts = Some(value);
2012    }
2013    pub(super) fn set_window(&mut self, value: Located<DeployRestartDuration>) {
2014        self.window = Some(value);
2015    }
2016    pub(super) fn push_extension(&mut self, value: FieldReference) {
2017        self.extension_fields.push(value);
2018    }
2019    pub(super) fn push_unknown(&mut self, value: FieldReference) {
2020        self.unknown_fields.push(value);
2021    }
2022    /// Returns the complete restart-policy mapping span.
2023    #[must_use]
2024    pub const fn span(&self) -> SourceSpan {
2025        self.span
2026    }
2027    /// Returns the explicit restart condition.
2028    #[must_use]
2029    pub const fn condition(&self) -> Option<&Located<DeployRestartCondition>> {
2030        self.condition.as_ref()
2031    }
2032    /// Returns the raw delay spelling.
2033    #[must_use]
2034    pub const fn delay(&self) -> Option<&Located<DeployRestartDuration>> {
2035        self.delay.as_ref()
2036    }
2037    /// Returns the raw maximum-attempts YAML scalar.
2038    #[must_use]
2039    pub const fn max_attempts(&self) -> Option<&Located<DeployRestartMaxAttempts>> {
2040        self.max_attempts.as_ref()
2041    }
2042    /// Returns the raw restart window spelling.
2043    #[must_use]
2044    pub const fn window(&self) -> Option<&Located<DeployRestartDuration>> {
2045        self.window.as_ref()
2046    }
2047    /// Returns retained restart-policy extensions.
2048    #[must_use]
2049    pub fn extension_fields(&self) -> &[FieldReference] {
2050        &self.extension_fields
2051    }
2052    /// Returns retained unknown restart-policy fields.
2053    #[must_use]
2054    pub fn unknown_fields(&self) -> &[FieldReference] {
2055        &self.unknown_fields
2056    }
2057}
2058
2059/// A deploy restart condition with unknown and deferred values retained.
2060#[derive(Debug, Clone, PartialEq, Eq)]
2061#[non_exhaustive]
2062pub enum DeployRestartCondition {
2063    /// Do not restart failed tasks.
2064    None,
2065    /// Restart after failure.
2066    OnFailure,
2067    /// Restart regardless of termination result.
2068    Any,
2069    /// A deferred expression.
2070    Expression(String),
2071    /// An unknown retained condition.
2072    Other(String),
2073}
2074impl DeployRestartCondition {
2075    pub(crate) fn parse(value: String) -> Self {
2076        match value.as_str() {
2077            "none" => Self::None,
2078            "on-failure" => Self::OnFailure,
2079            "any" => Self::Any,
2080            _ if value.contains('$') => Self::Expression(value),
2081            _ => Self::Other(value),
2082        }
2083    }
2084}
2085
2086/// A raw deploy restart duration spelling.
2087#[derive(Debug, Clone, PartialEq, Eq)]
2088pub struct DeployRestartDuration(String);
2089impl DeployRestartDuration {
2090    pub(crate) const fn new(value: String) -> Self {
2091        Self(value)
2092    }
2093    /// Returns the exact duration spelling.
2094    #[must_use]
2095    pub fn raw(&self) -> &str {
2096        &self.0
2097    }
2098}
2099
2100/// A raw deploy max-attempts YAML integer/string scalar category and spelling.
2101#[derive(Debug, Clone, PartialEq, Eq)]
2102#[non_exhaustive]
2103pub enum DeployRestartMaxAttempts {
2104    /// A YAML integer scalar without range validation.
2105    YamlNumber(String),
2106    /// A YAML string scalar without numeric validation.
2107    String(String),
2108}
2109
2110/// A deploy rollback configuration with independent raw-preserving members.
2111#[derive(Debug, Clone, PartialEq, Eq)]
2112pub struct DeployRollbackConfig {
2113    span: SourceSpan,
2114    parallelism: Option<Located<DeployRollbackParallelism>>,
2115    delay: Option<Located<String>>,
2116    monitor: Option<Located<String>>,
2117    failure_action: Option<Located<String>>,
2118    max_failure_ratio: Option<Located<DeployRollbackMaxFailureRatio>>,
2119    order: Option<Located<DeployRollbackOrder>>,
2120    extension_fields: Vec<FieldReference>,
2121    unknown_fields: Vec<FieldReference>,
2122}
2123impl DeployRollbackConfig {
2124    pub(super) const fn new(span: SourceSpan) -> Self {
2125        Self {
2126            span,
2127            parallelism: None,
2128            delay: None,
2129            monitor: None,
2130            failure_action: None,
2131            max_failure_ratio: None,
2132            order: None,
2133            extension_fields: Vec::new(),
2134            unknown_fields: Vec::new(),
2135        }
2136    }
2137    pub(super) fn set_parallelism(&mut self, value: Located<DeployRollbackParallelism>) {
2138        self.parallelism = Some(value);
2139    }
2140    pub(super) fn set_delay(&mut self, value: Located<String>) {
2141        self.delay = Some(value);
2142    }
2143    pub(super) fn set_monitor(&mut self, value: Located<String>) {
2144        self.monitor = Some(value);
2145    }
2146    pub(super) fn set_failure_action(&mut self, value: Located<String>) {
2147        self.failure_action = Some(value);
2148    }
2149    pub(super) fn set_max_failure_ratio(&mut self, value: Located<DeployRollbackMaxFailureRatio>) {
2150        self.max_failure_ratio = Some(value);
2151    }
2152    pub(super) fn set_order(&mut self, value: Located<DeployRollbackOrder>) {
2153        self.order = Some(value);
2154    }
2155    pub(super) fn push_extension(&mut self, value: FieldReference) {
2156        self.extension_fields.push(value);
2157    }
2158    pub(super) fn push_unknown(&mut self, value: FieldReference) {
2159        self.unknown_fields.push(value);
2160    }
2161    /// Returns the complete rollback-config mapping span.
2162    #[must_use]
2163    pub const fn span(&self) -> SourceSpan {
2164        self.span
2165    }
2166    /// Returns the raw rollback parallelism scalar.
2167    #[must_use]
2168    pub const fn parallelism(&self) -> Option<&Located<DeployRollbackParallelism>> {
2169        self.parallelism.as_ref()
2170    }
2171    /// Returns the raw rollback delay string.
2172    #[must_use]
2173    pub const fn delay(&self) -> Option<&Located<String>> {
2174        self.delay.as_ref()
2175    }
2176    /// Returns the raw rollback monitor string.
2177    #[must_use]
2178    pub const fn monitor(&self) -> Option<&Located<String>> {
2179        self.monitor.as_ref()
2180    }
2181    /// Returns the raw rollback failure action.
2182    #[must_use]
2183    pub const fn failure_action(&self) -> Option<&Located<String>> {
2184        self.failure_action.as_ref()
2185    }
2186    /// Returns the raw rollback maximum failure ratio.
2187    #[must_use]
2188    pub const fn max_failure_ratio(&self) -> Option<&Located<DeployRollbackMaxFailureRatio>> {
2189        self.max_failure_ratio.as_ref()
2190    }
2191    /// Returns the rollback order.
2192    #[must_use]
2193    pub const fn order(&self) -> Option<&Located<DeployRollbackOrder>> {
2194        self.order.as_ref()
2195    }
2196    /// Returns retained rollback-config extensions.
2197    #[must_use]
2198    pub fn extension_fields(&self) -> &[FieldReference] {
2199        &self.extension_fields
2200    }
2201    /// Returns retained rollback-config malformed or unknown fields.
2202    #[must_use]
2203    pub fn unknown_fields(&self) -> &[FieldReference] {
2204        &self.unknown_fields
2205    }
2206}
2207/// Raw rollback parallelism scalar category and spelling.
2208#[derive(Debug, Clone, PartialEq, Eq)]
2209#[non_exhaustive]
2210pub enum DeployRollbackParallelism {
2211    /// A YAML integer scalar without range validation.
2212    YamlInteger(String),
2213    /// A strict YAML string scalar without numeric validation.
2214    String(String),
2215}
2216/// Raw rollback maximum-failure-ratio scalar category and spelling.
2217#[derive(Debug, Clone, PartialEq, Eq)]
2218#[non_exhaustive]
2219pub enum DeployRollbackMaxFailureRatio {
2220    /// A YAML number scalar without range validation.
2221    YamlNumber(String),
2222    /// A strict YAML string scalar without numeric validation.
2223    String(String),
2224}
2225/// Rollback order with unsupported values retained.
2226#[derive(Debug, Clone, PartialEq, Eq)]
2227#[non_exhaustive]
2228pub enum DeployRollbackOrder {
2229    /// Stop the old task before starting the new task.
2230    StopFirst,
2231    /// Start the new task before stopping the old task.
2232    StartFirst,
2233    /// A retained provider-specific order.
2234    Other(String),
2235}
2236impl DeployRollbackOrder {
2237    pub(crate) fn parse(value: String) -> Self {
2238        match value.as_str() {
2239            "stop-first" => Self::StopFirst,
2240            "start-first" => Self::StartFirst,
2241            _ => Self::Other(value),
2242        }
2243    }
2244    pub(crate) const fn is_documented(&self) -> bool {
2245        matches!(self, Self::StopFirst | Self::StartFirst)
2246    }
2247}
2248
2249/// A deploy rolling-update configuration with independent raw-preserving members.
2250#[derive(Debug, Clone, PartialEq, Eq)]
2251pub struct DeployUpdateConfig {
2252    span: SourceSpan,
2253    parallelism: Option<Located<DeployUpdateParallelism>>,
2254    delay: Option<Located<String>>,
2255    monitor: Option<Located<String>>,
2256    failure_action: Option<Located<String>>,
2257    max_failure_ratio: Option<Located<DeployUpdateMaxFailureRatio>>,
2258    order: Option<Located<DeployUpdateOrder>>,
2259    extension_fields: Vec<FieldReference>,
2260    unknown_fields: Vec<FieldReference>,
2261}
2262impl DeployUpdateConfig {
2263    pub(super) const fn new(span: SourceSpan) -> Self {
2264        Self {
2265            span,
2266            parallelism: None,
2267            delay: None,
2268            monitor: None,
2269            failure_action: None,
2270            max_failure_ratio: None,
2271            order: None,
2272            extension_fields: Vec::new(),
2273            unknown_fields: Vec::new(),
2274        }
2275    }
2276    pub(super) fn set_parallelism(&mut self, value: Located<DeployUpdateParallelism>) {
2277        self.parallelism = Some(value);
2278    }
2279    pub(super) fn set_delay(&mut self, value: Located<String>) {
2280        self.delay = Some(value);
2281    }
2282    pub(super) fn set_monitor(&mut self, value: Located<String>) {
2283        self.monitor = Some(value);
2284    }
2285    pub(super) fn set_failure_action(&mut self, value: Located<String>) {
2286        self.failure_action = Some(value);
2287    }
2288    pub(super) fn set_max_failure_ratio(&mut self, value: Located<DeployUpdateMaxFailureRatio>) {
2289        self.max_failure_ratio = Some(value);
2290    }
2291    pub(super) fn set_order(&mut self, value: Located<DeployUpdateOrder>) {
2292        self.order = Some(value);
2293    }
2294    pub(super) fn push_extension(&mut self, value: FieldReference) {
2295        self.extension_fields.push(value);
2296    }
2297    pub(super) fn push_unknown(&mut self, value: FieldReference) {
2298        self.unknown_fields.push(value);
2299    }
2300    /// Returns the complete update-config mapping span.
2301    #[must_use]
2302    pub const fn span(&self) -> SourceSpan {
2303        self.span
2304    }
2305    /// Returns the raw update parallelism scalar.
2306    #[must_use]
2307    pub const fn parallelism(&self) -> Option<&Located<DeployUpdateParallelism>> {
2308        self.parallelism.as_ref()
2309    }
2310    /// Returns the raw update delay string.
2311    #[must_use]
2312    pub const fn delay(&self) -> Option<&Located<String>> {
2313        self.delay.as_ref()
2314    }
2315    /// Returns the raw update monitor string.
2316    #[must_use]
2317    pub const fn monitor(&self) -> Option<&Located<String>> {
2318        self.monitor.as_ref()
2319    }
2320    /// Returns the raw update failure action.
2321    #[must_use]
2322    pub const fn failure_action(&self) -> Option<&Located<String>> {
2323        self.failure_action.as_ref()
2324    }
2325    /// Returns the raw maximum failure ratio.
2326    #[must_use]
2327    pub const fn max_failure_ratio(&self) -> Option<&Located<DeployUpdateMaxFailureRatio>> {
2328        self.max_failure_ratio.as_ref()
2329    }
2330    /// Returns the update order.
2331    #[must_use]
2332    pub const fn order(&self) -> Option<&Located<DeployUpdateOrder>> {
2333        self.order.as_ref()
2334    }
2335    /// Returns retained update-config extensions.
2336    #[must_use]
2337    pub fn extension_fields(&self) -> &[FieldReference] {
2338        &self.extension_fields
2339    }
2340    /// Returns retained update-config malformed or unknown fields.
2341    #[must_use]
2342    pub fn unknown_fields(&self) -> &[FieldReference] {
2343        &self.unknown_fields
2344    }
2345}
2346/// Raw update parallelism scalar category and spelling.
2347#[derive(Debug, Clone, PartialEq, Eq)]
2348#[non_exhaustive]
2349pub enum DeployUpdateParallelism {
2350    /// A YAML integer scalar without range validation.
2351    YamlInteger(String),
2352    /// A strict YAML string scalar without numeric validation.
2353    String(String),
2354}
2355/// Raw update maximum-failure-ratio scalar category and spelling.
2356#[derive(Debug, Clone, PartialEq, Eq)]
2357#[non_exhaustive]
2358pub enum DeployUpdateMaxFailureRatio {
2359    /// A YAML number scalar without range validation.
2360    YamlNumber(String),
2361    /// A strict YAML string scalar without numeric validation.
2362    String(String),
2363}
2364/// Update order with unsupported values retained.
2365#[derive(Debug, Clone, PartialEq, Eq)]
2366#[non_exhaustive]
2367pub enum DeployUpdateOrder {
2368    /// Stop the old task before starting the new task.
2369    StopFirst,
2370    /// Start the new task before stopping the old task.
2371    StartFirst,
2372    /// A retained provider-specific order.
2373    Other(String),
2374}
2375impl DeployUpdateOrder {
2376    pub(crate) fn parse(value: String) -> Self {
2377        match value.as_str() {
2378            "stop-first" => Self::StopFirst,
2379            "start-first" => Self::StartFirst,
2380            _ => Self::Other(value),
2381        }
2382    }
2383    pub(crate) const fn is_documented(&self) -> bool {
2384        matches!(self, Self::StopFirst | Self::StartFirst)
2385    }
2386}
2387
2388/// Authored deploy placement with source-aware child fields.
2389#[derive(Debug, Clone, PartialEq, Eq)]
2390pub struct DeployPlacement {
2391    span: SourceSpan,
2392    constraints: Option<Vec<Located<String>>>,
2393    preferences: Option<Vec<DeployPlacementPreference>>,
2394    max_replicas_per_node: Option<Located<DeployPlacementMaxReplicasPerNode>>,
2395    extension_fields: Vec<FieldReference>,
2396    unknown_fields: Vec<FieldReference>,
2397}
2398
2399impl DeployPlacement {
2400    pub(super) const fn new(span: SourceSpan) -> Self {
2401        Self {
2402            span,
2403            constraints: None,
2404            preferences: None,
2405            max_replicas_per_node: None,
2406            extension_fields: Vec::new(),
2407            unknown_fields: Vec::new(),
2408        }
2409    }
2410
2411    pub(super) fn set_constraints(&mut self, constraints: Vec<Located<String>>) {
2412        self.constraints = Some(constraints);
2413    }
2414
2415    pub(super) fn set_preferences(&mut self, preferences: Vec<DeployPlacementPreference>) {
2416        self.preferences = Some(preferences);
2417    }
2418
2419    pub(super) fn set_max_replicas_per_node(&mut self, value: Located<DeployPlacementMaxReplicasPerNode>) {
2420        self.max_replicas_per_node = Some(value);
2421    }
2422
2423    pub(super) fn push_extension(&mut self, value: FieldReference) {
2424        self.extension_fields.push(value);
2425    }
2426
2427    pub(super) fn push_unknown(&mut self, value: FieldReference) {
2428        self.unknown_fields.push(value);
2429    }
2430
2431    /// Returns the complete placement mapping span.
2432    #[must_use]
2433    pub const fn span(&self) -> SourceSpan {
2434        self.span
2435    }
2436
2437    /// Returns ordered raw constraints, including duplicates and empty strings.
2438    #[must_use]
2439    pub fn constraints(&self) -> Option<&[Located<String>]> {
2440        self.constraints.as_deref()
2441    }
2442
2443    /// Returns ordered placement preferences, including explicit empty mappings.
2444    #[must_use]
2445    pub fn preferences(&self) -> Option<&[DeployPlacementPreference]> {
2446        self.preferences.as_deref()
2447    }
2448
2449    /// Returns the authored max-replicas-per-node scalar spelling and category.
2450    #[must_use]
2451    pub const fn max_replicas_per_node(&self) -> Option<&Located<DeployPlacementMaxReplicasPerNode>> {
2452        self.max_replicas_per_node.as_ref()
2453    }
2454
2455    /// Returns retained placement extensions.
2456    #[must_use]
2457    pub fn extension_fields(&self) -> &[FieldReference] {
2458        &self.extension_fields
2459    }
2460
2461    /// Returns retained unknown placement fields.
2462    #[must_use]
2463    pub fn unknown_fields(&self) -> &[FieldReference] {
2464        &self.unknown_fields
2465    }
2466}
2467
2468/// One authored placement preference mapping.
2469#[derive(Debug, Clone, PartialEq, Eq)]
2470pub struct DeployPlacementPreference {
2471    span: SourceSpan,
2472    spread: Option<Located<String>>,
2473    extension_fields: Vec<FieldReference>,
2474    unknown_fields: Vec<FieldReference>,
2475}
2476
2477impl DeployPlacementPreference {
2478    pub(super) const fn new(span: SourceSpan) -> Self {
2479        Self {
2480            span,
2481            spread: None,
2482            extension_fields: Vec::new(),
2483            unknown_fields: Vec::new(),
2484        }
2485    }
2486
2487    pub(super) fn set_spread(&mut self, spread: Located<String>) {
2488        self.spread = Some(spread);
2489    }
2490
2491    pub(super) fn push_extension(&mut self, value: FieldReference) {
2492        self.extension_fields.push(value);
2493    }
2494
2495    pub(super) fn push_unknown(&mut self, value: FieldReference) {
2496        self.unknown_fields.push(value);
2497    }
2498
2499    /// Returns the complete preference mapping span.
2500    #[must_use]
2501    pub const fn span(&self) -> SourceSpan {
2502        self.span
2503    }
2504
2505    /// Returns the raw optional spread expression without evaluating it.
2506    #[must_use]
2507    pub const fn spread(&self) -> Option<&Located<String>> {
2508        self.spread.as_ref()
2509    }
2510
2511    /// Returns retained preference extensions.
2512    #[must_use]
2513    pub fn extension_fields(&self) -> &[FieldReference] {
2514        &self.extension_fields
2515    }
2516
2517    /// Returns retained unknown preference fields.
2518    #[must_use]
2519    pub fn unknown_fields(&self) -> &[FieldReference] {
2520        &self.unknown_fields
2521    }
2522}
2523
2524/// Raw deploy placement max-replicas-per-node scalar category and spelling.
2525#[derive(Debug, Clone, PartialEq, Eq)]
2526#[non_exhaustive]
2527pub enum DeployPlacementMaxReplicasPerNode {
2528    /// A YAML integer scalar without range validation.
2529    YamlInteger(String),
2530    /// A YAML string scalar without numeric validation.
2531    String(String),
2532}
2533
2534/// One recognized deploy subfield and its source reference.
2535#[derive(Debug, Clone, PartialEq, Eq)]
2536pub struct DeployField {
2537    kind: DeployFieldKind,
2538    reference: FieldReference,
2539}
2540
2541impl DeployField {
2542    pub(super) const fn new(kind: DeployFieldKind, reference: FieldReference) -> Self {
2543        Self { kind, reference }
2544    }
2545
2546    /// Returns the field's specification-level identity.
2547    #[must_use]
2548    pub const fn kind(&self) -> DeployFieldKind {
2549        self.kind
2550    }
2551
2552    /// Returns source spans for reading or editing the retained value.
2553    #[must_use]
2554    pub const fn reference(&self) -> &FieldReference {
2555        &self.reference
2556    }
2557}
2558
2559/// Recognized fields from the current Compose Deploy Specification.
2560#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2561#[non_exhaustive]
2562pub enum DeployFieldKind {
2563    /// Service-discovery endpoint mode.
2564    EndpointMode,
2565    /// Platform-service labels.
2566    Labels,
2567    /// Replication or job mode.
2568    Mode,
2569    /// Node-placement rules.
2570    Placement,
2571    /// Desired replica count.
2572    Replicas,
2573    /// Resource limits and reservations.
2574    Resources,
2575    /// Deploy-level restart policy.
2576    RestartPolicy,
2577    /// Rollback behavior.
2578    RollbackConfig,
2579    /// Rolling-update behavior.
2580    UpdateConfig,
2581}
2582
2583impl DeployFieldKind {
2584    pub(super) fn from_name(name: &str) -> Option<Self> {
2585        Some(match name {
2586            "endpoint_mode" => Self::EndpointMode,
2587            "labels" => Self::Labels,
2588            "mode" => Self::Mode,
2589            "placement" => Self::Placement,
2590            "replicas" => Self::Replicas,
2591            "resources" => Self::Resources,
2592            "restart_policy" => Self::RestartPolicy,
2593            "rollback_config" => Self::RollbackConfig,
2594            "update_config" => Self::UpdateConfig,
2595            _ => return None,
2596        })
2597    }
2598}