2
0
Fork 0
mirror of https://github.com/leptos-rs/leptos synced 2025-02-03 07:23:26 +00:00

hexagonal architecture ()

Co-authored-by: Sam <@>
This commit is contained in:
Sam Jude 2025-01-24 23:29:07 -05:00 committed by GitHub
parent 09bbae2a72
commit f8acdf9168
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1397 additions and 0 deletions

View file

@ -0,0 +1,14 @@
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb

View file

@ -0,0 +1,111 @@
[package]
name = "leptos-hexagonal-design"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
leptos = { version = "0.7.0" }
leptos_router = { version = "0.7.0" }
axum = { version = "0.7", optional = true }
console_error_panic_hook = "0.1"
leptos_axum = { version = "0.7.0", optional = true }
leptos_meta = { version = "0.7.0" }
tokio = { version = "1", features = ["rt-multi-thread"], optional = true }
tower = { version = "0.4", optional = true }
tower-http = { version = "0.5", features = ["fs"], optional = true }
wasm-bindgen = "=0.2.99"
thiserror = "1"
tracing = { version = "0.1", optional = true }
http = "1"
mockall = "0.13.1"
cfg-if = "1.0.0"
serde = "1.0.215"
pin-project-lite = "0.2.15"
[features]
config_1 = []
hydrate = ["leptos/hydrate"]
ssr = [
"dep:axum",
"dep:tokio",
"dep:tower",
"dep:tower-http",
"dep:leptos_axum",
"leptos/ssr",
"leptos_meta/ssr",
"leptos_router/ssr",
"dep:tracing",
]
# Defines a size-optimized profile for the WASM bundle in release mode
[profile.wasm-release]
inherits = "release"
opt-level = 'z'
lto = true
codegen-units = 1
panic = "abort"
[package.metadata.leptos]
# The name used by wasm-bindgen/cargo-leptos for the JS/WASM bundle. Defaults to the crate name
output-name = "leptos-hexagonal-design"
# The site root folder is where cargo-leptos generate all output. WARNING: all content of this folder will be erased on a rebuild. Use it in your server setup.
site-root = "target/site"
# The site-root relative folder where all compiled output (JS, WASM and CSS) is written
# Defaults to pkg
site-pkg-dir = "pkg"
# [Optional] The source CSS file. If it ends with .sass or .scss then it will be compiled by dart-sass into CSS. The CSS is optimized by Lightning CSS before being written to <site-root>/<site-pkg>/app.css
style-file = "style/main.scss"
# Assets source dir. All files found here will be copied and synchronized to site-root.
# The assets-dir cannot have a sub directory with the same name/path as site-pkg-dir.
#
# Optional. Env: LEPTOS_ASSETS_DIR.
assets-dir = "public"
# The IP and port (ex: 127.0.0.1:3000) where the server serves the content. Use it in your server setup.
site-addr = "127.0.0.1:3000"
# The port to use for automatic reload monitoring
reload-port = 3001
# [Optional] Command to use when running end2end tests. It will run in the end2end dir.
# [Windows] for non-WSL use "npx.cmd playwright test"
# This binary name can be checked in Powershell with Get-Command npx
end2end-cmd = "npx playwright test"
end2end-dir = "end2end"
# The browserlist query used for optimizing the CSS.
browserquery = "defaults"
# The environment Leptos will run in, usually either "DEV" or "PROD"
env = "DEV"
# The features to use when compiling the bin target
#
# Optional. Can be over-ridden with the command line parameter --bin-features
bin-features = ["ssr"]
# If the --no-default-features flag should be used when compiling the bin target
#
# Optional. Defaults to false.
bin-default-features = false
# The features to use when compiling the lib target
#
# Optional. Can be over-ridden with the command line parameter --lib-features
lib-features = ["hydrate"]
# If the --no-default-features flag should be used when compiling the lib target
#
# Optional. Defaults to false.
lib-default-features = false
# The profile to use for the lib target when compiling for release
#
# Optional. Defaults to "release".
lib-profile-release = "wasm-release"

