use std::collections::BTreeMap;
const SNAPSHOT: &str = include_str!("../../rustigram-types/tests/spec/bot-api-10.2.json");
#[derive(serde::Deserialize)]
struct Spec {
methods: BTreeMap<String, BTreeMap<String, serde_json::Value>>,
}
fn block_at(text: &str, from: usize) -> &str {
let Some(open) = text[from..].find('{').map(|o| from + o) else {
return "";
};
let bytes = text.as_bytes();
let mut depth = 0_i32;
for i in open..text.len() {
match bytes[i] {
b'{' => depth += 1,
b'}' => {
depth -= 1;
if depth == 0 {
return &text[open + 1..i];
}
}
_ => {}
}
}
&text[open + 1..]
}
fn occurrences<'a>(haystack: &'a str, needle: &str) -> Vec<&'a str> {
let mut out = Vec::new();
let mut from = 0;
while let Some(at) = haystack[from..].find(needle) {
let start = from + at;
let before_ok = start == 0 || {
let c = haystack[..start].chars().next_back().unwrap_or(' ');
!c.is_alphanumeric() && c != '_'
};
if before_ok {
out.push(&haystack[start + needle.len()..]);
}
from = start + needle.len();
}
out
}
fn reachable(impl_body: &str, field: &str) -> bool {
if occurrences(impl_body, &format!("params.{field}"))
.iter()
.any(|rest| rest.trim_start().starts_with('='))
{
return true;
}
for rest in occurrences(impl_body, field) {
let rest = rest.trim_start();
if rest.starts_with(',') || rest.starts_with('}') {
return true;
}
if let Some(value) = rest.strip_prefix(':') {
let value = value
.trim_start()
.split([',', '\n', '}'])
.next()
.unwrap_or("")
.trim();
if !matches!(value, "None" | "Default::default()" | "") {
return true;
}
}
}
false
}
struct Builder {
api_method: String,
params_struct: String,
impl_body: String,
}
fn builders(source: &str) -> Vec<Builder> {
let mut params_of: BTreeMap<String, String> = BTreeMap::new();
let mut from = 0;
while let Some(at) = source[from..].find("\npub struct ") {
let start = from + at;
let name: String = source[start + 12..]
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
let body = block_at(source, start);
if let Some(rest) = body.split("params:").nth(1) {
let ps: String = rest
.trim_start()
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if ps.ends_with("Params") {
params_of.insert(name, ps);
}
}
from = start + 12;
}
let mut impl_of: BTreeMap<String, String> = BTreeMap::new();
let mut from = 0;
while let Some(at) = source[from..].find("\nimpl ") {
let start = from + at;
let name: String = source[start + 6..]
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if source[start + 6 + name.len()..]
.trim_start()
.starts_with('{')
{
impl_of
.entry(name)
.or_default()
.push_str(block_at(source, start));
}
from = start + 6;
}
let mut method_of: BTreeMap<String, String> = BTreeMap::new();
let mut from = 0;
while let Some(at) = source[from..].find("impl_into_future!(") {
let start = from + at + "impl_into_future!(".len();
let name: String = source[start..]
.trim_start()
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
if let Some(method) = source[start..]
.split(");")
.next()
.and_then(|args| args.split('"').nth(1))
{
method_of.entry(name).or_insert_with(|| method.to_owned());
}
from = start;
}
for marker in [
"\nimpl IntoFuture for ",
"\nimpl std::future::IntoFuture for ",
] {
let mut from = 0;
while let Some(at) = source[from..].find(marker) {
let start = from + at + marker.len();
let name: String = source[start..]
.chars()
.take_while(|c| c.is_alphanumeric() || *c == '_')
.collect();
let body = block_at(source, start);
for call in ["post_json(\"", "post_multipart(\""] {
if let Some(m) = body.split(call).nth(1).and_then(|r| r.split('"').next()) {
method_of
.entry(name.clone())
.or_insert_with(|| m.to_owned());
break;
}
}
from = start;
}
}
let mut unmapped = Vec::new();
let out: Vec<Builder> = params_of
.into_iter()
.filter_map(|(builder, params_struct)| {
let Some(api_method) = method_of.get(&builder) else {
unmapped.push(builder);
return None;
};
Some(Builder {
api_method: api_method.clone(),
params_struct,
impl_body: impl_of.get(&builder).cloned().unwrap_or_default(),
})
})
.collect();
assert!(
unmapped.is_empty(),
"{} builder(s) hold a params struct but could not be matched to an API \
method, so they would be skipped silently:\n {}",
unmapped.len(),
unmapped.join("\n ")
);
out
}
fn params_fields(source: &str, params_struct: &str) -> Vec<(String, String)> {
let Some(at) = source.find(&format!("struct {params_struct} {{")) else {
return Vec::new();
};
let mut out = Vec::new();
let mut renamed: Option<String> = None;
for line in block_at(source, at).lines() {
let trimmed = line.trim();
if let Some(r) = trimmed
.split("rename = \"")
.nth(1)
.and_then(|r| r.split('"').next())
{
renamed = Some(r.to_owned());
continue;
}
if trimmed.starts_with('#') || trimmed.starts_with("//") {
continue;
}
if let Some((name, _)) = trimmed.trim_start_matches("pub ").split_once(':') {
let name = name.trim();
if !name.is_empty() && name.chars().all(|c| c.is_alphanumeric() || c == '_') {
out.push((
name.to_owned(),
renamed.take().unwrap_or_else(|| name.to_owned()),
));
continue;
}
}
renamed = None;
}
out
}
#[test]
fn every_declared_parameter_is_reachable() {
let source = include_str!("../src/methods/sending.rs");
let mut sources = String::from(source);
for extra in [
include_str!("../src/methods/payments.rs"),
include_str!("../src/methods/editing.rs"),
include_str!("../src/methods/chat_management.rs"),
include_str!("../src/methods/inline.rs"),
include_str!("../src/methods/stickers.rs"),
include_str!("../src/methods/getters.rs"),
include_str!("../src/methods/bot_settings.rs"),
include_str!("../src/methods/games.rs"),
include_str!("../src/methods/stories.rs"),
include_str!("../src/methods/forum.rs"),
] {
sources.push('\n');
sources.push_str(extra);
}
let spec: Spec = serde_json::from_str(SNAPSHOT).expect("the snapshot parses");
let found = builders(&sources);
assert!(
found.len() > 100,
"parsed only {} builders — the layout changed and this test would check \
almost nothing",
found.len()
);
let mut unreachable = Vec::new();
for builder in &found {
let Some(params) = spec.methods.get(&builder.api_method) else {
continue;
};
for (rust, wire) in params_fields(&sources, &builder.params_struct) {
if params.contains_key(&wire) && !reachable(&builder.impl_body, &rust) {
unreachable.push(format!(" {}.{wire}", builder.api_method));
}
}
}
unreachable.sort();
unreachable.dedup();
assert!(
unreachable.is_empty(),
"{} spec parameter(s) are declared, serialised, and impossible to set. \
The coverage suite scores these as covered because the field exists — \
add a setter:\n{}",
unreachable.len(),
unreachable.join("\n")
);
}