use std::collections::HashMap;
use crate::abi::{DisclosureLevel, LayeredWrapper, SmartDefault};
use crate::codegen::parser::{parse_endpoint_params, ParsedParam};
use crate::manifest::{EndpointDef, Manifest};
pub fn generate_layers(manifest: &Manifest) -> Vec<LayeredWrapper> {
manifest
.endpoints
.iter()
.map(|ep| generate_endpoint_layers(ep, &manifest.defaults))
.collect()
}
fn generate_endpoint_layers(
endpoint: &EndpointDef,
defaults: &HashMap<String, toml::Value>,
) -> LayeredWrapper {
let params = parse_endpoint_params(endpoint);
let smart_defaults = resolve_smart_defaults(¶ms, defaults);
let beginner_params: Vec<String> = params
.iter()
.filter(|p| !p.optional)
.map(|p| format_param(p))
.collect();
let intermediate_params: Vec<String> = params
.iter()
.filter(|p| {
!p.optional
|| defaults.contains_key(&p.name)
|| is_commonly_used_type(p)
})
.map(|p| format_param(p))
.collect();
let expert_params: Vec<String> = params.iter().map(|p| format_param(p)).collect();
let beginner_code = generate_wrapper_code(
&endpoint.name,
&beginner_params,
&smart_defaults,
DisclosureLevel::Beginner,
);
let intermediate_code = generate_wrapper_code(
&endpoint.name,
&intermediate_params,
&smart_defaults,
DisclosureLevel::Intermediate,
);
let expert_code = generate_wrapper_code(
&endpoint.name,
&expert_params,
&smart_defaults,
DisclosureLevel::Expert,
);
LayeredWrapper {
endpoint_name: endpoint.name.clone(),
beginner_signature: beginner_params,
intermediate_signature: intermediate_params,
expert_signature: expert_params,
smart_defaults,
beginner_code,
intermediate_code,
expert_code,
}
}
fn resolve_smart_defaults(
params: &[ParsedParam],
defaults: &HashMap<String, toml::Value>,
) -> Vec<SmartDefault> {
params
.iter()
.filter(|p| p.optional)
.filter_map(|p| {
defaults.get(&p.name).map(|val| SmartDefault {
param_name: p.name.clone(),
default_value: toml_value_to_string(val),
})
})
.collect()
}
fn format_param(param: &ParsedParam) -> String {
let suffix = if param.optional { "?" } else { "" };
format!("{}{}: {:?}", param.name, suffix, param.param_type)
}
fn is_commonly_used_type(param: &ParsedParam) -> bool {
use crate::codegen::parser::ParamType;
matches!(
param.param_type,
ParamType::String | ParamType::Int | ParamType::Bool
)
}
fn toml_value_to_string(val: &toml::Value) -> String {
match val {
toml::Value::String(s) => format!("\"{}\"", s),
toml::Value::Integer(i) => i.to_string(),
toml::Value::Boolean(b) => b.to_string(),
toml::Value::Float(f) => f.to_string(),
other => format!("{}", other),
}
}
fn generate_wrapper_code(
name: &str,
params: &[String],
smart_defaults: &[SmartDefault],
level: DisclosureLevel,
) -> String {
let level_prefix = match level {
DisclosureLevel::Beginner => "beginner",
DisclosureLevel::Intermediate => "intermediate",
DisclosureLevel::Expert => "expert",
};
let mut lines = Vec::new();
lines.push(format!(
"/// {}-level wrapper for `{}`",
level_prefix, name
));
lines.push(format!(
"fn {}_{}({}) {{",
level_prefix,
name,
params.join(", ")
));
if level != DisclosureLevel::Expert {
for sd in smart_defaults {
let param_present = params.iter().any(|p| p.starts_with(&sd.param_name));
if !param_present {
lines.push(format!(
" let {} = {}; // smart default",
sd.param_name, sd.default_value
));
}
}
}
lines.push(format!(" {}(/* delegate to full API */)", name));
lines.push("}".to_string());
lines.join("\n")
}
pub fn get_beginner_wrapper(manifest: &Manifest, endpoint_name: &str) -> Option<LayeredWrapper> {
manifest
.endpoints
.iter()
.find(|ep| ep.name == endpoint_name)
.map(|ep| generate_endpoint_layers(ep, &manifest.defaults))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::{EndpointDef, LevelConfig, Manifest, ProjectConfig};
fn test_manifest() -> Manifest {
let mut defaults = HashMap::new();
defaults.insert("role".to_string(), toml::Value::String("user".to_string()));
defaults.insert("limit".to_string(), toml::Value::Integer(20));
defaults.insert("page".to_string(), toml::Value::Integer(1));
defaults.insert(
"locale".to_string(),
toml::Value::String("en-GB".to_string()),
);
Manifest {
project: ProjectConfig {
name: "test-api".to_string(),
description: String::new(),
},
endpoints: vec![
EndpointDef {
name: "create_user".to_string(),
params: vec![
"username: string".to_string(),
"email: string".to_string(),
"role?: string".to_string(),
"permissions?: list".to_string(),
"mfa?: bool".to_string(),
"locale?: string".to_string(),
],
required: vec!["username".to_string(), "email".to_string()],
description: String::new(),
},
EndpointDef {
name: "search".to_string(),
params: vec![
"query: string".to_string(),
"filters?: map".to_string(),
"sort?: string".to_string(),
"page?: int".to_string(),
"limit?: int".to_string(),
"cursor?: string".to_string(),
"facets?: list".to_string(),
],
required: vec!["query".to_string()],
description: String::new(),
},
],
levels: LevelConfig::default(),
defaults,
}
}
#[test]
fn test_beginner_layer_only_required_params() {
let m = test_manifest();
let layers = generate_layers(&m);
let create_user = &layers[0];
assert_eq!(
create_user.beginner_signature.len(),
2,
"beginner should only expose required params"
);
}
#[test]
fn test_expert_layer_all_params() {
let m = test_manifest();
let layers = generate_layers(&m);
let create_user = &layers[0];
assert_eq!(
create_user.expert_signature.len(),
6,
"expert should expose all params"
);
}
#[test]
fn test_smart_defaults_resolved() {
let m = test_manifest();
let layers = generate_layers(&m);
let create_user = &layers[0];
let default_names: Vec<&str> = create_user
.smart_defaults
.iter()
.map(|sd| sd.param_name.as_str())
.collect();
assert!(
default_names.contains(&"role"),
"should have default for 'role'"
);
assert!(
default_names.contains(&"locale"),
"should have default for 'locale'"
);
}
#[test]
fn test_beginner_code_includes_defaults() {
let m = test_manifest();
let layers = generate_layers(&m);
let create_user = &layers[0];
assert!(
create_user.beginner_code.contains("smart default"),
"beginner code should apply smart defaults"
);
}
#[test]
fn test_intermediate_has_more_params_than_beginner() {
let m = test_manifest();
let layers = generate_layers(&m);
let search = &layers[1];
assert!(
search.intermediate_signature.len() > search.beginner_signature.len(),
"intermediate should expose more params than beginner"
);
}
}