View file

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <https://unlicense.org>

View file

@ -0,0 +1,142 @@
### Leptos Hexagonal Design
This Blog Post / Github Repository is about applying principles of hexagonal design
- Isolating Business Logic from Sub Domains
- Decoupling design to improve flexibility and testablity
- Applying the principles hierachically so that sub domains which talk to external services also implement also implement hexagonal architecture
There are specific constraints that guide our design decisions
- Server Functions Can't be Generic
- Boxed Traits Objects Have overhead, so we only want to use as much generic code as possible avoid Trait Objects
The way this works is we define the functionality of our program in the main domain (i.e the business problem and processes our app is trying to solve / proceduralize). We then create sub domains and external services, although they are represented the same. External services are usually the end nodes of your app's architectural graph. Our main application builds it's service layout using configuration flags.
```rust
pub fn config() -> MainAppHandlerAlias {
cfg_if::cfg_if! {
if #[cfg(feature="open_ai_wrapper")] {
fn server_handler_config_1() -> MainAppHandler<
AuthService<PostgresDb, Redis>,
AiMessageGen<PostgresDb,OpenAiWrapper>,
> {
MainAppHandler::new_with_postgres_and_redis_open_ai()
}
server_handler_config_1()
} else {
fn server_handler_config_2() -> MainAppHandler<
AuthService<MySql, MemCache>,
OtherAiMessageGen<MySql,HuggingFaceWrapper>,
> {
MainAppHandler::new_with_my_sql_memcache_hugging_face()
}
server_handler_config_2()
}
}
}
```
And we pass in our handler which implements a trait
```rust
pub trait HandlerServerFn {
pub fn server_fn_1_inner(&self);
}
impl<S,S2> HandlerServerFn for MainAppHandler<S:SubDomain1Trait,S2:SubDomain2Trait> {
pub fn server_fn_1_inner(&self) {
// do thing
}
}
```
in our main fn we produce our applications service graph and pass it to our leptos router.
```rust
main () {
let leptos_options = conf.leptos_options;
let routes = generate_route_list(crate::app::App);
// our feature flag based config function.
let handler = config();
let handler_c = handler.clone();
// we implement FromRef<ServerState> for LeptosOptions
let server_state = ServerState {
handler,
leptos_options: leptos_options.clone(),
};
let app = Router::new()
.leptos_routes_with_context(
&server_state,
routes,
// We pass in the MainAppHandler struct as context so we can fetch it anywhere context is available on the server.
// This includes in middleware we define on server functions (see middleware.rs)
move || provide_context(handler_c.clone()),
{
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
},
)
.fallback(leptos_axum::file_and_error_handler::<
ServerState<HandlerStructAlias>,
_,
>(shell))
.with_state(server_state);
}
```
and then in our server functions
```rust
#[server]
pub async fn server_fn_1() -> Result<(),ServerFnError> {
// we type alias every variation of our services we plan on configuring. The alternative is using Box<dyn Trait> which isn't bad - just slower.
Ok(expect_context::<MainAppHandlerAlias>().server_fn_1_inner())
}
```
And then we can mock and service trait in any combination like so
```rust
#[tokio::test]
pub async fn test_subdomain_1_with_mocks() -> Result<(), Box<dyn Error>> {
let mut mock_external_service_1 = MockExternalServiceTrait1::new();
mock_external_service_1
.expect_external_service_1_method()
.returning(|| {
println!("Mock external service 1");
Ok(ExternalService1Data)
});
let mut mock_external_service_2 = MockExternalServiceTrait2::new();
mock_external_service_2
.expect_external_service_2_method()
.returning(|| {
println!("Mock external service 2");
Ok(ExternalService2Data)
});
let real_subdomain_1_with_mock_externals = SubDomainStruct1 {
external_service_1: mock_external_service_1,
external_service_2: mock_external_service_2,
};
let data = real_subdomain_1_with_mock_externals
.sub_domain_1_method()
.await?;
assert_eq!(data, SubDomain1Data);
Ok(())
}
```
Check out the code in the repository for a working example.
Run the tests with
` cargo test --features ssr `
and otherwise run
` cargo leptos serve `
and navigate to `127.0.0.1:3000`
here's a picture
![alt text](leptos_hexagonal_architecture.png)

