use crate::parser::{
EndpointData, EndpointMetadata, ParamValue, ParameterMetadata, Services, Type,
};
use anyhow::{Context, Result, anyhow, bail};
use serde_json::{Number, Value};
use std::collections::HashMap;
pub trait ConvertValue {
fn convert_value(&self, value: &str) -> Result<Value>;
}
impl Services {
pub fn extract_endpoints(&self) -> (Vec<String>, HashMap<String, EndpointMetadata>) {
let mut endpoint_names = Vec::new();
let mut endpoint_data = HashMap::new();
for service in &self.services {
for endpoint in &service.endpoints {
endpoint_names.push(endpoint.name.clone());
let param_names_and_types = endpoint
.parameters
.iter()
.map(|param| ParameterMetadata {
name: param.name.clone(),
ty: param.ty.clone(),
})
.collect();
let returns_stream = endpoint.stream_response.is_some();
let metadata = EndpointMetadata {
service_name: service.name.clone(),
method_id: endpoint.code,
params: param_names_and_types,
is_stream: returns_stream,
};
endpoint_data.insert(endpoint.name.clone(), metadata);
}
}
(endpoint_names, endpoint_data)
}
}
impl ConvertValue for Type {
fn convert_value(&self, value: &str) -> Result<Value> {
Ok(match self {
Type::String
| Type::UUID
| Type::Bytea
| Type::IpAddr
| Type::NanoId { .. }
| Type::BlockchainDecimal
| Type::BlockchainAddress
| Type::BlockchainTransactionHash => Value::String(value.to_string()),
Type::UInt32 => Value::Number(Number::from(value.parse::<u32>()?)),
Type::Int32 => Value::Number(Number::from(value.parse::<i32>()?)),
Type::Int64 | Type::TimeStampMs => Value::Number(Number::from(value.parse::<i64>()?)),
Type::Float64 => Value::Number(
Number::from_f64(value.parse::<f64>()?)
.ok_or_else(|| anyhow!("`{value}` is not a finite number"))?,
),
Type::Boolean => Value::Bool(value.parse::<bool>()?),
Type::Unit => Value::Null,
Type::Object => serde_json::from_str(value)
.with_context(|| format!("`{value}` is not valid JSON for an Object field"))?,
Type::Optional(inner) => {
if value.is_empty() {
Value::Null
} else {
inner.convert_value(value)?
}
}
Type::Vec(inner) => Value::Array(
split_top_level(value, ',')
.iter()
.map(|v| inner.convert_value(v))
.collect::<Result<Vec<_>>>()?,
),
Type::Struct { name, fields } => {
let supplied: HashMap<&str, &str> = split_top_level(value, ',')
.into_iter()
.filter_map(|pair| pair.split_once(':'))
.map(|(k, v)| (k.trim(), v.trim()))
.collect();
let mut map = serde_json::Map::new();
for field in fields {
match supplied.get(field.name.as_str()) {
Some(v) => {
map.insert(field.name.clone(), field.ty.convert_value(v)?);
}
None if matches!(field.ty, Type::Optional(_)) => {}
None => bail!("struct `{name}`: missing required field `{}`", field.name),
}
}
Value::Object(map)
}
Type::Enum { name, variants } => {
if let Some(variant) = variants.iter().find(|v| v.name == value) {
Value::Number(Number::from(variant.value))
} else if let Ok(n) = value.parse::<i64>() {
if variants.iter().any(|v| v.value == n) {
Value::Number(Number::from(n))
} else {
bail!("enum `{name}`: no variant has value {n}");
}
} else {
bail!(
"enum `{name}`: `{value}` is not a variant. Expected one of: {}",
variants
.iter()
.map(|v| v.name.as_str())
.collect::<Vec<_>>()
.join(", ")
);
}
}
Type::StructRef(name) | Type::EnumRef { name, .. } => serde_json::from_str(value)
.with_context(|| format!("`{value}` is not valid JSON for `{name}`"))?,
Type::StructTable { struct_ref } => serde_json::from_str(value)
.with_context(|| format!("`{value}` is not valid JSON for table `{struct_ref}`"))?,
other => {
bail!("unsupported parameter type {other:?} — endpoint-validator needs updating")
}
})
}
}
fn split_top_level(value: &str, sep: char) -> Vec<&str> {
let (mut parts, mut depth, mut start, mut quoted) = (Vec::new(), 0i32, 0usize, false);
for (i, c) in value.char_indices() {
match c {
'"' => quoted = !quoted,
'{' | '[' if !quoted => depth += 1,
'}' | ']' if !quoted => depth -= 1,
c if c == sep && depth == 0 && !quoted => {
parts.push(value[start..i].trim());
start = i + c.len_utf8();
}
_ => {}
}
}
parts.push(value[start..].trim());
parts.into_iter().filter(|p| !p.is_empty()).collect()
}
pub fn extract_param_defaults(
endpoints: &HashMap<String, EndpointData>,
) -> HashMap<String, HashMap<String, String>> {
let mut result = HashMap::new();
for (method_id, endpoint_data) in endpoints {
let mut param_map = HashMap::new();
for (param_name, param_value) in &endpoint_data.params {
let value_str = match param_value {
ParamValue::String(s) => s.clone(),
ParamValue::Number(n) => n.to_string(),
ParamValue::Bool(b) => b.to_string(),
ParamValue::Array(arr) => format!("{:?}", arr),
ParamValue::Object(obj) => format!("{:?}", obj),
};
param_map.insert(param_name.to_string(), value_str);
}
result.insert(method_id.to_string(), param_map);
}
result
}