├── .gitignore ├── feign ├── src │ ├── re_exports │ │ ├── anyhow.rs │ │ ├── serde.rs │ │ ├── reqwest.rs │ │ ├── serde_json.rs │ │ ├── serde_derive.rs │ │ └── mod.rs │ ├── tests.rs │ └── lib.rs ├── Cargo.toml └── README.md ├── images └── icon.png ├── Cargo.toml ├── feign-macros ├── README.md ├── Cargo.toml └── src │ └── lib.rs ├── test ├── test-server │ ├── Cargo.toml │ └── src │ │ └── main.rs └── test-feign │ ├── Cargo.toml │ └── src │ └── main.rs ├── README.md └── guides └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | Cargo.lock 3 | 4 | .idea/ 5 | -------------------------------------------------------------------------------- /feign/src/re_exports/anyhow.rs: -------------------------------------------------------------------------------- 1 | pub use anyhow::*; 2 | -------------------------------------------------------------------------------- /feign/src/re_exports/serde.rs: -------------------------------------------------------------------------------- 1 | pub use serde::*; 2 | -------------------------------------------------------------------------------- /feign/src/re_exports/reqwest.rs: -------------------------------------------------------------------------------- 1 | pub use reqwest::*; 2 | -------------------------------------------------------------------------------- /feign/src/re_exports/serde_json.rs: -------------------------------------------------------------------------------- 1 | pub use serde_json::*; 2 | -------------------------------------------------------------------------------- /feign/src/re_exports/serde_derive.rs: -------------------------------------------------------------------------------- 1 | pub use serde_derive::*; 2 | -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niuhuan/feign-rs/HEAD/images/icon.png -------------------------------------------------------------------------------- /feign/src/re_exports/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod anyhow; 2 | pub mod reqwest; 3 | pub mod serde; 4 | pub mod serde_derive; 5 | pub mod serde_json; 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | package.version = "0.3.2" 3 | resolver = "2" 4 | members = ["feign-macros", "feign", "test/test-feign", "test/test-server"] 5 | -------------------------------------------------------------------------------- /feign-macros/README.md: -------------------------------------------------------------------------------- 1 | Feign project code generator 2 | ============================ 3 | 4 | Feign : restful http client of rust 5 | 6 | ###links 7 | - crate : https://crates.io/crates/feign 8 | - repository : https://github.com/niuhuan/feign-rs 9 | -------------------------------------------------------------------------------- /test/test-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-server" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | serde = "1.0.133" 10 | serde_derive = "1.0.133" 11 | serde_json = "1.0.75" 12 | warp = "0.3.2" 13 | tokio = { version = "1.15.0", features = ["macros", "rt-multi-thread"] } 14 | -------------------------------------------------------------------------------- /test/test-feign/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test-feign" 3 | version = "0.1.0" 4 | edition = "2021" 5 | default-run = "main" 6 | 7 | [[bin]] 8 | name = "main" 9 | path = "src/main.rs" 10 | 11 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 12 | 13 | [dependencies] 14 | feign = { path = "../../feign" } 15 | serde = "1" 16 | serde_derive = "1" 17 | tokio = { version = "1.15", features = ["macros", "rt-multi-thread"] } 18 | -------------------------------------------------------------------------------- /feign/src/tests.rs: -------------------------------------------------------------------------------- 1 | #[test] 2 | fn test_host_round() { 3 | use crate::{Host, HostRound}; 4 | let host_round = 5 | HostRound::new(vec!["a".to_string(), "b".to_string(), "c".to_string()]).unwrap(); 6 | assert_eq!(host_round.host(), "a"); 7 | assert_eq!(host_round.host(), "b"); 8 | assert_eq!(host_round.host(), "c"); 9 | assert_eq!(host_round.host(), "a"); 10 | assert_eq!(host_round.host(), "b"); 11 | assert_eq!(host_round.host(), "c"); 12 | } 13 | -------------------------------------------------------------------------------- /feign-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "feign-macros" 3 | version.workspace = true 4 | edition = "2021" 5 | authors = ["niuhuan "] 6 | description = "Rest client of Rust" 7 | license = "MIT" 8 | repository = "https://github.com/niuhuan/feign-rs" 9 | 10 | [dependencies] 11 | darling = "0.20.10" 12 | proc-macro-error = "1.0.4" 13 | proc-macro2 = "1.0.92" 14 | quote = "1.0.38" 15 | syn = { version = "2.0.95", features = ["full"] } 16 | 17 | [lib] 18 | proc-macro = true 19 | -------------------------------------------------------------------------------- /feign/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "feign" 3 | version.workspace = true 4 | edition = "2021" 5 | authors = ["niuhuan "] 6 | description = "Rest client of Rust" 7 | license = "MIT" 8 | repository = "https://github.com/niuhuan/feign-rs" 9 | 10 | [dependencies] 11 | anyhow = "1" 12 | serde = "1" 13 | serde_derive = "1" 14 | serde_json = "1" 15 | reqwest = { version = "0", features = ["json"], default-features = false } 16 | feign-macros = { path = "../feign-macros" } 17 | 18 | [lib] 19 | 20 | [features] 21 | default = ["reqwest/default"] 22 | native-tls = ["reqwest/native-tls"] 23 | rustls-tls = ["reqwest/rustls-tls"] 24 | rustls-tls-manual-roots = ["reqwest/rustls-tls-manual-roots"] 25 | rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"] 26 | rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"] 27 | -------------------------------------------------------------------------------- /test/test-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use warp::Filter; 2 | 3 | use serde_derive::Deserialize; 4 | use serde_derive::Serialize; 5 | 6 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 7 | pub struct User { 8 | pub id: i64, 9 | pub name: String, 10 | } 11 | 12 | #[tokio::main] 13 | async fn main() { 14 | let find_by_id = warp::path!("user" / "find_by_id" / i64).map(|id| { 15 | serde_json::to_string(&User { 16 | id, 17 | name: "hello".to_string(), 18 | }) 19 | .unwrap() 20 | }); 21 | 22 | let new_user = warp::post() 23 | .and(warp::path!("user" / "new_user")) 24 | .and(warp::body::json()) 25 | .map(move |user: User| serde_json::to_string(&user.name).unwrap()); 26 | 27 | let put_user = warp::put() 28 | .and(warp::path!("user" / "put_user" / i64)) 29 | .and(warp::body::json()) 30 | .map(move |id: i64, mut user: User| { 31 | user.id = id; 32 | serde_json::to_string(&user).unwrap() 33 | }); 34 | 35 | warp::serve(find_by_id.or(new_user).or(put_user)) 36 | .run(([127, 0, 0, 1], 3030)) 37 | .await; 38 | } 39 | -------------------------------------------------------------------------------- /feign/README.md: -------------------------------------------------------------------------------- 1 |