View file

@ -0,0 +1,167 @@
{
"name": "end2end",
"version": "1.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "end2end",
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.44.1",
"@types/node": "^20.12.12",
"typescript": "^5.4.5"
}
},
"node_modules/@playwright/test": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.44.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@types/node": {
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.44.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
}
},
"node_modules/typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true,
"license": "MIT"
}
},
"dependencies": {
"@playwright/test": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true,
"requires": {
"playwright": "1.44.1"
}
},
"@types/node": {
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true,
"requires": {
"undici-types": "~5.26.4"
}
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"playwright": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"dev": true,
"requires": {
"fsevents": "2.3.2",
"playwright-core": "1.44.1"
}
},
"playwright-core": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true
},
"typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"dev": true
},
"undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
}
}
}

View file

@ -0,0 +1,15 @@
{
"name": "end2end",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@playwright/test": "^1.44.1",
"@types/node": "^20.12.12",
"typescript": "^5.4.5"
}
}

View file

@ -0,0 +1,105 @@
import type { PlaywrightTestConfig } from "@playwright/test";
import { devices, defineConfig } from "@playwright/test";
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: "./tests",
/* Maximum time one test can run for. */
timeout: 30 * 1000,
expect: {
/**
* Maximum time expect() should wait for the condition to be met.
* For example in `await expect(locator).toHaveText();`
*/
timeout: 5000,
},
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */
actionTimeout: 0,
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://localhost:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: {
...devices["Desktop Chrome"],
},
},
{
name: "firefox",
use: {
...devices["Desktop Firefox"],
},
},
{
name: "webkit",
use: {
...devices["Desktop Safari"],
},
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: {
// ...devices['Pixel 5'],
// },
// },
// {
// name: 'Mobile Safari',
// use: {
// ...devices['iPhone 12'],
// },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: {
// channel: 'msedge',
// },
// },
// {
// name: 'Google Chrome',
// use: {
// channel: 'chrome',
// },
// },
],
/* Folder for test artifacts such as screenshots, videos, traces, etc. */
// outputDir: 'test-results/',
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// port: 3000,
// },
});

View file

@ -0,0 +1,9 @@
import { test, expect } from "@playwright/test";
test("homepage has title and heading text", async ({ page }) => {
await page.goto("http://localhost:3000/");
await expect(page).toHaveTitle("Welcome to Leptos");
await expect(page.locator("h1")).toHaveText("Welcome to Leptos!");
});

View file

@ -0,0 +1,109 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
// "rootDir": "./", /* Specify the root folder within your source files. */
// "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

Binary file not shown.

After

(image error) Size: 292 KiB

Binary file not shown.

After

Width: 48px  |  Height: 48px  |  Size: 15 KiB

View file

