Skip to main content

endpoint_validator/parser/
services.rs

1use crate::parser::{
2    EndpointData, EndpointMetadata, ParamValue, ParameterMetadata, Services, Type,
3};
4use anyhow::{Context, Result, anyhow, bail};
5use serde_json::{Number, Value};
6use std::collections::HashMap;
7
8/// Turning a `config.toml` string into a wire value.
9///
10/// A trait because `Type` lives in `endpoint-libs` now, so an inherent impl is
11/// not ours to write.
12pub trait ConvertValue {
13    fn convert_value(&self, value: &str) -> Result<Value>;
14}
15
16impl Services {
17    pub fn extract_endpoints(&self) -> (Vec<String>, HashMap<String, EndpointMetadata>) {
18        let mut endpoint_names = Vec::new();
19        let mut endpoint_data = HashMap::new();
20
21        for service in &self.services {
22            for endpoint in &service.endpoints {
23                endpoint_names.push(endpoint.name.clone());
24
25                let param_names_and_types = endpoint
26                    .parameters
27                    .iter()
28                    .map(|param| ParameterMetadata {
29                        name: param.name.clone(),
30                        ty: param.ty.clone(),
31                    })
32                    .collect();
33
34                let returns_stream = endpoint.stream_response.is_some();
35
36                let metadata = EndpointMetadata {
37                    service_name: service.name.clone(),
38                    method_id: endpoint.code,
39                    params: param_names_and_types,
40                    is_stream: returns_stream,
41                };
42
43                endpoint_data.insert(endpoint.name.clone(), metadata);
44            }
45        }
46
47        (endpoint_names, endpoint_data)
48    }
49}
50
51impl ConvertValue for Type {
52    /// Converts a `config.toml` string into the JSON value the wire expects.
53    ///
54    /// Mirrors `endpoint_libs::model::Type::to_json_schema`: whatever that says
55    /// a field looks like on the wire is what this must produce, or the server
56    /// rejects the call.
57    fn convert_value(&self, value: &str) -> Result<Value> {
58        Ok(match self {
59            Type::String
60            | Type::UUID
61            | Type::Bytea
62            | Type::IpAddr
63            | Type::NanoId { .. }
64            | Type::BlockchainDecimal
65            | Type::BlockchainAddress
66            | Type::BlockchainTransactionHash => Value::String(value.to_string()),
67
68            Type::UInt32 => Value::Number(Number::from(value.parse::<u32>()?)),
69            Type::Int32 => Value::Number(Number::from(value.parse::<i32>()?)),
70            Type::Int64 | Type::TimeStampMs => Value::Number(Number::from(value.parse::<i64>()?)),
71            Type::Float64 => Value::Number(
72                Number::from_f64(value.parse::<f64>()?)
73                    .ok_or_else(|| anyhow!("`{value}` is not a finite number"))?,
74            ),
75            Type::Boolean => Value::Bool(value.parse::<bool>()?),
76            Type::Unit => Value::Null,
77            Type::Object => serde_json::from_str(value)
78                .with_context(|| format!("`{value}` is not valid JSON for an Object field"))?,
79
80            Type::Optional(inner) => {
81                if value.is_empty() {
82                    Value::Null
83                } else {
84                    inner.convert_value(value)?
85                }
86            }
87            Type::Vec(inner) => Value::Array(
88                split_top_level(value, ',')
89                    .iter()
90                    .map(|v| inner.convert_value(v))
91                    .collect::<Result<Vec<_>>>()?,
92            ),
93
94            Type::Struct { name, fields } => {
95                let supplied: HashMap<&str, &str> = split_top_level(value, ',')
96                    .into_iter()
97                    .filter_map(|pair| pair.split_once(':'))
98                    .map(|(k, v)| (k.trim(), v.trim()))
99                    .collect();
100
101                let mut map = serde_json::Map::new();
102                for field in fields {
103                    match supplied.get(field.name.as_str()) {
104                        Some(v) => {
105                            map.insert(field.name.clone(), field.ty.convert_value(v)?);
106                        }
107                        // A missing Optional is simply absent; a missing required
108                        // field is a config error worth naming.
109                        None if matches!(field.ty, Type::Optional(_)) => {}
110                        None => bail!("struct `{name}`: missing required field `{}`", field.name),
111                    }
112                }
113                Value::Object(map)
114            }
115
116            // Enums go over the wire as their integer value, not their name --
117            // see `enum_to_schema` upstream, which emits `type: integer` with a
118            // `const` per variant. Accept either spelling in config.
119            Type::Enum { name, variants } => {
120                if let Some(variant) = variants.iter().find(|v| v.name == value) {
121                    Value::Number(Number::from(variant.value))
122                } else if let Ok(n) = value.parse::<i64>() {
123                    if variants.iter().any(|v| v.value == n) {
124                        Value::Number(Number::from(n))
125                    } else {
126                        bail!("enum `{name}`: no variant has value {n}");
127                    }
128                } else {
129                    bail!(
130                        "enum `{name}`: `{value}` is not a variant. Expected one of: {}",
131                        variants
132                            .iter()
133                            .map(|v| v.name.as_str())
134                            .collect::<Vec<_>>()
135                            .join(", ")
136                    );
137                }
138            }
139
140            // References cannot be resolved without the registry, so accept raw
141            // JSON and let the server validate. Better than the previous
142            // behaviour, which silently sent the string.
143            Type::StructRef(name) | Type::EnumRef { name, .. } => serde_json::from_str(value)
144                .with_context(|| format!("`{value}` is not valid JSON for `{name}`"))?,
145            Type::StructTable { struct_ref } => serde_json::from_str(value)
146                .with_context(|| format!("`{value}` is not valid JSON for table `{struct_ref}`"))?,
147
148            // `Type` is #[non_exhaustive] upstream: a new variant must fail
149            // loudly here rather than be silently mis-encoded.
150            other => {
151                bail!("unsupported parameter type {other:?} — endpoint-validator needs updating")
152            }
153        })
154    }
155}
156
157/// Splits on `sep`, ignoring separators nested inside `{}`, `[]` or quotes.
158///
159/// The previous implementation used a plain `split(',')`, which corrupted any
160/// nested struct or array value.
161fn split_top_level(value: &str, sep: char) -> Vec<&str> {
162    let (mut parts, mut depth, mut start, mut quoted) = (Vec::new(), 0i32, 0usize, false);
163    for (i, c) in value.char_indices() {
164        match c {
165            '"' => quoted = !quoted,
166            '{' | '[' if !quoted => depth += 1,
167            '}' | ']' if !quoted => depth -= 1,
168            c if c == sep && depth == 0 && !quoted => {
169                parts.push(value[start..i].trim());
170                start = i + c.len_utf8();
171            }
172            _ => {}
173        }
174    }
175    parts.push(value[start..].trim());
176    parts.into_iter().filter(|p| !p.is_empty()).collect()
177}
178
179pub fn extract_param_defaults(
180    endpoints: &HashMap<String, EndpointData>,
181) -> HashMap<String, HashMap<String, String>> {
182    let mut result = HashMap::new();
183
184    for (method_id, endpoint_data) in endpoints {
185        let mut param_map = HashMap::new();
186        for (param_name, param_value) in &endpoint_data.params {
187            let value_str = match param_value {
188                ParamValue::String(s) => s.clone(),
189                ParamValue::Number(n) => n.to_string(),
190                ParamValue::Bool(b) => b.to_string(),
191                ParamValue::Array(arr) => format!("{:?}", arr),
192                ParamValue::Object(obj) => format!("{:?}", obj),
193            };
194            param_map.insert(param_name.to_string(), value_str);
195        }
196        result.insert(method_id.to_string(), param_map);
197    }
198
199    result
200}