2 | Feign-RS (Rest client of Rust) 3 |

4 | 5 | ### [Start to use](https://github.com/niuhuan/feign-rs/tree/master/guides) 6 | 7 | ## Examples 8 | 9 | ```rust 10 | use serde_derive::Deserialize; 11 | use serde_derive::Serialize; 12 | use std::collections::HashMap; 13 | use feign::{client, ClientResult, Args}; 14 | 15 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 16 | pub struct User { 17 | pub id: i64, 18 | pub name: String, 19 | } 20 | 21 | #[derive(Args)] 22 | pub struct PutUserArgs { 23 | #[feign_path] 24 | pub id: i64, 25 | #[feign_query] 26 | pub q: String, 27 | #[feign_json] 28 | pub data: User, 29 | #[feign_headers] 30 | pub headers: HashMap, 31 | } 32 | 33 | #[client(host = "http://127.0.0.1:3000", path = "/user")] 34 | pub trait UserClient { 35 | 36 | #[get(path = "/find_by_id/")] 37 | async fn find_by_id(&self, #[path] id: i64) -> ClientResult>; 38 | 39 | #[post(path = "/new_user")] 40 | async fn new_user(&self, #[json] user: &User) -> ClientResult>; 41 | 42 | #[put(path = "/put_user/")] 43 | async fn put_user(&self, #[args] args: PutUserArgs) -> ClientResult; 44 | } 45 | 46 | #[tokio::main] 47 | async fn main() { 48 | let user_client: UserClient = UserClient::new(); 49 | 50 | match user_client.find_by_id(12).await { 51 | Ok(option) => match option { 52 | Some(user) => println!("user : {}", user.name), 53 | None => println!("none"), 54 | }, 55 | Err(err) => panic!("{}", err), 56 | }; 57 | } 58 | ``` 59 | 60 | ## Features 61 | 62 | - Easy to use 63 | - Asynchronous request 64 | - Configurable agent 65 | - Supports form, JSON 66 | - Reconfig host 67 | - Additional request processer 68 | - Custom deserializer 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ![](images/icon.png) 4 | 5 |
6 | 7 |

8 | Feign-RS (Rest client of Rust) 9 |