@ -0,0 +1,87 @@
use leptos::prelude::*;
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
use leptos_router::{
components::{Route, Router, Routes},
StaticSegment,
};
#[cfg(feature = "ssr")]
use super::{server_types::HandlerStructAlias, traits::HandlerTrait};
use crate::ui_types::*;
pub fn shell(options: LeptosOptions) -> impl IntoView {
view! {
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<AutoReload options=options.clone() />
<HydrationScripts options/>
<MetaTags/>
</head>
<body>
<App/>
</body>
</html>
}
}
#[component]
pub fn App() -> impl IntoView {
// Provides context that manages stylesheets, titles, meta tags, etc.
provide_meta_context();
view! {
// injects a stylesheet into the document <head>
// id=leptos means cargo-leptos will hot-reload this stylesheet
<Stylesheet id="leptos" href="/pkg/leptos-hexagonal-design.css"/>
// sets the document title
<Title text="Welcome to Leptos"/>
// content for this welcome page
<Router>
<main>
<Routes fallback=|| "Page not found.".into_view()>
<Route path=StaticSegment("") view=HomePage/>
</Routes>
</main>
</Router>
}
}
/// Renders the home page of your application.
#[component]
fn HomePage() -> impl IntoView {
let server_fn_1 = ServerAction::<ServerFunction1>::new();
let server_fn_2 = ServerAction::<ServerFunction2>::new();
let server_fn_3 = ServerAction::<ServerFunction3>::new();
Effect::new(move |_| {
server_fn_1.dispatch(ServerFunction1 {});
server_fn_2.dispatch(ServerFunction2 {});
server_fn_3.dispatch(ServerFunction3 {});
});
}
#[server]
#[middleware(crate::middleware::SubDomain1Layer)]
pub async fn server_function_1() -> Result<UiMappingFromDomainData, ServerFnError> {
Ok(expect_context::<HandlerStructAlias>()
.server_fn_1()
.await?
.into())
}
#[server]
pub async fn server_function_2() -> Result<UiMappingFromDomainData2, ServerFnError> {
Ok(expect_context::<HandlerStructAlias>()
.server_fn_2()
.await?
.into())
}
#[server]
pub async fn server_function_3() -> Result<UiMappingFromDomainData3, ServerFnError> {
Ok(expect_context::<HandlerStructAlias>()
.server_fn_3()
.await?
.into())
}

View file

@ -0,0 +1,23 @@
use super::server_types::*;
pub fn config() -> HandlerStructAlias {
cfg_if::cfg_if! {
if #[cfg(feature="config_1")] {
fn server_handler_config_1() -> HandlerStruct<
SubDomainStruct1<ExternalService1_1, ExternalService2_1>,
SubDomainStruct2<ExternalService1_1>,
> {
HandlerStruct::default()
}
server_handler_config_1()
} else {
fn server_handler_config_2() -> HandlerStruct<
SubDomainStruct1<ExternalService1_2, ExternalService2_2>,
SubDomainStruct2<ExternalService1_2>,
> {
HandlerStruct::new()
}
server_handler_config_2()
}
}
}

View file

