Skip to main content

cml/types/
program.rs

1// Copyright 2025 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::types::common::*;
6use crate::{ContextSpanned, Error};
7pub use cm_types::{
8    Availability, BorrowedName, BoundedName, DeliveryType, DependencyType, HandleType, Name,
9    OnTerminate, ParseError, Path, RelativePath, StartupMode, StorageId, Url,
10};
11use serde::{Serialize, de};
12use serde_json::Value;
13
14use std::fmt;
15use std::path::PathBuf;
16use std::sync::Arc;
17
18use indexmap::IndexMap;
19
20#[derive(Debug, PartialEq, Default, Serialize)]
21pub struct Program {
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub runner: Option<Name>,
24    #[serde(flatten)]
25    pub info: IndexMap<String, Value>,
26}
27
28impl<'de> de::Deserialize<'de> for Program {
29    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
30    where
31        D: de::Deserializer<'de>,
32    {
33        struct Visitor;
34
35        const EXPECTED_PROGRAM: &'static str =
36            "a JSON object that includes a `runner` string property";
37        const EXPECTED_RUNNER: &'static str = "a non-empty `runner` string property no more than 255 characters in length \
38            that consists of [A-Za-z0-9_.-] and starts with [A-Za-z0-9_]";
39
40        impl<'de> de::Visitor<'de> for Visitor {
41            type Value = Program;
42
43            fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44                f.write_str(EXPECTED_PROGRAM)
45            }
46
47            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
48            where
49                A: de::MapAccess<'de>,
50            {
51                let mut info = IndexMap::new();
52                let mut runner = None;
53                while let Some(e) = map.next_entry::<String, Value>()? {
54                    let (k, v) = e;
55                    if &k == "runner" {
56                        if let Value::String(s) = v {
57                            runner = Some(s);
58                        } else {
59                            return Err(de::Error::invalid_value(
60                                de::Unexpected::Map,
61                                &EXPECTED_RUNNER,
62                            ));
63                        }
64                    } else {
65                        info.insert(k, v);
66                    }
67                }
68                let runner = runner
69                    .map(|r| {
70                        Name::new(r.clone()).map_err(|e| match e {
71                            ParseError::InvalidValue => de::Error::invalid_value(
72                                serde::de::Unexpected::Str(&r),
73                                &EXPECTED_RUNNER,
74                            ),
75                            ParseError::TooLong | ParseError::Empty => {
76                                de::Error::invalid_length(r.len(), &EXPECTED_RUNNER)
77                            }
78                            _ => {
79                                panic!("unexpected parse error: {:?}", e);
80                            }
81                        })
82                    })
83                    .transpose()?;
84                Ok(Program { runner, info })
85            }
86        }
87
88        deserializer.deserialize_map(Visitor)
89    }
90}
91
92impl Hydrate for Program {
93    type Output = ContextProgram;
94
95    fn hydrate(self, file: &Arc<PathBuf>) -> Result<Self::Output, Error> {
96        let runner = self.runner.map(|raw_name| {
97            let validated_name = Name::new(raw_name.clone()).map_err(|e| {
98                    let msg = match e {
99                    ParseError::InvalidValue => format!(
100                        "Runner name '{}' contains invalid characters. Expected [A-Za-z0-9_.-] starting with [A-Za-z0-9_].",
101                        raw_name
102                    ),
103                    ParseError::TooLong | ParseError::Empty => {
104                        format!("Runner name must be between 1 and 255 characters long.")
105                    }
106                    _ => {
107                        panic!("unexpected parse error: {:?}", e);
108                    }
109                };
110
111                Error::merge(msg, Some(file.clone()))
112            })?;
113            Ok::<ContextSpanned<BoundedName<255>>, Error>(ContextSpanned {
114                value: validated_name,
115                origin: file.clone(),
116            })
117        }).transpose()?;
118
119        Ok(ContextProgram { runner, info: self.info })
120    }
121}
122
123#[derive(Debug, PartialEq, Serialize, Default, Clone)]
124pub struct ContextProgram {
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub runner: Option<ContextSpanned<Name>>,
127    #[serde(flatten)]
128    pub info: IndexMap<String, serde_json::Value>,
129}