diff --git a/.github/workflows/samples-rust-server.yaml b/.github/workflows/samples-rust-server.yaml
index 7530b82530d6..aa24e23dfc28 100644
--- a/.github/workflows/samples-rust-server.yaml
+++ b/.github/workflows/samples-rust-server.yaml
@@ -5,10 +5,12 @@ on:
paths:
- "samples/server/petstore/rust-server/**"
- "samples/server/petstore/rust-axum/**"
+ - "samples/server/petstore/rust-salvo/**"
pull_request:
paths:
- "samples/server/petstore/rust-server/**"
- "samples/server/petstore/rust-axum/**"
+ - "samples/server/petstore/rust-salvo/**"
jobs:
build:
@@ -22,6 +24,7 @@ jobs:
- samples/server/petstore/rust-server/
- samples/server/petstore/rust-server-deprecated/
- samples/server/petstore/rust-axum/
+ - samples/server/petstore/rust-salvo/
steps:
- uses: actions/checkout@v5
- uses: actions-rs/toolchain@v1
diff --git a/bin/configs/manual/rust-salvo-petstore.yaml b/bin/configs/manual/rust-salvo-petstore.yaml
new file mode 100644
index 000000000000..30fb04bbe3fe
--- /dev/null
+++ b/bin/configs/manual/rust-salvo-petstore.yaml
@@ -0,0 +1,11 @@
+generatorName: rust-salvo
+outputDir: samples/server/petstore/rust-salvo/output/petstore
+inputSpec: modules/openapi-generator/src/test/resources/3_0/petstore.yaml
+templateDir: modules/openapi-generator/src/main/resources/rust-salvo
+generateAliasAsModel: true
+additionalProperties:
+ hideGenerationTimestamp: "true"
+ packageName: petstore_salvo
+ homePageUrl: https://github.com/openapitools/openapi-generator
+globalProperties:
+ skipFormModel: false
diff --git a/docs/generators.md b/docs/generators.md
index d9d5bafcaf3c..ed22c4b53e16 100644
--- a/docs/generators.md
+++ b/docs/generators.md
@@ -147,6 +147,7 @@ The following generators are available:
* [ruby-on-rails](generators/ruby-on-rails.md)
* [ruby-sinatra](generators/ruby-sinatra.md)
* [rust-axum (beta)](generators/rust-axum.md)
+* [rust-salvo (beta)](generators/rust-salvo.md)
* [rust-server](generators/rust-server.md)
* [rust-server-deprecated](generators/rust-server-deprecated.md)
* [scala-akka-http-server (beta)](generators/scala-akka-http-server.md)
diff --git a/docs/generators/rust-salvo.md b/docs/generators/rust-salvo.md
new file mode 100644
index 000000000000..712527544e35
--- /dev/null
+++ b/docs/generators/rust-salvo.md
@@ -0,0 +1,236 @@
+---
+title: Documentation for the rust-salvo Generator
+---
+
+## METADATA
+
+| Property | Value | Notes |
+| -------- | ----- | ----- |
+| generator name | rust-salvo | pass this to the generate command after -g |
+| generator stability | BETA | |
+| generator type | SERVER | |
+| generator language | Rust | |
+| generator default templating engine | mustache | |
+| helpTxt | Generates a Rust server library using Salvo web framework. | |
+
+## CONFIG OPTIONS
+These options may be applied as additional-properties (cli) or configOptions (plugins). Refer to [configuration docs](https://openapi-generator.tech/docs/configuration) for more details.
+
+| Option | Description | Values | Default |
+| ------ | ----------- | ------ | ------- |
+|enableAuthMiddleware|Enable authentication middleware| |false|
+|enableCorsMiddleware|Enable CORS middleware| |false|
+|enableRequestValidation|Enable request validation middleware| |true|
+|enableResponseValidation|Enable response validation middleware| |false|
+|packageName|Rust crate name (convention: snake_case).| |salvo_openapi|
+|packageVersion|Rust crate version.| |null|
+
+## IMPORT MAPPING
+
+| Type/Alias | Imports |
+| ---------- | ------- |
+
+
+## INSTANTIATION TYPES
+
+| Type/Alias | Instantiated By |
+| ---------- | --------------- |
+|array|Vec|
+|map|std::collections::HashMap|
+
+
+## LANGUAGE PRIMITIVES
+
+
+- String
+- bool
+- char
+- f32
+- f64
+- i16
+- i32
+- i64
+- i8
+- isize
+- str
+- u16
+- u32
+- u64
+- u8
+- usize
+
+
+## RESERVED WORDS
+
+
+- Self
+- abstract
+- as
+- async
+- await
+- become
+- box
+- break
+- const
+- continue
+- crate
+- do
+- dyn
+- else
+- enum
+- extern
+- false
+- final
+- fn
+- for
+- if
+- impl
+- in
+- let
+- loop
+- macro
+- match
+- mod
+- move
+- mut
+- override
+- priv
+- pub
+- ref
+- return
+- self
+- static
+- struct
+- super
+- trait
+- true
+- try
+- type
+- typeof
+- unsafe
+- unsized
+- use
+- virtual
+- where
+- while
+- yield
+
+
+## FEATURE SET
+
+
+### Client Modification Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|BasePath|✗|ToolingExtension
+|Authorizations|✗|ToolingExtension
+|UserAgent|✗|ToolingExtension
+|MockServer|✗|ToolingExtension
+
+### Data Type Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Custom|✗|OAS2,OAS3
+|Int32|✓|OAS2,OAS3
+|Int64|✓|OAS2,OAS3
+|Float|✓|OAS2,OAS3
+|Double|✓|OAS2,OAS3
+|Decimal|✓|ToolingExtension
+|String|✓|OAS2,OAS3
+|Byte|✓|OAS2,OAS3
+|Binary|✓|OAS2,OAS3
+|Boolean|✓|OAS2,OAS3
+|Date|✓|OAS2,OAS3
+|DateTime|✓|OAS2,OAS3
+|Password|✓|OAS2,OAS3
+|File|✓|OAS2
+|Uuid|✗|
+|Array|✓|OAS2,OAS3
+|Null|✗|OAS3
+|AnyType|✗|OAS2,OAS3
+|Object|✓|OAS2,OAS3
+|Maps|✓|ToolingExtension
+|CollectionFormat|✓|OAS2
+|CollectionFormatMulti|✓|OAS2
+|Enum|✓|OAS2,OAS3
+|ArrayOfEnum|✓|ToolingExtension
+|ArrayOfModel|✓|ToolingExtension
+|ArrayOfCollectionOfPrimitives|✓|ToolingExtension
+|ArrayOfCollectionOfModel|✓|ToolingExtension
+|ArrayOfCollectionOfEnum|✓|ToolingExtension
+|MapOfEnum|✓|ToolingExtension
+|MapOfModel|✓|ToolingExtension
+|MapOfCollectionOfPrimitives|✓|ToolingExtension
+|MapOfCollectionOfModel|✓|ToolingExtension
+|MapOfCollectionOfEnum|✓|ToolingExtension
+
+### Documentation Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Readme|✗|ToolingExtension
+|Model|✓|ToolingExtension
+|Api|✓|ToolingExtension
+
+### Global Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Host|✓|OAS2,OAS3
+|BasePath|✓|OAS2,OAS3
+|Info|✗|OAS2,OAS3
+|Schemes|✗|OAS2,OAS3
+|PartialSchemes|✓|OAS2,OAS3
+|Consumes|✓|OAS2
+|Produces|✓|OAS2
+|ExternalDocumentation|✗|OAS2,OAS3
+|Examples|✗|OAS2,OAS3
+|XMLStructureDefinitions|✗|OAS2,OAS3
+|MultiServer|✗|OAS3
+|ParameterizedServer|✗|OAS3
+|ParameterStyling|✗|OAS3
+|Callbacks|✗|OAS3
+|LinkObjects|✗|OAS3
+
+### Parameter Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Path|✓|OAS2,OAS3
+|Query|✓|OAS2,OAS3
+|Header|✓|OAS2,OAS3
+|Body|✓|OAS2
+|FormUnencoded|✓|OAS2
+|FormMultipart|✓|OAS2
+|Cookie|✓|OAS3
+
+### Schema Support Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|Simple|✓|OAS2,OAS3
+|Composite|✓|OAS2,OAS3
+|Polymorphism|✗|OAS2,OAS3
+|Union|✗|OAS3
+|allOf|✓|OAS2,OAS3
+|anyOf|✓|OAS3
+|oneOf|✓|OAS3
+|not|✗|OAS3
+
+### Security Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|BasicAuth|✓|OAS2,OAS3
+|ApiKey|✓|OAS2,OAS3
+|OpenIDConnect|✗|OAS3
+|BearerToken|✓|OAS3
+|OAuth2_Implicit|✗|OAS2,OAS3
+|OAuth2_Password|✗|OAS2,OAS3
+|OAuth2_ClientCredentials|✗|OAS2,OAS3
+|OAuth2_AuthorizationCode|✗|OAS2,OAS3
+|SignatureAuth|✗|OAS3
+|AWSV4Signature|✗|ToolingExtension
+
+### Wire Format Feature
+| Name | Supported | Defined By |
+| ---- | --------- | ---------- |
+|JSON|✓|OAS2,OAS3
+|XML|✗|OAS2,OAS3
+|PROTOBUF|✗|ToolingExtension
+|Custom|✓|OAS2,OAS3
diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustSalvoServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustSalvoServerCodegen.java
new file mode 100644
index 000000000000..73d40ebbb337
--- /dev/null
+++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustSalvoServerCodegen.java
@@ -0,0 +1,551 @@
+/*
+ * Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.openapitools.codegen.languages;
+
+import com.samskivert.mustache.Mustache;
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.Operation;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.media.Schema;
+import io.swagger.v3.oas.models.parameters.Parameter;
+import io.swagger.v3.oas.models.parameters.RequestBody;
+import io.swagger.v3.oas.models.responses.ApiResponse;
+import io.swagger.v3.oas.models.servers.Server;
+import io.swagger.v3.oas.models.tags.Tag;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.openapitools.codegen.*;
+import org.openapitools.codegen.meta.GeneratorMetadata;
+import org.openapitools.codegen.meta.Stability;
+import org.openapitools.codegen.meta.features.GlobalFeature;
+import org.openapitools.codegen.meta.features.SchemaSupportFeature;
+import org.openapitools.codegen.meta.features.SecurityFeature;
+import org.openapitools.codegen.meta.features.WireFormatFeature;
+import org.openapitools.codegen.model.ModelMap;
+import org.openapitools.codegen.model.ModelsMap;
+import org.openapitools.codegen.model.OperationMap;
+import org.openapitools.codegen.model.OperationsMap;
+import org.openapitools.codegen.utils.ModelUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.math.BigInteger;
+import java.nio.file.Path;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static org.openapitools.codegen.utils.StringUtils.camelize;
+import static org.openapitools.codegen.utils.StringUtils.underscore;
+
+public class RustSalvoServerCodegen extends AbstractRustCodegen implements CodegenConfig {
+ public static final String PROJECT_NAME = "salvo-openapi-server";
+
+ private String packageName;
+ private String packageVersion;
+ private Boolean enableRequestValidation = true;
+ private Boolean enableResponseValidation = false;
+ private Boolean enableAuthMiddleware = false;
+ private Boolean enableCorsMiddleware = false;
+
+ private String externCrateName;
+
+ // Types specific to Salvo
+ private static final String uuidType = "uuid::Uuid";
+ private static final String bytesType = "Vec";
+ private static final String dateType = "chrono::naive::NaiveDate";
+ private static final String dateTimeType = "chrono::DateTime";
+ private static final String stringType = "String";
+ private static final String objectType = "serde_json::Value";
+ private static final String mapType = "std::collections::HashMap";
+ private static final String vecType = "Vec";
+
+ // MIME types
+ private static final String jsonMimeType = "application/json";
+ private static final String formUrlEncodedMimeType = "application/x-www-form-urlencoded";
+ private static final String multipartMimeType = "multipart/form-data";
+ private static final String plainTextMimeType = "text/plain";
+
+ // Salvo-specific routing and handler mapping
+ private final Map> routeMap = new HashMap<>();
+ private boolean hasAuthHandlers = false;
+
+ private final Logger LOGGER = LoggerFactory.getLogger(RustSalvoServerCodegen.class);
+
+ public RustSalvoServerCodegen() {
+ super();
+
+ modifyFeatureSet(features -> features
+ .wireFormatFeatures(EnumSet.of(
+ WireFormatFeature.JSON,
+ WireFormatFeature.Custom
+ ))
+ .securityFeatures(EnumSet.of(
+ SecurityFeature.ApiKey,
+ SecurityFeature.BasicAuth,
+ SecurityFeature.BearerToken
+ ))
+ .schemaSupportFeatures(EnumSet.of(
+ SchemaSupportFeature.Simple,
+ SchemaSupportFeature.Composite,
+ SchemaSupportFeature.oneOf,
+ SchemaSupportFeature.anyOf,
+ SchemaSupportFeature.allOf
+ ))
+ .excludeGlobalFeatures(
+ GlobalFeature.Info,
+ GlobalFeature.ExternalDocumentation,
+ GlobalFeature.Examples,
+ GlobalFeature.XMLStructureDefinitions,
+ GlobalFeature.MultiServer,
+ GlobalFeature.ParameterizedServer,
+ GlobalFeature.ParameterStyling,
+ GlobalFeature.Callbacks,
+ GlobalFeature.LinkObjects
+ )
+ );
+
+ generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata)
+ .stability(Stability.BETA)
+ .build();
+
+ hideGenerationTimestamp = Boolean.FALSE;
+ outputFolder = Path.of("generated-code", "rust-salvo").toString();
+ embeddedTemplateDir = templateDir = "rust-salvo";
+
+ importMapping = new HashMap<>();
+ modelTemplateFiles.clear();
+ apiTemplateFiles.put("handlers.mustache", ".rs");
+
+ // Setup type mappings for Salvo
+ setupTypeMappings();
+
+ // Setup CLI options
+ setupCliOptions();
+
+ // Setup supporting files
+ setupSupportingFiles();
+ }
+
+ private void setupTypeMappings() {
+ defaultIncludes = new HashSet<>(Set.of("map", "array"));
+
+ languageSpecificPrimitives = new HashSet<>(Set.of(
+ "bool", "char", "i8", "i16", "i32", "i64",
+ "u8", "u16", "u32", "u64", "isize", "usize",
+ "f32", "f64", "str", stringType
+ ));
+
+ instantiationTypes = new HashMap<>(Map.of(
+ "array", vecType,
+ "map", mapType
+ ));
+
+ typeMapping = new HashMap<>(Map.ofEntries(
+ new AbstractMap.SimpleEntry<>("number", "f64"),
+ new AbstractMap.SimpleEntry<>("integer", "i32"),
+ new AbstractMap.SimpleEntry<>("long", "i64"),
+ new AbstractMap.SimpleEntry<>("float", "f32"),
+ new AbstractMap.SimpleEntry<>("double", "f64"),
+ new AbstractMap.SimpleEntry<>("string", stringType),
+ new AbstractMap.SimpleEntry<>("UUID", uuidType),
+ new AbstractMap.SimpleEntry<>("URI", stringType),
+ new AbstractMap.SimpleEntry<>("byte", "u8"),
+ new AbstractMap.SimpleEntry<>("ByteArray", bytesType),
+ new AbstractMap.SimpleEntry<>("binary", bytesType),
+ new AbstractMap.SimpleEntry<>("boolean", "bool"),
+ new AbstractMap.SimpleEntry<>("date", dateType),
+ new AbstractMap.SimpleEntry<>("DateTime", dateTimeType),
+ new AbstractMap.SimpleEntry<>("password", stringType),
+ new AbstractMap.SimpleEntry<>("File", bytesType),
+ new AbstractMap.SimpleEntry<>("file", bytesType),
+ new AbstractMap.SimpleEntry<>("array", vecType),
+ new AbstractMap.SimpleEntry<>("map", mapType),
+ new AbstractMap.SimpleEntry<>("object", objectType),
+ new AbstractMap.SimpleEntry<>("AnyType", objectType)
+ ));
+ }
+
+ private void setupCliOptions() {
+ cliOptions = new ArrayList<>(List.of(
+ new CliOption(CodegenConstants.PACKAGE_NAME,
+ "Rust crate name (convention: snake_case).")
+ .defaultValue("salvo_openapi"),
+ new CliOption(CodegenConstants.PACKAGE_VERSION,
+ "Rust crate version."),
+ new CliOption("enableRequestValidation",
+ "Enable request validation middleware")
+ .defaultValue(Boolean.TRUE.toString()),
+ new CliOption("enableResponseValidation",
+ "Enable response validation middleware")
+ .defaultValue(Boolean.FALSE.toString()),
+ new CliOption("enableAuthMiddleware",
+ "Enable authentication middleware")
+ .defaultValue(Boolean.FALSE.toString()),
+ new CliOption("enableCorsMiddleware",
+ "Enable CORS middleware")
+ .defaultValue(Boolean.FALSE.toString())
+ ));
+ }
+
+ private void setupSupportingFiles() {
+ supportingFiles.add(new SupportingFile("Cargo.mustache", "", "Cargo.toml"));
+ supportingFiles.add(new SupportingFile("gitignore", "", ".gitignore"));
+ supportingFiles.add(new SupportingFile("lib.mustache", "src", "lib.rs"));
+ supportingFiles.add(new SupportingFile("main.mustache", "src", "main.rs"));
+ supportingFiles.add(new SupportingFile("models.mustache", "src", "models.rs"));
+ supportingFiles.add(new SupportingFile("handlers-mod.mustache", "src/handlers", "mod.rs"));
+ supportingFiles.add(new SupportingFile("middleware.mustache", "src", "middleware.rs"));
+ supportingFiles.add(new SupportingFile("routes.mustache", "src", "routes.rs"));
+ supportingFiles.add(new SupportingFile("README.mustache", "", "README.md").doNotOverwrite());
+ }
+
+ @Override
+ public CodegenType getTag() {
+ return CodegenType.SERVER;
+ }
+
+ @Override
+ public String getName() {
+ return "rust-salvo";
+ }
+
+ @Override
+ public String getHelp() {
+ return "Generates a Rust server library using Salvo web framework.";
+ }
+
+ @Override
+ public void processOpts() {
+ super.processOpts();
+
+ setPackageName((String) additionalProperties.getOrDefault(CodegenConstants.PACKAGE_NAME, "salvo_openapi"));
+
+ if (additionalProperties.containsKey(CodegenConstants.PACKAGE_VERSION)) {
+ setPackageVersion((String) additionalProperties.get(CodegenConstants.PACKAGE_VERSION));
+ }
+
+ // Process Salvo-specific options; ensure defaults land in additionalProperties
+ // so templates can branch on them and tests can inspect them.
+ if (additionalProperties.containsKey("enableRequestValidation")) {
+ enableRequestValidation = convertPropertyToBooleanAndWriteBack("enableRequestValidation");
+ } else {
+ additionalProperties.put("enableRequestValidation", enableRequestValidation);
+ }
+
+ if (additionalProperties.containsKey("enableResponseValidation")) {
+ enableResponseValidation = convertPropertyToBooleanAndWriteBack("enableResponseValidation");
+ } else {
+ additionalProperties.put("enableResponseValidation", enableResponseValidation);
+ }
+
+ if (additionalProperties.containsKey("enableAuthMiddleware")) {
+ enableAuthMiddleware = convertPropertyToBooleanAndWriteBack("enableAuthMiddleware");
+ } else {
+ additionalProperties.put("enableAuthMiddleware", enableAuthMiddleware);
+ }
+
+ if (additionalProperties.containsKey("enableCorsMiddleware")) {
+ enableCorsMiddleware = convertPropertyToBooleanAndWriteBack("enableCorsMiddleware");
+ } else {
+ additionalProperties.put("enableCorsMiddleware", enableCorsMiddleware);
+ }
+
+ additionalProperties.put(CodegenConstants.PACKAGE_NAME, packageName);
+ additionalProperties.put("externCrateName", externCrateName);
+ }
+
+ private void setPackageName(String packageName) {
+ this.packageName = packageName;
+ this.externCrateName = packageName.replace('-', '_');
+ }
+
+ private void setPackageVersion(String packageVersion) {
+ this.packageVersion = packageVersion;
+ }
+
+ @Override
+ public String apiPackage() {
+ return "src" + File.separator + "handlers";
+ }
+
+ @Override
+ public void preprocessOpenAPI(OpenAPI openAPI) {
+ Info info = openAPI.getInfo();
+
+ if (packageVersion == null || packageVersion.isEmpty()) {
+ List versionComponents = new ArrayList<>(Arrays.asList(info.getVersion().split("[.]")));
+ if (versionComponents.isEmpty()) {
+ versionComponents.add("1");
+ }
+ while (versionComponents.size() < 3) {
+ versionComponents.add("0");
+ }
+ setPackageVersion(String.join(".", versionComponents));
+ }
+
+ additionalProperties.put(CodegenConstants.PACKAGE_VERSION, packageVersion);
+ }
+
+ @Override
+ public String toApiName(String name) {
+ return name.isEmpty() ?
+ "default" :
+ sanitizeIdentifier(name, CasingType.SNAKE_CASE, "api", "API", true);
+ }
+
+ @Override
+ public String toApiFilename(String name) {
+ return toApiName(name);
+ }
+
+ @Override
+ public String apiFileFolder() {
+ return Path.of(outputFolder, apiPackage().replace('.', File.separatorChar)).toString();
+ }
+
+ @Override
+ public String toOperationId(String operationId) {
+ return sanitizeIdentifier(operationId, CasingType.SNAKE_CASE, "handler", "handler", true);
+ }
+
+ @Override
+ public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List servers) {
+ CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
+
+ // Salvo-specific operation processing
+ String handlerName = underscore(op.operationId);
+ String method = httpMethod.toLowerCase(Locale.ROOT);
+ op.vendorExtensions.put("x-handler-name", handlerName);
+ op.vendorExtensions.put("x-route-path", convertPathToSalvoFormat(path));
+ op.vendorExtensions.put("x-http-method", method);
+
+ // Group operations by route for Salvo router setup. AuthSchemes is
+ // populated later in postProcessOperationsWithModels because
+ // CodegenOperation.authMethods is set by DefaultGenerator AFTER
+ // fromOperation returns.
+ String salvoPath = convertPathToSalvoFormat(path);
+ routeMap.computeIfAbsent(salvoPath, k -> new ArrayList<>())
+ .add(new SalvoOperation(method, handlerName, op.vendorExtensions, Collections.emptyList()));
+
+ return op;
+ }
+
+ private String convertPathToSalvoFormat(String path) {
+ // Salvo 0.76+ uses {name} for path parameters, matching OpenAPI.
+ return path;
+ }
+
+ @Override
+ public OperationsMap postProcessOperationsWithModels(OperationsMap operationsMap, List allModels) {
+ OperationMap operations = operationsMap.getOperations();
+ operations.put("classnamePascalCase", camelize(operations.getClassname()));
+
+ for (CodegenOperation op : operations.getOperation()) {
+ List opAuthSchemes = new ArrayList<>();
+ if (op.authMethods != null && !op.authMethods.isEmpty()) {
+ LinkedHashSet seen = new LinkedHashSet<>();
+ for (CodegenSecurity sec : op.authMethods) {
+ SalvoAuthScheme s = SalvoAuthScheme.fromCodegen(sec);
+ if (s != null && seen.add(s.kind)) {
+ opAuthSchemes.add(s);
+ }
+ }
+ }
+
+ boolean opHasAuth = !opAuthSchemes.isEmpty();
+ if (opHasAuth) {
+ hasAuthHandlers = true;
+ op.vendorExtensions.put("x-has-auth", true);
+ op.vendorExtensions.put("x-salvo-auth-schemes", opAuthSchemes);
+ op.vendorExtensions.put("x-salvo-auth-multi", opAuthSchemes.size() > 1);
+ }
+
+ // Mirror the schemes onto the matching SalvoOperation so routes.mustache
+ // can attach per-route hoops without having to flatten vendorExtensions.
+ String handlerName = (String) op.vendorExtensions.get("x-handler-name");
+ String method = (String) op.vendorExtensions.get("x-http-method");
+ if (handlerName != null && method != null) {
+ for (ArrayList group : routeMap.values()) {
+ for (SalvoOperation so : group) {
+ if (method.equals(so.method) && handlerName.equals(so.handlerName)) {
+ so.authSchemes = opAuthSchemes;
+ so.hasAuth = opHasAuth;
+ so.multiAuth = opAuthSchemes.size() > 1;
+ }
+ }
+ }
+ }
+ }
+
+ if (hasAuthHandlers) {
+ operations.put("hasAuthHandlers", true);
+ operations.getOperation().forEach(op -> op.vendorExtensions.put("hasAuthHandlers", true));
+ }
+
+ return operationsMap;
+ }
+
+ @Override
+ public Map postProcessSupportingFileData(Map bundle) {
+ generateYAMLSpecFile(bundle);
+
+ // Add Salvo-specific routing information
+ List routeGroups = routeMap.entrySet().stream()
+ .map(entry -> new SalvoRouteGroup(entry.getKey(), entry.getValue()))
+ .sorted(Comparator.comparing(group -> group.path))
+ .collect(Collectors.toList());
+
+ bundle.put("salvoRoutes", routeGroups);
+ bundle.put("hasAuthHandlers", hasAuthHandlers);
+ bundle.put("enableRequestValidation", enableRequestValidation);
+ bundle.put("enableResponseValidation", enableResponseValidation);
+ bundle.put("enableAuthMiddleware", enableAuthMiddleware);
+ bundle.put("enableCorsMiddleware", enableCorsMiddleware);
+
+ return super.postProcessSupportingFileData(bundle);
+ }
+
+
+
+ @Override
+ public void postProcessFile(File file, String fileType) {
+ super.postProcessFile(file, fileType);
+ if (file == null) {
+ return;
+ }
+
+ String fileName = file.toString();
+ String cmd = System.getenv("RUST_POST_PROCESS_FILE");
+ if (StringUtils.isEmpty(cmd)) {
+ cmd = "rustfmt";
+ }
+
+ if ("rs".equals(FilenameUtils.getExtension(fileName))) {
+ executePostProcessor(new String[]{cmd, "--edition", "2021", fileName});
+ }
+ }
+
+ // Helper classes for Salvo-specific data structures
+ static class SalvoOperation {
+ public String method;
+ public String handlerName;
+ public Map vendorExtensions;
+ public List authSchemes;
+ public boolean hasAuth;
+ public boolean multiAuth;
+
+ SalvoOperation(String method, String handlerName, Map vendorExtensions,
+ List authSchemes) {
+ this.method = method;
+ this.handlerName = handlerName;
+ this.vendorExtensions = vendorExtensions;
+ this.authSchemes = authSchemes == null ? Collections.emptyList() : authSchemes;
+ this.hasAuth = !this.authSchemes.isEmpty();
+ this.multiAuth = this.authSchemes.size() > 1;
+ }
+ }
+
+ static class SalvoRouteGroup {
+ public String path;
+ public ArrayList operations;
+
+ SalvoRouteGroup(String path, ArrayList operations) {
+ this.path = path;
+ this.operations = operations;
+ }
+ }
+
+ // Auth scheme descriptor exposed to templates. `kind` is used to pick the
+ // factory function in routes.mustache; `schemaName` shows up in the
+ // `#[salvo::oapi::endpoint(security(...))]` annotation along with any
+ // declared OAuth2/OIDC scopes.
+ static class SalvoAuthScheme {
+ public String kind; // "apiKey" | "basic" | "bearer" | "oauth2"
+ public String factory; // factory fn in middleware.rs, null for oauth2
+ public String schemaName; // name used in OpenAPI security() annotation
+ public List scopes;
+
+ SalvoAuthScheme(String kind, String factory, String schemaName, List scopes) {
+ this.kind = kind;
+ this.factory = factory;
+ this.schemaName = schemaName;
+ this.scopes = scopes;
+ }
+
+ static SalvoAuthScheme fromCodegen(CodegenSecurity sec) {
+ List scopeList = extractScopes(sec);
+ if (Boolean.TRUE.equals(sec.isApiKey)) {
+ return new SalvoAuthScheme("apiKey", "api_key_auth", "ApiKeyAuth", scopeList);
+ }
+ if (Boolean.TRUE.equals(sec.isBasic) && Boolean.TRUE.equals(sec.isBasicBearer)) {
+ return new SalvoAuthScheme("bearer", "bearer_auth", "BearerAuth", scopeList);
+ }
+ if (Boolean.TRUE.equals(sec.isBasic) && Boolean.TRUE.equals(sec.isBasicBasic)) {
+ return new SalvoAuthScheme("basic", "basic_auth_hoop", "BasicAuth", scopeList);
+ }
+ if (Boolean.TRUE.equals(sec.isBasic)) {
+ return new SalvoAuthScheme("basic", "basic_auth_hoop", "BasicAuth", scopeList);
+ }
+ // OAuth2 / OpenIdConnect: leave runtime enforcement to the user,
+ // but surface the scheme + scopes so the OpenAPI annotation stays
+ // faithful to the source spec.
+ if (Boolean.TRUE.equals(sec.isOAuth)) {
+ return new SalvoAuthScheme("oauth2", null, "OAuth2", scopeList);
+ }
+ return null;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static List extractScopes(CodegenSecurity sec) {
+ List out = new ArrayList<>();
+ if (sec.scopes == null) {
+ return out;
+ }
+ for (Map entry : sec.scopes) {
+ Object name = entry.get("scope");
+ if (name != null) {
+ out.add(new SalvoAuthScope(name.toString()));
+ }
+ }
+ return out;
+ }
+ }
+
+ // Single OAuth2/OIDC scope name. Held in its own class so mustache can
+ // render the comma-separated list naturally with `{{#-last}}` semantics.
+ static class SalvoAuthScope {
+ public String scope;
+
+ SalvoAuthScope(String scope) {
+ this.scope = scope;
+ }
+ }
+
+ @Override
+ public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
+ super.postProcessModelProperty(model, property);
+ // Surface a serde rename whenever the Rust field name (already snake_cased
+ // and keyword-escaped by AbstractRustCodegen.toVarName) differs from the
+ // OpenAPI wire name, so JSON contracts stay intact (e.g. `petId` on the
+ // wire, `pet_id` in Rust).
+ if (property.baseName != null && property.name != null
+ && !property.name.equals(property.baseName)) {
+ property.vendorExtensions.put("x-salvo-serde-rename", true);
+ }
+ }
+}
diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
index 04da8d20435b..57a8381c0c22 100644
--- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
+++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig
@@ -125,6 +125,7 @@ org.openapitools.codegen.languages.RubyOnRailsServerCodegen
org.openapitools.codegen.languages.RubySinatraServerCodegen
org.openapitools.codegen.languages.RustAxumServerCodegen
org.openapitools.codegen.languages.RustClientCodegen
+org.openapitools.codegen.languages.RustSalvoServerCodegen
org.openapitools.codegen.languages.RustServerCodegen
org.openapitools.codegen.languages.RustServerCodegenDeprecated
org.openapitools.codegen.languages.ScalatraServerCodegen
diff --git a/modules/openapi-generator/src/main/resources/rust-salvo/Cargo.mustache b/modules/openapi-generator/src/main/resources/rust-salvo/Cargo.mustache
new file mode 100644
index 000000000000..5662d1e81ead
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/rust-salvo/Cargo.mustache
@@ -0,0 +1,58 @@
+[package]
+name = "{{{packageName}}}"
+version = "{{{packageVersion}}}"
+{{#infoEmail}}
+authors = ["{{{.}}}"]
+{{/infoEmail}}
+{{^infoEmail}}
+authors = ["OpenAPI Generator team and contributors"]
+{{/infoEmail}}
+{{#appDescription}}
+description = "{{{.}}}"
+{{/appDescription}}
+{{#licenseInfo}}
+license = "{{.}}"
+{{/licenseInfo}}
+{{^licenseInfo}}
+license = "Unlicense"
+{{/licenseInfo}}
+edition = "2021"
+{{#publishRustRegistry}}
+publish = ["{{.}}"]
+{{/publishRustRegistry}}
+{{#repositoryUrl}}
+repository = "{{.}}"
+{{/repositoryUrl}}
+{{#documentationUrl}}
+documentation = "{{.}}"
+{{/documentationUrl}}
+{{#homePageUrl}}
+homepage = "{{.}}"
+{{/homePageUrl}}
+
+[features]
+default = ["server"]
+server = []
+
+[dependencies]
+salvo = { version = "0.93", features = [
+ "oapi",
+ "cors",
+ "jwt-auth",
+ "serve-static",
+] }
+async-trait = "0.1"
+base64 = "0.22"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+anyhow = "1"
+chrono = { version = "0.4", features = ["serde"] }
+{{#hasUUIDs}}
+uuid = { version = "1", features = ["serde", "v4"] }
+{{/hasUUIDs}}
+{{#enableRequestValidation}}
+validator = { version = "0.20", features = ["derive"] }
+{{/enableRequestValidation}}
diff --git a/modules/openapi-generator/src/main/resources/rust-salvo/README.mustache b/modules/openapi-generator/src/main/resources/rust-salvo/README.mustache
new file mode 100644
index 000000000000..1345dcf66366
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/rust-salvo/README.mustache
@@ -0,0 +1,122 @@
+# {{#appName}}{{.}}{{/appName}}{{^appName}}{{packageName}}{{/appName}}
+
+{{#appDescription}}
+{{.}}
+{{/appDescription}}
+
+This server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project using the **rust-salvo** generator.
+
+- API version: {{#appVersion}}{{.}}{{/appVersion}}{{^appVersion}}1.0.0{{/appVersion}}
+- Package version: {{packageVersion}}
+{{^hideGenerationTimestamp}}
+- Build date: {{generatedDate}}
+{{/hideGenerationTimestamp}}
+- Generator version: {{generatorVersion}}
+
+{{#externalDocumentationDescription}}
+For more information, please visit [{{{externalDocumentationDescription}}}]({{{externalDocumentationURL}}})
+{{/externalDocumentationDescription}}
+
+## Stack
+
+| Crate | Purpose |
+| ----- | ------- |
+| [salvo](https://salvo.rs/) (≥ 0.93) | Web framework + `salvo::oapi` for the OpenAPI-aware endpoint macro |
+| [serde](https://serde.rs/) | (De)serialization for models |
+| [tokio](https://tokio.rs/) | Async runtime |
+| [tracing](https://crates.io/crates/tracing) | Structured logging |
+{{#hasUUIDs}}
+| [uuid](https://crates.io/crates/uuid) | UUID type for model fields |
+{{/hasUUIDs}}
+{{#enableRequestValidation}}
+| [validator](https://crates.io/crates/validator) | Request-body validation via `#[derive(Validate)]` |
+{{/enableRequestValidation}}
+
+## What the generator produces
+
+The crate is laid out so the framework-facing surface lives in `src/` and the business logic is something you fill in:
+
+```
+src/
+├── lib.rs # crate entry: re-exports + create_service / create_router
+├── main.rs # binary entry: binds the router on 0.0.0.0:7878
+├── models.rs # data models (derive Debug, Clone, Serialize, Deserialize, ToSchema{{#enableRequestValidation}}, Validate{{/enableRequestValidation}})
+├── routes.rs # Router::with_path("/...") chains mapping methods to handlers
+├── handlers/ # one module per OpenAPI tag
+│ ├── mod.rs
+│ └── .rs # one handler stub per operation
+{{#enableAuthMiddleware}}
+└── middleware.rs # ApiKey / Basic / Bearer middleware structs (based on the spec's securitySchemes)
+{{/enableAuthMiddleware}}
+```
+
+Handlers use the typed extractors from `salvo::oapi::extract` (`JsonBody`, `PathParam`, `QueryParam`, `HeaderParam`, `FormBody`) so that the `#[salvo::oapi::endpoint]` macro picks up the OpenAPI metadata automatically. Models derive `salvo::oapi::ToSchema` for the same reason — they can flow through the extractors and show up in the generated OpenAPI doc.
+
+## Filling in the implementation
+
+Each generated handler is a stub that returns `StatusCode::NOT_IMPLEMENTED`. Replace the body with your business logic, for example:
+
+```rust
+#[salvo::oapi::endpoint(operation_id = "get_pet_by_id", tags("pet"))]
+pub async fn get_pet_by_id(
+ pet_id: PathParam,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ let pet = Pet::new("doggie".to_string(), vec!["url".to_string()]);
+ res.render(Json(pet));
+ Ok(())
+}
+```
+
+## Building and running
+
+```bash
+cargo build # build
+cargo run # run the server on http://0.0.0.0:7878
+RUST_LOG=debug cargo run # with verbose logging
+```
+
+`tracing-subscriber` honours `RUST_LOG`, so set it to whatever level you need (`error`, `warn`, `info`, `debug`, `trace`).
+
+## API endpoints
+
+{{#apiDocumentationUrl}}
+API documentation is available at: {{.}}
+{{/apiDocumentationUrl}}
+
+{{#apiInfo}}
+{{#apis}}
+### {{classname}}
+
+{{#operations}}
+{{#operation}}
+- **{{httpMethod}}** `{{path}}` — {{summary}}{{^summary}}{{operationId}}{{/summary}}
+{{/operation}}
+{{/operations}}
+
+{{/apis}}
+{{/apiInfo}}
+
+## Extending
+
+- **Authentication** — `{{#enableAuthMiddleware}}auth is enabled{{/enableAuthMiddleware}}{{^enableAuthMiddleware}}re-generate with `--additional-properties=enableAuthMiddleware=true` (or set it in `bin/configs/...yaml`) to scaffold `src/middleware.rs` with `ApiKeyAuth`, `BasicAuth`, and `BearerAuth` handlers wired to the spec's `securitySchemes`{{/enableAuthMiddleware}}.
+- **CORS** — `{{#enableCorsMiddleware}}CORS is enabled{{/enableCorsMiddleware}}{{^enableCorsMiddleware}}re-generate with `--additional-properties=enableCorsMiddleware=true` to attach `salvo::cors::Cors::new()` as a hoop on the service{{/enableCorsMiddleware}}.
+- **Validation** — `{{#enableRequestValidation}}request validation is enabled, models derive `Validate`{{/enableRequestValidation}}{{^enableRequestValidation}}re-generate with `--additional-properties=enableRequestValidation=true` to derive `Validate` on models and pull in the `validator` crate{{/enableRequestValidation}}.
+- **Database** — drop one of [sqlx](https://crates.io/crates/sqlx), [Diesel](https://crates.io/crates/diesel), or [SeaORM](https://crates.io/crates/sea-orm) into `Cargo.toml` and inject a connection pool through `Depot` (Salvo's per-request typed storage).
+
+## Testing
+
+```bash
+cargo test
+```
+
+Salvo's `TestClient` (re-exported as `salvo::test::TestClient`) is the idiomatic way to drive handlers from integration tests.
+
+## License
+
+{{#licenseInfo}}
+This project is licensed under {{.}}.
+{{/licenseInfo}}
+{{^licenseInfo}}
+This project is unlicensed.
+{{/licenseInfo}}
diff --git a/modules/openapi-generator/src/main/resources/rust-salvo/gitignore b/modules/openapi-generator/src/main/resources/rust-salvo/gitignore
new file mode 100644
index 000000000000..a9ff9faa89e5
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/rust-salvo/gitignore
@@ -0,0 +1,30 @@
+# Generated by Cargo
+/target/
+Cargo.lock
+
+# Editor files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS generated files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# Log files
+*.log
+
+# Environment variables
+.env
+.env.local
+
+# Backup files
+*.bak
+*.backup
diff --git a/modules/openapi-generator/src/main/resources/rust-salvo/handlers-mod.mustache b/modules/openapi-generator/src/main/resources/rust-salvo/handlers-mod.mustache
new file mode 100644
index 000000000000..b3a909dae427
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/rust-salvo/handlers-mod.mustache
@@ -0,0 +1,18 @@
+{{>partial_header}}
+//! API handlers module
+//!
+//! This module contains HTTP request handlers for the API endpoints.
+//! Each handler function processes incoming requests and returns responses.
+
+{{#apiInfo}}
+{{#apis}}
+pub mod {{classFilename}};
+{{/apis}}
+{{/apiInfo}}
+
+// Re-exports for easier access
+{{#apiInfo}}
+{{#apis}}
+pub use {{classFilename}}::*;
+{{/apis}}
+{{/apiInfo}}
diff --git a/modules/openapi-generator/src/main/resources/rust-salvo/handlers.mustache b/modules/openapi-generator/src/main/resources/rust-salvo/handlers.mustache
new file mode 100644
index 000000000000..640508728c40
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/rust-salvo/handlers.mustache
@@ -0,0 +1,66 @@
+{{>partial_header}}
+//! {{#appName}}{{.}}{{/appName}}{{^appName}}{{packageName}}{{/appName}} handlers
+//!
+//! API version: {{#appVersion}}{{.}}{{/appVersion}}{{^appVersion}}1.0.0{{/appVersion}}
+//!
+
+use salvo::prelude::*;
+use salvo::oapi::extract::{JsonBody, PathParam, QueryParam, HeaderParam, FormBody};
+use serde::{Deserialize, Serialize};
+{{#hasUUIDs}}
+use uuid::Uuid;
+{{/hasUUIDs}}
+{{#enableRequestValidation}}
+use validator::Validate;
+{{/enableRequestValidation}}
+
+use crate::models::{self, *};
+
+{{#operations}}
+{{#operation}}
+{{#notes}}
+/// {{.}}
+{{/notes}}
+{{^notes}}
+/// {{summary}}{{^summary}}{{operationId}}{{/summary}}
+{{/notes}}
+#[salvo::oapi::endpoint(
+ operation_id = "{{operationId}}",
+ tags("{{classname}}"){{#vendorExtensions.x-has-auth}},
+ security({{#vendorExtensions.x-salvo-auth-schemes}}("{{schemaName}}" = [{{#scopes}}"{{scope}}"{{^-last}}, {{/-last}}{{/scopes}}]){{^-last}}, {{/-last}}{{/vendorExtensions.x-salvo-auth-schemes}}){{/vendorExtensions.x-has-auth}}
+)]
+pub async fn {{operationId}}(
+{{#hasParams}}
+{{#allParams}}
+{{#isQueryParam}}
+ {{paramName}}: QueryParam<{{{dataType}}}, {{#required}}true{{/required}}{{^required}}false{{/required}}>,
+{{/isQueryParam}}
+{{#isPathParam}}
+ {{paramName}}: PathParam<{{{dataType}}}>,
+{{/isPathParam}}
+{{#isHeaderParam}}
+ {{paramName}}: HeaderParam<{{{dataType}}}, {{#required}}true{{/required}}{{^required}}false{{/required}}>,
+{{/isHeaderParam}}
+{{#isBodyParam}}
+ {{paramName}}: JsonBody<{{{dataType}}}>,
+{{/isBodyParam}}
+{{#isFormParam}}
+ {{paramName}}: FormBody<{{{dataType}}}>,
+{{/isFormParam}}
+{{/allParams}}
+{{/hasParams}}
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ {{#notes}}
+ // {{.}}
+ {{/notes}}
+ {{^notes}}
+ // TODO: Implement {{operationId}}
+ {{/notes}}
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+{{/operation}}
+{{/operations}}
diff --git a/modules/openapi-generator/src/main/resources/rust-salvo/lib.mustache b/modules/openapi-generator/src/main/resources/rust-salvo/lib.mustache
new file mode 100644
index 000000000000..85de2e3348e0
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/rust-salvo/lib.mustache
@@ -0,0 +1,55 @@
+#![allow(
+ missing_docs,
+ trivial_casts,
+ unused_variables,
+ unused_mut,
+ unused_extern_crates,
+ non_camel_case_types,
+ unused_imports,
+ unused_attributes
+)]
+#![allow(
+ clippy::derive_partial_eq_without_eq,
+ clippy::disallowed_names,
+ clippy::too_many_arguments
+)]
+
+pub const BASE_PATH: &str = "{{{basePathWithoutHost}}}";
+{{#appVersion}}
+pub const API_VERSION: &str = "{{{.}}}";
+{{/appVersion}}
+
+pub mod handlers;
+pub mod models;
+pub mod routes;
+{{#enableAuthMiddleware}}
+pub mod middleware;
+{{/enableAuthMiddleware}}
+
+pub use handlers::*;
+pub use models::*;
+pub use routes::*;
+
+use salvo::prelude::*;
+
+{{#enableAuthMiddleware}}
+use middleware::*;
+{{/enableAuthMiddleware}}
+
+/// Create and configure the Salvo service.
+///
+/// Authentication middleware is attached per route (see `routes.rs`) so that
+/// public operations are not blocked by a global auth hoop.
+pub fn create_service() -> Service {
+ let router = create_router();
+
+ Service::new(router)
+ {{#enableCorsMiddleware}}
+ .hoop(salvo::cors::Cors::new())
+ {{/enableCorsMiddleware}}
+}
+
+/// Create the main router with all API routes
+pub fn create_router() -> Router {
+ routes::create_router()
+}
diff --git a/modules/openapi-generator/src/main/resources/rust-salvo/main.mustache b/modules/openapi-generator/src/main/resources/rust-salvo/main.mustache
new file mode 100644
index 000000000000..8e53e81ccb53
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/rust-salvo/main.mustache
@@ -0,0 +1,18 @@
+use {{externCrateName}}::create_service;
+use salvo::prelude::*;
+use tracing_subscriber;
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ // Initialize tracing
+ tracing_subscriber::fmt::init();
+
+ let service = create_service();
+ let acceptor = TcpListener::new("0.0.0.0:7878").bind().await;
+
+ tracing::info!("{{#appName}}{{.}}{{/appName}}{{^appName}}Salvo OpenAPI Server{{/appName}} listening on http://0.0.0.0:7878");
+
+ Server::new(acceptor).serve(service).await;
+
+ Ok(())
+}
diff --git a/modules/openapi-generator/src/main/resources/rust-salvo/middleware.mustache b/modules/openapi-generator/src/main/resources/rust-salvo/middleware.mustache
new file mode 100644
index 000000000000..0a9a24852c29
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/rust-salvo/middleware.mustache
@@ -0,0 +1,203 @@
+{{>partial_header}}
+//! Authentication and middleware for {{#appName}}{{.}}{{/appName}}{{^appName}}{{packageName}}{{/appName}}
+//!
+
+use salvo::prelude::*;
+use serde::{Deserialize, Serialize};
+{{#hasAuthMethods}}
+use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
+
+/// Case-insensitively strip an `Authorization` scheme prefix
+/// (e.g. `Bearer ` or `Basic `) and return the trimmed credential.
+fn strip_scheme_prefix<'a>(header: &'a str, scheme: &str) -> Option<&'a str> {
+ if header.len() <= scheme.len() {
+ return None;
+ }
+ let (prefix, rest) = header.split_at(scheme.len());
+ if !prefix.eq_ignore_ascii_case(scheme) {
+ return None;
+ }
+ let rest = rest.strip_prefix(' ')?;
+ Some(rest.trim_start())
+}
+{{/hasAuthMethods}}
+
+{{#hasAuthMethods}}
+{{#authMethods}}
+{{#isApiKey}}
+/// API Key authentication middleware
+#[derive(Debug, Clone)]
+pub struct ApiKeyAuth {
+ pub key_name: String,
+ pub api_key: String,
+}
+
+impl ApiKeyAuth {
+ pub fn new(key_name: impl Into, api_key: impl Into) -> Self {
+ Self {
+ key_name: key_name.into(),
+ api_key: api_key.into(),
+ }
+ }
+}
+
+#[async_trait::async_trait]
+impl Handler for ApiKeyAuth {
+ async fn handle(&self, req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
+ {{#isKeyInHeader}}
+ let provided_key = req.headers().get(&self.key_name)
+ .and_then(|v| v.to_str().ok());
+ {{/isKeyInHeader}}
+ {{#isKeyInQuery}}
+ let provided_key = req.query::(&self.key_name);
+ {{/isKeyInQuery}}
+
+ if let Some(key) = provided_key {
+ if key == self.api_key {
+ ctrl.call_next(req, depot, res).await;
+ return;
+ }
+ }
+
+ res.status_code(StatusCode::UNAUTHORIZED);
+ res.render("Unauthorized: Invalid API Key");
+ ctrl.skip_rest();
+ }
+}
+
+/// Factory used as a per-route `.hoop(api_key_auth())`.
+/// Replace the placeholder with credentials sourced from your config.
+pub fn api_key_auth() -> ApiKeyAuth {
+ ApiKeyAuth::new("{{keyParamName}}", "your-api-key")
+}
+{{/isApiKey}}
+
+{{#isBasic}}
+{{#isBasicBasic}}
+/// Basic authentication middleware
+#[derive(Debug, Clone)]
+pub struct BasicAuth {
+ pub username: String,
+ pub password: String,
+}
+
+impl BasicAuth {
+ pub fn new(username: impl Into, password: impl Into) -> Self {
+ Self {
+ username: username.into(),
+ password: password.into(),
+ }
+ }
+}
+
+#[async_trait::async_trait]
+impl Handler for BasicAuth {
+ async fn handle(&self, req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
+ if let Some(auth_header) = req.headers().get("authorization")
+ .and_then(|v| v.to_str().ok()) {
+
+ if let Some(encoded) = strip_scheme_prefix(auth_header, "Basic") {
+ if let Ok(decoded) = BASE64_STANDARD.decode(encoded) {
+ if let Ok(credentials) = String::from_utf8(decoded) {
+ let parts: Vec<&str> = credentials.splitn(2, ':').collect();
+ if parts.len() == 2 && parts[0] == self.username && parts[1] == self.password {
+ ctrl.call_next(req, depot, res).await;
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ res.status_code(StatusCode::UNAUTHORIZED);
+ let _ = res.add_header("WWW-Authenticate", "Basic realm=\"API\"", true);
+ res.render("Unauthorized: Invalid credentials");
+ ctrl.skip_rest();
+ }
+}
+
+/// Factory used as a per-route `.hoop(basic_auth_hoop())`.
+pub fn basic_auth_hoop() -> BasicAuth {
+ BasicAuth::new("admin", "password")
+}
+{{/isBasicBasic}}
+
+{{#isBasicBearer}}
+/// Bearer token authentication middleware
+#[derive(Debug, Clone)]
+pub struct BearerAuth {
+ pub token: String,
+}
+
+impl BearerAuth {
+ pub fn new(token: impl Into) -> Self {
+ Self {
+ token: token.into(),
+ }
+ }
+}
+
+#[async_trait::async_trait]
+impl Handler for BearerAuth {
+ async fn handle(&self, req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
+ if let Some(auth_header) = req.headers().get("authorization")
+ .and_then(|v| v.to_str().ok()) {
+
+ if let Some(token) = strip_scheme_prefix(auth_header, "Bearer") {
+ if token == self.token {
+ ctrl.call_next(req, depot, res).await;
+ return;
+ }
+ }
+ }
+
+ res.status_code(StatusCode::UNAUTHORIZED);
+ res.render("Unauthorized: Invalid token");
+ ctrl.skip_rest();
+ }
+}
+
+/// Factory used as a per-route `.hoop(bearer_auth())`.
+pub fn bearer_auth() -> BearerAuth {
+ BearerAuth::new("your-bearer-token")
+}
+{{/isBasicBearer}}
+{{/isBasic}}
+{{/authMethods}}
+
+/// Backwards-compatible entry point used by older generated code paths.
+///
+/// Returns a no-op handler so attaching it as a hoop is harmless; per-route
+/// authentication is wired up via the per-scheme factories above.
+pub fn auth_middleware() -> impl Handler {
+ NoneHandler
+}
+
+#[derive(Debug, Clone)]
+pub struct NoneHandler;
+
+#[async_trait::async_trait]
+impl Handler for NoneHandler {
+ async fn handle(&self, req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
+ ctrl.call_next(req, depot, res).await;
+ }
+}
+{{/hasAuthMethods}}
+
+{{^hasAuthMethods}}
+/// No authentication methods defined
+pub fn auth_middleware() -> impl Handler {
+ // No authentication required
+ NoneHandler
+}
+
+#[derive(Debug, Clone)]
+pub struct NoneHandler;
+
+#[async_trait::async_trait]
+impl Handler for NoneHandler {
+ async fn handle(&self, req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
+ ctrl.call_next(req, depot, res).await;
+ }
+}
+{{/hasAuthMethods}}
diff --git a/modules/openapi-generator/src/main/resources/rust-salvo/models.mustache b/modules/openapi-generator/src/main/resources/rust-salvo/models.mustache
new file mode 100644
index 000000000000..67306bc6a3a8
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/rust-salvo/models.mustache
@@ -0,0 +1,75 @@
+{{>partial_header}}
+//! Data models for {{#appName}}{{.}}{{/appName}}{{^appName}}{{packageName}}{{/appName}}
+//!
+//! API version: {{#appVersion}}{{.}}{{/appVersion}}{{^appVersion}}1.0.0{{/appVersion}}
+//!
+
+#![allow(unused_qualifications)]
+
+use salvo::oapi::ToSchema;
+use serde::{Deserialize, Serialize};
+{{#hasUUIDs}}
+use uuid::Uuid;
+{{/hasUUIDs}}
+{{#enableRequestValidation}}
+use validator::Validate;
+{{/enableRequestValidation}}
+
+// Self-reference so `models::Foo` (the form emitted by the codegen for
+// cross-model references) resolves inside this file.
+use crate::models;
+
+{{#models}}
+{{#model}}
+{{#description}}
+/// {{.}}
+{{/description}}
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema{{#enableRequestValidation}}, Validate{{/enableRequestValidation}})]
+{{#vendorExtensions.x-is-string}}
+#[serde(transparent)]
+{{/vendorExtensions.x-is-string}}
+pub struct {{classname}} {
+{{#vars}}
+{{#description}}
+ /// {{.}}
+{{/description}}
+ {{#enableRequestValidation}}
+ {{#hasValidation}}
+ #[validate({{#hasMinLength}}length(min = {{minLength}}){{/hasMinLength}}{{#hasMaxLength}}{{#hasMinLength}}, {{/hasMinLength}}length(max = {{maxLength}}){{/hasMaxLength}}{{#hasMinimum}}{{#hasMinLength}}{{hasMaxLength}}, {{/hasMinLength}}{{#hasMaxLength}}, {{/hasMaxLength}}range(min = {{minimum}}){{/hasMinimum}}{{#hasMaximum}}{{#hasMinimum}}, {{/hasMinimum}}{{^hasMinimum}}{{#hasMinLength}}{{hasMaxLength}}, {{/hasMinLength}}{{#hasMaxLength}}, {{/hasMaxLength}}{{/hasMinimum}}range(max = {{maximum}}){{/hasMaximum}})]
+ {{/hasValidation}}
+ {{/enableRequestValidation}}
+ {{#required}}
+ {{#vendorExtensions.x-salvo-serde-rename}}
+ #[serde(rename = "{{baseName}}")]
+ {{/vendorExtensions.x-salvo-serde-rename}}
+ pub {{{name}}}: {{{dataType}}},
+ {{/required}}
+ {{^required}}
+ {{#vendorExtensions.x-salvo-serde-rename}}
+ #[serde(rename = "{{baseName}}", skip_serializing_if = "Option::is_none")]
+ {{/vendorExtensions.x-salvo-serde-rename}}
+ {{^vendorExtensions.x-salvo-serde-rename}}
+ #[serde(skip_serializing_if = "Option::is_none")]
+ {{/vendorExtensions.x-salvo-serde-rename}}
+ pub {{{name}}}: Option<{{{dataType}}}>,
+ {{/required}}
+{{/vars}}
+}
+
+impl {{classname}} {
+ pub fn new({{#requiredVars}}{{{name}}}: {{{dataType}}}{{^-last}}, {{/-last}}{{/requiredVars}}) -> Self {
+ Self {
+{{#vars}}
+{{#required}}
+ {{{name}}},
+{{/required}}
+{{^required}}
+ {{{name}}}: None,
+{{/required}}
+{{/vars}}
+ }
+ }
+}
+
+{{/model}}
+{{/models}}
diff --git a/modules/openapi-generator/src/main/resources/rust-salvo/partial_header.mustache b/modules/openapi-generator/src/main/resources/rust-salvo/partial_header.mustache
new file mode 100644
index 000000000000..ddd262106435
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/rust-salvo/partial_header.mustache
@@ -0,0 +1,18 @@
+/*
+ * {{#appName}}{{.}}{{/appName}}{{^appName}}{{packageName}}{{/appName}}
+ *
+{{#appDescription}}
+ * {{.}}
+ *
+{{/appDescription}}
+{{#version}}
+ * The version of the OpenAPI document: {{.}}
+{{/version}}
+{{#contact}}
+{{#contactEmail}}
+ * Contact: {{.}}
+{{/contactEmail}}
+{{/contact}}
+ *
+ * Generated by: https://openapi-generator.tech
+ */
diff --git a/modules/openapi-generator/src/main/resources/rust-salvo/routes.mustache b/modules/openapi-generator/src/main/resources/rust-salvo/routes.mustache
new file mode 100644
index 000000000000..e14a4da458f2
--- /dev/null
+++ b/modules/openapi-generator/src/main/resources/rust-salvo/routes.mustache
@@ -0,0 +1,31 @@
+{{>partial_header}}
+//! API routing module
+//!
+//! This module defines the routing configuration for all API endpoints.
+
+use salvo::prelude::*;
+use crate::handlers::*;
+{{#enableAuthMiddleware}}
+{{#hasAuthHandlers}}
+use crate::middleware::*;
+{{/hasAuthHandlers}}
+{{/enableAuthMiddleware}}
+
+/// Create the main router with all API routes
+pub fn create_router() -> Router {
+ let mut router = Router::new();
+
+ {{#salvoRoutes}}
+ // Routes for {{path}}
+ router = router.push(
+ Router::with_path("{{path}}"){{#operations}}
+ .push(
+ Router::new()
+ .{{method}}({{handlerName}}){{#enableAuthMiddleware}}{{#hasAuth}}{{#authSchemes}}{{#factory}}
+ .hoop({{factory}}()){{/factory}}{{/authSchemes}}{{/hasAuth}}{{/enableAuthMiddleware}}
+ ){{/operations}}
+ );
+ {{/salvoRoutes}}
+
+ router
+}
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustSalvoServerCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustSalvoServerCodegenTest.java
new file mode 100644
index 000000000000..79405bfb22f7
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rust/RustSalvoServerCodegenTest.java
@@ -0,0 +1,239 @@
+package org.openapitools.codegen.rust;
+
+import org.openapitools.codegen.DefaultGenerator;
+import org.openapitools.codegen.TestUtils;
+import org.openapitools.codegen.config.CodegenConfigurator;
+import org.openapitools.codegen.languages.RustSalvoServerCodegen;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import static org.openapitools.codegen.TestUtils.linearize;
+
+public class RustSalvoServerCodegenTest {
+
+ @Test
+ public void testInitialConfigValues() throws Exception {
+ final RustSalvoServerCodegen codegen = new RustSalvoServerCodegen();
+ codegen.processOpts();
+
+ // Test default values
+ Assert.assertEquals(codegen.getName(), "rust-salvo");
+ Assert.assertEquals(codegen.additionalProperties().get("packageName"), "salvo_openapi");
+ Assert.assertTrue(codegen.additionalProperties().containsKey("enableRequestValidation"));
+ Assert.assertTrue(codegen.additionalProperties().containsKey("enableAuthMiddleware"));
+ Assert.assertTrue(codegen.additionalProperties().containsKey("enableCorsMiddleware"));
+ }
+
+ @Test
+ public void testBasicGeneration() throws IOException {
+ Path target = Files.createTempDirectory("salvo-test");
+ System.out.println("📁 生成目录: " + target.toAbsolutePath());
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("rust-salvo")
+ .setInputSpec("src/test/resources/3_0/rust/rust-salvo-basic-test.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+
+ List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ System.out.println("✅ 生成了 " + files.size() + " 个文件:");
+ files.stream().limit(10).forEach(f -> System.out.println(" 📄 " + f.getName()));
+ System.out.println("🗂️ 完整目录结构:");
+ printDirectoryTree(target.toFile(), " ");
+ // 注释掉删除文件的代码,这样我们可以检查生成的文件
+ // files.forEach(File::deleteOnExit);
+
+ // Verify core files are generated
+ TestUtils.assertFileExists(Path.of(target.toString(), "Cargo.toml"));
+ TestUtils.assertFileExists(Path.of(target.toString(), "src/lib.rs"));
+ TestUtils.assertFileExists(Path.of(target.toString(), "src/main.rs"));
+ TestUtils.assertFileExists(Path.of(target.toString(), "src/models.rs"));
+ TestUtils.assertFileExists(Path.of(target.toString(), "src/routes.rs"));
+ TestUtils.assertFileExists(Path.of(target.toString(), "src/handlers/mod.rs"));
+ }
+
+ @Test
+ public void testHandlerGeneration() throws IOException {
+ Path target = Files.createTempDirectory("salvo-handlers-test");
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("rust-salvo")
+ .setInputSpec("src/test/resources/3_0/rust/rust-salvo-basic-test.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+
+ List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ files.forEach(File::deleteOnExit);
+
+ // Verify handler files are generated
+ Path handlersModPath = Path.of(target.toString(), "src/handlers/mod.rs");
+ TestUtils.assertFileExists(handlersModPath);
+
+ // Check that the handlers module exports are present.
+ // The basic test spec is tagged "pets", so the per-tag module should be `pets`.
+ TestUtils.assertFileContains(handlersModPath, "pub mod pets;");
+ TestUtils.assertFileContains(handlersModPath, "pub use pets::*;");
+ }
+
+ @Test
+ public void testCargoTomlGeneration() throws IOException {
+ Path target = Files.createTempDirectory("salvo-cargo-test");
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("rust-salvo")
+ .setInputSpec("src/test/resources/3_0/rust/rust-salvo-basic-test.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+
+ List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ files.forEach(File::deleteOnExit);
+
+ // Verify Cargo.toml contains required dependencies
+ Path cargoPath = Path.of(target.toString(), "Cargo.toml");
+ TestUtils.assertFileExists(cargoPath);
+ TestUtils.assertFileContains(cargoPath, "salvo = { version = \"0.93\"");
+ TestUtils.assertFileContains(cargoPath, "serde = { version = \"1\"");
+ TestUtils.assertFileContains(cargoPath, "tokio = { version = \"1\"");
+ TestUtils.assertFileContains(cargoPath, "serde_json = \"1\"");
+ }
+
+ @Test
+ public void testMiddlewareGeneration() throws IOException {
+ Path target = Files.createTempDirectory("salvo-middleware-test");
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("rust-salvo")
+ .setInputSpec("src/test/resources/3_0/rust/rust-salvo-auth-test.yaml")
+ .setSkipOverwrite(false)
+ .addAdditionalProperty("enableAuthMiddleware", "true")
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+
+ List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ files.forEach(File::deleteOnExit);
+
+ // The auth-test spec declares ApiKey, Basic, and Bearer schemes; all three
+ // structs plus their per-route factory functions should appear.
+ Path middlewarePath = Path.of(target.toString(), "src/middleware.rs");
+ TestUtils.assertFileExists(middlewarePath);
+ TestUtils.assertFileContains(middlewarePath, "pub struct ApiKeyAuth");
+ TestUtils.assertFileContains(middlewarePath, "pub struct BasicAuth");
+ TestUtils.assertFileContains(middlewarePath, "pub struct BearerAuth");
+ TestUtils.assertFileContains(middlewarePath, "pub fn auth_middleware()");
+ TestUtils.assertFileContains(middlewarePath, "pub fn api_key_auth()");
+ TestUtils.assertFileContains(middlewarePath, "pub fn basic_auth_hoop()");
+ TestUtils.assertFileContains(middlewarePath, "pub fn bearer_auth()");
+ // Case-insensitive Authorization scheme prefix matching.
+ TestUtils.assertFileContains(middlewarePath, "eq_ignore_ascii_case");
+
+ // Routes file should wire auth per-route (no global hoop in lib.rs).
+ Path routesPath = Path.of(target.toString(), "src/routes.rs");
+ TestUtils.assertFileContains(routesPath, ".hoop(api_key_auth())");
+ TestUtils.assertFileContains(routesPath, ".hoop(bearer_auth())");
+ TestUtils.assertFileContains(routesPath, ".hoop(basic_auth_hoop())");
+
+ Path libPath = Path.of(target.toString(), "src/lib.rs");
+ TestUtils.assertFileNotContains(libPath, ".hoop(auth_middleware())");
+ }
+
+ @Test
+ public void testOAuth2ScopesPropagateToEndpointAnnotation() throws IOException {
+ Path target = Files.createTempDirectory("salvo-oauth-scopes-test");
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("rust-salvo")
+ .setInputSpec("src/test/resources/3_0/petstore.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+
+ List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ files.forEach(File::deleteOnExit);
+
+ // Petstore's `petstore_auth` OAuth2 scheme grants `write:pets` and
+ // `read:pets`; the generated endpoint annotation must carry those
+ // exact scopes rather than an empty list.
+ Path petHandlersPath = Path.of(target.toString(), "src/handlers/pet.rs");
+ TestUtils.assertFileExists(petHandlersPath);
+ TestUtils.assertFileContains(petHandlersPath,
+ "security((\"OAuth2\" = [\"write:pets\", \"read:pets\"]))");
+ }
+
+ @Test
+ public void testSerdeRenameOnCamelCaseFields() throws IOException {
+ Path target = Files.createTempDirectory("salvo-rename-test");
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("rust-salvo")
+ .setInputSpec("src/test/resources/3_0/petstore.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+
+ List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ files.forEach(File::deleteOnExit);
+
+ // Petstore has `petId`, `shipDate`, `photoUrls` — Rust snake_cases them
+ // to `pet_id` / `ship_date` / `photo_urls`; a serde rename must preserve
+ // the OpenAPI wire name.
+ Path modelsPath = Path.of(target.toString(), "src/models.rs");
+ TestUtils.assertFileExists(modelsPath);
+ TestUtils.assertFileContains(modelsPath, "#[serde(rename = \"petId\"");
+ TestUtils.assertFileContains(modelsPath, "#[serde(rename = \"shipDate\"");
+ TestUtils.assertFileContains(modelsPath, "#[serde(rename = \"photoUrls\"");
+ // Identical names (e.g. `id`, `name`) should NOT get a redundant rename.
+ TestUtils.assertFileNotContains(modelsPath, "#[serde(rename = \"id\"");
+ }
+
+ @Test
+ public void testValidationSupport() throws IOException {
+ Path target = Files.createTempDirectory("salvo-validation-test");
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("rust-salvo")
+ .setInputSpec("src/test/resources/3_0/rust/rust-salvo-validation-test.yaml")
+ .setSkipOverwrite(false)
+ .addAdditionalProperty("enableRequestValidation", "true")
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+
+ List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ files.forEach(File::deleteOnExit);
+
+ // Verify validation dependencies are included when enabled
+ Path cargoPath = Path.of(target.toString(), "Cargo.toml");
+ TestUtils.assertFileExists(cargoPath);
+ TestUtils.assertFileContains(cargoPath, "validator = { version = \"0.20\"");
+ }
+
+ @Test
+ public void testRouteGeneration() throws IOException {
+ Path target = Files.createTempDirectory("salvo-routes-test");
+ final CodegenConfigurator configurator = new CodegenConfigurator()
+ .setGeneratorName("rust-salvo")
+ .setInputSpec("src/test/resources/3_0/rust/rust-salvo-basic-test.yaml")
+ .setSkipOverwrite(false)
+ .setOutputDir(target.toAbsolutePath().toString().replace("\\", "/"));
+
+ List files = new DefaultGenerator().opts(configurator.toClientOptInput()).generate();
+ files.forEach(File::deleteOnExit);
+
+ // Verify routes file contains expected Salvo routing code
+ Path routesPath = Path.of(target.toString(), "src/routes.rs");
+ TestUtils.assertFileExists(routesPath);
+ TestUtils.assertFileContains(routesPath, "use salvo::prelude::*;");
+ TestUtils.assertFileContains(routesPath, "pub fn create_router()");
+ TestUtils.assertFileContains(routesPath, "Router::new()");
+ }
+
+ private static void printDirectoryTree(File dir, String indent) {
+ if (dir.isDirectory()) {
+ System.out.println(indent + "📁 " + dir.getName() + "/");
+ File[] files = dir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ printDirectoryTree(file, indent + " ");
+ } else {
+ System.out.println(indent + " 📄 " + file.getName());
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/modules/openapi-generator/src/test/resources/3_0/rust/rust-salvo-auth-test.yaml b/modules/openapi-generator/src/test/resources/3_0/rust/rust-salvo-auth-test.yaml
new file mode 100644
index 000000000000..83d792b86701
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/rust/rust-salvo-auth-test.yaml
@@ -0,0 +1,106 @@
+openapi: 3.0.1
+info:
+ title: Salvo Auth Test API
+ version: 1.0.0
+ description: Test API for Salvo server with authentication
+paths:
+ /protected:
+ get:
+ operationId: getProtected
+ summary: Get protected resource
+ security:
+ - ApiKeyAuth: []
+ responses:
+ '200':
+ description: Protected resource
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ message:
+ type: string
+ '401':
+ description: Unauthorized
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+ /admin:
+ post:
+ operationId: adminAction
+ summary: Admin only action
+ security:
+ - BearerAuth: []
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ action:
+ type: string
+ responses:
+ '200':
+ description: Action completed
+ '401':
+ description: Unauthorized
+ '403':
+ description: Forbidden
+ /report:
+ get:
+ operationId: getReport
+ summary: Get a report (Basic-auth protected)
+ security:
+ - BasicAuth: []
+ responses:
+ '200':
+ description: Report payload
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ title:
+ type: string
+ '401':
+ description: Unauthorized
+ /search:
+ get:
+ operationId: searchAny
+ summary: Endpoint that accepts any of the configured auth schemes (OR)
+ security:
+ - ApiKeyAuth: []
+ - BearerAuth: []
+ - BasicAuth: []
+ responses:
+ '200':
+ description: Results
+ '401':
+ description: Unauthorized
+components:
+ schemas:
+ Error:
+ type: object
+ required:
+ - code
+ - message
+ properties:
+ code:
+ type: integer
+ format: int32
+ message:
+ type: string
+ securitySchemes:
+ ApiKeyAuth:
+ type: apiKey
+ in: header
+ name: X-API-Key
+ BearerAuth:
+ type: http
+ scheme: bearer
+ bearerFormat: JWT
+ BasicAuth:
+ type: http
+ scheme: basic
diff --git a/modules/openapi-generator/src/test/resources/3_0/rust/rust-salvo-basic-test.yaml b/modules/openapi-generator/src/test/resources/3_0/rust/rust-salvo-basic-test.yaml
new file mode 100644
index 000000000000..7337ea20b00e
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/rust/rust-salvo-basic-test.yaml
@@ -0,0 +1,122 @@
+openapi: 3.0.1
+info:
+ title: Basic Salvo Test API
+ version: 1.0.0
+ description: Test API for Salvo server code generation
+paths:
+ /pets:
+ get:
+ operationId: listPets
+ summary: List all pets
+ tags:
+ - pets
+ parameters:
+ - name: limit
+ in: query
+ description: How many items to return at one time (max 100)
+ required: false
+ schema:
+ type: integer
+ format: int32
+ maximum: 100
+ responses:
+ '200':
+ description: A paged array of pets
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ $ref: "#/components/schemas/Pet"
+ default:
+ description: unexpected error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+ post:
+ operationId: createPet
+ summary: Create a pet
+ tags:
+ - pets
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/NewPet"
+ responses:
+ '201':
+ description: Pet created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Pet"
+ default:
+ description: unexpected error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+ /pets/{petId}:
+ get:
+ operationId: showPetById
+ summary: Info for a specific pet
+ tags:
+ - pets
+ parameters:
+ - name: petId
+ in: path
+ required: true
+ description: The id of the pet to retrieve
+ schema:
+ type: integer
+ format: int64
+ responses:
+ '200':
+ description: Expected response to a valid request
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Pet"
+ default:
+ description: unexpected error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/Error"
+components:
+ schemas:
+ Pet:
+ type: object
+ required:
+ - id
+ - name
+ properties:
+ id:
+ type: integer
+ format: int64
+ name:
+ type: string
+ tag:
+ type: string
+ NewPet:
+ type: object
+ required:
+ - name
+ properties:
+ name:
+ type: string
+ tag:
+ type: string
+ Error:
+ type: object
+ required:
+ - code
+ - message
+ properties:
+ code:
+ type: integer
+ format: int32
+ message:
+ type: string
diff --git a/modules/openapi-generator/src/test/resources/3_0/rust/rust-salvo-validation-test.yaml b/modules/openapi-generator/src/test/resources/3_0/rust/rust-salvo-validation-test.yaml
new file mode 100644
index 000000000000..289f913ae406
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/rust/rust-salvo-validation-test.yaml
@@ -0,0 +1,102 @@
+openapi: 3.0.1
+info:
+ title: Salvo Validation Test API
+ version: 1.0.0
+ description: Test API for Salvo server with request validation
+paths:
+ /users:
+ post:
+ operationId: createUser
+ summary: Create a user with validation
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/User"
+ responses:
+ '201':
+ description: User created
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/User"
+ '400':
+ description: Validation error
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/ValidationError"
+ /users/{userId}/email:
+ put:
+ operationId: updateUserEmail
+ summary: Update user email with validation
+ parameters:
+ - name: userId
+ in: path
+ required: true
+ schema:
+ type: integer
+ format: int64
+ minimum: 1
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/EmailUpdate"
+ responses:
+ '200':
+ description: Email updated
+ '400':
+ description: Validation error
+components:
+ schemas:
+ User:
+ type: object
+ required:
+ - name
+ - email
+ - age
+ properties:
+ id:
+ type: integer
+ format: int64
+ name:
+ type: string
+ minLength: 2
+ maxLength: 100
+ email:
+ type: string
+ format: email
+ pattern: '^[\w\-\.]+@([\w-]+\.)+[\w-]{2,}$'
+ age:
+ type: integer
+ minimum: 0
+ maximum: 150
+ bio:
+ type: string
+ maxLength: 500
+ EmailUpdate:
+ type: object
+ required:
+ - email
+ properties:
+ email:
+ type: string
+ format: email
+ pattern: '^[\w\-\.]+@([\w-]+\.)+[\w-]{2,}$'
+ ValidationError:
+ type: object
+ required:
+ - error
+ - message
+ properties:
+ error:
+ type: string
+ message:
+ type: string
+ details:
+ type: array
+ items:
+ type: string
diff --git a/samples/server/petstore/rust-salvo/Cargo.toml b/samples/server/petstore/rust-salvo/Cargo.toml
new file mode 100644
index 000000000000..c086d1f01388
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/Cargo.toml
@@ -0,0 +1,3 @@
+[workspace]
+members = ["output/*"]
+resolver = "2"
diff --git a/samples/server/petstore/rust-salvo/output/petstore/.gitignore b/samples/server/petstore/rust-salvo/output/petstore/.gitignore
new file mode 100644
index 000000000000..a9ff9faa89e5
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/.gitignore
@@ -0,0 +1,30 @@
+# Generated by Cargo
+/target/
+Cargo.lock
+
+# Editor files
+.vscode/
+.idea/
+*.swp
+*.swo
+*~
+
+# OS generated files
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
+
+# Log files
+*.log
+
+# Environment variables
+.env
+.env.local
+
+# Backup files
+*.bak
+*.backup
diff --git a/samples/server/petstore/rust-salvo/output/petstore/.openapi-generator-ignore b/samples/server/petstore/rust-salvo/output/petstore/.openapi-generator-ignore
new file mode 100644
index 000000000000..7484ee590a38
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/.openapi-generator-ignore
@@ -0,0 +1,23 @@
+# OpenAPI Generator Ignore
+# Generated by openapi-generator https://github.com/openapitools/openapi-generator
+
+# Use this file to prevent files from being overwritten by the generator.
+# The patterns follow closely to .gitignore or .dockerignore.
+
+# As an example, the C# client generator defines ApiClient.cs.
+# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
+#ApiClient.cs
+
+# You can match any string of characters against a directory, file or extension with a single asterisk (*):
+#foo/*/qux
+# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
+
+# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
+#foo/**/qux
+# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
+
+# You can also negate patterns with an exclamation (!).
+# For example, you can ignore all files in a docs folder with the file extension .md:
+#docs/*.md
+# Then explicitly reverse the ignore rule for a single file:
+#!docs/README.md
diff --git a/samples/server/petstore/rust-salvo/output/petstore/.openapi-generator/FILES b/samples/server/petstore/rust-salvo/output/petstore/.openapi-generator/FILES
new file mode 100644
index 000000000000..b9043f03a5f7
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/.openapi-generator/FILES
@@ -0,0 +1,13 @@
+.gitignore
+.openapi-generator-ignore
+Cargo.toml
+README.md
+src/handlers/mod.rs
+src/handlers/pet.rs
+src/handlers/store.rs
+src/handlers/user.rs
+src/lib.rs
+src/main.rs
+src/middleware.rs
+src/models.rs
+src/routes.rs
diff --git a/samples/server/petstore/rust-salvo/output/petstore/.openapi-generator/VERSION b/samples/server/petstore/rust-salvo/output/petstore/.openapi-generator/VERSION
new file mode 100644
index 000000000000..ca7bf6e46889
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/.openapi-generator/VERSION
@@ -0,0 +1 @@
+7.23.0-SNAPSHOT
diff --git a/samples/server/petstore/rust-salvo/output/petstore/Cargo.toml b/samples/server/petstore/rust-salvo/output/petstore/Cargo.toml
new file mode 100644
index 000000000000..3884c5a81f42
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/Cargo.toml
@@ -0,0 +1,30 @@
+[package]
+name = "petstore_salvo"
+version = "1.0.0"
+authors = ["OpenAPI Generator team and contributors"]
+description = "This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters."
+license = "Apache-2.0"
+edition = "2021"
+homepage = "https://github.com/openapitools/openapi-generator"
+
+[features]
+default = ["server"]
+server = []
+
+[dependencies]
+salvo = { version = "0.93", features = [
+ "oapi",
+ "cors",
+ "jwt-auth",
+ "serve-static",
+] }
+async-trait = "0.1"
+base64 = "0.22"
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal"] }
+tracing = "0.1"
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+anyhow = "1"
+chrono = { version = "0.4", features = ["serde"] }
+validator = { version = "0.20", features = ["derive"] }
diff --git a/samples/server/petstore/rust-salvo/output/petstore/README.md b/samples/server/petstore/rust-salvo/output/petstore/README.md
new file mode 100644
index 000000000000..2a3a80c3bc79
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/README.md
@@ -0,0 +1,115 @@
+# OpenAPI Petstore
+
+This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+
+This server was generated by the [OpenAPI Generator](https://openapi-generator.tech) project using the **rust-salvo** generator.
+
+- API version: 1.0.0
+- Package version: 1.0.0
+- Generator version: 7.23.0-SNAPSHOT
+
+
+## Stack
+
+| Crate | Purpose |
+| ----- | ------- |
+| [salvo](https://salvo.rs/) (≥ 0.93) | Web framework + `salvo::oapi` for the OpenAPI-aware endpoint macro |
+| [serde](https://serde.rs/) | (De)serialization for models |
+| [tokio](https://tokio.rs/) | Async runtime |
+| [tracing](https://crates.io/crates/tracing) | Structured logging |
+| [validator](https://crates.io/crates/validator) | Request-body validation via `#[derive(Validate)]` |
+
+## What the generator produces
+
+The crate is laid out so the framework-facing surface lives in `src/` and the business logic is something you fill in:
+
+```
+src/
+├── lib.rs # crate entry: re-exports + create_service / create_router
+├── main.rs # binary entry: binds the router on 0.0.0.0:7878
+├── models.rs # data models (derive Debug, Clone, Serialize, Deserialize, ToSchema, Validate)
+├── routes.rs # Router::with_path("/...") chains mapping methods to handlers
+├── handlers/ # one module per OpenAPI tag
+│ ├── mod.rs
+│ └── .rs # one handler stub per operation
+```
+
+Handlers use the typed extractors from `salvo::oapi::extract` (`JsonBody`, `PathParam`, `QueryParam`, `HeaderParam`, `FormBody`) so that the `#[salvo::oapi::endpoint]` macro picks up the OpenAPI metadata automatically. Models derive `salvo::oapi::ToSchema` for the same reason — they can flow through the extractors and show up in the generated OpenAPI doc.
+
+## Filling in the implementation
+
+Each generated handler is a stub that returns `StatusCode::NOT_IMPLEMENTED`. Replace the body with your business logic, for example:
+
+```rust
+#[salvo::oapi::endpoint(operation_id = "get_pet_by_id", tags("pet"))]
+pub async fn get_pet_by_id(
+ pet_id: PathParam,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ let pet = Pet::new("doggie".to_string(), vec!["url".to_string()]);
+ res.render(Json(pet));
+ Ok(())
+}
+```
+
+## Building and running
+
+```bash
+cargo build # build
+cargo run # run the server on http://0.0.0.0:7878
+RUST_LOG=debug cargo run # with verbose logging
+```
+
+`tracing-subscriber` honours `RUST_LOG`, so set it to whatever level you need (`error`, `warn`, `info`, `debug`, `trace`).
+
+## API endpoints
+
+
+### pet
+
+- **POST** `/pet` — Add a new pet to the store
+- **DELETE** `/pet/{petId}` — Deletes a pet
+- **GET** `/pet/findByStatus` — Finds Pets by status
+- **GET** `/pet/findByTags` — Finds Pets by tags
+- **GET** `/pet/{petId}` — Find pet by ID
+- **PUT** `/pet` — Update an existing pet
+- **POST** `/pet/{petId}` — Updates a pet in the store with form data
+- **POST** `/pet/{petId}/uploadImage` — uploads an image
+
+### store
+
+- **DELETE** `/store/order/{orderId}` — Delete purchase order by ID
+- **GET** `/store/inventory` — Returns pet inventories by status
+- **GET** `/store/order/{orderId}` — Find purchase order by ID
+- **POST** `/store/order` — Place an order for a pet
+
+### user
+
+- **POST** `/user` — Create user
+- **POST** `/user/createWithArray` — Creates list of users with given input array
+- **POST** `/user/createWithList` — Creates list of users with given input array
+- **DELETE** `/user/{username}` — Delete user
+- **GET** `/user/{username}` — Get user by user name
+- **GET** `/user/login` — Logs user into the system
+- **GET** `/user/logout` — Logs out current logged in user session
+- **PUT** `/user/{username}` — Updated user
+
+
+## Extending
+
+- **Authentication** — `re-generate with `--additional-properties=enableAuthMiddleware=true` (or set it in `bin/configs/...yaml`) to scaffold `src/middleware.rs` with `ApiKeyAuth`, `BasicAuth`, and `BearerAuth` handlers wired to the spec's `securitySchemes`.
+- **CORS** — `re-generate with `--additional-properties=enableCorsMiddleware=true` to attach `salvo::cors::Cors::new()` as a hoop on the service.
+- **Validation** — `request validation is enabled, models derive `Validate`.
+- **Database** — drop one of [sqlx](https://crates.io/crates/sqlx), [Diesel](https://crates.io/crates/diesel), or [SeaORM](https://crates.io/crates/sea-orm) into `Cargo.toml` and inject a connection pool through `Depot` (Salvo's per-request typed storage).
+
+## Testing
+
+```bash
+cargo test
+```
+
+Salvo's `TestClient` (re-exported as `salvo::test::TestClient`) is the idiomatic way to drive handlers from integration tests.
+
+## License
+
+This project is licensed under Apache-2.0.
diff --git a/samples/server/petstore/rust-salvo/output/petstore/src/handlers/mod.rs b/samples/server/petstore/rust-salvo/output/petstore/src/handlers/mod.rs
new file mode 100644
index 000000000000..c164a7cd5dbc
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/src/handlers/mod.rs
@@ -0,0 +1,23 @@
+/*
+ * OpenAPI Petstore
+ *
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ *
+ * Generated by: https://openapi-generator.tech
+ */
+
+//! API handlers module
+//!
+//! This module contains HTTP request handlers for the API endpoints.
+//! Each handler function processes incoming requests and returns responses.
+
+pub mod pet;
+pub mod store;
+pub mod user;
+
+// Re-exports for easier access
+pub use pet::*;
+pub use store::*;
+pub use user::*;
diff --git a/samples/server/petstore/rust-salvo/output/petstore/src/handlers/pet.rs b/samples/server/petstore/rust-salvo/output/petstore/src/handlers/pet.rs
new file mode 100644
index 000000000000..6daf30095b29
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/src/handlers/pet.rs
@@ -0,0 +1,155 @@
+/*
+ * OpenAPI Petstore
+ *
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ *
+ * Generated by: https://openapi-generator.tech
+ */
+
+//! OpenAPI Petstore handlers
+//!
+//! API version: 1.0.0
+//!
+
+use salvo::prelude::*;
+use salvo::oapi::extract::{JsonBody, PathParam, QueryParam, HeaderParam, FormBody};
+use serde::{Deserialize, Serialize};
+use validator::Validate;
+
+use crate::models::{self, *};
+
+///
+#[salvo::oapi::endpoint(
+ operation_id = "add_pet",
+ tags("pet"),
+ security(("OAuth2" = ["write:pets", "read:pets"]))
+)]
+pub async fn add_pet(
+ pet: JsonBody,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ //
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+///
+#[salvo::oapi::endpoint(
+ operation_id = "delete_pet",
+ tags("pet"),
+ security(("OAuth2" = ["write:pets", "read:pets"]))
+)]
+pub async fn delete_pet(
+ pet_id: PathParam,
+ api_key: HeaderParam,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ //
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+/// Multiple status values can be provided with comma separated strings
+#[salvo::oapi::endpoint(
+ operation_id = "find_pets_by_status",
+ tags("pet"),
+ security(("OAuth2" = ["read:pets"]))
+)]
+pub async fn find_pets_by_status(
+ status: QueryParam, true>,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ // Multiple status values can be provided with comma separated strings
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+/// Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
+#[salvo::oapi::endpoint(
+ operation_id = "find_pets_by_tags",
+ tags("pet"),
+ security(("OAuth2" = ["read:pets"]))
+)]
+pub async fn find_pets_by_tags(
+ tags: QueryParam, true>,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ // Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+/// Returns a single pet
+#[salvo::oapi::endpoint(
+ operation_id = "get_pet_by_id",
+ tags("pet"),
+ security(("ApiKeyAuth" = []))
+)]
+pub async fn get_pet_by_id(
+ pet_id: PathParam,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ // Returns a single pet
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+///
+#[salvo::oapi::endpoint(
+ operation_id = "update_pet",
+ tags("pet"),
+ security(("OAuth2" = ["write:pets", "read:pets"]))
+)]
+pub async fn update_pet(
+ pet: JsonBody,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ //
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+///
+#[salvo::oapi::endpoint(
+ operation_id = "update_pet_with_form",
+ tags("pet"),
+ security(("OAuth2" = ["write:pets", "read:pets"]))
+)]
+pub async fn update_pet_with_form(
+ pet_id: PathParam,
+ name: FormBody,
+ status: FormBody,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ //
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+///
+#[salvo::oapi::endpoint(
+ operation_id = "upload_file",
+ tags("pet"),
+ security(("OAuth2" = ["write:pets", "read:pets"]))
+)]
+pub async fn upload_file(
+ pet_id: PathParam,
+ additional_metadata: FormBody,
+ file: FormBody>,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ //
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
diff --git a/samples/server/petstore/rust-salvo/output/petstore/src/handlers/store.rs b/samples/server/petstore/rust-salvo/output/petstore/src/handlers/store.rs
new file mode 100644
index 000000000000..d36cdc37c846
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/src/handlers/store.rs
@@ -0,0 +1,82 @@
+/*
+ * OpenAPI Petstore
+ *
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ *
+ * Generated by: https://openapi-generator.tech
+ */
+
+//! OpenAPI Petstore handlers
+//!
+//! API version: 1.0.0
+//!
+
+use salvo::prelude::*;
+use salvo::oapi::extract::{JsonBody, PathParam, QueryParam, HeaderParam, FormBody};
+use serde::{Deserialize, Serialize};
+use validator::Validate;
+
+use crate::models::{self, *};
+
+/// For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
+#[salvo::oapi::endpoint(
+ operation_id = "delete_order",
+ tags("store")
+)]
+pub async fn delete_order(
+ order_id: PathParam,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ // For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+/// Returns a map of status codes to quantities
+#[salvo::oapi::endpoint(
+ operation_id = "get_inventory",
+ tags("store"),
+ security(("ApiKeyAuth" = []))
+)]
+pub async fn get_inventory(
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ // Returns a map of status codes to quantities
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+/// For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions
+#[salvo::oapi::endpoint(
+ operation_id = "get_order_by_id",
+ tags("store")
+)]
+pub async fn get_order_by_id(
+ order_id: PathParam,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ // For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+///
+#[salvo::oapi::endpoint(
+ operation_id = "place_order",
+ tags("store")
+)]
+pub async fn place_order(
+ order: JsonBody,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ //
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
diff --git a/samples/server/petstore/rust-salvo/output/petstore/src/handlers/user.rs b/samples/server/petstore/rust-salvo/output/petstore/src/handlers/user.rs
new file mode 100644
index 000000000000..b26201c6fd06
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/src/handlers/user.rs
@@ -0,0 +1,149 @@
+/*
+ * OpenAPI Petstore
+ *
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ *
+ * Generated by: https://openapi-generator.tech
+ */
+
+//! OpenAPI Petstore handlers
+//!
+//! API version: 1.0.0
+//!
+
+use salvo::prelude::*;
+use salvo::oapi::extract::{JsonBody, PathParam, QueryParam, HeaderParam, FormBody};
+use serde::{Deserialize, Serialize};
+use validator::Validate;
+
+use crate::models::{self, *};
+
+/// This can only be done by the logged in user.
+#[salvo::oapi::endpoint(
+ operation_id = "create_user",
+ tags("user"),
+ security(("ApiKeyAuth" = []))
+)]
+pub async fn create_user(
+ user: JsonBody,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ // This can only be done by the logged in user.
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+///
+#[salvo::oapi::endpoint(
+ operation_id = "create_users_with_array_input",
+ tags("user"),
+ security(("ApiKeyAuth" = []))
+)]
+pub async fn create_users_with_array_input(
+ user: JsonBody>,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ //
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+///
+#[salvo::oapi::endpoint(
+ operation_id = "create_users_with_list_input",
+ tags("user"),
+ security(("ApiKeyAuth" = []))
+)]
+pub async fn create_users_with_list_input(
+ user: JsonBody>,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ //
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+/// This can only be done by the logged in user.
+#[salvo::oapi::endpoint(
+ operation_id = "delete_user",
+ tags("user"),
+ security(("ApiKeyAuth" = []))
+)]
+pub async fn delete_user(
+ username: PathParam,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ // This can only be done by the logged in user.
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+///
+#[salvo::oapi::endpoint(
+ operation_id = "get_user_by_name",
+ tags("user")
+)]
+pub async fn get_user_by_name(
+ username: PathParam,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ //
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+///
+#[salvo::oapi::endpoint(
+ operation_id = "login_user",
+ tags("user")
+)]
+pub async fn login_user(
+ username: QueryParam,
+ password: QueryParam,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ //
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+///
+#[salvo::oapi::endpoint(
+ operation_id = "logout_user",
+ tags("user"),
+ security(("ApiKeyAuth" = []))
+)]
+pub async fn logout_user(
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ //
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
+/// This can only be done by the logged in user.
+#[salvo::oapi::endpoint(
+ operation_id = "update_user",
+ tags("user"),
+ security(("ApiKeyAuth" = []))
+)]
+pub async fn update_user(
+ username: PathParam,
+ user: JsonBody,
+ res: &mut Response,
+) -> Result<(), StatusError> {
+ // This can only be done by the logged in user.
+
+ res.status_code(StatusCode::NOT_IMPLEMENTED);
+ Ok(())
+}
+
diff --git a/samples/server/petstore/rust-salvo/output/petstore/src/lib.rs b/samples/server/petstore/rust-salvo/output/petstore/src/lib.rs
new file mode 100644
index 000000000000..1f029bc12076
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/src/lib.rs
@@ -0,0 +1,44 @@
+#![allow(
+ missing_docs,
+ trivial_casts,
+ unused_variables,
+ unused_mut,
+ unused_extern_crates,
+ non_camel_case_types,
+ unused_imports,
+ unused_attributes
+)]
+#![allow(
+ clippy::derive_partial_eq_without_eq,
+ clippy::disallowed_names,
+ clippy::too_many_arguments
+)]
+
+pub const BASE_PATH: &str = "/v2";
+pub const API_VERSION: &str = "1.0.0";
+
+pub mod handlers;
+pub mod models;
+pub mod routes;
+
+pub use handlers::*;
+pub use models::*;
+pub use routes::*;
+
+use salvo::prelude::*;
+
+
+/// Create and configure the Salvo service.
+///
+/// Authentication middleware is attached per route (see `routes.rs`) so that
+/// public operations are not blocked by a global auth hoop.
+pub fn create_service() -> Service {
+ let router = create_router();
+
+ Service::new(router)
+}
+
+/// Create the main router with all API routes
+pub fn create_router() -> Router {
+ routes::create_router()
+}
diff --git a/samples/server/petstore/rust-salvo/output/petstore/src/main.rs b/samples/server/petstore/rust-salvo/output/petstore/src/main.rs
new file mode 100644
index 000000000000..f22a8c112db3
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/src/main.rs
@@ -0,0 +1,18 @@
+use petstore_salvo::create_service;
+use salvo::prelude::*;
+use tracing_subscriber;
+
+#[tokio::main]
+async fn main() -> Result<(), Box> {
+ // Initialize tracing
+ tracing_subscriber::fmt::init();
+
+ let service = create_service();
+ let acceptor = TcpListener::new("0.0.0.0:7878").bind().await;
+
+ tracing::info!("OpenAPI Petstore listening on http://0.0.0.0:7878");
+
+ Server::new(acceptor).serve(service).await;
+
+ Ok(())
+}
diff --git a/samples/server/petstore/rust-salvo/output/petstore/src/middleware.rs b/samples/server/petstore/rust-salvo/output/petstore/src/middleware.rs
new file mode 100644
index 000000000000..9cf9ae61cd83
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/src/middleware.rs
@@ -0,0 +1,92 @@
+/*
+ * OpenAPI Petstore
+ *
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ *
+ * Generated by: https://openapi-generator.tech
+ */
+
+//! Authentication and middleware for OpenAPI Petstore
+//!
+
+use salvo::prelude::*;
+use serde::{Deserialize, Serialize};
+use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
+
+/// Case-insensitively strip an `Authorization` scheme prefix
+/// (e.g. `Bearer ` or `Basic `) and return the trimmed credential.
+fn strip_scheme_prefix<'a>(header: &'a str, scheme: &str) -> Option<&'a str> {
+ if header.len() <= scheme.len() {
+ return None;
+ }
+ let (prefix, rest) = header.split_at(scheme.len());
+ if !prefix.eq_ignore_ascii_case(scheme) {
+ return None;
+ }
+ let rest = rest.strip_prefix(' ')?;
+ Some(rest.trim_start())
+}
+
+
+/// API Key authentication middleware
+#[derive(Debug, Clone)]
+pub struct ApiKeyAuth {
+ pub key_name: String,
+ pub api_key: String,
+}
+
+impl ApiKeyAuth {
+ pub fn new(key_name: impl Into, api_key: impl Into) -> Self {
+ Self {
+ key_name: key_name.into(),
+ api_key: api_key.into(),
+ }
+ }
+}
+
+#[async_trait::async_trait]
+impl Handler for ApiKeyAuth {
+ async fn handle(&self, req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
+ let provided_key = req.headers().get(&self.key_name)
+ .and_then(|v| v.to_str().ok());
+
+ if let Some(key) = provided_key {
+ if key == self.api_key {
+ ctrl.call_next(req, depot, res).await;
+ return;
+ }
+ }
+
+ res.status_code(StatusCode::UNAUTHORIZED);
+ res.render("Unauthorized: Invalid API Key");
+ ctrl.skip_rest();
+ }
+}
+
+/// Factory used as a per-route `.hoop(api_key_auth())`.
+/// Replace the placeholder with credentials sourced from your config.
+pub fn api_key_auth() -> ApiKeyAuth {
+ ApiKeyAuth::new("api_key", "your-api-key")
+}
+
+
+/// Backwards-compatible entry point used by older generated code paths.
+///
+/// Returns a no-op handler so attaching it as a hoop is harmless; per-route
+/// authentication is wired up via the per-scheme factories above.
+pub fn auth_middleware() -> impl Handler {
+ NoneHandler
+}
+
+#[derive(Debug, Clone)]
+pub struct NoneHandler;
+
+#[async_trait::async_trait]
+impl Handler for NoneHandler {
+ async fn handle(&self, req: &mut Request, depot: &mut Depot, res: &mut Response, ctrl: &mut FlowCtrl) {
+ ctrl.call_next(req, depot, res).await;
+ }
+}
+
diff --git a/samples/server/petstore/rust-salvo/output/petstore/src/models.rs b/samples/server/petstore/rust-salvo/output/petstore/src/models.rs
new file mode 100644
index 000000000000..1e275250ff57
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/src/models.rs
@@ -0,0 +1,219 @@
+/*
+ * OpenAPI Petstore
+ *
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ *
+ * Generated by: https://openapi-generator.tech
+ */
+
+//! Data models for OpenAPI Petstore
+//!
+//! API version: 1.0.0
+//!
+
+#![allow(unused_qualifications)]
+
+use salvo::oapi::ToSchema;
+use serde::{Deserialize, Serialize};
+use validator::Validate;
+
+// Self-reference so `models::Foo` (the form emitted by the codegen for
+// cross-model references) resolves inside this file.
+use crate::models;
+
+/// Describes the result of uploading an image resource
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Validate)]
+pub struct ApiResponse {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub code: Option,
+ #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
+ pub r#type: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub message: Option,
+}
+
+impl ApiResponse {
+ pub fn new() -> Self {
+ Self {
+ code: None,
+ r#type: None,
+ message: None,
+ }
+ }
+}
+
+/// A category for a pet
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Validate)]
+pub struct Category {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub id: Option,
+ #[validate()]
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub name: Option,
+}
+
+impl Category {
+ pub fn new() -> Self {
+ Self {
+ id: None,
+ name: None,
+ }
+ }
+}
+
+/// An order for a pets from the pet store
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Validate)]
+pub struct Order {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub id: Option,
+ #[serde(rename = "petId", skip_serializing_if = "Option::is_none")]
+ pub pet_id: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub quantity: Option,
+ #[serde(rename = "shipDate", skip_serializing_if = "Option::is_none")]
+ pub ship_date: Option>,
+ /// Order Status
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub status: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub complete: Option,
+}
+
+impl Order {
+ pub fn new() -> Self {
+ Self {
+ id: None,
+ pet_id: None,
+ quantity: None,
+ ship_date: None,
+ status: None,
+ complete: None,
+ }
+ }
+}
+
+/// A pet for sale in the pet store
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Validate)]
+pub struct Pet {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub id: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub category: Option,
+ pub name: String,
+ #[serde(rename = "photoUrls")]
+ pub photo_urls: Vec,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub tags: Option>,
+ /// pet status in the store
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub status: Option,
+}
+
+impl Pet {
+ pub fn new(name: String, photo_urls: Vec) -> Self {
+ Self {
+ id: None,
+ category: None,
+ name,
+ photo_urls,
+ tags: None,
+ status: None,
+ }
+ }
+}
+
+/// A tag for a pet
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Validate)]
+pub struct Tag {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub id: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub name: Option,
+}
+
+impl Tag {
+ pub fn new() -> Self {
+ Self {
+ id: None,
+ name: None,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Validate)]
+pub struct UpdatePetWithFormRequest {
+ /// Updated name of the pet
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub name: Option,
+ /// Updated status of the pet
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub status: Option,
+}
+
+impl UpdatePetWithFormRequest {
+ pub fn new() -> Self {
+ Self {
+ name: None,
+ status: None,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Validate)]
+pub struct UploadFileRequest {
+ /// Additional data to pass to server
+ #[serde(rename = "additionalMetadata", skip_serializing_if = "Option::is_none")]
+ pub additional_metadata: Option,
+ /// file to upload
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub file: Option>,
+}
+
+impl UploadFileRequest {
+ pub fn new() -> Self {
+ Self {
+ additional_metadata: None,
+ file: None,
+ }
+ }
+}
+
+/// A User who is purchasing from the pet store
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema, Validate)]
+pub struct User {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub id: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub username: Option,
+ #[serde(rename = "firstName", skip_serializing_if = "Option::is_none")]
+ pub first_name: Option,
+ #[serde(rename = "lastName", skip_serializing_if = "Option::is_none")]
+ pub last_name: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub email: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub password: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub phone: Option,
+ /// User Status
+ #[serde(rename = "userStatus", skip_serializing_if = "Option::is_none")]
+ pub user_status: Option,
+}
+
+impl User {
+ pub fn new() -> Self {
+ Self {
+ id: None,
+ username: None,
+ first_name: None,
+ last_name: None,
+ email: None,
+ password: None,
+ phone: None,
+ user_status: None,
+ }
+ }
+}
+
diff --git a/samples/server/petstore/rust-salvo/output/petstore/src/routes.rs b/samples/server/petstore/rust-salvo/output/petstore/src/routes.rs
new file mode 100644
index 000000000000..aa401899a76a
--- /dev/null
+++ b/samples/server/petstore/rust-salvo/output/petstore/src/routes.rs
@@ -0,0 +1,160 @@
+/*
+ * OpenAPI Petstore
+ *
+ * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
+ *
+ * The version of the OpenAPI document: 1.0.0
+ *
+ * Generated by: https://openapi-generator.tech
+ */
+
+//! API routing module
+//!
+//! This module defines the routing configuration for all API endpoints.
+
+use salvo::prelude::*;
+use crate::handlers::*;
+
+/// Create the main router with all API routes
+pub fn create_router() -> Router {
+ let mut router = Router::new();
+
+ // Routes for /pet
+ router = router.push(
+ Router::with_path("/pet")
+ .push(
+ Router::new()
+ .put(update_pet)
+ )
+ .push(
+ Router::new()
+ .post(add_pet)
+ )
+ );
+ // Routes for /pet/findByStatus
+ router = router.push(
+ Router::with_path("/pet/findByStatus")
+ .push(
+ Router::new()
+ .get(find_pets_by_status)
+ )
+ );
+ // Routes for /pet/findByTags
+ router = router.push(
+ Router::with_path("/pet/findByTags")
+ .push(
+ Router::new()
+ .get(find_pets_by_tags)
+ )
+ );
+ // Routes for /pet/{petId}
+ router = router.push(
+ Router::with_path("/pet/{petId}")
+ .push(
+ Router::new()
+ .get(get_pet_by_id)
+ )
+ .push(
+ Router::new()
+ .post(update_pet_with_form)
+ )
+ .push(
+ Router::new()
+ .delete(delete_pet)
+ )
+ );
+ // Routes for /pet/{petId}/uploadImage
+ router = router.push(
+ Router::with_path("/pet/{petId}/uploadImage")
+ .push(
+ Router::new()
+ .post(upload_file)
+ )
+ );
+ // Routes for /store/inventory
+ router = router.push(
+ Router::with_path("/store/inventory")
+ .push(
+ Router::new()
+ .get(get_inventory)
+ )
+ );
+ // Routes for /store/order
+ router = router.push(
+ Router::with_path("/store/order")
+ .push(
+ Router::new()
+ .post(place_order)
+ )
+ );
+ // Routes for /store/order/{orderId}
+ router = router.push(
+ Router::with_path("/store/order/{orderId}")
+ .push(
+ Router::new()
+ .get(get_order_by_id)
+ )
+ .push(
+ Router::new()
+ .delete(delete_order)
+ )
+ );
+ // Routes for /user
+ router = router.push(
+ Router::with_path("/user")
+ .push(
+ Router::new()
+ .post(create_user)
+ )
+ );
+ // Routes for /user/createWithArray
+ router = router.push(
+ Router::with_path("/user/createWithArray")
+ .push(
+ Router::new()
+ .post(create_users_with_array_input)
+ )
+ );
+ // Routes for /user/createWithList
+ router = router.push(
+ Router::with_path("/user/createWithList")
+ .push(
+ Router::new()
+ .post(create_users_with_list_input)
+ )
+ );
+ // Routes for /user/login
+ router = router.push(
+ Router::with_path("/user/login")
+ .push(
+ Router::new()
+ .get(login_user)
+ )
+ );
+ // Routes for /user/logout
+ router = router.push(
+ Router::with_path("/user/logout")
+ .push(
+ Router::new()
+ .get(logout_user)
+ )
+ );
+ // Routes for /user/{username}
+ router = router.push(
+ Router::with_path("/user/{username}")
+ .push(
+ Router::new()
+ .get(get_user_by_name)
+ )
+ .push(
+ Router::new()
+ .put(update_user)
+ )
+ .push(
+ Router::new()
+ .delete(delete_user)
+ )
+ );
+
+ router
+}