@ -0,0 +1,150 @@
pub mod app;
pub mod ui_types;
#[cfg(feature = "ssr")]
pub mod config;
#[cfg(feature = "ssr")]
pub mod middleware;
#[cfg(feature = "ssr")]
pub mod server_types;
#[cfg(feature = "ssr")]
pub mod trait_impl;
#[cfg(feature = "ssr")]
pub mod traits;
#[cfg(feature = "hydrate")]
#[wasm_bindgen::prelude::wasm_bindgen]
pub fn hydrate() {
use crate::app::*;
console_error_panic_hook::set_once();
leptos::mount::hydrate_body(App);
}
#[cfg(test)]
pub mod tests {
use super::server_types::*;
use super::traits::*;
use std::error::Error;
#[tokio::test]
pub async fn test_subdomain_1_with_mocks() -> Result<(), Box<dyn Error>> {
let mut mock_external_service_1 = MockExternalServiceTrait1::new();
mock_external_service_1
.expect_external_service_1_method()
.returning(|| {
println!("Mock external service 1");
Ok(ExternalService1Data)
});
let mut mock_external_service_2 = MockExternalServiceTrait2::new();
mock_external_service_2
.expect_external_service_2_method()
.returning(|| {
println!("Mock external service 2");
Ok(ExternalService2Data)
});
let real_subdomain_1_with_mock_externals = SubDomainStruct1 {
external_service_1: mock_external_service_1,
external_service_2: mock_external_service_2,
};
let data = real_subdomain_1_with_mock_externals
.sub_domain_1_method()
.await?;
assert_eq!(data, SubDomain1Data);
Ok(())
}
#[tokio::test]
pub async fn test_subdomain_2_with_mocks() -> Result<(), Box<dyn Error>> {
let mut mock_external_service_1 = MockExternalServiceTrait1::new();
mock_external_service_1
.expect_external_service_1_method()
.returning(|| {
println!("Mock external service 1 AGAIN");
Ok(ExternalService1Data)
});
let real_subdomain_2_with_mock_externals = SubDomainStruct2 {
external_service_1: mock_external_service_1,
};
let data = real_subdomain_2_with_mock_externals
.sub_domain_2_method()
.await?;
assert_eq!(data, SubDomain2Data);
Ok(())
}
#[tokio::test]
pub async fn test_handler_with_mocks() -> Result<(), Box<dyn Error>> {
let mut mock_subdomain_1_trait = MockSubDomainTrait1::new();
mock_subdomain_1_trait
.expect_sub_domain_1_method()
.returning(|| {
println!("Mock Subdomain 1");
Ok(SubDomain1Data)
});
let mut mock_subdomain_2_trait = MockSubDomainTrait2::new();
mock_subdomain_2_trait
.expect_sub_domain_2_method()
.returning(|| {
println!("Mock Subdomain 2");
Ok(SubDomain2Data)
});
let real_handler_with_mock_subdomains = HandlerStruct {
sub_domain_1: mock_subdomain_1_trait,
sub_domain_2: mock_subdomain_2_trait,
};
let data = real_handler_with_mock_subdomains.server_fn_1().await?;
assert_eq!(data, DomainData);
let data = real_handler_with_mock_subdomains.server_fn_2().await?;
assert_eq!(data, DomainData2);
let data = real_handler_with_mock_subdomains.server_fn_3().await?;
assert_eq!(data, DomainData3);
Ok(())
}
fn mock_subdomain_1() -> SubDomainStruct1<MockExternalServiceTrait1, MockExternalServiceTrait2>
{
let mut mock_external_service_1 = MockExternalServiceTrait1::new();
mock_external_service_1
.expect_external_service_1_method()
.returning(|| {
println!("Mock external service 1");
Ok(ExternalService1Data)
});
let mut mock_external_service_2 = MockExternalServiceTrait2::new();
mock_external_service_2
.expect_external_service_2_method()
.returning(|| {
println!("Mock external service 2");
Ok(ExternalService2Data)
});
let real_subdomain_1_with_mock_externals = SubDomainStruct1 {
external_service_1: mock_external_service_1,
external_service_2: mock_external_service_2,
};
real_subdomain_1_with_mock_externals
}
#[tokio::test]
pub async fn test_handler_with_mock_and_real_mix() -> Result<(), Box<dyn Error>> {
let sub_domain_1 = mock_subdomain_1();
let mut mock_subdomain_2_trait = MockSubDomainTrait2::new();
mock_subdomain_2_trait
.expect_sub_domain_2_method()
.returning(|| {
println!("Mock Subdomain 2");
Ok(SubDomain2Data)
});
let real_handler = HandlerStruct {
sub_domain_1,
sub_domain_2: mock_subdomain_2_trait,
};
let data = real_handler.server_fn_1().await?;
assert_eq!(data, DomainData);
let data = real_handler.server_fn_2().await?;
assert_eq!(data, DomainData2);
let data = real_handler.server_fn_3().await?;
assert_eq!(data, DomainData3);
Ok(())
}
}

View file

@ -0,0 +1,52 @@
#[cfg(feature = "ssr")]
#[tokio::main]
async fn main() {
use axum::Router;
use leptos::logging::log;
use leptos::prelude::*;
use leptos_axum::{generate_route_list, LeptosRoutes};
use leptos_hexagonal_design::{
app::*,
config::config,
server_types::{HandlerStructAlias, ServerState},
};
let conf = get_configuration(None).unwrap();
let addr = conf.leptos_options.site_addr;
let leptos_options = conf.leptos_options;
let routes = generate_route_list(App);
let handler = config();
let handler_c = handler.clone();
let server_state = ServerState {
handler,
leptos_options: leptos_options.clone(),
};
let app = Router::new()
.leptos_routes_with_context(
&server_state,
routes,
move || provide_context(handler_c.clone()),
{
let leptos_options = leptos_options.clone();
move || shell(leptos_options.clone())
},
)
.fallback(leptos_axum::file_and_error_handler::<
ServerState<HandlerStructAlias>,
_,
>(shell))
.with_state(server_state);
log!("listening on http://{}", &addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app.into_make_service())
.await
.unwrap();
}
#[cfg(not(feature = "ssr"))]
pub fn main() {
// no client-side main function
// unless we want this to work with e.g., Trunk for pure client-side testing
// see lib.rs for hydration function instead
}

