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