forked from 64bit/async-openai
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Generate extensible versions of methods by allowing generic requests …
…and response Fixes 64bit#280
- Loading branch information
Showing
8 changed files
with
168 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
[workspace] | ||
members = [ "async-openai", "examples/*" ] | ||
members = [ "async-openai", "async-openai-macros", "examples/*" ] | ||
# Only check / build main crates by default (check all with `--workspace`) | ||
default-members = ["async-openai"] | ||
default-members = ["async-openai", "async-openai-macros"] | ||
resolver = "2" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
[package] | ||
name = "async-openai-macros" | ||
version = "0.1.0" | ||
authors = ["Jean-Sébastien Bour <[email protected]>"] | ||
categories = ["api-bindings", "web-programming", "asynchronous"] | ||
keywords = ["openai", "async", "openapi", "ai"] | ||
description = "Procedural macros for async-openai" | ||
edition = "2021" | ||
rust-version = "1.70" | ||
license = "MIT" | ||
readme = "README.md" | ||
homepage = "https://github.com/64bit/async-openai" | ||
repository = "https://github.com/64bit/async-openai" | ||
|
||
[lib] | ||
proc-macro = true | ||
|
||
[dependencies] | ||
darling = "0.20" | ||
itertools = "0.13" | ||
proc-macro2 = "1" | ||
quote = "1" | ||
syn = "2" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<div align="center"> | ||
<a href="https://docs.rs/async-openai-macros"> | ||
<img width="50px" src="https://raw.githubusercontent.com/64bit/async-openai/assets/create-image-b64-json/img-1.png" /> | ||
</a> | ||
</div> | ||
<h1 align="center"> async-openai-macros </h1> | ||
<p align="center"> Procedural macros for async-openai </p> | ||
<div align="center"> | ||
<a href="https://crates.io/crates/async-openai-macros"> | ||
<img src="https://img.shields.io/crates/v/async-openai-macros.svg" /> | ||
</a> | ||
<a href="https://docs.rs/async-openai-macros"> | ||
<img src="https://docs.rs/async-openai-macros/badge.svg" /> | ||
</a> | ||
</div> | ||
<div align="center"> | ||
<sub>Logo created by this <a href="https://github.com/64bit/async-openai/tree/main/examples/create-image-b64-json">repo itself</a></sub> | ||
</div> | ||
|
||
## Overview | ||
|
||
This crate contains the procedural macros for `async-openai`. It is not meant to be used directly. | ||
|
||
## License | ||
|
||
This project is licensed under [MIT license](https://github.com/64bit/async-openai/blob/main/LICENSE). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
use darling::{ast::NestedMeta, FromMeta}; | ||
use itertools::{Either, Itertools}; | ||
use proc_macro2::TokenStream; | ||
use quote::{format_ident, quote}; | ||
use syn::{parse2, parse_macro_input, Expr, FnArg, ItemFn, Meta, MetaList}; | ||
|
||
#[proc_macro_attribute] | ||
pub fn extensible( | ||
_: proc_macro::TokenStream, | ||
item: proc_macro::TokenStream, | ||
) -> proc_macro::TokenStream { | ||
let item = parse_macro_input!(item as ItemFn); | ||
extensible_impl(item) | ||
.unwrap_or_else(syn::Error::into_compile_error) | ||
.into() | ||
} | ||
|
||
fn extensible_impl(mut item: ItemFn) -> syn::Result<TokenStream> { | ||
// Prepare a generic method with a different name | ||
let mut extension = item.clone(); | ||
extension.sig.ident = format_ident!("{}_ext", extension.sig.ident); | ||
|
||
// Remove our attributes from original method arguments | ||
for input in &mut item.sig.inputs { | ||
match input { | ||
FnArg::Receiver(_) => (), | ||
FnArg::Typed(arg) => arg.attrs.retain(|attr| match &attr.meta { | ||
Meta::List(meta) => !attr_is_ours(meta), | ||
_ => true, | ||
}), | ||
} | ||
} | ||
|
||
// Gather request parameters that must be replaced by generics and their optional bounds | ||
let mut i = 0; | ||
let generics = extension | ||
.sig | ||
.inputs | ||
.iter_mut() | ||
.filter_map(|input| match input { | ||
FnArg::Receiver(_) => None, | ||
FnArg::Typed(arg) => { | ||
let (mine, other): (Vec<_>, Vec<_>) = | ||
arg.attrs | ||
.clone() | ||
.into_iter() | ||
.partition_map(|attr| match &attr.meta { | ||
Meta::List(meta) if attr_is_ours(meta) => Either::Left( | ||
Request::from_list( | ||
&NestedMeta::parse_meta_list(meta.tokens.clone()).unwrap(), | ||
) | ||
.unwrap(), | ||
), | ||
_ => Either::Right(attr), | ||
}); | ||
let bounds = mine.into_iter().next(); | ||
arg.attrs = other; | ||
bounds.map(|b| { | ||
let ident = format_ident!("__EXTENSIBLE_REQUEST_{i}"); | ||
arg.ty = Box::new(parse2(quote! { #ident }).unwrap()); | ||
i += 1; | ||
(ident, b) | ||
}) | ||
} | ||
}) | ||
.collect::<Vec<_>>(); | ||
|
||
// Add generics and their optional bounds to our method's generics | ||
for (ident, Request { bounds }) in generics { | ||
let bounds = bounds.map(|b| quote! { + #b }); | ||
extension | ||
.sig | ||
.generics | ||
.params | ||
.push(parse2(quote! { #ident : ::serde::Serialize #bounds })?) | ||
} | ||
|
||
// Make the result type generic too | ||
extension.sig.output = parse2(quote! { -> Result<__EXTENSIBLE_RESPONSE, OpenAIError> })?; | ||
extension.sig.generics.params.push(parse2( | ||
quote! { __EXTENSIBLE_RESPONSE: serde::de::DeserializeOwned }, | ||
)?); | ||
|
||
Ok(quote! { | ||
#item | ||
|
||
#extension | ||
}) | ||
} | ||
|
||
#[derive(FromMeta)] | ||
struct Request { | ||
bounds: Option<Expr>, | ||
} | ||
|
||
fn attr_is_ours(meta: &MetaList) -> bool { | ||
meta.path.get_ident().map(|ident| ident.to_string()) == Some("request".to_string()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters