use super::symbol::external_symbol_kind;
use super::{SymbolId, SymbolKind};
use reflectapi_schema::{Enum, Fields, Schema, Struct, Type, Typespace};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct SchemaIds {
pub schema_id: SymbolId,
pub functions: HashMap<String, SymbolId>,
pub types: HashMap<String, SymbolId>,
pub members: HashMap<(String, String), SymbolId>,
}
pub fn build_schema_ids(schema: &Schema) -> SchemaIds {
let mut ids = SchemaIds {
schema_id: SymbolId::new(
SymbolKind::Schema,
vec!["__schema__".to_string(), schema.name.clone()],
),
functions: HashMap::new(),
types: HashMap::new(),
members: HashMap::new(),
};
for function in &schema.functions {
ids.functions
.entry(function.name.clone())
.or_insert_with(|| SymbolId::new(SymbolKind::Endpoint, vec![function.name.clone()]));
}
let mut input_seen = ids.types.clone();
let mut output_seen = ids.types.clone();
register_typespace_ids(&schema.input_types, &mut input_seen, &mut ids.members);
register_typespace_ids(&schema.output_types, &mut output_seen, &mut ids.members);
for (fqn, input_id) in &input_seen {
if let Some(output_id) = output_seen.get(fqn) {
if input_id == output_id {
let input_ty = schema.input_types.get_type(fqn);
let output_ty = schema.output_types.get_type(fqn);
if let (Some(input_ty), Some(output_ty)) = (input_ty, output_ty) {
if input_ty != output_ty {
let disambiguated =
SymbolId::with_disambiguator(output_id.kind, output_id.path.clone(), 1);
if let Some(output_ty) = schema.output_types.get_type(fqn) {
reregister_members(output_ty, &disambiguated, &mut ids.members);
}
output_seen.insert(fqn.clone(), disambiguated);
}
}
}
}
}
ids.types.extend(input_seen);
for (fqn, output_id) in output_seen {
if output_id.disambiguator > 0 {
ids.types.insert(format!("__output__::{fqn}"), output_id);
} else {
ids.types.entry(fqn).or_insert(output_id);
}
}
ids
}
fn register_typespace_ids(
typespace: &Typespace,
seen: &mut HashMap<String, SymbolId>,
members: &mut HashMap<(String, String), SymbolId>,
) {
for ty in typespace.types() {
let type_name = ty.name().to_string();
let id = seen
.entry(type_name.clone())
.or_insert_with(|| {
let kind = match ty {
Type::Primitive(_) => SymbolKind::Primitive,
Type::Struct(_) => SymbolKind::Struct,
Type::Enum(_) => SymbolKind::Enum,
};
SymbolId::new(kind, split_path(&type_name))
})
.clone();
register_type_members(ty, &id, members);
}
}
fn register_type_members(
ty: &Type,
parent_id: &SymbolId,
members: &mut HashMap<(String, String), SymbolId>,
) {
let parent_fqn = parent_id.qualified_name();
match ty {
Type::Primitive(_) => {}
Type::Struct(s) => {
register_struct_members(s, &parent_fqn, parent_id, members);
}
Type::Enum(e) => {
register_enum_members(e, &parent_fqn, parent_id, members);
}
}
}
fn register_struct_members(
s: &Struct,
parent_fqn: &str,
parent_id: &SymbolId,
members: &mut HashMap<(String, String), SymbolId>,
) {
match &s.fields {
Fields::Named(fields) => {
for field in fields {
let mut path = parent_id.path.clone();
path.push(field.name.clone());
members
.entry((parent_fqn.to_string(), field.name.clone()))
.or_insert_with(|| SymbolId::new(SymbolKind::Field, path));
}
}
Fields::Unnamed(fields) => {
for (i, _field) in fields.iter().enumerate() {
let arg_name = format!("arg{i:02}");
let mut path = parent_id.path.clone();
path.push(arg_name.clone());
members
.entry((parent_fqn.to_string(), arg_name))
.or_insert_with(|| SymbolId::new(SymbolKind::Field, path));
}
}
Fields::None => {}
}
}
fn register_enum_members(
e: &Enum,
parent_fqn: &str,
parent_id: &SymbolId,
members: &mut HashMap<(String, String), SymbolId>,
) {
for variant in &e.variants {
let mut variant_path = parent_id.path.clone();
variant_path.push(variant.name.clone());
let variant_id = members
.entry((parent_fqn.to_string(), variant.name.clone()))
.or_insert_with(|| SymbolId::new(SymbolKind::Variant, variant_path.clone()))
.clone();
match &variant.fields {
Fields::Named(fields) => {
for field in fields {
let mut field_path = variant_id.path.clone();
field_path.push(field.name.clone());
let variant_fqn = variant_id.qualified_name();
members
.entry((variant_fqn, field.name.clone()))
.or_insert_with(|| SymbolId::new(SymbolKind::Field, field_path));
}
}
Fields::Unnamed(fields) => {
for (i, _field) in fields.iter().enumerate() {
let arg_name = format!("arg{i:02}");
let mut field_path = variant_id.path.clone();
field_path.push(arg_name.clone());
let variant_fqn = variant_id.qualified_name();
members
.entry((variant_fqn, arg_name))
.or_insert_with(|| SymbolId::new(SymbolKind::Field, field_path));
}
}
Fields::None => {}
}
}
}
fn reregister_members(
ty: &Type,
new_parent_id: &SymbolId,
members: &mut HashMap<(String, String), SymbolId>,
) {
let parent_fqn = new_parent_id.qualified_name();
match ty {
Type::Primitive(_) => {}
Type::Struct(s) => {
register_struct_members(s, &parent_fqn, new_parent_id, members);
}
Type::Enum(e) => {
register_enum_members(e, &parent_fqn, new_parent_id, members);
}
}
}
fn split_path(fqn: &str) -> Vec<String> {
fqn.split("::").map(|s| s.to_string()).collect()
}
impl SchemaIds {
pub fn type_id(&self, fqn: &str) -> SymbolId {
self.types
.get(fqn)
.cloned()
.unwrap_or_else(|| SymbolId::new(infer_kind(fqn), split_path(fqn)))
}
pub fn member_id(&self, parent_fqn: &str, member_name: &str) -> SymbolId {
self.members
.get(&(parent_fqn.to_string(), member_name.to_string()))
.cloned()
.unwrap_or_else(|| {
let mut path = split_path(parent_fqn);
path.push(member_name.to_string());
SymbolId::new(SymbolKind::Field, path)
})
}
}
fn infer_kind(fqn: &str) -> SymbolKind {
external_symbol_kind(fqn).unwrap_or(SymbolKind::Struct)
}
#[cfg(test)]
mod tests {
use super::*;
use reflectapi_schema::{Field, Variant};
#[test]
fn test_split_path() {
assert_eq!(
split_path("std::option::Option"),
vec!["std", "option", "Option"]
);
assert_eq!(
split_path("myapi::proto::Headers"),
vec!["myapi", "proto", "Headers"]
);
assert_eq!(split_path("SimpleType"), vec!["SimpleType"]);
}
#[test]
fn test_build_schema_ids_basic() {
let mut schema = Schema::new();
schema.name = "Test".to_string();
let mut s = Struct::new("api::User");
s.fields = Fields::Named(vec![Field::new("name".into(), "String".into())]);
schema.input_types.insert_type(s.into());
let ids = build_schema_ids(&schema);
assert_eq!(ids.schema_id.kind, SymbolKind::Schema);
assert!(ids.schema_id.path.contains(&"__schema__".to_string()));
let user_id = ids.type_id("api::User");
assert_eq!(user_id.kind, SymbolKind::Struct);
assert_eq!(user_id.path, vec!["api", "User"]);
let field_id = ids.member_id("api::User", "name");
assert_eq!(field_id.kind, SymbolKind::Field);
assert_eq!(field_id.path, vec!["api", "User", "name"]);
}
#[test]
fn test_disambiguated_types() {
let mut schema = Schema::new();
schema.name = "Test".to_string();
let mut input_foo = Struct::new("Foo");
input_foo.fields = Fields::Named(vec![Field::new("a".into(), "u32".into())]);
schema.input_types.insert_type(input_foo.into());
let mut output_foo = Struct::new("Foo");
output_foo.fields = Fields::Named(vec![Field::new("b".into(), "u64".into())]);
schema.output_types.insert_type(output_foo.into());
let ids = build_schema_ids(&schema);
let output_id = ids
.types
.get("__output__::Foo")
.expect("disambiguated output type should exist");
assert_eq!(output_id.disambiguator, 1);
}
#[test]
fn test_enum_member_ids() {
let mut schema = Schema::new();
schema.name = "Test".to_string();
let mut e = Enum::new("api::Status".into());
e.variants = vec![Variant::new("Active".into())];
schema.input_types.insert_type(e.into());
let ids = build_schema_ids(&schema);
let enum_id = ids.type_id("api::Status");
assert_eq!(enum_id.kind, SymbolKind::Enum);
let variant_id = ids.member_id("api::Status", "Active");
assert_eq!(variant_id.kind, SymbolKind::Variant);
assert_eq!(variant_id.path, vec!["api", "Status", "Active"]);
}
#[test]
fn test_zero_padded_tuple_field_ordering() {
let mut schema = Schema::new();
schema.name = "Test".to_string();
let mut tuple_struct = Struct::new("BigTuple");
let fields: Vec<Field> = (0..12)
.map(|i| Field::new(format!("{i}"), format!("u{}", 8 + i).into()))
.collect();
tuple_struct.fields = Fields::Unnamed(fields);
schema.input_types.insert_type(tuple_struct.into());
let ids = build_schema_ids(&schema);
let field0 = ids.member_id("BigTuple", "arg00");
assert_eq!(field0.path.last().unwrap(), "arg00");
let field10 = ids.member_id("BigTuple", "arg10");
assert_eq!(field10.path.last().unwrap(), "arg10");
}
#[test]
fn test_schema_root_id_does_not_collide_with_type() {
let mut schema = Schema::new();
schema.name = "User".to_string();
let user_struct = Struct::new("User");
schema.input_types.insert_type(user_struct.into());
let ids = build_schema_ids(&schema);
let struct_id = ids.type_id("User");
assert_ne!(
ids.schema_id, struct_id,
"Schema root ID should not collide with struct ID"
);
assert!(ids.schema_id.path.contains(&"__schema__".to_string()));
}
}