View file

@ -0,0 +1,84 @@
use axum::{
body::Body,
http::{Request, Response},
};
use leptos::prelude::expect_context;
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use tower::{Layer, Service};
use crate::{
server_types::{HandlerStructAlias, ServerState},
traits::SubDomainTrait1,
};
use pin_project_lite::pin_project;
#[derive(Clone)]
pub struct SubDomain1Layer;
impl<S> Layer<S> for SubDomain1Layer {
type Service = SubDomain1MiddleWare<S>;
fn layer(&self, inner: S) -> Self::Service {
SubDomain1MiddleWare { inner }
}
}
pub struct SubDomain1MiddleWare<S> {
inner: S,
}
impl<S, ReqBody> Service<Request<ReqBody>> for SubDomain1MiddleWare<S>
where
S: Service<Request<ReqBody>, Response = Response<Body>>,
S::Error: std::fmt::Debug,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = SubDomain1Future<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
let req_fut = self.inner.call(req);
SubDomain1Future { req_fut }
}
}
pin_project! {
pub struct SubDomain1Future<F> {
#[pin]
req_fut: F,
}
}
impl<F, Err> Future for SubDomain1Future<F>
where
F: Future<Output = Result<Response<Body>, Err>>,
{
type Output = Result<Response<Body>, Err>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let subdomain_1 = expect_context::<ServerState<HandlerStructAlias>>()
.handler
.sub_domain_1;
let mut subdomain_1_fut = subdomain_1.sub_domain_1_method();
match Pin::as_mut(&mut subdomain_1_fut).poll(cx) {
Poll::Ready(Ok(_)) => {
println!("Middleware for Subdomain 1 Passed, calling request...");
this.req_fut.poll(cx)
}
Poll::Ready(Err(_)) => Poll::Ready(Ok(Response::builder()
.status(http::StatusCode::FORBIDDEN)
.body(Body::from("Access denied"))
.unwrap())),
Poll::Pending => Poll::Pending,
}
}
}

View file

