Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discover module #3

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
/target
*target
/Cargo.lock
git_cmd.md
git_cmd.md
26 changes: 13 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
[package]
name = "utoipa_auto_discovery"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
authors = ["RxDiscovery"]
rust-version = "1.69"
keywords = ["utoipa","openapi","swagger", "path", "auto"]
authors = ["RxDiscovery", "ProbablyClem"]
rust-version = "1.69"
keywords = ["utoipa", "openapi", "swagger", "path", "auto"]
description = "Rust Macros to automate the addition of Paths/Schemas to Utoipa crate, simulating Reflection during the compilation phase"
categories = ["parsing","development-tools::procedural-macro-helpers","web-programming"]
categories = [
"parsing",
"development-tools::procedural-macro-helpers",
"web-programming",
]
license = "MIT OR Apache-2.0"
readme = "README.md"
repository = "https://github.com/rxdiscovery/utoipa_auto_discovery"
homepage = "https://github.com/rxdiscovery/utoipa_auto_discovery"


[lib]
proc-macro = true

[dependencies]
quote = "1.0.28"
syn = { version ="2.0.18", features = [ "full" ]}
proc-macro2 = "1.0.59"

[dependencies]
utoipa-auto-macro = { version = "0.4.0", path = "./utoipa-auto-macro" }


[build-dependencies]
[dev-dependencies]
utoipa = { version = "4.1.0", features = ["preserve_path_order"] }
41 changes: 37 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,16 @@ then add the `#[utoipa_auto_discovery]` macro just before the #[derive(OpenApi)]
Put `#[utoipa_auto_discovery]` before #[derive(OpenApi)] and `#[openapi]` macros.

```rust
#[utoipa_auto_discovery(paths = "( MODULE_TREE::MODULE_NAME => MODULE_SRC_FILE_PATH ) ; ( MODULE_TREE::MODULE_NAME => MODULE_SRC_FILE_PATH ) ; ... ;")]
#[utoipa_auto_discovery(paths = "MODULE_SRC_FILE_PATH, MODULE_SRC_FILE_PATH, ...")]
```

the paths receives a String which must respect this structure :

`" ( MODULE_TREE_PATH => MODULE_SRC_FILE_PATH ) ;"`
`MODULE_SRC_FILE_PATH, MODULE_SRC_FILE_PATH, ...`"`

you can add several pairs (Module Path => Src Path ) by separating them with a semicolon ";".
you can add several paths by separating them with a coma ",".

### Import from filename

Here's an example of how to add all the methods contained in the test_controller and test2_controller modules.
you can also combine automatic and manual addition, as here we've added a method manually to the documentation "other_controller::get_users".
Expand All @@ -97,7 +99,7 @@ use utoipa_auto_discovery::utoipa_auto_discovery;

...
#[utoipa_auto_discovery(
paths = "( crate::rest::test_controller => ./src/rest/test_controller.rs ) ; ( crate::rest::test2_controller => ./src/rest/test2_controller.rs )"
paths = "./src/rest/test_controller.rs,./src/rest/test2_controller.rs "
)]
#[derive(OpenApi)]
#[openapi(
Expand All @@ -120,6 +122,36 @@ pub struct ApiDoc;

```

### Import from module

Here's an example of how to add all the methods contained in the rest module.

```rust
...

use utoipa_auto_discovery::utoipa_auto_discovery;

...
#[utoipa_auto_discovery(
paths = "./src/rest"
)]
#[derive(OpenApi)]
#[openapi(
components(
schemas(TestDTO)
),
tags(
(name = "todo", description = "Todo management endpoints.")
),
modifiers(&SecurityAddon)
)]

pub struct ApiDoc;

...

```

## exclude a method of automatic scanning

you can exclude a function from the Doc Path list by adding the following macro `#[utoipa_ignore]` .
Expand Down Expand Up @@ -149,4 +181,5 @@ sub-modules within a module containing methods tagged with utoipa::path are also
# Features

- [x] automatic path detection
- [x] automatic import from module
- [ ] automatic schema detection (in progress)
260 changes: 1 addition & 259 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,259 +1 @@
extern crate proc_macro;

use std::{fs::File, io::Read, path::PathBuf};

use proc_macro::TokenStream;

use quote::{quote, ToTokens};
use syn::parse_macro_input;

fn rem_first_and_last(value: &str) -> &str {
let mut chars = value.chars();
chars.next();
chars.next_back();
chars.as_str()
}

fn parse_file<T: Into<PathBuf>>(filepath: T) -> Result<syn::File, ()> {
let pb: PathBuf = filepath.into();

if pb.is_file() {
let mut file = File::open(pb).unwrap();
let mut content = String::new();
file.read_to_string(&mut content).unwrap();

let sf = syn::parse_file(&content).unwrap();
Ok(sf)
} else {
Err(())
}
}

fn get_all_mod_uto_functions(item: &syn::ItemMod, fns_name: &mut Vec<String>) {
let sub_items = &item.content.iter().next().unwrap().1;

let mod_name = item.ident.to_string();

for it in sub_items {
match it {
syn::Item::Mod(m) => get_all_mod_uto_functions(m, fns_name),
syn::Item::Fn(f) => {
if !f.attrs.is_empty()
&& !f.attrs.iter().any(|attr| {
if let Some(name) = attr.path().get_ident() {
name.eq("utoipa_ignore")
} else {
false
}
})
{
for i in 0..f.attrs.len() {
if f.attrs[i]
.meta
.path()
.segments
.iter()
.any(|item| item.ident.eq("utoipa"))
{
fns_name.push(format!("{}::{}", mod_name, f.sig.ident));
}
}
}
}

_ => {}
}
}
}

