use std::borrow::Cow;
use heck::AsSnakeCase;
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(cap: &CapabilityImpl, builder: &SchemaBuilder) -> ClassSpec<'static> {
let capability = AsSnakeCase(cap.ident.state_tn.to_string()).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(name: &str, 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 description = extract_doc_string(&file.attrs);
let mut structs = std::collections::BTreeMap::new();
for struct_name in builder.struct_names() {
if let Some(schema) = builder.schema_for(&struct_name) {
structs.insert(Cow::Owned(struct_name), schema.into_owned());
}
}
InterfaceSpec {
capability: name.to_string().into(),
description: description.map(|s| s.into()),
classes,
structs,
}
}
fn extract_doc_string(attrs: &[Attribute]) -> Option<String> {
let mut lines = Vec::new();
for attr in attrs {
if attr.path().is_ident("doc")
&& let Meta::NameValue(nv) = &attr.meta
&& let Expr::Lit(expr_lit) = &nv.value
&& 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> {
builder.resolve_type(&output.ok_type)
}
#[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() -> Result<Self, CapturedError> { Ok(Self) }
fn reset(&mut self) -> Result<(), CapturedError> { Ok(()) }
fn register(&self, c: &MyClient) -> Result<(), CapturedError> { Ok(()) }
fn calculate(&self, c: &MyClient, input: f32) -> Result<f32, CapturedError> {
Ok(input * 2.0)
}
fn process(&self, c: &MyClient, data: Option<Vec<u8>>) -> Result<InputStruct, CapturedError> {
todo!()
}
}
}).unwrap();
let output = build_spec("MyServer", &file);
let expected: InterfaceSpec<'static> = serde_json::from_str(
r#"{
"capability": "MyServer",
"description": null,
"classes": [
{
"name": "my_server",
"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
}
]
}
}
]
}
],
"structs": {
"InputStruct": {
"documentation": null,
"fields": [
{
"name": "foo",
"documentation": null,
"data_type": { "PrimitiveList": "U8" },
"nullable": false
}
]
},
"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
}
]
}
}
}"#,
)
.unwrap();
assert_eq!(&output, &expected);
}
}