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