@ -0,0 +1,102 @@
use super::traits::*;
use leptos::config::LeptosOptions;
use thiserror::Error;
#[derive(Clone)]
pub struct ServerState<Handler: HandlerTrait> {
pub handler: Handler,
pub leptos_options: LeptosOptions,
}
#[cfg(feature = "config_1")]
pub type HandlerStructAlias = HandlerStruct<
SubDomainStruct1<ExternalService1_1, ExternalService2_1>,
SubDomainStruct2<ExternalService1_1>,
>;
#[cfg(not(feature = "config_1"))]
pub type HandlerStructAlias = HandlerStruct<
SubDomainStruct1<ExternalService1_2, ExternalService2_2>,
SubDomainStruct2<ExternalService1_2>,
>;
#[derive(Clone, Default)]
pub struct HandlerStruct<SubDomain1: SubDomainTrait1, SubDomain2: SubDomainTrait2> {
pub sub_domain_1: SubDomain1,
pub sub_domain_2: SubDomain2,
}
#[derive(Clone, Default)]
pub struct SubDomainStruct1<
ExternalService1: ExternalServiceTrait1,
ExternalService2: ExternalServiceTrait2,
> {
pub external_service_1: ExternalService1,
pub external_service_2: ExternalService2,
}
#[derive(Clone, Default)]
pub struct SubDomainStruct2<ExternalService1: ExternalServiceTrait1> {
pub external_service_1: ExternalService1,
}
#[derive(Clone, Default)]
pub struct ExternalService1_1;
#[derive(Clone, Default)]
pub struct ExternalService1_2;
#[derive(Clone, Default)]
pub struct ExternalService2_1;
#[derive(Clone, Default)]
pub struct ExternalService2_2;
#[derive(Clone, Default)]
pub struct ExternalService1;
#[derive(Clone, PartialEq, Debug)]
pub struct DomainData;
#[derive(Clone, PartialEq, Debug)]
pub struct DomainData2;
#[derive(Clone, PartialEq, Debug)]
pub struct DomainData3;
#[derive(Clone, PartialEq, Debug)]
pub struct SubDomain1Data;
#[derive(Clone, PartialEq, Debug)]
pub struct SubDomain2Data;
#[derive(Clone)]
pub struct ExternalService1Data;
#[derive(Clone)]
pub struct ExternalService2Data;
#[derive(Clone, Error, Debug)]
pub enum DomainError {
#[error("Underlying Subdomain 1 Error")]
SubDomain1Error(#[from] SubDomain1Error),
#[error("Underlying Subdomain 2 Error")]
SubDomain2Error(#[from] SubDomain2Error),
}
#[derive(Clone, Error, Debug)]
pub enum SubDomain1Error {
#[error("Sub Domain 1 Error")]
SubDomain1Error,
#[error("Underlying Service 1")]
ExternalService1Error(#[from] ExternalService1Error),
#[error("Underlying Service 2")]
ExternalService2Error(#[from] ExternalService2Error),
}
#[derive(Clone, Error, Debug)]
pub enum SubDomain2Error {
#[error("Sub Domain 2 Error")]
SubDomain2Error,
#[error("Underlying Service 1")]
ExternalService1Error(#[from] ExternalService1Error),
}
#[derive(Clone, Error, Debug)]
pub enum ExternalService1Error {
#[error("Service 1 Error")]
Error,
}
#[derive(Clone, Error, Debug)]
pub enum ExternalService2Error {
#[error("Service 2 Error")]
Error,
}

View file

@ -0,0 +1,149 @@
use crate::ui_types::*;
use super::server_types::*;
use super::traits::*;
use axum::async_trait;
use axum::extract::FromRef;
use leptos::config::LeptosOptions;
// So we can pass our server state as state into our leptos router.
impl<Handler: HandlerTrait + Clone> FromRef<ServerState<Handler>> for LeptosOptions {
fn from_ref(input: &ServerState<Handler>) -> Self {
input.leptos_options.clone()
}
}
#[async_trait]
impl<SubDomain1, SubDomain2> HandlerTrait for HandlerStruct<SubDomain1, SubDomain2>
where
SubDomain1: SubDomainTrait1 + Send + Sync,
SubDomain2: SubDomainTrait2 + Send + Sync,
{
async fn server_fn_1(&self) -> Result<DomainData, DomainError> {
Ok(self.sub_domain_1.sub_domain_1_method().await?.into())
}
async fn server_fn_2(&self) -> Result<DomainData2, DomainError> {
Ok(self.sub_domain_2.sub_domain_2_method().await?.into())
}
async fn server_fn_3(&self) -> Result<DomainData3, DomainError> {
Ok((
self.sub_domain_1.sub_domain_1_method().await?,
self.sub_domain_2.sub_domain_2_method().await?,
)
.into())
}
}
#[async_trait]
impl<ExternalService1, ExternalService2> SubDomainTrait1
for SubDomainStruct1<ExternalService1, ExternalService2>
where
ExternalService1: ExternalServiceTrait1 + Send + Sync,
ExternalService2: ExternalServiceTrait2 + Send + Sync,
{
async fn sub_domain_1_method(&self) -> Result<SubDomain1Data, SubDomain1Error> {
Ok((
self.external_service_1.external_service_1_method().await?,
self.external_service_2.external_service_2_method().await?,
)
.into())
}
}
#[async_trait]
impl<ExternalService1> SubDomainTrait2 for SubDomainStruct2<ExternalService1>
where
ExternalService1: ExternalServiceTrait1 + Send + Sync,
{
async fn sub_domain_2_method(&self) -> Result<SubDomain2Data, SubDomain2Error> {
Ok(self
.external_service_1
.external_service_1_method()
.await?
.into())
}
}
#[async_trait]
impl ExternalServiceTrait1 for ExternalService1_1 {
async fn external_service_1_method(
&self,
) -> Result<ExternalService1Data, ExternalService1Error> {
println!("External Service 1 From External Service 1_1");
Ok(ExternalService1Data)
}
}
#[async_trait]
impl ExternalServiceTrait1 for ExternalService1_2 {
async fn external_service_1_method(
&self,
) -> Result<ExternalService1Data, ExternalService1Error> {
println!("External Service 1 From External Service 1_2");
Ok(ExternalService1Data)
}
}
#[async_trait]
impl ExternalServiceTrait2 for ExternalService2_1 {
async fn external_service_2_method(
&self,
) -> Result<ExternalService2Data, ExternalService2Error> {
println!("External Service 2 From External Service 2_1");
Ok(ExternalService2Data)
}
}
#[async_trait]
impl ExternalServiceTrait2 for ExternalService2_2 {
async fn external_service_2_method(
&self,
) -> Result<ExternalService2Data, ExternalService2Error> {
println!("External Service 2 From External Service 2_2");
Ok(ExternalService2Data)
}
}
// Sub Domain mapping
impl From<(ExternalService1Data, ExternalService2Data)> for SubDomain1Data {
fn from(_: (ExternalService1Data, ExternalService2Data)) -> Self {
Self
}
}
impl From<ExternalService1Data> for SubDomain2Data {
fn from(_: ExternalService1Data) -> Self {
Self
}
}
// Domain Mapping
impl From<SubDomain1Data> for DomainData {
fn from(_: SubDomain1Data) -> Self {
Self
}
}
impl From<SubDomain2Data> for DomainData2 {
fn from(_: SubDomain2Data) -> Self {
Self
}
}
impl From<(SubDomain1Data, SubDomain2Data)> for DomainData3 {
fn from(_: (SubDomain1Data, SubDomain2Data)) -> Self {
Self
}
}
// Ui Mapping
impl From<DomainData> for UiMappingFromDomainData {
fn from(_: DomainData) -> Self {
Self
}
}
impl From<DomainData2> for UiMappingFromDomainData2 {
fn from(_: DomainData2) -> Self {
Self
}
}
impl From<DomainData3> for UiMappingFromDomainData3 {
fn from(_: DomainData3) -> Self {
Self
}
}

View file

@ -0,0 +1,42 @@
use super::server_types::*;
use axum::async_trait;
use mockall::automock;
pub trait New {
fn new() -> Self;
}
#[automock]
#[async_trait]
pub trait HandlerTrait {
async fn server_fn_1(&self) -> Result<DomainData, DomainError>;
async fn server_fn_2(&self) -> Result<DomainData2, DomainError>;
async fn server_fn_3(&self) -> Result<DomainData3, DomainError>;
}
#[automock]
#[async_trait]
pub trait SubDomainTrait1 {
async fn sub_domain_1_method(&self) -> Result<SubDomain1Data, SubDomain1Error>;
}
#[automock]
#[async_trait]
pub trait SubDomainTrait2 {
async fn sub_domain_2_method(&self) -> Result<SubDomain2Data, SubDomain2Error>;
}
#[automock]
#[async_trait]
pub trait ExternalServiceTrait1 {
async fn external_service_1_method(
&self,
) -> Result<ExternalService1Data, ExternalService1Error>;
}
#[automock]
#[async_trait]
pub trait ExternalServiceTrait2 {
async fn external_service_2_method(
&self,
) -> Result<ExternalService2Data, ExternalService2Error>;
}

View file

@ -0,0 +1,8 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
pub struct UiMappingFromDomainData;
#[derive(Serialize, Deserialize)]
pub struct UiMappingFromDomainData2;
#[derive(Serialize, Deserialize)]
pub struct UiMappingFromDomainData3;

View file

@ -0,0 +1,4 @@
body {
font-family: sans-serif;
text-align: center;
}