shenyu_client_rust/
lib.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18#![deny(
19    // The following are allowed by default lints according to
20    // https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html
21    absolute_paths_not_starting_with_crate,
22    explicit_outlives_requirements,
23    macro_use_extern_crate,
24    redundant_lifetimes,
25    anonymous_parameters,
26    bare_trait_objects,
27    // elided_lifetimes_in_paths, // allow anonymous lifetime
28    missing_copy_implementations,
29    missing_debug_implementations,
30    missing_docs,
31    // single_use_lifetimes, // TODO: fix lifetime names only used once
32    // trivial_casts,
33    trivial_numeric_casts,
34    unreachable_pub,
35    // unsafe_code,
36    unstable_features,
37    // unused_crate_dependencies,
38    unused_lifetimes,
39    unused_macro_rules,
40    unused_extern_crates,
41    unused_import_braces,
42    unused_qualifications,
43    unused_results,
44    variant_size_differences,
45
46    warnings, // treat all wanings as errors
47
48    clippy::all,
49    // clippy::restriction,
50    clippy::pedantic,
51    // clippy::nursery, // It's still under development
52    clippy::cargo,
53)]
54#![allow(
55    // Some explicitly allowed Clippy lints, must have clear reason to allow
56    clippy::blanket_clippy_restriction_lints, // allow clippy::restriction
57    clippy::implicit_return, // actually omitting the return keyword is idiomatic Rust code
58    clippy::module_name_repetitions, // repeation of module name in a struct name is not big deal
59    clippy::multiple_crate_versions, // multi-version dependency crates is not able to fix
60    clippy::missing_errors_doc, // TODO: add error docs
61    clippy::missing_panics_doc, // TODO: add panic docs
62    clippy::panic_in_result_fn,
63    clippy::shadow_same, // Not too much bad
64    clippy::shadow_reuse, // Not too much bad
65    clippy::exhaustive_enums,
66    clippy::exhaustive_structs,
67    clippy::indexing_slicing,
68    clippy::separated_literal_suffix, // conflicts with clippy::unseparated_literal_suffix
69    clippy::single_char_lifetime_names, // TODO: change lifetime names
70)]
71#![doc = include_str!("../README.md")]
72
73use crate::model::UriInfo;
74
75/// A mod for CI.
76pub mod ci;
77/// Config structs.
78pub mod config;
79/// Shenyu client core.
80pub mod core;
81/// Error handling.
82pub mod error;
83/// Macros.
84pub mod macros;
85/// Structs.
86pub mod model;
87
88#[allow(missing_docs)]
89pub trait IRouter {
90    /// Get app name.
91    fn app_name(&self) -> &str;
92
93    /// Get uri infos.
94    fn uri_infos(&self) -> &Vec<UriInfo>;
95}
96
97#[allow(missing_docs)]
98#[cfg(feature = "axum")]
99pub mod axum_impl {
100    use super::model::UriInfo;
101    use crate::IRouter;
102    use axum::extract::Request;
103    use axum::response::IntoResponse;
104    use axum::routing::MethodRouter;
105    use axum::Router;
106    use std::convert::Infallible;
107    use tower_service::Service;
108
109    /// A router that can be used to register routes.
110    ///
111    /// This is a wrapper around `Router` that provides a more ergonomic API.
112    /// It allows you to define routes and nest other routers or services.
113    ///
114    /// # Fields
115    ///
116    /// * `app_name` - The name of the application.
117    /// * `uri_infos` - A vector of URI information.
118    ///
119    /// # Examples
120    /// ```rust
121    ///
122    /// use axum::routing::{get, post};
123    /// use shenyu_client_rust::axum_impl::ShenYuRouter;
124    ///
125    /// async fn health_handler() -> &'static str {
126    ///     "OK"
127    /// }
128    ///
129    /// async fn create_user_handler() -> &'static str {
130    ///     "User created"
131    /// }
132    ///
133    /// async fn not_found_handler() -> &'static str {
134    ///     "Not found"
135    /// }
136    ///
137    /// let app = ShenYuRouter::<()>::new("shenyu_client_app")
138    ///     .nest("/api", ShenYuRouter::new("api"))
139    ///     .route("/health", "get", get(health_handler))
140    ///     .route("/users", "post", post(create_user_handler));
141    ///
142    /// ```
143    ///
144    #[derive(Debug, Clone)]
145    pub struct ShenYuRouter<S = ()> {
146        app_name: String,
147        inner: Router<S>,
148        uri_infos: Vec<UriInfo>,
149    }
150
151    impl<S> ShenYuRouter<S>
152    where
153        S: Clone + Send + Sync + 'static,
154    {
155        #[must_use]
156        pub fn new(app_name: &str) -> Self {
157            Self {
158                app_name: app_name.to_string(),
159                inner: Router::new(),
160                uri_infos: Vec::new(),
161            }
162        }
163
164        #[must_use]
165        pub fn uri_info(mut self, uri_info: UriInfo) -> Self {
166            self.uri_infos.push(uri_info);
167            self
168        }
169
170        #[must_use]
171        pub fn route(mut self, path: &str, method: &str, method_router: MethodRouter<S>) -> Self {
172            self.inner = self.inner.route(path, method_router);
173            self.uri_infos.push(UriInfo {
174                path: path.to_string(),
175                rule_name: path.to_string(),
176                service_name: None,
177                method_name: method.to_string(),
178            });
179            self
180        }
181
182        #[must_use]
183        pub fn route_service<T>(mut self, path: &str, method: &str, service: T) -> Self
184        where
185            T: Service<Request, Error = Infallible> + Clone + Send + 'static,
186            T::Response: IntoResponse,
187            T::Future: Send + 'static,
188        {
189            self.inner = self.inner.route_service(path, service);
190            self.uri_infos.push(UriInfo {
191                path: path.to_string(),
192                rule_name: path.to_string(),
193                service_name: None,
194                method_name: method.to_string(),
195            });
196            self
197        }
198
199        #[must_use]
200        #[track_caller]
201        pub fn nest(mut self, path: &str, route: ShenYuRouter<S>) -> Self {
202            self.inner = self.inner.nest(path, route.inner);
203            self.uri_infos.extend(route.uri_infos);
204            self
205        }
206
207        #[must_use]
208        #[track_caller]
209        pub fn nest_service<T>(mut self, path: &str, method: &str, service: T) -> Self
210        where
211            T: Service<Request, Error = Infallible> + Clone + Send + 'static,
212            T::Response: IntoResponse,
213            T::Future: Send + 'static,
214        {
215            self.inner = self.inner.nest_service(path, service);
216            self.uri_infos.push(UriInfo {
217                path: path.to_string(),
218                rule_name: path.to_string(),
219                service_name: None,
220                method_name: method.to_string(),
221            });
222            self
223        }
224
225        #[must_use]
226        pub fn uri_infos(&self) -> &Vec<UriInfo> {
227            &self.uri_infos
228        }
229
230        #[must_use]
231        #[track_caller]
232        pub fn merge<R>(mut self, other: ShenYuRouter<R>) -> Self
233        where
234            R: Into<Router<S>>,
235            S: Clone + Send + Sync + 'static,
236            Router<S>: From<Router<R>>,
237        {
238            self.inner = self.inner.merge(other.inner);
239            self.uri_infos.extend(other.uri_infos);
240            self
241        }
242    }
243
244    impl<S> From<ShenYuRouter<S>> for Router<S>
245    where
246        S: Clone + Send + Sync + 'static,
247    {
248        fn from(val: ShenYuRouter<S>) -> Self {
249            val.inner
250        }
251    }
252
253    impl<S> IRouter for ShenYuRouter<S> {
254        fn app_name(&self) -> &str {
255            &self.app_name
256        }
257
258        fn uri_infos(&self) -> &Vec<UriInfo> {
259            &self.uri_infos
260        }
261    }
262}
263
264#[allow(missing_docs)]
265#[cfg(feature = "actix-web")]
266pub mod actix_web_impl {
267    use super::model::UriInfo;
268    use crate::IRouter;
269
270    /// A router that can be used to register routes.
271    ///
272    /// This is a wrapper around `Router` that provides a more ergonomic API.
273    /// It allows you to define routes and nest other routers or services.
274    ///
275    /// # Fields
276    ///
277    /// * `app_name` - The name of the application.
278    /// * `uri_infos` - A vector of URI information.
279    #[derive(Debug, Clone)]
280    pub struct ShenYuRouter {
281        app_name: String,
282        uri_infos: Vec<UriInfo>,
283    }
284
285    impl ShenYuRouter {
286        #[must_use]
287        pub fn new(app_name: &str) -> Self {
288            Self {
289                app_name: app_name.to_string(),
290                uri_infos: Vec::new(),
291            }
292        }
293
294        pub fn route(&mut self, path: &str, method: &str) {
295            self.uri_infos.push(UriInfo {
296                path: path.to_string().clone(),
297                rule_name: path.to_string().clone(),
298                service_name: None,
299                method_name: method.to_string(),
300            });
301        }
302    }
303
304    impl IRouter for ShenYuRouter {
305        fn app_name(&self) -> &str {
306            &self.app_name
307        }
308
309        fn uri_infos(&self) -> &Vec<UriInfo> {
310            &self.uri_infos
311        }
312    }
313
314    /// Macro to register the `ShenYu` client once.
315    ///
316    /// This macro ensures that the `ShenYu` client is registered only once using a `OnceLock`.
317    /// It initializes the client with the provided configuration, router, and port, and sets up
318    /// a shutdown hook to deregister the client upon receiving a `ctrl_c` signal.
319    ///
320    /// # Arguments
321    ///
322    /// * `$config` - The configuration for the `ShenYu` client.
323    /// * `$router` - The router instance.
324    /// * `$port` - The port number.
325    #[macro_export]
326    macro_rules! register_once {
327        ($config:expr, $router:expr, $port:literal) => {
328            use std::sync::OnceLock;
329            use $crate::IRouter;
330
331            static ONCE: OnceLock<()> = OnceLock::new();
332            ONCE.get_or_init(|| {
333                let client = {
334                    let res = $crate::core::ShenyuClient::new(
335                        $config,
336                        $router.app_name(),
337                        $router.uri_infos(),
338                        $port,
339                    );
340                    let client = res.unwrap();
341                    client
342                };
343                client.register().expect("Failed to register");
344                actix_web::rt::spawn(async move {
345                    // Add shutdown hook
346                    tokio::select! {
347                        _ = actix_web::rt::signal::ctrl_c() => {
348                            client.offline_register();
349                        }
350                    }
351                });
352            });
353        };
354    }
355
356    /// Macro to define routes for the `ShenYu` router.
357    ///
358    /// This macro allows you to define routes for the `ShenYu` router in a concise manner.
359    /// It supports both regular routes and nested routes.
360    ///
361    /// # Arguments
362    ///
363    /// * `$router` - The router instance.
364    /// * `$app` - The Actix web application instance.
365    /// * `$path` - The path for the route.
366    /// * `$method` - The HTTP method for the route (e.g., `get`, `post`).
367    /// * `$handler` - The handler function for the route.
368    ///
369    #[macro_export]
370    macro_rules! shenyu_router {
371        ($router:expr, $app:expr, $($path:expr => $method:ident($handler:expr))*) => {
372            $(
373                // fixme the handler method name
374                $router.route($path, stringify!($method));
375                $app = $app.service(actix_web::web::resource($path).route(actix_web::web::$method().to($handler)));
376            )*
377        }
378    }
379}
380
381#[cfg(test)]
382#[cfg(feature = "axum")]
383mod tests_axum {
384    use super::axum_impl::ShenYuRouter;
385    use crate::config::ShenYuConfig;
386    use crate::core::ShenyuClient;
387    use crate::IRouter;
388    use axum::routing::{get, post};
389    use serde_json::Value;
390    use std::collections::HashMap;
391
392    async fn health_handler() -> &'static str {
393        "OK"
394    }
395
396    async fn create_user_handler() -> &'static str {
397        "User created"
398    }
399
400    #[tokio::test]
401    async fn test_login() {
402        let mut hashmap = HashMap::new();
403        _ = hashmap.insert("username", "admin");
404        _ = hashmap.insert("password", "123456");
405        let params = [
406            ("userName", hashmap.get("username").copied().unwrap()),
407            ("password", hashmap.get("password").copied().unwrap()),
408        ];
409
410        // Fix the URL to include the scheme
411        let res = ureq::get("http://127.0.0.1:9095/platform/login")
412            .query_pairs(params)
413            .call()
414            .unwrap();
415        let res_data: Value = res.into_json().unwrap();
416        print!("res_data: {:?}", res_data);
417        print!("res_data:token {:?}", res_data["data"]["token"]);
418    }
419
420    #[tokio::test]
421    async fn build_client() {
422        let app = ShenYuRouter::<()>::new("shenyu_client_app")
423            .nest("/api", ShenYuRouter::new("api"))
424            .route("/health", "get", get(health_handler))
425            .route("/users", "post", post(create_user_handler));
426        let config = ShenYuConfig::from_yaml_file("config.yml").unwrap();
427        let res = ShenyuClient::new(config, app.app_name(), app.uri_infos(), 9527);
428        assert!(&res.is_ok());
429        let client = &mut res.unwrap();
430
431        client.register().unwrap();
432        client.offline_register();
433    }
434
435    #[test]
436    fn it_works() {
437        let binding = ShenYuRouter::<()>::new("shenyu_client_app");
438        let app = binding
439            .nest("/api", ShenYuRouter::new("api"))
440            .route("/health", "get", get(health_handler))
441            .route("/users", "post", post(create_user_handler));
442        let uri_infos = app.uri_infos();
443        assert_eq!(uri_infos.len(), 2);
444        assert_eq!(uri_infos[0].path, "/health");
445        assert_eq!(uri_infos[1].path, "/users");
446    }
447}
448
449#[cfg(test)]
450#[cfg(feature = "actix-web")]
451mod tests_actix_web {
452    use super::actix_web_impl::ShenYuRouter;
453    use crate::config::ShenYuConfig;
454    use crate::core::ShenyuClient;
455    use crate::IRouter;
456
457    #[tokio::test]
458    async fn build_client() {
459        let app = ShenYuRouter::new("shenyu_client_app");
460        let config = ShenYuConfig::from_yaml_file("config.yml").unwrap();
461        let res = ShenyuClient::new(config, app.app_name(), app.uri_infos(), 9527);
462        assert!(&res.is_ok());
463        let client = &mut res.unwrap();
464
465        client.register().unwrap();
466        client.offline_register();
467    }
468}