10 | 11 | ### [Start to use](https://github.com/niuhuan/feign-rs/tree/master/guides) 12 | 13 | ## Examples 14 | 15 | ```rust 16 | use serde_derive::Deserialize; 17 | use serde_derive::Serialize; 18 | use std::collections::HashMap; 19 | use feign::{client, ClientResult, Args}; 20 | 21 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 22 | pub struct User { 23 | pub id: i64, 24 | pub name: String, 25 | } 26 | 27 | #[derive(Args)] 28 | pub struct PutUserArgs { 29 | #[feign_path] 30 | pub id: i64, 31 | #[feign_query] 32 | pub q: String, 33 | #[feign_json] 34 | pub data: User, 35 | #[feign_headers] 36 | pub headers: HashMap, 37 | } 38 | 39 | #[client(host = "http://127.0.0.1:3000", path = "/user")] 40 | pub trait UserClient { 41 | 42 | #[get(path = "/find_by_id/")] 43 | async fn find_by_id(&self, #[path] id: i64) -> ClientResult>; 44 | 45 | #[post(path = "/new_user")] 46 | async fn new_user(&self, #[json] user: &User) -> ClientResult>; 47 | 48 | #[put(path = "/put_user/")] 49 | async fn put_user(&self, #[args] args: PutUserArgs) -> ClientResult; 50 | } 51 | 52 | #[tokio::main] 53 | async fn main() { 54 | let user_client: UserClient = UserClient::new(); 55 | 56 | match user_client.find_by_id(12).await { 57 | Ok(option) => match option { 58 | Some(user) => println!("user : {}", user.name), 59 | None => println!("none"), 60 | }, 61 | Err(err) => panic!("{}", err), 62 | }; 63 | } 64 | ``` 65 | 66 | ## Features 67 | 68 | - Easy to use 69 | - Asynchronous request 70 | - Configurable agent 71 | - Supports form, JSON 72 | - Reconfig host 73 | - Additional request processer 74 | - Custom deserializer 75 | -------------------------------------------------------------------------------- /feign/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::any::Any; 2 | use std::fmt::{Debug, Display, Formatter}; 3 | 4 | pub use anyhow::Result as ClientResult; 5 | pub use feign_macros::*; 6 | pub use reqwest::RequestBuilder; 7 | 8 | pub mod re_exports; 9 | #[cfg(test)] 10 | mod tests; 11 | 12 | /// Http methods enumed 13 | #[derive(Debug)] 14 | pub enum HttpMethod { 15 | Get, 16 | Post, 17 | Put, 18 | Patch, 19 | Delete, 20 | Head, 21 | } 22 | 23 | #[derive(Debug)] 24 | pub enum RequestBody { 25 | None, 26 | Json(T), 27 | Form(T), 28 | } 29 | 30 | pub trait Host: Display + Debug + Sync + Send + 'static { 31 | fn host(&self) -> &str; 32 | } 33 | 34 | impl Host for String { 35 | fn host(&self) -> &str { 36 | self.as_str() 37 | } 38 | } 39 | 40 | pub struct HostRound { 41 | index: std::sync::Mutex, 42 | hosts: Vec, 43 | } 44 | 45 | impl HostRound { 46 | pub fn new(hosts: Vec) -> ClientResult { 47 | if hosts.is_empty() { 48 | return Err(anyhow::anyhow!("HostRound hosts is empty")); 49 | } 50 | Ok(HostRound { 51 | index: std::sync::Mutex::new(0), 52 | hosts, 53 | }) 54 | } 55 | } 56 | 57 | impl Display for HostRound { 58 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 59 | write!(f, "{:?}", self.hosts) 60 | } 61 | } 62 | 63 | impl Debug for HostRound { 64 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 65 | f.debug_struct("HostRound") 66 | .field("hosts", &self.hosts) 67 | .finish() 68 | } 69 | } 70 | 71 | impl Host for HostRound { 72 | fn host(&self) -> &str { 73 | let mut index = self.index.lock().unwrap(); 74 | let host = self.hosts.get(*index).unwrap(); 75 | *index = (*index + 1) % self.hosts.len(); 76 | host.as_str() 77 | } 78 | } 79 | 80 | #[derive(Debug)] 81 | pub struct State { 82 | value: Box, 83 | marker: std::marker::PhantomData, 84 | } 85 | 86 | impl State 87 | where 88 | S: Any + Send + Sync + 'static, 89 | { 90 | pub fn new(value: S) -> Self { 91 | Self { 92 | value: Box::new(value), 93 | marker: std::marker::PhantomData, 94 | } 95 | } 96 | 97 | pub fn get(&self) -> &S { 98 | self.value.downcast_ref().unwrap() 99 | } 100 | 101 | pub fn downcast_ref(&self) -> ClientResult<&T> { 102 | self.value.downcast_ref().ok_or(anyhow::anyhow!(format!( 103 | "State downcast failed: have &{}, want &{}", 104 | std::any::type_name::(), 105 | std::any::type_name::() 106 | ))) 107 | } 108 | } 109 | 110 | /// A deserialization function that converts bytes to a string. 111 | /// Example: 112 | /// ```ignore 113 | /// #[post(path = "/new_user", deserialize = "feign::text")] 114 | /// async fn new_user_text(&self, #[json] user: &User) -> ClientResult; 115 | /// ``` 116 | pub async fn text(body: &[u8]) -> ClientResult { 117 | Ok(String::from_utf8_lossy(body).into_owned()) 118 | } 119 | -------------------------------------------------------------------------------- /test/test-feign/src/main.rs: -------------------------------------------------------------------------------- 1 | use feign::re_exports::{reqwest, serde_json}; 2 | use feign::{client, Args, ClientResult, RequestBody}; 3 | use serde_derive::Deserialize; 4 | use serde_derive::Serialize; 5 | use std::collections::HashMap; 6 | use std::fmt::Debug; 7 | use std::sync::Arc; 8 | use tokio::sync::RwLock; 9 | 10 | async fn client_builder() -> ClientResult { 11 | Ok(reqwest::ClientBuilder::new().build().unwrap()) 12 | } 13 | 14 | async fn before_send( 15 | mut request_builder: reqwest::RequestBuilder, 16 | body: RequestBody, 17 | state: &Arc>, 18 | ) -> ClientResult { 19 | *state.write().await += 1; 20 | 21 | let (client, request) = request_builder.build_split(); 22 | match request { 23 | Ok(request) => { 24 | println!( 25 | "============= (Before_send)\n\ 26 | {:?} => {}\n\ 27 | {:?}\n\ 28 | {:?}\n\ 29 | {:?}", 30 | request.method(), 31 | request.url().as_str(), 32 | request.headers(), 33 | body, 34 | state, 35 | ); 36 | request_builder = reqwest::RequestBuilder::from_parts(client, request); 37 | Ok(request_builder.header("a", "b")) 38 | } 39 | Err(err) => Err(err.into()), 40 | } 41 | } 42 | 43 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 44 | pub struct User { 45 | pub id: i64, 46 | pub name: String, 47 | } 48 | 49 | #[derive(Args)] 50 | pub struct PutUserArgs { 51 | #[feign_path] 52 | pub id: i64, 53 | #[feign_query] 54 | pub q: String, 55 | #[feign_json] 56 | pub data: User, 57 | #[feign_headers] 58 | pub headers: HashMap, 59 | } 60 | 61 | async fn decode serde::Deserialize<'de>>(body: &[u8]) -> ClientResult { 62 | Ok(serde_json::from_slice(body)?) 63 | } 64 | 65 | #[client( 66 | host = "http://127.0.0.1:3030", 67 | path = "/user", 68 | client_builder = "client_builder", 69 | before_send = "before_send" 70 | )] 71 | pub trait UserClient { 72 | #[get(path = "/find_by_id/", deserialize = "decode")] 73 | async fn find_by_id(&self, #[path] id: i64) -> ClientResult>; 74 | #[post(path = "/new_user")] 75 | async fn new_user(&self, #[json] user: &User) -> ClientResult>; 76 | #[post(path = "/new_user", deserialize = "feign::text")] 77 | async fn new_user_text(&self, #[json] user: &User) -> ClientResult; 78 | #[get(path = "/headers")] 79 | async fn headers( 80 | &self, 81 | #[json] age: &i64, 82 | #[headers] headers: &HashMap, 83 | ) -> ClientResult>; 84 | #[put(path = "/put_user/")] 85 | async fn put_user(&self, #[args] args: PutUserArgs) -> ClientResult; 86 | } 87 | 88 | #[tokio::main] 89 | async fn main() { 90 | let user_client = UserClient::builder() 91 | .with_host_arc(Arc::new(String::from("http://127.0.0.1:3030"))) 92 | .with_state(Arc::new(RwLock::new(0))) 93 | .build(); 94 | 95 | match user_client.find_by_id(12).await { 96 | Ok(option) => match option { 97 | Some(user) => println!("user : {}", user.name), 98 | None => println!("none"), 99 | }, 100 | Err(err) => eprintln!("{}", err), 101 | }; 102 | 103 | match user_client 104 | .new_user(&User { 105 | id: 123, 106 | name: "name".to_owned(), 107 | }) 108 | .await 109 | { 110 | Ok(option) => match option { 111 | Some(result) => println!("result : {}", result), 112 | None => println!("none"), 113 | }, 114 | Err(err) => eprintln!("{}", err), 115 | }; 116 | 117 | match user_client 118 | .new_user_text(&User { 119 | id: 123, 120 | name: "name".to_owned(), 121 | }) 122 | .await 123 | { 124 | Ok(result) => println!("result : {}", result), 125 | Err(err) => eprintln!("{}", err), 126 | }; 127 | 128 | let mut headers = HashMap::::new(); 129 | headers.insert(String::from("C"), String::from("D")); 130 | 131 | match user_client.headers(&12, &headers).await { 132 | Ok(option) => match option { 133 | Some(user) => println!("user : {}", user.name), 134 | None => println!("none"), 135 | }, 136 | Err(err) => eprintln!("{}", err), 137 | }; 138 | 139 | match user_client 140 | .put_user(PutUserArgs { 141 | id: 123, 142 | q: "q".to_owned(), 143 | data: User { 144 | id: 456, 145 | name: "name".to_owned(), 146 | }, 147 | headers: headers, 148 | }) 149 | .await 150 | { 151 | Ok(user) => println!("result : {:?}", user), 152 | Err(err) => eprintln!("{}", err), 153 | }; 154 | } 155 | -------------------------------------------------------------------------------- /guides/README.md: -------------------------------------------------------------------------------- 1 | ## How to use 2 | 3 | ### Demo server 4 | 5 | A server has any restful interface (like this : find_user_by_id, new_user) 6 | 7 | ```shell 8 | curl 127.1:3000/user/find_by_id/1 9 | # -> {"id":1,"name":"hello"} 10 | 11 | curl -X POST 127.1:3000/user/new_user \ 12 | -H 'Content-Type: application/json' \ 13 | -d '{"id":1,"name":"Link"}' 14 | # -> "Link" ➜ ~ 15 | ``` 16 | 17 | ### Dependencies 18 | 19 | - Add feign dependency to **Cargo.toml** 20 | - Add serde's dependencies to **Cargo.toml**, because inputs or outputs entities must be **Serialize / Deserialize** 21 | 22 | ```toml 23 | [dependencies] 24 | feign = "0" 25 | serde = "1.0" 26 | serde_derive = "1.0" 27 | # runtime 28 | tokio = { version = "1.15", features = ["macros", "rt-multi-thread"] } 29 | ``` 30 | 31 | ### Entites 32 | 33 | Add a user entity add derives serde_derive::Deserialize and serde_derive::Serialize 34 | 35 | ```rust 36 | use serde_derive::Deserialize; 37 | use serde_derive::Serialize; 38 | 39 | #[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)] 40 | pub struct User { 41 | pub id: i64, 42 | pub name: String, 43 | } 44 | ``` 45 | 46 | ### Feign client 47 | 48 | - Use feign::client macro and trait make a feign client, host is server address, path is controller context. (The host 49 | can be dynamically replaced and can be ignored) 50 | - In the trait, use the method macro and path args make a request, the member method must async and first arg is 51 | recover(&self) 52 | - Use #\[json] / #\[form] post body, use #\[path] for replace \ in request path 53 | 54 | ```rust 55 | 56 | use feign::{client, ClientResult}; 57 | 58 | #[client(host = "http://127.0.0.1:3000", path = "/user")] 59 | pub trait UserClient { 60 | 61 | #[get(path = "/find_by_id/")] 62 | async fn find_by_id(&self, #[path] id: i64) -> ClientResult>; 63 | 64 | #[post(path = "/new_user")] 65 | async fn new_user(&self, #[json] user: &User) -> ClientResult>; 66 | 67 | } 68 | ``` 69 | 70 | ### Demo 71 | 72 | Use 73 | 74 | ```rust 75 | #[tokio::main] 76 | async fn main() { 77 | let user_client: UserClient = UserClient::new(); 78 | 79 | match user_client.find_by_id(12).await { 80 | Ok(option) => match option { 81 | Some(user) => println!("user : {}", user.name), 82 | None => println!("none"), 83 | }, 84 | Err(err) => panic!("{}", err), 85 | }; 86 | 87 | match user_client 88 | .new_user(&User { 89 | id: 123, 90 | name: "name".to_owned(), 91 | }) 92 | .await 93 | { 94 | Ok(option) => match option { 95 | Some(result) => println!("result : {}", result), 96 | None => println!("none"), 97 | }, 98 | Err(err) => panic!("{}", err), 99 | }; 100 | } 101 | ``` 102 | 103 | ```text 104 | user : hello 105 | result : name 106 | ``` 107 | 108 | ## Options 109 | 110 | ### Put headers 111 | 112 | ```rust 113 | #[get(path = "/headers")] 114 | async fn headers( 115 | &self, 116 | #[json] age: &i64, 117 | #[headers] headers: HashMap, 118 | ) -> ClientResult>; 119 | ``` 120 | 121 | ### Dynamic modify host with set_host 122 | 123 | ```rust 124 | #[client(path = "/user")] 125 | pub trait UserClient {} 126 | 127 | #[tokio::main] 128 | async fn main() { 129 | let user_client: UserClient = UserClient::builder() 130 | .with_host(String::from("http://127.0.0.1:3001")) 131 | .build(); 132 | } 133 | ``` 134 | 135 | ```rust 136 | #[client(path = "/user")] 137 | pub trait UserClient {} 138 | 139 | #[tokio::main] 140 | async fn main() { 141 | let user_client: UserClient = UserClient::builder() 142 | .with_host_arc(Arc::new(String::from("http://127.0.0.1:3001"))) 143 | .build(); 144 | } 145 | ``` 146 | 147 | ##### load balance 148 | 149 | implement `feign::Host` trait, or use `feign::HostRound` 150 | 151 | ```rust 152 | let user_client: UserClient = UserClient::builder() 153 | .with_host(feign::HostRound::new(vec!["http://127.0.0.1:3031".to_string(), "http://127.0.0.1:3032".to_string()]).unwrap()) 154 | .build(); 155 | ``` 156 | 157 | ### Customer reqwest client builder 158 | 159 | Add reqwest to dependencies and enable json feature, or use feign re_exports reqwest. 160 | 161 | ```toml 162 | reqwest = { version = "0", features = ["json"] } 163 | ``` 164 | 165 | or 166 | 167 | ```rust 168 | use feign::re_exports::reqwest; 169 | ``` 170 | 171 | Impl a async fn Result>, put fn name to arg client_builder 172 | 173 | ```rust 174 | use feign::{client, ClientResult}; 175 | 176 | async fn client_builder() -> ClientResult { 177 | Ok(reqwest::ClientBuilder::new().build().unwrap()) 178 | } 179 | 180 | #[client( 181 | host = "http://127.0.0.1:3000", 182 | path = "/user", 183 | client_builder = "client_builder" 184 | )] 185 | pub trait UserClient {} 186 | ``` 187 | 188 | ### Customer additional reqwest request builder 189 | 190 | #### before_send 191 | 192 | If you want check hash of json body, sign to header. Or log the request. 193 | 194 | ```rust 195 | async fn before_send( 196 | mut request_builder: reqwest::RequestBuilder, 197 | body: RequestBody, 198 | state: &Arc>, 199 | ) -> ClientResult { 200 | *state.write().await += 1; 201 | 202 | let (client, request) = request_builder.build_split(); 203 | match request { 204 | Ok(request) => { 205 | println!( 206 | "============= (Before_send)\n\ 207 | {:?} => {}\n\ 208 | {:?}\n\ 209 | {:?}\n\ 210 | {:?}", 211 | request.method(), 212 | request.url().as_str(), 213 | request.headers(), 214 | body, 215 | state, 216 | ); 217 | request_builder = reqwest::RequestBuilder::from_parts(client, request); 218 | Ok(request_builder.header("a", "b")) 219 | } 220 | Err(err) => Err(err.into()), 221 | } 222 | } 223 | ``` 224 | 225 | Set before_send arg with function name 226 | 227 | ```rust 228 | #[client( 229 | host = "http://127.0.0.1:3000", 230 | path = "/user", 231 | client_builder = "client_builder", 232 | before_send = "before_send" 233 | )] 234 | pub trait UserClient { 235 | #[get(path = "/find_by_id/")] 236 | async fn find_by_id(&self, #[path] id: i64) -> ClientResult>; 237 | #[post(path = "/new_user")] 238 | async fn new_user(&self, #[json] user: &User) -> ClientResult>; 239 | } 240 | ``` 241 | 242 | Optionally set the `State`: 243 | 244 | ```rust 245 | let user_client = UserClient::builder() 246 | ... 247 | .with_state(Arc::new(RwLock::new(0))) 248 | .build(); 249 | ``` 250 | 251 | Result 252 | 253 | ```text 254 | ============= (Before_send) 255 | GET => 127.0.0.1/user/find_by_id/12 256 | {} 257 | None 258 | user : hello 259 | ============= (Before_send) 260 | POST => 127.0.0.1/user/new_user 261 | {"content-type": "application/json"} 262 | Json(User { id: 123, name: "name" }) 263 | Some(RwLock { data: 1 }) 264 | result : name 265 | ``` 266 | 267 | ### Custom deserialize 268 | 269 | Add serde_json to Cargo.toml 270 | 271 | ```toml 272 | serde_json = "1" 273 | ``` 274 | 275 | create async deserializer, result type same as field method, or use generic type. 276 | 277 | ```rust 278 | async fn decode serde::Deserialize<'de>>(body: &[u8]) -> ClientResult { 279 | Ok(serde_json::from_slice(body)?) 280 | } 281 | ``` 282 | 283 | set deserialize, field method result type same as deserializer 284 | 285 | ```rust 286 | #[get(path = "/find_by_id/", deserialize = "decode")] 287 | async fn find_by_id(&self, #[path] id: i64) -> ClientResult>; 288 | 289 | #[post(path = "/new_user", deserialize = "feign::text")] 290 | async fn new_user_text(&self, #[json] user: &User) -> ClientResult; 291 | ``` 292 | 293 | ```rust 294 | match user_client 295 | .new_user_text(&User { 296 | id: 123, 297 | name: "name".to_owned(), 298 | }) 299 | .await 300 | { 301 | Ok(result) => println!("result : {}", result), 302 | Err(err) => panic!("{}", err), 303 | }; 304 | ``` 305 | 306 | result (Raw text) 307 | 308 | ```text 309 | result : "name" 310 | ``` 311 | -------------------------------------------------------------------------------- /feign-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use crate::RequestBody::{Form, Json}; 4 | use darling::ast::NestedMeta; 5 | use darling::{Error, FromMeta}; 6 | use proc_macro::TokenStream; 7 | use proc_macro_error::{abort, proc_macro_error}; 8 | use quote::quote; 9 | use syn::spanned::Spanned; 10 | use syn::{parse_macro_input, FnArg, TraitItemFn}; 11 | 12 | /// Make a restful http client 13 | /// 14 | /// # Examples 15 | /// 16 | /// ```ignore 17 | /// #[client(host = "http://127.0.0.1:3000", path = "/user")] 18 | /// pub trait UserClient { 19 | /// #[get(path = "/find_by_id/")] 20 | /// async fn find_by_id(&self, #[path] id: i64) -> ClientResult>; 21 | /// #[post(path = "/new_user")] 22 | /// async fn new_user(&self, #[json] user: &User) -> Result, Box>; 23 | /// } 24 | /// ``` 25 | /// 26 | #[proc_macro_error] 27 | #[proc_macro_attribute] 28 | pub fn client(args: TokenStream, input: TokenStream) -> TokenStream { 29 | let args = match NestedMeta::parse_meta_list(args.into()) { 30 | Ok(v) => v, 31 | Err(e) => { 32 | return TokenStream::from(Error::from(e).write_errors()); 33 | } 34 | }; 35 | let input = parse_macro_input!(input as syn::ItemTrait); 36 | let args: ClientArgs = match ClientArgs::from_list(&args) { 37 | Ok(v) => v, 38 | Err(e) => return TokenStream::from(e.write_errors()), 39 | }; 40 | 41 | let reqwest_client_builder = match args.client_builder { 42 | Some(builder) => { 43 | let builder_token: proc_macro2::TokenStream = builder.parse().unwrap(); 44 | quote! { 45 | #builder_token().await? 46 | } 47 | } 48 | None => quote! { 49 | ::feign::re_exports::reqwest::ClientBuilder::new().build()? 50 | }, 51 | }; 52 | 53 | let vis = &input.vis; 54 | let name = &input.ident; 55 | let base_host = &args.host; 56 | let base_path = &args.path; 57 | 58 | let methods = input 59 | .items 60 | .iter() 61 | .filter_map(|item| match item { 62 | syn::TraitItem::Fn(m) => Some(m), 63 | _ => None, 64 | }) 65 | .map(|m| gen_method(m, args.before_send.as_ref(), &reqwest_client_builder)); 66 | 67 | let builder_name: proc_macro2::TokenStream = 68 | format!("{}Builder", quote! {#name}).parse().unwrap(); 69 | 70 | let tokens = quote! { 71 | 72 | #[derive(Debug)] 73 | #vis struct #name { 74 | host: std::sync::Arc, 75 | path: String, 76 | state: feign::State, 77 | } 78 | 79 | impl #name<()> { 80 | pub fn new() -> #name<()> { 81 | #name::<()>{ 82 | host: std::sync::Arc::new(String::from(#base_host)), 83 | path: String::from(#base_path), 84 | state: feign::State::new(()), 85 | } 86 | } 87 | 88 | pub fn builder() -> #builder_name<()> { 89 | #builder_name::<()>::new() 90 | } 91 | } 92 | 93 | impl #name where T: std::any::Any + core::marker::Send + core::marker::Sync + 'static{ 94 | #(#methods)* 95 | } 96 | 97 | #vis struct #builder_name(#name); 98 | 99 | impl #builder_name<()> { 100 | pub fn new() -> Self { 101 | Self(#name::<()>::new()) 102 | } 103 | } 104 | 105 | impl #builder_name { 106 | 107 | pub fn build(self) -> #name { 108 | self.0 109 | } 110 | 111 | pub fn with_host(mut self, host: impl feign::Host) -> Self { 112 | self.with_host_arc(std::sync::Arc::new(host)) 113 | } 114 | 115 | pub fn with_host_arc(mut self, host: std::sync::Arc) -> Self { 116 | self.0.host = host; 117 | self 118 | } 119 | 120 | pub fn with_state(mut self, state: S) -> #builder_name { 121 | #builder_name(#name::{ 122 | host: self.0.host, 123 | path: self.0.path, 124 | state: feign::State::new(state), 125 | }) 126 | } 127 | } 128 | }; 129 | 130 | tokens.into() 131 | } 132 | 133 | /// Gen feign methods 134 | fn gen_method( 135 | method: &TraitItemFn, 136 | before_send: Option<&String>, 137 | reqwest_client_builder: &proc_macro2::TokenStream, 138 | ) -> proc_macro2::TokenStream { 139 | if method.sig.asyncness.is_none() { 140 | abort!( 141 | &method.sig.span(), 142 | "Non-asynchronous calls are not currently supported" 143 | ) 144 | } 145 | 146 | let name = &method.sig.ident; 147 | let inputs = &method.sig.inputs; 148 | let output = &method.sig.output; 149 | let attr = method.attrs.iter().next(); 150 | let http_method_ident = match attr.map(|a| a.path().get_ident()).flatten() { 151 | Some(ident) => ident, 152 | None => { 153 | abort!(&method.span(), "Expects an http method") 154 | } 155 | }; 156 | 157 | let _http_method = if let Some(m) = http_method_from_ident(http_method_ident) { 158 | m 159 | } else { 160 | abort!( 161 | &http_method_ident.span(), 162 | "Expect one of get, post, put, patch, delete, head." 163 | ) 164 | }; 165 | 166 | let _http_method_token = http_method_to_token(_http_method); 167 | 168 | let request: Request = match Request::from_meta(&attr.unwrap().meta) { 169 | Ok(v) => v, 170 | Err(err) => return TokenStream::from(err.write_errors()).into(), 171 | }; 172 | 173 | let req_path = &request.path; 174 | 175 | let mut path_variables = Vec::new(); 176 | let mut querys = Vec::new(); 177 | let mut body = None; 178 | let mut headers = None; 179 | let mut args = None; 180 | 181 | match inputs.first() { 182 | Some(FnArg::Receiver(_)) => {} 183 | _ => abort!(&method.sig.span(), "first arg must be &self"), 184 | }; 185 | 186 | inputs 187 | .iter() 188 | .filter_map(|fn_arg| match fn_arg { 189 | FnArg::Receiver(_) => None, 190 | FnArg::Typed(ty) => Some((ty, &ty.attrs.first()?.path().segments.first()?.ident)), 191 | }) 192 | .for_each(|(ty, p)| match &*p.to_string() { 193 | "path" => path_variables.push(&ty.pat), 194 | "query" => querys.push(&ty.pat), 195 | "json" => match body { 196 | None => body = Some(Json(&ty.pat)), 197 | _ => abort!(&ty.span(), "json or form only once"), 198 | }, 199 | "form" => match body { 200 | None => body = Some(Form(&ty.pat)), 201 | _ => abort!(&ty.span(), "json or form only once"), 202 | }, 203 | "headers" => match headers { 204 | None => headers = Some(&ty.pat), 205 | _ => abort!(&ty.span(), "headers only once"), 206 | }, 207 | "args" => match args { 208 | None => args = Some(&ty.pat), 209 | _ => abort!(&ty.span(), "args only once"), 210 | }, 211 | other => abort!( 212 | &ty.span(), 213 | format!("not allowed param type : {}", other).as_str() 214 | ), 215 | }); 216 | 217 | let path_variables = if path_variables.is_empty() { 218 | quote! {} 219 | } else { 220 | let mut stream = proc_macro2::TokenStream::new(); 221 | for pv in path_variables { 222 | let id = format!("<{}>", quote! {#pv}); 223 | stream.extend(quote! { 224 | .replace(#id, format!("{}", #pv).as_str()) 225 | }); 226 | } 227 | stream 228 | }; 229 | 230 | let mut query = if querys.is_empty() { 231 | quote! {} 232 | } else { 233 | quote! { 234 | req = req.query(&[#(#querys),*]); 235 | } 236 | }; 237 | 238 | let (mut req_body, mut req_body_enum) = match body { 239 | None => (quote! {}, quote! {feign::RequestBody::<()>::None}), 240 | Some(Form(form)) => ( 241 | quote! { 242 | req = req.form(#form); 243 | }, 244 | quote! {feign::RequestBody::Form(#form)}, 245 | ), 246 | Some(Json(json)) => ( 247 | quote! { 248 | req = req.json(#json); 249 | }, 250 | quote! {feign::RequestBody::Json(#json)}, 251 | ), 252 | }; 253 | 254 | let mut headers = match headers { 255 | None => quote! {}, 256 | Some(headers) => quote! { 257 | for header in #headers { 258 | req = req.header(header.0,header.1); 259 | } 260 | }, 261 | }; 262 | 263 | let mut args_path = quote! {}; 264 | if let Some(args) = args { 265 | args_path = quote! { 266 | for path in #args.path() { 267 | request_path = request_path.replace(path.0, path.1.as_str()); 268 | } 269 | }; 270 | query = quote! { 271 | if let Some(query) = #args.query() { 272 | req = req.query(&query); 273 | } 274 | }; 275 | // allready has req_body 276 | if body.is_some() { 277 | req_body = quote! { 278 | #req_body 279 | match #args.body() { 280 | feign::RequestBody::None => {}, 281 | _ => { 282 | return Err(feign::re_exports::anyhow::anyhow!("json or form can only once")); 283 | }, 284 | } 285 | }; 286 | } else { 287 | req_body = quote! { 288 | let req_body = #args.body(); 289 | match &req_body { 290 | feign::RequestBody::None => {}, 291 | feign::RequestBody::Form(form) => { 292 | req = req.form(form); 293 | }, 294 | feign::RequestBody::Json(json) => { 295 | req = req.json(json); 296 | }, 297 | } 298 | }; 299 | req_body_enum = quote! {req_body}; 300 | } 301 | headers = quote! { 302 | #headers 303 | if let Some(headers) = #args.headers() { 304 | for header in headers { 305 | req = req.header(header.0, header.1); 306 | } 307 | } 308 | }; 309 | }; 310 | 311 | let inputs = inputs 312 | .iter() 313 | .filter_map(|fn_arg| match fn_arg { 314 | FnArg::Receiver(_) => None, 315 | FnArg::Typed(a) => { 316 | let mut a = a.clone(); 317 | a.attrs.clear(); 318 | Some(FnArg::Typed(a)) 319 | } 320 | }) 321 | .collect::>(); 322 | 323 | let before_send_builder = match before_send { 324 | Some(builder) => { 325 | let builder_token: proc_macro2::TokenStream = builder.clone().parse().unwrap(); 326 | quote! { 327 | let req = #builder_token( 328 | req, 329 | #req_body_enum, 330 | self.state.downcast_ref()?, 331 | ).await?; 332 | } 333 | } 334 | None => quote! {}, 335 | }; 336 | 337 | let deserialize = match request.deserialize { 338 | None => quote! {::feign::re_exports::serde_json::from_slice(&bytes)}, 339 | Some(deserialize) => { 340 | let builder_token: proc_macro2::TokenStream = deserialize.parse().unwrap(); 341 | quote! {#builder_token(&bytes).await} 342 | } 343 | }; 344 | 345 | quote! { 346 | pub async fn #name(&self, #inputs) #output { 347 | let mut request_path = String::from(#req_path)#path_variables; 348 | #args_path 349 | let url = format!("{}{}{}", self.host, self.path, request_path); 350 | let mut req = #reqwest_client_builder 351 | .#http_method_ident(url.as_str()); 352 | #query 353 | #req_body 354 | #headers 355 | #before_send_builder 356 | let bytes = req 357 | .send() 358 | .await? 359 | .error_for_status()? 360 | .bytes() 361 | .await?; 362 | Ok(#deserialize?) 363 | } 364 | } 365 | } 366 | 367 | /// Http methods enumed 368 | enum HttpMethod { 369 | Get, 370 | Post, 371 | Put, 372 | Patch, 373 | Delete, 374 | Head, 375 | } 376 | 377 | fn http_method_from_ident(ident: &syn::Ident) -> Option { 378 | Some(match &*ident.to_string() { 379 | "get" => HttpMethod::Get, 380 | "post" => HttpMethod::Post, 381 | "put" => HttpMethod::Put, 382 | "patch" => HttpMethod::Patch, 383 | "delete" => HttpMethod::Delete, 384 | "head" => HttpMethod::Head, 385 | _ => return None, 386 | }) 387 | } 388 | 389 | fn http_method_to_token(method: HttpMethod) -> proc_macro2::TokenStream { 390 | match method { 391 | HttpMethod::Get => "feign::HttpMethod::Get", 392 | HttpMethod::Post => "feign::HttpMethod::Post", 393 | HttpMethod::Put => "feign::HttpMethod::Put", 394 | HttpMethod::Patch => "feign::HttpMethod::Patch", 395 | HttpMethod::Delete => "feign::HttpMethod::Delete", 396 | HttpMethod::Head => "feign::HttpMethod::Head", 397 | } 398 | .parse() 399 | .unwrap() 400 | } 401 | 402 | /// body types 403 | enum RequestBody<'a> { 404 | Form(&'a Box), 405 | Json(&'a Box), 406 | } 407 | 408 | /// Args of client 409 | #[derive(Debug, FromMeta)] 410 | struct ClientArgs { 411 | #[darling(default)] 412 | pub host: String, 413 | #[darling(default)] 414 | pub path: String, 415 | #[darling(default)] 416 | pub client_builder: Option, 417 | #[darling(default)] 418 | pub before_send: Option, 419 | } 420 | 421 | /// Args of request 422 | #[derive(Debug, FromMeta)] 423 | struct Request { 424 | pub path: String, 425 | #[darling(default)] 426 | pub deserialize: Option, 427 | } 428 | 429 | /// Derive macro for the `Args` trait 430 | /// 431 | /// This macro automatically implements the `Args` trait for a struct, 432 | /// providing implementations for `request_path` and `request_builder` methods 433 | /// based on field attributes like `#[path]`, `#[query]`, `#[json]`, `#[form]`, `#[headers]`. 434 | /// 435 | /// # Examples 436 | /// 437 | /// ```ignore 438 | /// use feign::Args; 439 | /// 440 | /// #[derive(Args)] 441 | /// struct MyArgs { 442 | /// #[path] 443 | /// pub id: i64, 444 | /// #[query] 445 | /// pub name: String, 446 | /// #[json] 447 | /// pub data: UserData, 448 | /// #[headers] 449 | /// pub auth: String, 450 | /// } 451 | /// ``` 452 | #[proc_macro_error] 453 | #[proc_macro_derive( 454 | Args, 455 | attributes(feign_path, feign_query, feign_json, feign_form, feign_headers) 456 | )] 457 | pub fn derive_args(input: TokenStream) -> TokenStream { 458 | let input = parse_macro_input!(input as syn::DeriveInput); 459 | let name = &input.ident; 460 | 461 | let fields = match &input.data { 462 | syn::Data::Struct(data) => &data.fields, 463 | _ => abort!( 464 | &input.ident.span(), 465 | "Args derive macro only supports structs" 466 | ), 467 | }; 468 | 469 | let mut path_fields: Vec<(&syn::Ident, &syn::Type)> = Vec::new(); 470 | let mut query_fields: Vec<(&syn::Ident, &syn::Type)> = Vec::new(); 471 | let mut json_field: Option<(&syn::Ident, &syn::Type)> = None; 472 | let mut form_field: Option<(&syn::Ident, &syn::Type)> = None; 473 | let mut headers_field: Option<(&syn::Ident, &syn::Type)> = None; 474 | 475 | for field in fields.iter() { 476 | let field_name = field.ident.as_ref().unwrap(); 477 | let field_type = &field.ty; 478 | 479 | let mut has_path = false; 480 | let mut has_query = false; 481 | let mut has_json = false; 482 | let mut has_form = false; 483 | let mut has_headers = false; 484 | 485 | for attr in &field.attrs { 486 | if let syn::Meta::Path(path) = &attr.meta { 487 | if let Some(ident) = path.get_ident() { 488 | match ident.to_string().as_str() { 489 | "feign_path" => has_path = true, 490 | "feign_query" => has_query = true, 491 | "feign_json" => has_json = true, 492 | "feign_form" => has_form = true, 493 | "feign_headers" => has_headers = true, 494 | _ => {} 495 | } 496 | } 497 | } 498 | } 499 | 500 | if has_path { 501 | path_fields.push((field_name, field_type)); 502 | } else if has_query { 503 | query_fields.push((field_name, field_type)); 504 | } else if has_json { 505 | match json_field { 506 | None => json_field = Some((field_name, field_type)), 507 | _ => abort!(&field.span(), "json only once"), 508 | } 509 | } else if has_form { 510 | match form_field { 511 | None => form_field = Some((field_name, field_type)), 512 | _ => abort!(&field.span(), "form only once"), 513 | } 514 | } else if has_headers { 515 | match headers_field { 516 | None => headers_field = Some((field_name, field_type)), 517 | _ => abort!(&field.span(), "headers only once"), 518 | } 519 | } 520 | } 521 | 522 | if json_field.is_some() && form_field.is_some() { 523 | abort!(&fields.span(), "json or form only once"); 524 | } 525 | 526 | // Generate request_path method 527 | let path = if path_fields.is_empty() { 528 | quote! {} 529 | } else { 530 | let path_pairs: Vec<_> = path_fields 531 | .iter() 532 | .map(|(field_name, _)| { 533 | let id = format!("<{}>", field_name); 534 | quote! { 535 | (#id, format!("{}", self.#field_name)) 536 | } 537 | }) 538 | .collect(); 539 | quote! { 540 | vec![#(#path_pairs),*] 541 | } 542 | }; 543 | 544 | // Generate request_builder method 545 | let query = if query_fields.is_empty() { 546 | quote! {None} 547 | } else { 548 | let query_pairs: Vec<_> = query_fields 549 | .iter() 550 | .map(|(field_name, _)| { 551 | quote! { 552 | (stringify!(#field_name), format!("{}", self.#field_name)) 553 | } 554 | }) 555 | .collect(); 556 | quote! { 557 | Some(vec![#(#query_pairs),*]) 558 | } 559 | }; 560 | 561 | let (body, body_type) = match (form_field, json_field) { 562 | (Some((field_name, ty)), None) => ( 563 | quote! { 564 | feign::RequestBody::Form(&self.#field_name) 565 | }, 566 | quote! {feign::RequestBody<&#ty>}, 567 | ), 568 | (None, Some((field_name, ty))) => ( 569 | quote! { 570 | feign::RequestBody::Json(&self.#field_name) 571 | }, 572 | quote! {feign::RequestBody<&#ty>}, 573 | ), 574 | _ => ( 575 | quote! {feign::RequestBody::<()>::None}, 576 | quote! {feign::RequestBody<()>}, 577 | ), 578 | }; 579 | 580 | let (headers, headers_type) = match headers_field { 581 | None => ( 582 | quote! {None}, 583 | quote! {Option<&std::collections::HashMap>}, 584 | ), 585 | Some((field_name, ty)) => ( 586 | quote! { 587 | Some(&self.#field_name) 588 | }, 589 | quote! {Option<&#ty>}, 590 | ), 591 | }; 592 | 593 | let expanded = quote! { 594 | impl #name { 595 | pub fn path(&self) -> Vec<(&'static str, String)> { 596 | #path 597 | } 598 | 599 | pub fn query(&self) -> Option> { 600 | #query 601 | } 602 | 603 | pub fn body(&self) -> #body_type { 604 | #body 605 | } 606 | 607 | pub fn headers(&self) -> #headers_type { 608 | #headers 609 | } 610 | } 611 | }; 612 | 613 | TokenStream::from(expanded) 614 | } 615 | --------------------------------------------------------------------------------