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}