fn get_all_uto_functions(src_path: String) -> Vec<String> {
let mut fns_name: Vec<String> = vec![];

let sc = parse_file(src_path);
if let Ok(sc) = sc {
let items = sc.items;

for i in items {
match i {
syn::Item::Mod(m) => get_all_mod_uto_functions(&m, &mut fns_name),
syn::Item::Fn(f) => {
if !f.attrs.is_empty()
&& !f.attrs.iter().any(|attr| {
if let Some(name) = attr.path().get_ident() {
name.eq("utoipa_ignore")
} else {
false
}
})
{
for i in 0..f.attrs.len() {
if f.attrs[i]
.meta
.path()
.segments
.iter()
.any(|item| item.ident.eq("utoipa"))
{
fns_name.push(f.sig.ident.to_string());
}
}
}
}

_ => {}
}
}
}

fns_name
}

#[proc_macro_attribute]
pub fn utoipa_ignore(
_attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let input = parse_macro_input!(item as syn::Item);
let code = quote!(
#input
);

TokenStream::from(code)
}

#[proc_macro_attribute]
pub fn utoipa_auto_discovery(
attr: proc_macro::TokenStream,
item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
let mut input = parse_macro_input!(item as syn::ItemStruct);

let mut paths: String = "".to_string();

if !attr.is_empty() {
let mut it = attr.into_iter();
let tok = it.next();

if let Some(proc_macro::TokenTree::Ident(ident)) = tok {
if ident.to_string().eq("paths") {
let tok = it.next();
if let Some(proc_macro::TokenTree::Punct(punct)) = tok {
if punct.to_string().eq("=") {
let tok = it.next();
if let Some(tok) = tok {
match tok {
proc_macro::TokenTree::Literal(lit) => {
paths = lit.to_string();
}
_ => {
panic!("malformed paths !")
}
}
}
}
}
}
}
}

let mut pairs: Vec<(String, String)> = vec![];

let str_paths = trim_parentheses(rem_first_and_last(paths.as_str()));

if str_paths.contains('|') {
panic!("Please use the new syntax ! paths=\"(MODULE_TREE_PATH => MODULE_SRC_PATH) ;\"")
}

let paths = str_paths.split(';');

for p in paths {
let pair = p.split_once("=>");

if let Some(pair) = pair {
pairs.push((trim_whites(pair.0), trim_whites(pair.1)));
}
}

if !pairs.is_empty() {
let mut uto_paths: String = String::new();

for p in pairs {
let list_fn = get_all_uto_functions(p.1);

if !list_fn.is_empty() {
for i in list_fn {
uto_paths.push_str(format!("{}::{},", p.0, i).as_str());
}
}
}

let attrs = &mut input.attrs;

if !attrs.iter().any(|elm| elm.path().is_ident("derive")) {
panic!("Please put utoipa_auto_discovery before #[derive] and #[openapi]");
}

if !attrs.iter().any(|elm| elm.path().is_ident("openapi")) {
panic!("Please put utoipa_auto_discovery before #[derive] and #[openapi]");
}

let mut is_ok: bool = false;
#[warn(clippy::needless_range_loop)]
for i in 0..attrs.len() {
if attrs[i].path().is_ident("openapi") {
is_ok = true;
let mut src_uto_macro = attrs[i].to_token_stream().to_string();

src_uto_macro = src_uto_macro.replace("#[openapi(", "");
src_uto_macro = src_uto_macro.replace(")]", "");

if !src_uto_macro.contains("paths(") {
let new_paths = format!("paths({}", uto_paths);
src_uto_macro = format!("{}), {}", new_paths, src_uto_macro);

let stream: proc_macro2::TokenStream = src_uto_macro.parse().unwrap();

let new_attr: syn::Attribute = syn::parse_quote! { #[openapi( #stream )] };

attrs[i] = new_attr;
} else {
let new_paths = format!("paths({}", uto_paths);

src_uto_macro = src_uto_macro.replace("paths(", new_paths.as_str());

let stream: proc_macro2::TokenStream = src_uto_macro.parse().unwrap();

let new_attr: syn::Attribute = syn::parse_quote! { #[openapi( #stream )] };

attrs[i] = new_attr;
}
}
}

if !is_ok {
panic!("No utoipa::openapi Macro found !");
}
}

let code = quote!(
#input
);

TokenStream::from(code)
}

fn trim_whites(str: &str) -> String {
let s = str.trim();

let s: String = s.replace('\n', "");

s
}

fn trim_parentheses(str: &str) -> String {
let s = str.trim();

let s: String = s.replace(['(', ')'], "");

s
}
pub use utoipa_auto_macro::*;
8 changes: 8 additions & 0 deletions tests/controllers/controller1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
use utoipa_auto_macro::utoipa_ignore;

#[utoipa::path(post, path = "/route1")]
pub fn route1() {}

#[utoipa_ignore]
#[utoipa::path(post, path = "/route-ignored")]
pub fn route_ignored() {}
2 changes: 2 additions & 0 deletions tests/controllers/controller2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#[utoipa::path(post, path = "/route3")]
pub fn route3() {}
2 changes: 2 additions & 0 deletions tests/controllers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod controller1;
pub mod controller2;
Loading