Skip to main content

dora_message/
descriptor.rs

1#![warn(missing_docs)]
2
3use crate::{
4    config::{CommunicationConfig, Input, InputMapping, NodeRunConfig},
5    id::{DataId, NodeId, OperatorId},
6};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use serde_with_expand_env::with_expand_envs;
10use std::{
11    collections::{BTreeMap, BTreeSet},
12    fmt,
13    path::PathBuf,
14};
15
16pub const SHELL_SOURCE: &str = "shell";
17/// Set the [`Node::path`] field to this value to treat the node as a
18/// [_dynamic node_](https://docs.rs/dora-node-api/latest/dora_node_api/).
19pub const DYNAMIC_SOURCE: &str = "dynamic";
20
21/// # Dataflow Specification
22///
23/// The main configuration structure for defining a Dora dataflow. Dataflows are
24/// specified through YAML files that describe the nodes, their connections, and
25/// execution parameters.
26///
27/// ## Structure
28///
29/// A dataflow consists of:
30/// - **Nodes**: The computational units that process data
31/// - **Communication**: Optional communication configuration
32/// - **Deployment**: Optional deployment configuration (unstable)
33/// - **Debug options**: Optional development and debugging settings (unstable)
34///
35/// ## Example
36///
37/// ```yaml
38/// nodes:
39///  - id: webcam
40///     operator:
41///       python: webcam.py
42///       inputs:
43///         tick: dora/timer/millis/100
44///       outputs:
45///         - image
46///   - id: plot
47///     operator:
48///       python: plot.py
49///       inputs:
50///         image: webcam/image
51/// ```
52#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
53#[serde(deny_unknown_fields)]
54#[schemars(title = "dora-rs specification")]
55pub struct Descriptor {
56    /// List of nodes in the dataflow
57    ///
58    /// This is the most important field of the dataflow specification.
59    /// Each node must be identified by a unique `id`:
60    ///
61    /// ## Example
62    ///
63    /// ```yaml
64    /// nodes:
65    ///   - id: foo
66    ///     path: path/to/the/executable
67    ///     # ... (see below)
68    ///   - id: bar
69    ///     path: path/to/another/executable
70    ///     # ... (see below)
71    /// ```
72    ///
73    /// For each node, you need to specify the `path` of the executable or script that Dora should run when starting the node.
74    /// Most of the other node fields are optional, but you typically want to specify at least some `inputs` and/or `outputs`.
75    pub nodes: Vec<Node>,
76
77    /// Global Environment variables inherited by all nodes (optional)
78    ///
79    /// ## Example
80    ///
81    /// ```yaml
82    /// env:
83    ///     MY_VAR: "my_var"
84    ///
85    /// nodes:
86    ///   - id: foo
87    ///     path: path/to/the/executable
88    ///     # ... (see below)
89    ///   - id: bar
90    ///     path: path/to/another/executable
91    ///     # ... (see below)
92    /// ```
93    ///
94    /// Note that, If there is an env at the node level, Node level env will have more priority than the global env
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub env: Option<BTreeMap<String, EnvValue>>,
97
98    /// Communication configuration (optional, uses defaults)
99    #[schemars(skip)]
100    #[serde(default)]
101    pub communication: CommunicationConfig,
102
103    /// Deployment configuration (optional, unstable)
104    #[schemars(skip)]
105    #[serde(rename = "_unstable_deploy")]
106    pub deploy: Option<Deploy>,
107
108    /// Debug options (optional, unstable)
109    #[schemars(skip)]
110    #[serde(default, rename = "_unstable_debug")]
111    pub debug: Debug,
112}
113
114/// Specifies when a node should be restarted.
115#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, JsonSchema)]
116#[serde(rename_all = "kebab-case")]
117pub enum RestartPolicy {
118    /// Never restart the node (default)
119    #[default]
120    Never,
121    /// Restart the node if it exits with a non-zero exit code.
122    OnFailure,
123    /// Always restart the node when it exits, regardless of exit code.
124    ///
125    /// The node will not be restarted on the following conditions:
126    ///
127    /// - The node was stopped by the user (e.g., via `dora stop`).
128    /// - All inputs to the node have been closed and the node finished with a non-zero exit code.
129    Always,
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
133#[serde(deny_unknown_fields)]
134pub struct Deploy {
135    /// Target machine for deployment
136    pub machine: Option<String>,
137    /// Working directory for the deployment
138    pub working_dir: Option<PathBuf>,
139}
140
141#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
142pub struct Debug {
143    /// Whether to publish all messages to Zenoh for debugging
144    #[serde(default)]
145    pub publish_all_messages_to_zenoh: bool,
146}
147
148/// # Dora Node Configuration
149///
150/// A node represents a computational unit in a Dora dataflow. Each node runs as a
151/// separate process and can communicate with other nodes through inputs and outputs.
152#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
153#[serde(deny_unknown_fields)]
154pub struct Node {
155    /// Unique node identifier. Must not contain `/` characters.
156    ///
157    /// Node IDs can be arbitrary strings with the following limitations:
158    ///
159    /// - They must not contain any `/` characters (slashes).
160    /// - We do not recommend using whitespace characters (e.g. spaces) in IDs
161    ///
162    /// Each node must have an ID field.
163    ///
164    /// ## Example
165    ///
166    /// ```yaml
167    /// nodes:
168    ///   - id: camera_node
169    ///   - id: some_other_node
170    /// ```
171    pub id: NodeId,
172
173    /// Human-readable node name for documentation.
174    ///
175    /// This optional field can be used to define a more descriptive name in addition to a short
176    /// [`id`](Self::id).
177    ///
178    /// ## Example
179    ///
180    /// ```yaml
181    /// nodes:
182    ///   - id: camera_node
183    ///     name: "Camera Input Handler"
184    pub name: Option<String>,
185
186    /// Detailed description of the node's functionality.
187    ///
188    /// ## Example
189    ///
190    /// ```yaml
191    /// nodes:
192    ///   - id: camera_node
193    ///     description: "Captures video frames from webcam"
194    /// ```
195    pub description: Option<String>,
196
197    /// Path to executable or script that should be run.
198    ///
199    /// Specifies the path of the executable or script that Dora should run when starting the
200    /// dataflow.
201    /// This can point to a normal executable (e.g. when using a compiled language such as Rust) or
202    /// a Python script.
203    ///
204    /// Dora will automatically append a `.exe` extension on Windows systems when the specified
205    /// file name has no extension.
206    ///
207    /// ## Example
208    ///
209    /// ```yaml
210    /// nodes:
211    ///   - id: rust-example
212    ///     path: target/release/rust-node
213    ///   - id: python-example
214    ///     path: ./receive_data.py
215    /// ```
216    ///
217    /// ## URL as Path
218    ///
219    /// The `path` field can also point to a URL instead of a local path.
220    /// In this case, Dora will download the given file when starting the dataflow.
221    ///
222    /// Note that this is quite an old feature and using this functionality is **not recommended**
223    /// anymore. Instead, we recommend using a [`git`][Self::git] and/or [`build`](Self::build)
224    /// key.
225    #[serde(default, skip_serializing_if = "Option::is_none")]
226    pub path: Option<String>,
227
228    /// Command-line arguments passed to the executable.
229    ///
230    /// The command-line arguments that should be passed to the executable/script specified in `path`.
231    /// The arguments should be separated by space.
232    /// This field is optional and defaults to an empty argument list.
233    ///
234    /// ## Example
235    /// ```yaml
236    /// nodes:
237    ///   - id: example
238    ///     path: example-node
239    ///     args: -v --some-flag foo
240    /// ```
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub args: Option<String>,
243
244    /// Environment variables for node builds and execution.
245    ///
246    /// Key-value map of environment variables that should be set for both the
247    /// [`build`](Self::build) operation and the node execution (i.e. when the node is spawned
248    /// through [`path`](Self::path)).
249    ///
250    /// Supports strings, numbers, and booleans.
251    ///
252    /// ## Example
253    ///
254    /// ```yaml
255    /// nodes:
256    ///   - id: example-node
257    ///     path: path/to/node
258    ///     env:
259    ///       DEBUG: true
260    ///       PORT: 8080
261    ///       API_KEY: "secret-key"
262    /// ```
263    pub env: Option<BTreeMap<String, EnvValue>>,
264
265    /// Multiple operators running in a shared runtime process.
266    ///
267    /// Operators are an experimental, lightweight alternative to nodes.
268    /// Instead of running as a separate process, operators are linked into a runtime process.
269    /// This allows running multiple operators to share a single address space (not supported for
270    /// Python currently).
271    ///
272    /// Operators are defined as part of the node list, as children of a runtime node.
273    /// A runtime node is a special node that specifies no [`path`](Self::path) field, but contains
274    /// an `operators` field instead.
275    ///
276    /// ## Example
277    ///
278    /// ```yaml
279    /// nodes:
280    ///   - id: runtime-node
281    ///     operators:
282    ///       - id: processor
283    ///         python: process.py
284    /// ```
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub operators: Option<RuntimeNode>,
287
288    /// Single operator configuration.
289    ///
290    /// This is a convenience field for defining runtime nodes that contain only a single operator.
291    /// This field is an alternative to the [`operators`](Self::operators) field, which can be used
292    /// if there is only a single operator defined for the runtime node.
293    ///
294    /// ## Example
295    ///
296    /// ```yaml
297    /// nodes:
298    ///   - id: runtime-node
299    ///     operator:
300    ///       id: processor
301    ///       python: script.py
302    ///       outputs: [data]
303    /// ```
304    #[serde(default, skip_serializing_if = "Option::is_none")]
305    pub operator: Option<SingleOperatorDefinition>,
306
307    /// Legacy node configuration (deprecated).
308    ///
309    /// Please use the top-level [`path`](Self::path), [`args`](Self::args), etc. fields instead.
310    #[serde(default, skip_serializing_if = "Option::is_none")]
311    pub custom: Option<CustomNode>,
312
313    /// Output data identifiers produced by this node.
314    ///
315    /// List of output identifiers that the node sends.
316    /// Must contain all `output_id` values that the node uses when sending output, e.g. through the
317    /// [`send_output`](https://docs.rs/dora-node-api/latest/dora_node_api/struct.DoraNode.html#method.send_output)
318    /// function.
319    ///
320    /// ## Example
321    ///
322    /// ```yaml
323    /// nodes:
324    ///   - id: example-node
325    ///     outputs:
326    ///       - processed_image
327    ///       - metadata
328    /// ```
329    #[serde(default)]
330    pub outputs: BTreeSet<DataId>,
331
332    /// Input data connections from other nodes.
333    ///
334    /// Defines the inputs that this node is subscribing to.
335    ///
336    /// The `inputs` field should be a key-value map of the following format:
337    ///
338    /// `input_id: source_node_id/source_node_output_id`
339    ///
340    /// The components are defined as follows:
341    ///
342    ///   - `input_id` is the local identifier that should be used for this input.
343    ///
344    ///     This will map to the `id` field of
345    ///     [`Event::Input`](https://docs.rs/dora-node-api/latest/dora_node_api/enum.Event.html#variant.Input)
346    ///     events sent to the node event loop.
347    ///   - `source_node_id` should be the `id` field of the node that sends the output that we want
348    ///     to subscribe to
349    ///   - `source_node_output_id` should be the identifier of the output that that we want
350    ///     to subscribe to
351    ///
352    /// ## Example
353    ///
354    /// ```yaml
355    /// nodes:
356    ///   - id: example-node
357    ///     outputs:
358    ///       - one
359    ///       - two
360    ///   - id: receiver
361    ///     inputs:
362    ///         my_input: example-node/two
363    /// ```
364    #[serde(default)]
365    pub inputs: BTreeMap<DataId, Input>,
366
367    /// Redirect stdout/stderr to a data output.
368    ///
369    /// This field can be used to send all stdout and stderr output of the node as a Dora output.
370    /// Each output line is sent as a separate message.
371    ///
372    ///
373    /// ## Example
374    ///
375    /// ```yaml
376    /// nodes:
377    ///   - id: example
378    ///     send_stdout_as: stdout_output
379    ///   - id: logger
380    ///     inputs:
381    ///         example_output: example/stdout_output
382    /// ```
383    #[serde(skip_serializing_if = "Option::is_none")]
384    pub send_stdout_as: Option<String>,
385
386    /// Build commands executed during `dora build`. Each line runs separately.
387    ///
388    /// The `build` key specifies the command that should be invoked for building the node.
389    /// The key expects a single- or multi-line string.
390    ///
391    /// Each line is run as a separate command.
392    /// Spaces are used to separate arguments.
393    ///
394    /// Note that all the environment variables specified in the [`env`](Self::env) field are also
395    /// applied to the build commands.
396    ///
397    /// ## Special treatment of `pip`
398    ///
399    /// Build lines that start with `pip` or `pip3` are treated in a special way:
400    /// If the `--uv` argument is passed to the `dora build` command, all `pip`/`pip3` commands are
401    /// run through the [`uv` package manager](https://docs.astral.sh/uv/).
402    ///
403    /// ## Example
404    ///
405    /// ```yaml
406    /// nodes:
407    /// - id: build-example
408    ///   build: cargo build -p receive_data --release
409    ///   path: target/release/receive_data
410    /// - id: multi-line-example
411    ///   build: |
412    ///       pip install requirements.txt
413    ///       pip install -e some/local/package
414    ///   path: package
415    /// ```
416    ///
417    /// In the above example, the `pip` commands will be replaced by `uv pip` when run through
418    /// `dora build --uv`.
419    #[serde(default, skip_serializing_if = "Option::is_none")]
420    pub build: Option<String>,
421
422    /// Git repository URL for downloading nodes.
423    ///
424    /// The `git` key allows downloading nodes (i.e. their source code) from git repositories.
425    /// This can be especially useful for distributed dataflows.
426    ///
427    /// When a `git` key is specified, `dora build` automatically clones the specified repository
428    /// (or reuse an existing clone).
429    /// Then it checks out the specified [`branch`](Self::branch), [`tag`](Self::tag), or
430    /// [`rev`](Self::rev), or the default branch if none of them are specified.
431    /// Afterwards it runs the [`build`](Self::build) command if specified.
432    ///
433    /// Note that the git clone directory is set as working directory for both the
434    /// [`build`](Self::build) command and the specified [`path`](Self::path).
435    ///
436    /// ## Example
437    ///
438    /// ```yaml
439    /// nodes:
440    ///   - id: rust-node
441    ///     git: https://github.com/dora-rs/dora.git
442    ///     build: cargo build -p rust-dataflow-example-node
443    ///     path: target/debug/rust-dataflow-example-node
444    /// ```
445    ///
446    /// In the above example, `dora build` will first clone the specified `git` repository and then
447    /// run the specified `build` inside the local clone directory.
448    /// When `dora run` or `dora start` is invoked, the working directory will be the git clone
449    /// directory too. So a relative `path` will start from the clone directory.
450    #[serde(default, skip_serializing_if = "Option::is_none")]
451    pub git: Option<String>,
452
453    /// Git branch to checkout after cloning.
454    ///
455    /// The `branch` field is only allowed in combination with the [`git`](#git) field.
456    /// It specifies the branch that should be checked out after cloning.
457    /// Only one of `branch`, `tag`, or `rev` can be specified.
458    ///
459    /// ## Example
460    ///
461    /// ```yaml
462    /// nodes:
463    ///   - id: rust-node
464    ///     git: https://github.com/dora-rs/dora.git
465    ///     branch: some-branch-name
466    /// ```
467    #[serde(default, skip_serializing_if = "Option::is_none")]
468    pub branch: Option<String>,
469
470    /// Git tag to checkout after cloning.
471    ///
472    /// The `tag` field is only allowed in combination with the [`git`](#git) field.
473    /// It specifies the git tag that should be checked out after cloning.
474    /// Only one of `branch`, `tag`, or `rev` can be specified.
475    ///
476    /// ## Example
477    ///
478    /// ```yaml
479    /// nodes:
480    ///   - id: rust-node
481    ///     git: https://github.com/dora-rs/dora.git
482    ///     tag: v0.3.0
483    /// ```
484    #[serde(default, skip_serializing_if = "Option::is_none")]
485    pub tag: Option<String>,
486
487    /// Git revision (e.g. commit hash) to checkout after cloning.
488    ///
489    /// The `rev` field is only allowed in combination with the [`git`](#git) field.
490    /// It specifies the git revision (e.g. a commit hash) that should be checked out after cloning.
491    /// Only one of `branch`, `tag`, or `rev` can be specified.
492    ///
493    /// ## Example
494    ///
495    /// ```yaml
496    /// nodes:
497    ///   - id: rust-node
498    ///     git: https://github.com/dora-rs/dora.git
499    ///     rev: 64ab0d7c
500    /// ```
501    #[serde(default, skip_serializing_if = "Option::is_none")]
502    pub rev: Option<String>,
503
504    /// Whether this node should be restarted on exit or error.
505    ///
506    /// Defaults to `RestartPolicy::Never`.
507    #[serde(default)]
508    pub restart_policy: RestartPolicy,
509
510    /// Unstable machine deployment configuration
511    #[schemars(skip)]
512    #[serde(rename = "_unstable_deploy")]
513    pub deploy: Option<Deploy>,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct ResolvedNode {
518    pub id: NodeId,
519    pub name: Option<String>,
520    pub description: Option<String>,
521    pub env: Option<BTreeMap<String, EnvValue>>,
522
523    #[serde(default)]
524    pub deploy: Option<Deploy>,
525
526    #[serde(flatten)]
527    pub kind: CoreNodeKind,
528}
529
530impl ResolvedNode {
531    pub fn has_git_source(&self) -> bool {
532        self.kind
533            .as_custom()
534            .map(|n| n.source.is_git())
535            .unwrap_or_default()
536    }
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize)]
540#[serde(rename_all = "lowercase")]
541#[allow(clippy::large_enum_variant)]
542pub enum CoreNodeKind {
543    /// Dora runtime node
544    #[serde(rename = "operators")]
545    Runtime(RuntimeNode),
546    Custom(CustomNode),
547}
548
549impl CoreNodeKind {
550    pub fn as_custom(&self) -> Option<&CustomNode> {
551        match self {
552            CoreNodeKind::Runtime(_) => None,
553            CoreNodeKind::Custom(custom_node) => Some(custom_node),
554        }
555    }
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
559#[serde(transparent)]
560pub struct RuntimeNode {
561    /// List of operators running in this runtime
562    pub operators: Vec<OperatorDefinition>,
563}
564
565#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
566pub struct OperatorDefinition {
567    /// Unique operator identifier within the runtime
568    pub id: OperatorId,
569    #[serde(flatten)]
570    pub config: OperatorConfig,
571}
572
573#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
574pub struct SingleOperatorDefinition {
575    /// Operator identifier (optional for single operators)
576    pub id: Option<OperatorId>,
577    #[serde(flatten)]
578    pub config: OperatorConfig,
579}
580
581#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
582pub struct OperatorConfig {
583    /// Human-readable operator name
584    pub name: Option<String>,
585    /// Detailed description of the operator
586    pub description: Option<String>,
587
588    /// Input data connections
589    #[serde(default)]
590    pub inputs: BTreeMap<DataId, Input>,
591    /// Output data identifiers
592    #[serde(default)]
593    pub outputs: BTreeSet<DataId>,
594
595    /// Operator source configuration (Python, shared library, etc.)
596    #[serde(flatten)]
597    pub source: OperatorSource,
598
599    /// Build commands for this operator
600    #[serde(default, skip_serializing_if = "Option::is_none")]
601    pub build: Option<String>,
602    /// Redirect stdout to data output
603    #[serde(skip_serializing_if = "Option::is_none")]
604    pub send_stdout_as: Option<String>,
605}
606
607#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)]
608#[serde(rename_all = "kebab-case")]
609pub enum OperatorSource {
610    SharedLibrary(String),
611    Python(PythonSource),
612    #[schemars(skip)]
613    Wasm(String),
614}
615#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
616#[serde(from = "PythonSourceDef", into = "PythonSourceDef")]
617pub struct PythonSource {
618    pub source: String,
619    pub conda_env: Option<String>,
620}
621
622#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
623#[serde(untagged)]
624pub enum PythonSourceDef {
625    SourceOnly(String),
626    WithOptions {
627        source: String,
628        conda_env: Option<String>,
629    },
630}
631
632impl From<PythonSource> for PythonSourceDef {
633    fn from(input: PythonSource) -> Self {
634        match input {
635            PythonSource {
636                source,
637                conda_env: None,
638            } => Self::SourceOnly(source),
639            PythonSource { source, conda_env } => Self::WithOptions { source, conda_env },
640        }
641    }
642}
643
644impl From<PythonSourceDef> for PythonSource {
645    fn from(value: PythonSourceDef) -> Self {
646        match value {
647            PythonSourceDef::SourceOnly(source) => Self {
648                source,
649                conda_env: None,
650            },
651            PythonSourceDef::WithOptions { source, conda_env } => Self { source, conda_env },
652        }
653    }
654}
655
656#[derive(Debug, Serialize, Deserialize, Clone)]
657#[serde(deny_unknown_fields)]
658pub struct PythonOperatorConfig {
659    pub path: PathBuf,
660    #[serde(default)]
661    pub inputs: BTreeMap<DataId, InputMapping>,
662    #[serde(default)]
663    pub outputs: BTreeSet<DataId>,
664}
665
666#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
667pub struct CustomNode {
668    /// Path of the source code
669    ///
670    /// If you want to use a specific `conda` environment.
671    /// Provide the python path within the source.
672    ///
673    /// source: /home/peter/miniconda3/bin/python
674    ///
675    /// args: some_node.py
676    ///
677    /// Source can match any executable in PATH.
678    pub path: String,
679    pub source: NodeSource,
680    /// Args for the executable.
681    #[serde(default, skip_serializing_if = "Option::is_none")]
682    pub args: Option<String>,
683    /// Environment variables for the custom nodes
684    ///
685    /// Deprecated, use outer-level `env` field instead.
686    pub envs: Option<BTreeMap<String, EnvValue>>,
687    #[serde(default, skip_serializing_if = "Option::is_none")]
688    pub build: Option<String>,
689    /// Send stdout and stderr to another node
690    #[serde(skip_serializing_if = "Option::is_none")]
691    pub send_stdout_as: Option<String>,
692
693    #[serde(default)]
694    pub restart_policy: RestartPolicy,
695
696    #[serde(flatten)]
697    pub run_config: NodeRunConfig,
698}
699
700#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
701pub enum NodeSource {
702    Local,
703    GitBranch {
704        repo: String,
705        rev: Option<GitRepoRev>,
706    },
707}
708
709impl NodeSource {
710    pub fn is_git(&self) -> bool {
711        matches!(self, Self::GitBranch { .. })
712    }
713}
714
715#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
716pub enum ResolvedNodeSource {
717    Local,
718    GitCommit { repo: String, commit_hash: String },
719}
720
721#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
722pub enum GitRepoRev {
723    Branch(String),
724    Tag(String),
725    Rev(String),
726}
727
728#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
729#[serde(untagged)]
730pub enum EnvValue {
731    #[serde(deserialize_with = "with_expand_envs")]
732    Bool(bool),
733    #[serde(deserialize_with = "with_expand_envs")]
734    Integer(i64),
735    #[serde(deserialize_with = "with_expand_envs")]
736    Float(f64),
737    #[serde(deserialize_with = "with_expand_envs")]
738    String(String),
739}
740
741impl fmt::Display for EnvValue {
742    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
743        match self {
744            EnvValue::Bool(bool) => fmt.write_str(&bool.to_string()),
745            EnvValue::Integer(i64) => fmt.write_str(&i64.to_string()),
746            EnvValue::Float(f64) => fmt.write_str(&f64.to_string()),
747            EnvValue::String(str) => fmt.write_str(str),
748        }
749    }
750}