From 860a4fd0ad8089fd734dff4a117785a668257fe1 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 7 Jan 2020 13:09:19 +0100 Subject: [PATCH] api-macro: add basic struct handling - handle doc comments for descriptions - infer fields from structs when possible - perform some basic error checking Signed-off-by: Wolfgang Bumiller --- proxmox-api-macro/src/api/structs.rs | 116 ++++++++++++++++++++++++--- proxmox-api-macro/tests/types.rs | 8 +- 2 files changed, 108 insertions(+), 16 deletions(-) diff --git a/proxmox-api-macro/src/api/structs.rs b/proxmox-api-macro/src/api/structs.rs index e424e8c9..5782321b 100644 --- a/proxmox-api-macro/src/api/structs.rs +++ b/proxmox-api-macro/src/api/structs.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::convert::TryInto; use failure::Error; @@ -6,7 +7,8 @@ use proc_macro2::{Ident, TokenStream}; use quote::quote_spanned; use super::Schema; -use crate::util::{self, JSONObject}; +use crate::api; +use crate::util::{self, FieldName, JSONObject}; pub fn handle_struct(attribs: JSONObject, mut stru: syn::ItemStruct) -> Result { let mut schema: Schema = attribs.try_into()?; @@ -26,11 +28,11 @@ pub fn handle_struct(attribs: JSONObject, mut stru: syn::ItemStruct) -> Result handle_regular_struct(schema, &mut stru), + syn::Fields::Named(_) => handle_regular_struct(schema, &mut stru), } } -pub fn finish_schema( +fn finish_schema( schema: Schema, stru: &syn::ItemStruct, name: &Ident, @@ -49,16 +51,110 @@ pub fn finish_schema( }) } -pub fn handle_newtype_struct( - schema: Schema, - stru: &mut syn::ItemStruct, -) -> Result { +fn handle_newtype_struct(schema: Schema, stru: &mut syn::ItemStruct) -> Result { finish_schema(schema, &stru, &stru.ident) } -pub fn handle_regular_struct( - schema: Schema, +fn handle_regular_struct( + mut schema: Schema, stru: &mut syn::ItemStruct, ) -> Result { - todo!(); + // sanity check, first get us some quick by-name access to our fields: + // + // NOTE: We remove references we're "done with" and in the end fail with a list of extraneous + // fields if there are any. + let mut schema_fields: HashMap = HashMap::new(); + + // We also keep a reference to the SchemaObject around since we derive missing fields + // automatically. + if let api::SchemaItem::Object(ref mut obj) = &mut schema.item { + for field in &mut obj.properties { + schema_fields.insert(field.0.as_str().to_string(), field); + } + } else { + bail!(schema.span, "structs need an object schema"); + } + + let mut new_fields: Vec<(FieldName, bool, Schema)> = Vec::new(); + + if let syn::Fields::Named(ref fields) = &stru.fields { + for field in &fields.named { + let ident: &Ident = field + .ident + .as_ref() + .ok_or_else(|| format_err!(field => "field without name?"))?; + + let ident_name: String = ident.to_string(); + + let field_def: &mut (FieldName, bool, Schema) = match schema_fields.remove(&ident_name) + { + Some(field) => field, + None => { + new_fields.push(( + FieldName::new(ident_name.clone(), ident.span()), + false, + Schema::blank(ident.span()), + )); + new_fields.last_mut().unwrap() + } + }; + + handle_regular_field(field_def, field)?; + } + } else { + unreachable!(); + }; + + // add the fields we derived: + if let api::SchemaItem::Object(ref mut obj) = &mut schema.item { + obj.properties.extend(new_fields); + } else { + unreachable!(); + } + + finish_schema(schema, &stru, &stru.ident) +} + +/// Field handling: +/// +/// For each field we derive the description from doc-attributes if available. +fn handle_regular_field( + field_def: &mut (FieldName, bool, Schema), + field: &syn::Field, +) -> Result<(), Error> { + let schema: &mut Schema = &mut field_def.2; + + if schema.description.is_none() { + let (doc_comment, doc_span) = util::get_doc_comments(&field.attrs)?; + util::derive_descriptions(schema, &mut None, &doc_comment, doc_span)?; + } + + util::infer_type(schema, &field.ty)?; + + if is_option_type(&field.ty) { + if !field_def.1 { + bail!(&field.ty => "non-optional Option type?"); + } + } + + Ok(()) +} + +/// Note that we cannot handle renamed imports at all here... +fn is_option_type(ty: &syn::Type) -> bool { + if let syn::Type::Path(p) = ty { + if p.qself.is_some() { + return false; + } + let segs = &p.path.segments; + match segs.len() { + 1 => return segs.last().unwrap().ident == "Option", + 2 => { + return segs.first().unwrap().ident == "std" + && segs.last().unwrap().ident == "Option" + } + _ => return false, + } + } + false } diff --git a/proxmox-api-macro/tests/types.rs b/proxmox-api-macro/tests/types.rs index b03d52e4..0c3c6481 100644 --- a/proxmox-api-macro/tests/types.rs +++ b/proxmox-api-macro/tests/types.rs @@ -17,15 +17,11 @@ use serde_json::Value; pub struct OkString(String); #[api( - properties: { - test: { - type: String, - description: "Hello", - }, - }, + properties: {} )] /// A Foo. pub struct Foo { + /// A test string. test: String, }