diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..a0ca206 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,6 @@ +{ + "recommendations": [ + "matklad.rust-analyzer", + "GitHub.vscode-pull-request-github" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 3c90762..20159bd 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,9 @@ The command line args are a subset of those supported by `autorest`. ``` sh cargo run -- --help cargo run -- --input-file=../azure-rest-api-specs/specification/vmware/resource-manager/Microsoft.AVS/stable/2020-03-20/vmware.json +cargo +nightly run --features fmt -- --input-file ../OpenAPI-Specification/examples/v2.0/json/petstore.json ``` ## Status -It is early days. The generated code is not finished. No binaries have been published. You will probably get panics trying out other specs. \ No newline at end of file +It is early days. The generated code is not finished. No binaries have been published. You will probably get panics trying out other specs. I've posted some status videos to an [AutoRust YouTube playlist](https://www.youtube.com/playlist?list=PL6MfGfZ-qCMq1mYjzTdGhKOHfrMFZjjW_). TODO items are starting to be annotated with the GitHub issue numbers using [GitHub Pull Requests and Issues](https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github) extension for VS Code. \ No newline at end of file diff --git a/src/codegen.rs b/src/codegen.rs index 53886a5..34f8c1c 100644 --- a/src/codegen.rs +++ b/src/codegen.rs @@ -1,6 +1,6 @@ #![allow(unused_variables, dead_code)] -use crate::{format_code, pathitem_operations, Reference, Result, Spec}; -use autorust_openapi::{DataType, Operation, Parameter, ReferenceOr, Schema}; +use crate::{format_code, pathitem_operations, OperationVerb, Reference, Result, Spec}; +use autorust_openapi::{DataType, Operation, Parameter, PathItem, ReferenceOr, Schema}; use heck::{CamelCase, SnakeCase}; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; @@ -115,7 +115,7 @@ fn create_struct_field_type( let vec_items_typ = get_type_for_schema(&items)?; quote! {Vec<#vec_items_typ>} } - DataType::Integer => quote! {i32}, + DataType::Integer => quote! {i64}, DataType::Number => quote! {f64}, DataType::String => quote! {String}, DataType::Boolean => quote! {bool}, @@ -202,7 +202,7 @@ fn create_struct( } let st = quote! { - #[derive(Debug, PartialEq, Serialize, Deserialize)] + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct #nm { #props } @@ -226,7 +226,7 @@ fn trim_ref(path: &str) -> String { fn map_type(param_type: &DataType) -> TokenStream { match param_type { DataType::String => quote! { &str }, - DataType::Integer => quote! { i32 }, + DataType::Integer => quote! { i64 }, _ => { quote! {map_type} // TODO may be Err instead } @@ -272,7 +272,7 @@ fn create_function_params(cg: &CodeGen, op: &Operation) -> Result { for param in ¶meters { params.push(get_param_name_and_type(param)?); } - let slf = quote! { &self }; + let slf = quote! { configuration: &Configuration }; params.insert(0, slf); Ok(quote! { #(#params),* }) } @@ -287,7 +287,27 @@ fn get_type_for_schema(schema: &ReferenceOr) -> Result { ); Ok(quote! { #idt }) } - ReferenceOr::Item(_) => { + ReferenceOr::Item(schema) => { + if let Some(schema_type) = &schema.type_ { + let ts = match schema_type { + DataType::Array => { + let items = schema + .items + .as_ref() + .as_ref() + .ok_or_else(|| format!("array expected to have items"))?; + let vec_items_typ = get_type_for_schema(&items)?; + quote! {Vec<#vec_items_typ>} + } + DataType::Integer => quote! {i64}, + DataType::Number => quote! {f64}, + DataType::String => quote! {String}, + DataType::Boolean => quote! {bool}, + DataType::Object => quote! {serde::Value}, + }; + return Ok(ts); + } + // TODO probably need to create a struct // and have a way to name it let idt = ident("NoParamType2"); @@ -297,10 +317,10 @@ fn get_type_for_schema(schema: &ReferenceOr) -> Result { } // TODO is _ref_param not needed for a return -fn create_function_return(op: &Operation) -> Result { +fn create_function_return(verb: &OperationVerb) -> Result { // TODO error responses // TODO union of responses - for (_http_code, rsp) in op.responses.iter() { + for (_http_code, rsp) in verb.operation().responses.iter() { // println!("response key {:#?} {:#?}", key, rsp); if let Some(schema) = &rsp.schema { let tp = get_type_for_schema(schema)?; @@ -310,18 +330,32 @@ fn create_function_return(op: &Operation) -> Result { Ok(quote! { Result<()> }) } +/// Creating a function name from the path and verb when an operationId is not specified. +/// All azure-rest-api-specs operations should have an operationId. +fn create_function_name(path: &str, verb_name: &str) -> String { + let mut path = path + .split('/') + .filter(|&x| !x.is_empty()) + .collect::>(); + path.push(verb_name); + path.join("_") +} + fn create_function( cg: &CodeGen, path: &str, - op: &Operation, + item: &PathItem, + operation_verb: &OperationVerb, param_re: &Regex, ) -> Result { - let name_default = "operation_id_missing"; - let name = ident( - op.operation_id + let fname = ident( + operation_verb + .operation() + .operation_id .as_ref() - .map(String::as_ref) - .unwrap_or(name_default), + .unwrap_or(&create_function_name(path, operation_verb.verb_name())) + .to_snake_case() + .as_ref(), ); let params = parse_params(param_re, path); @@ -333,19 +367,28 @@ fn create_function( // get path parameters // Option if not required - let fparams = create_function_params(cg, op)?; + let fparams = create_function_params(cg, operation_verb.operation())?; // see if there is a body parameter - let fresponse = create_function_return(op)?; + let fresponse = create_function_return(operation_verb)?; + + let client_verb = match operation_verb { + OperationVerb::Get(_) => quote! { client.get(uri_str) }, + OperationVerb::Post(_) => quote! { client.post(uri_str) }, + OperationVerb::Put(_) => quote! { client.put(uri_str) }, + OperationVerb::Patch(_) => quote! { client.patch(uri_str) }, + OperationVerb::Delete(_) => quote! { client.delete(uri_str) }, + OperationVerb::Options(_) => quote! { client.options(uri_str) }, + OperationVerb::Head(_) => quote! { client.head(uri_str) }, + }; + // TODO #17 decode the different errors depending on http status + // TODO #18 other callbacks like auth let func = quote! { - pub async fn #name(#fparams) -> #fresponse { - let configuration = self.configuration; + pub async fn #fname(#fparams) -> #fresponse { let client = &configuration.client; - let uri_str = format!(#fpath, configuration.base_path, #uri_str_args); - // TODO client.get, put, post, delete - let mut req_builder = client.get(uri_str.as_str()); - // TODO other callbacks like auth + let uri_str = &format!(#fpath, &configuration.base_path, #uri_str_args); + let mut req_builder = #client_verb; let req = req_builder.build()?; let res = client.execute(req).await?; match res.error_for_status_ref() { @@ -370,7 +413,7 @@ pub fn create_client(cg: &CodeGen) -> Result { // println!("{}", path); for op in pathitem_operations(item) { // println!("{:?}", op.operation_id); - file.extend(create_function(cg, &path, &op, ¶m_re)) + file.extend(create_function(cg, path, item, &op, ¶m_re)) } } Ok(file) @@ -393,4 +436,9 @@ mod tests { let idt = ident("3.2"); assert_eq!(idt.to_string(), "_3_2"); } + + #[test] + fn test_create_function_name() { + assert_eq!(create_function_name("/pets", "get"), "pets_get"); + } } diff --git a/src/spec.rs b/src/spec.rs index 6fe4f03..112f144 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -1,5 +1,7 @@ use crate::{path_join, Reference, Result}; -use autorust_openapi::{AdditionalProperties, OpenAPI, Operation, Parameter, PathItem, ReferenceOr, Schema}; +use autorust_openapi::{ + AdditionalProperties, OpenAPI, Operation, Parameter, PathItem, ReferenceOr, Schema, +}; use indexmap::{IndexMap, IndexSet}; use std::{fs::File, io::prelude::*}; @@ -195,20 +197,58 @@ pub fn read_api_file(path: &str) -> Result { Ok(api) } -pub fn pathitem_operations(item: &PathItem) -> impl Iterator { +pub enum OperationVerb<'a> { + Get(&'a Operation), + Post(&'a Operation), + Put(&'a Operation), + Patch(&'a Operation), + Delete(&'a Operation), + Options(&'a Operation), + Head(&'a Operation), +} + +// Hold an operation and remembers the operation verb. +impl<'a> OperationVerb<'a> { + pub fn operation(&self) -> &'a Operation { + match self { + OperationVerb::Get(op) => op, + OperationVerb::Post(op) => op, + OperationVerb::Put(op) => op, + OperationVerb::Patch(op) => op, + OperationVerb::Delete(op) => op, + OperationVerb::Options(op) => op, + OperationVerb::Head(op) => op, + } + } + + pub fn verb_name(&self) -> &'static str { + match self { + OperationVerb::Get(_) => "get", + OperationVerb::Post(_) => "post", + OperationVerb::Put(_) => "put", + OperationVerb::Patch(_) => "patch", + OperationVerb::Delete(_) => "delete", + OperationVerb::Options(_) => "options", + OperationVerb::Head(_) => "head", + } + } +} + +pub fn pathitem_operations(item: &PathItem) -> impl Iterator { vec![ - item.get.as_ref(), - item.post.as_ref(), - item.put.as_ref(), - item.patch.as_ref(), - item.delete.as_ref(), - item.options.as_ref(), - item.head.as_ref(), + item.get.as_ref().map(OperationVerb::Get), + item.post.as_ref().map(OperationVerb::Post), + item.put.as_ref().map(OperationVerb::Put), + item.patch.as_ref().map(OperationVerb::Patch), + item.delete.as_ref().map(OperationVerb::Delete), + item.options.as_ref().map(OperationVerb::Options), + item.head.as_ref().map(OperationVerb::Head), ] .into_iter() .filter_map(|x| x) } +/// Holds a $ref string and remembers the type. #[derive(Clone, Debug, PartialEq)] pub enum RefString { PathItem(String), @@ -245,7 +285,7 @@ fn add_refs_for_schema(list: &mut Vec, schema: &Schema) { list.push(RefString::Schema(reference.to_owned())) } ReferenceOr::Item(schema) => add_refs_for_schema(list, schema), - } + }, }, _ => {} } @@ -279,7 +319,8 @@ pub fn get_refs(api: &OpenAPI) -> Vec { list.push(RefString::PathItem(reference.to_owned())) } ReferenceOr::Item(item) => { - for op in pathitem_operations(&item) { + for verb in pathitem_operations(&item) { + let op = verb.operation(); // parameters for prm in &op.parameters { match prm {