use std::borrow::Cow;
use pyro_spec::{CapabilityFunc, ClassSpec, InterfaceSpec, PyroField, PyroSchema, PyroType};
use syn::{Attribute, Expr, Lit, Meta};
use crate::ffi::has_attr;
use super::capability::CapabilityImpl;
use crate::struct_doc::SchemaBuilder;
pub fn build_class_spec<'b>(
cap: &CapabilityImpl,
builder: &'b SchemaBuilder,
) -> ClassSpec<'static> {
let capability = cap.ident.state_tn.to_string();
let description = extract_doc_string(&cap.attrs);
let methods = cap
.methods
.iter()
.map(|m| {
let name = m.name.to_string().into();
let description = extract_doc_string(&m.attrs).map(|s| s.into());
let output = fn_output_to_pyro_type(&m.output, builder);
let fields: Vec<PyroField<'static>> = m
.inputs
.iter()
.map(|(ident, ty)| {
let data_type = builder.resolve_type(ty);
let nullable = SchemaBuilder::is_option(ty);
PyroField::new(Cow::Owned(ident.to_string()), data_type, nullable)
})
.collect();
let input = PyroSchema {
documentation: None,
fields: fields.into(),
};
CapabilityFunc {
name,
description,
input,
output,
}
})
.collect();
let client = builder.schema_for(&cap.ident.client_tn.to_string());
let config = if let Some(config_tn) = &cap.ident.config_tn {
builder.schema_for(&config_tn.to_string())
} else {
None
};
ClassSpec {
name: capability.into(),
description: description.map(|s| s.into()),
client,
config,
methods,
}
}
pub fn build_spec(file: &syn::File) -> InterfaceSpec<'static> {
let builder = SchemaBuilder::from_file(file);
let mut classes: Vec<ClassSpec<'static>> = Vec::new();
let mut covered_items = Vec::new();
for item in &file.items {
if let syn::Item::Impl(item_impl) = item {
if !has_attr(&item_impl.attrs, "capability") {
continue;
}
let Ok(cap) = CapabilityImpl::new(item_impl.clone(), false, "", "") else {
continue;
};
classes.push(build_class_spec(&cap, &builder));
covered_items.push(cap.ident.client_tn.to_string());
if let Some(config_tn) = &cap.ident.config_tn {
covered_items.push(config_tn.to_string())
}
}
}
let (capability, description) = classes
.first()
.map(|c| (c.name.clone(), c.description.clone()))
.unwrap_or_else(|| (Cow::Borrowed(""), None));
InterfaceSpec {
capability,
description,
classes,
}
}
fn extract_doc_string(attrs: &[Attribute]) -> Option<String> {
let mut lines = Vec::new();
for attr in attrs {
if attr.path().is_ident("doc") {
if let Meta::NameValue(nv) = &attr.meta {
if let Expr::Lit(expr_lit) = &nv.value {
if let Lit::Str(lit_str) = &expr_lit.lit {
lines.push(lit_str.value().trim().to_string());
}
}
}
}
}
if lines.is_empty() {
None
} else {
Some(lines.join("\n"))
}
}
fn fn_output_to_pyro_type(
output: &super::paths::FnOutput,
builder: &SchemaBuilder,
) -> PyroType<'static> {
match output {
super::paths::FnOutput::None => PyroType::Null,
super::paths::FnOutput::Single(ty) => builder.resolve_type(ty),
super::paths::FnOutput::Result(ok_ty, _err_ty) => builder.resolve_type(ok_ty),
}
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
#[test]
fn test_spec_generation_full() {
let file: syn::File = syn::parse2(quote! {
#[config]
pub struct MyClient {
pub id: u32,
pub name: String,
}
#[config]
pub struct InputStruct {
pub foo: Bytes,
}
#[capability]
impl MyServer {
type Client = MyClient;
fn new() -> Self { Self }
fn reset(&mut self) {}
fn register(&self, c: &MyClient) {}
fn calculate(&self, c: &MyClient, input: f32) -> f32 {
input * 2.0
}
fn process(&self, c: &MyClient, data: Option<Vec<u8>>) -> Result<InputStruct, MyError> {
todo!()
}
}
}).unwrap();
let output = build_spec(&file);
let expected: InterfaceSpec<'static> = serde_json::from_str(
r#"{
"capability": "MyServer",
"description": "The Server Implementation",
"classes": [
{
"name": "MyServer",
"description": "The Server Implementation",
"client": {
"name": "MyClient",
"documentation": "The Client State",
"fields": [
{
"name": "id",
"documentation": "The id",
"data_type": { "PrimitiveScalar": "U32" },
"nullable": false
},
{
"name": "name",
"documentation": null,
"data_type": "Str",
"nullable": false
}
]
},
"config": null,
"methods": [
{
"name": "calculate",
"description": "Calculates a value",
"input": {
"documentation": null,
"fields": [
{
"name": "input",
"documentation": null,
"data_type": { "PrimitiveScalar": "F32" },
"nullable": false
}
]
},
"output": { "PrimitiveScalar": "F32" }
},
{
"name": "process",
"description": "Processes the data",
"input": {
"documentation": null,
"fields": [
{
"name": "data",
"documentation": null,
"data_type": { "PrimitiveList": "U8" },
"nullable": true
}
]
},
"output": {
"Group": [
{
"name": "foo",
"documentation": null,
"data_type": { "PrimitiveList": "U8" },
"nullable": false
}
]
}
}
]
}
]
}"#,
)
.unwrap();
assert_eq!(&output, &expected);
}
}