From d40c28b2b98fc892112fb2a9bb432b10b40af212 Mon Sep 17 00:00:00 2001 From: wing328 Date: Mon, 2 Apr 2018 12:53:06 +0800 Subject: [PATCH] add rust client, server generator --- .../openapitools/codegen/DefaultCodegen.java | 4 +- .../codegen/languages/RustClientCodegen.java | 485 ++++++++ .../codegen/languages/RustServerCodegen.java | 1099 +++++++++++++++++ .../codegen/utils/URLPathUtil.java | 1 + .../org.openapitools.codegen.CodegenConfig | 2 + .../main/resources/rust-server/Cargo.mustache | 27 +- .../resources/rust-server/client-mod.mustache | 437 +++++++ .../resources/rust-server/client.mustache | 342 ----- .../rust-server/example-client.mustache | 11 +- .../rust-server/example-server.mustache | 50 +- .../rust-server/example-server_lib.mustache | 18 +- .../example-server_server.mustache | 3 +- .../main/resources/rust-server/lib.mustache | 12 +- .../resources/rust-server/mimetypes.mustache | 8 +- .../resources/rust-server/models.mustache | 0 .../rust-server/server-auth.mustache | 95 ++ .../resources/rust-server/server-mod.mustache | 449 +++++++ .../resources/rust-server/server.mustache | 340 ----- .../resources/rust-server/swagger.mustache | 2 +- 19 files changed, 2651 insertions(+), 734 deletions(-) create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java create mode 100644 modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java create mode 100644 modules/openapi-generator/src/main/resources/rust-server/client-mod.mustache delete mode 100644 modules/openapi-generator/src/main/resources/rust-server/client.mustache mode change 100755 => 100644 modules/openapi-generator/src/main/resources/rust-server/models.mustache create mode 100644 modules/openapi-generator/src/main/resources/rust-server/server-auth.mustache create mode 100644 modules/openapi-generator/src/main/resources/rust-server/server-mod.mustache delete mode 100644 modules/openapi-generator/src/main/resources/rust-server/server.mustache diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 84e372c4854..b332770eca6 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -3972,9 +3972,7 @@ public class DefaultCodegen implements CodegenConfig { LOGGER.debug("debugging fromRequestBodyToFormParameters= " + body); Schema schema = getSchemaFromBody(body); if (StringUtils.isNotBlank(schema.get$ref())) { - schema = schemas.get( - getSimpleRef(schema.get$ref()) - ); + schema = schemas.get(getSimpleRef(schema.get$ref())); } if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { Map properties = schema.getProperties(); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java new file mode 100644 index 00000000000..737eb57fc7d --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustClientCodegen.java @@ -0,0 +1,485 @@ +package org.openapitools.codegen.languages; + +import org.openapitools.codegen.*; +import org.openapitools.codegen.utils.*; +import org.openapitools.codegen.mustache.*; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.*; +import io.swagger.v3.oas.models.media.*; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.parameters.*; + +import java.io.File; +import java.util.*; + +import org.apache.commons.lang3.StringUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class RustClientCodegen extends DefaultCodegen implements CodegenConfig { + static Logger LOGGER = LoggerFactory.getLogger(RustClientCodegen.class); + public static final String PACKAGE_NAME = "packageName"; + public static final String PACKAGE_VERSION = "packageVersion"; + + protected String packageName = "swagger"; + protected String packageVersion = "1.0.0"; + protected String apiDocPath = "docs/"; + protected String modelDocPath = "docs/"; + protected String apiFolder = "src/apis"; + protected String modelFolder= "src/models"; + + public CodegenType getTag() { + return CodegenType.CLIENT; + } + + public String getName() { + return "rust"; + } + + public String getHelp() { + return "Generates a Rust client library (beta)."; + } + + public RustClientCodegen() { + super(); + outputFolder = "generated-code/rust"; + modelTemplateFiles.put("model.mustache", ".rs"); + apiTemplateFiles.put("api.mustache", ".rs"); + + modelDocTemplateFiles.put("model_doc.mustache", ".md"); + apiDocTemplateFiles.put("api_doc.mustache", ".md"); + + embeddedTemplateDir = templateDir = "rust"; + + setReservedWordsLowerCase( + Arrays.asList( + "abstract", "alignof", "as", "become", "box", + "break", "const", "continue", "crate", "do", + "else", "enum", "extern", "false", "final", + "fn", "for", "if", "impl", "in", + "let", "loop", "macro", "match", "mod", + "move", "mut", "offsetof", "override", "priv", + "proc", "pub", "pure", "ref", "return", + "Self", "self", "sizeof", "static", "struct", + "super", "trait", "true", "type", "typeof", + "unsafe", "unsized", "use", "virtual", "where", + "while", "yield" + ) + ); + + defaultIncludes = new HashSet( + Arrays.asList( + "map", + "array") + ); + + languageSpecificPrimitives = new HashSet( + Arrays.asList( + "i8", "i16", "i32", "i64", + "u8", "u16", "u32", "u64", + "f32", "f64", + "char", "bool", "String", "Vec", "File") + ); + + instantiationTypes.clear(); + /*instantiationTypes.put("array", "GoArray"); + instantiationTypes.put("map", "GoMap");*/ + + typeMapping.clear(); + typeMapping.put("integer", "i32"); + typeMapping.put("long", "i64"); + typeMapping.put("number", "f32"); + typeMapping.put("float", "f32"); + typeMapping.put("double", "f64"); + typeMapping.put("boolean", "bool"); + typeMapping.put("string", "String"); + typeMapping.put("UUID", "String"); + typeMapping.put("date", "string"); + typeMapping.put("DateTime", "String"); + typeMapping.put("password", "String"); + // TODO(farcaller): map file + typeMapping.put("file", "File"); + typeMapping.put("binary", "Vec"); + typeMapping.put("ByteArray", "String"); + typeMapping.put("object", "Value"); + + // no need for rust + //importMapping = new HashMap(); + + cliOptions.clear(); + cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, "Rust package name (convention: lowercase).") + .defaultValue("swagger")); + cliOptions.add(new CliOption(CodegenConstants.PACKAGE_VERSION, "Rust package version.") + .defaultValue("1.0.0")); + cliOptions.add(new CliOption(CodegenConstants.HIDE_GENERATION_TIMESTAMP, "hides the timestamp when files were generated") + .defaultValue(Boolean.TRUE.toString())); + + } + + @Override + public void processOpts() { + super.processOpts(); + + // default HIDE_GENERATION_TIMESTAMP to true + if (!additionalProperties.containsKey(CodegenConstants.HIDE_GENERATION_TIMESTAMP)) { + additionalProperties.put(CodegenConstants.HIDE_GENERATION_TIMESTAMP, Boolean.TRUE.toString()); + } else { + additionalProperties.put(CodegenConstants.HIDE_GENERATION_TIMESTAMP, + Boolean.valueOf(additionalProperties().get(CodegenConstants.HIDE_GENERATION_TIMESTAMP).toString())); + } + + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) { + setPackageName((String) additionalProperties.get(CodegenConstants.PACKAGE_NAME)); + } + else { + setPackageName("swagger"); + } + + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_VERSION)) { + setPackageVersion((String) additionalProperties.get(CodegenConstants.PACKAGE_VERSION)); + } + else { + setPackageVersion("1.0.0"); + } + + additionalProperties.put(CodegenConstants.PACKAGE_NAME, packageName); + additionalProperties.put(CodegenConstants.PACKAGE_VERSION, packageVersion); + + additionalProperties.put("apiDocPath", apiDocPath); + additionalProperties.put("modelDocPath", modelDocPath); + + modelPackage = packageName; + apiPackage = packageName; + + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); + supportingFiles.add(new SupportingFile("git_push.sh.mustache", "", "git_push.sh")); + supportingFiles.add(new SupportingFile("gitignore.mustache", "", ".gitignore")); + supportingFiles.add(new SupportingFile("configuration.mustache", apiFolder, "configuration.rs")); + supportingFiles.add(new SupportingFile(".travis.yml", "", ".travis.yml")); + + supportingFiles.add(new SupportingFile("client.mustache", apiFolder, "client.rs")); + supportingFiles.add(new SupportingFile("api_mod.mustache", apiFolder, "mod.rs")); + supportingFiles.add(new SupportingFile("model_mod.mustache", modelFolder, "mod.rs")); + supportingFiles.add(new SupportingFile("lib.rs", "src", "lib.rs")); + supportingFiles.add(new SupportingFile("Cargo.mustache", "", "Cargo.toml")); + } + + @Override + public String escapeReservedWord(String name) + { + if (this.reservedWordsMappings().containsKey(name)) { + return this.reservedWordsMappings().get(name); + } + return '_' + name; + } + + @Override + public String apiFileFolder() { + return (outputFolder + File.separator + apiFolder).replace("/", File.separator); + } + + public String modelFileFolder() { + return (outputFolder + File.separator + modelFolder).replace("/", File.separator); + } + + @Override + public String toVarName(String name) { + // replace - with _ e.g. created-at => created_at + name = sanitizeName(name.replaceAll("-", "_")); + + // if it's all uppper case, do nothing + if (name.matches("^[A-Z_]*$")) + return name; + + // snake_case, e.g. PetId => pet_id + name = underscore(name); + + // for reserved word or word starting with number, append _ + if (isReservedWord(name)) + name = escapeReservedWord(name); + + // for reserved word or word starting with number, append _ + if (name.matches("^\\d.*")) + name = "var_" + name; + + return name; + } + + @Override + public String toParamName(String name) { + return toVarName(name); + } + + @Override + public String toModelName(String name) { + // camelize the model name + // phone_number => PhoneNumber + return camelize(toModelFilename(name)); + } + + @Override + public String toModelFilename(String name) { + if (!StringUtils.isEmpty(modelNamePrefix)) { + name = modelNamePrefix + "_" + name; + } + + if (!StringUtils.isEmpty(modelNameSuffix)) { + name = name + "_" + modelNameSuffix; + } + + name = sanitizeName(name); + + // model name cannot use reserved keyword, e.g. return + if (isReservedWord(name)) { + LOGGER.warn(name + " (reserved word) cannot be used as model name. Renamed to " + ("model_" + name)); + name = "model_" + name; // e.g. return => ModelReturn (after camelize) + } + + // model name starts with number + if (name.matches("^\\d.*")) { + LOGGER.warn(name + " (model name starts with number) cannot be used as model name. Renamed to " + ("model_" + name)); + name = "model_" + name; // e.g. 200Response => Model200Response (after camelize) + } + + return underscore(name); + } + + @Override + public String toApiFilename(String name) { + // replace - with _ e.g. created-at => created_at + name = name.replaceAll("-", "_"); // FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'. + + // e.g. PetApi.rs => pet_api.rs + return underscore(name) + "_api"; + } + + @Override + public String apiDocFileFolder() { + return (outputFolder + "/" + apiDocPath).replace('/', File.separatorChar); + } + + @Override + public String modelDocFileFolder() { + return (outputFolder + "/" + modelDocPath).replace('/', File.separatorChar); + } + + @Override + public String toModelDocFilename(String name) { + return toModelName(name); + } + + @Override + public String toApiDocFilename(String name) { + return toApiName(name); + } + + @Override + public String getTypeDeclaration(Schema p) { + if (p instanceof ArraySchema) { + ArraySchema ap = (ArraySchema) p; + Schema inner = ap.getItems(); + return "Vec<" + getTypeDeclaration(inner) + ">"; + } + else if (isMapSchema(p)) { + MapSchema mp = (MapSchema) p; + Schema inner = (Schema) mp.getAdditionalProperties(); + return "::std::collections::HashMap"; + } + + // Not using the supertype invocation, because we want to UpperCamelize + // the type. + String schemaType = getSchemaType(p); + if (typeMapping.containsKey(schemaType)) { + return typeMapping.get(schemaType); + } + + if (typeMapping.containsValue(schemaType)) { + return schemaType; + } + + if (languageSpecificPrimitives.contains(schemaType)) { + return schemaType; + } + + // return fully-qualified model name + // ::models::{{classnameFile}}::{{classname}} + return "::models::" + toModelName(schemaType); + } + + @Override + public String getSchemaType(Schema p) { + String schemaType = super.getSchemaType(p); + String type = null; + if(typeMapping.containsKey(schemaType)) { + type = typeMapping.get(schemaType); + if(languageSpecificPrimitives.contains(type)) + return (type); + } + else + type = schemaType; + return type; + } + + @Override + public String toOperationId(String operationId) { + String sanitizedOperationId = sanitizeName(operationId); + + // method name cannot use reserved keyword, e.g. return + if (isReservedWord(sanitizedOperationId)) { + LOGGER.warn(operationId + " (reserved word) cannot be used as method name. Renamed to " + underscore("call_" + operationId)); + sanitizedOperationId = "call_" + sanitizedOperationId; + } + + return underscore(sanitizedOperationId); + } + + @Override + public Map postProcessOperations(Map objs) { + @SuppressWarnings("unchecked") + Map objectMap = (Map) objs.get("operations"); + @SuppressWarnings("unchecked") + List operations = (List) objectMap.get("operation"); + for (CodegenOperation operation : operations) { + // http method verb conversion (e.g. PUT => Put) + operation.httpMethod = camelize(operation.httpMethod.toLowerCase()); + // update return type to conform to rust standard + /* + if (operation.returnType != null) { + if ( operation.returnType.startsWith("Vec") && !languageSpecificPrimitives.contains(operation.returnBaseType)) { + // array of model + String rt = operation.returnType; + int end = rt.lastIndexOf(">"); + if ( end > 0 ) { + operation.vendorExtensions.put("x-returnTypeInMethod", "Vec"); + operation.returnContainer = "List"; + } + } else if (operation.returnType.startsWith("::std::collections::HashMap"); + if ( end > 0 ) { + operation.vendorExtensions.put("x-returnTypeInMethod", "::std::collections::HashMap"); + operation.returnContainer = "Map"; + } + } else if (!languageSpecificPrimitives.contains(operation.returnType)) { + // add super:: to model, e.g. super::pet + operation.vendorExtensions.put("x-returnTypeInMethod", "super::" + operation.returnType); + } else { + // primitive type or array/map of primitive type + operation.vendorExtensions.put("x-returnTypeInMethod", operation.returnType); + } + } + + for (CodegenParameter p : operation.allParams) { + if (p.isListContainer && !languageSpecificPrimitives.contains(p.dataType)) { + // array of model + String rt = p.dataType; + int end = rt.lastIndexOf(">"); + if ( end > 0 ) { + p.dataType = "Vec<" + rt.substring("Vec<".length(), end).trim() + ">"; + } + } else if (p.isMapContainer && !languageSpecificPrimitives.contains(p.dataType)) { + // map of model + String rt = p.dataType; + int end = rt.lastIndexOf(">"); + if ( end > 0 ) { + p.dataType = "::std::collections::HashMap"; + } + } else if (!languageSpecificPrimitives.contains(p.dataType)) { + // add super:: to model, e.g. super::pet + p.dataType = "super::" + p.dataType; + } + }*/ + } + + return objs; + } + + @Override + protected boolean needToImport(String type) { + return !defaultIncludes.contains(type) + && !languageSpecificPrimitives.contains(type); + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + } + + public void setPackageVersion(String packageVersion) { + this.packageVersion = packageVersion; + } + + @Override + public String escapeQuotationMark(String input) { + // remove " to avoid code injection + return input.replace("\"", ""); + } + + @Override + public String escapeUnsafeCharacters(String input) { + return input.replace("*/", "*_/").replace("/*", "/_*"); + } + + + @Override + public String toEnumValue(String value, String datatype) { + if ("int".equals(datatype) || "double".equals(datatype) || "float".equals(datatype)) { + return value; + } else { + return escapeText(value); + } + } + + @Override + public String toEnumDefaultValue(String value, String datatype) { + return datatype + "_" + value; + } + + @Override + public String toEnumVarName(String name, String datatype) { + if (name.length() == 0) { + return "EMPTY"; + } + + // number + if ("int".equals(datatype) || "double".equals(datatype) || "float".equals(datatype)) { + String varName = name; + varName = varName.replaceAll("-", "MINUS_"); + varName = varName.replaceAll("\\+", "PLUS_"); + varName = varName.replaceAll("\\.", "_DOT_"); + return varName; + } + + // for symbol, e.g. $, # + if (getSymbolName(name) != null) { + return getSymbolName(name).toUpperCase(); + } + + // string + String enumName = sanitizeName(underscore(name).toUpperCase()); + enumName = enumName.replaceFirst("^_", ""); + enumName = enumName.replaceFirst("_$", ""); + + if (isReservedWord(enumName) || enumName.matches("\\d.*")) { // reserved word or starts with number + return escapeReservedWord(enumName); + } else { + return enumName; + } + } + + @Override + public String toEnumName(CodegenProperty property) { + String enumName = underscore(toModelName(property.name)).toUpperCase(); + + // remove [] for array or map of enum + enumName = enumName.replace("[]", ""); + + if (enumName.matches("\\d.*")) { // starts with number + return "_" + enumName; + } else { + return enumName; + } + } +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java new file mode 100644 index 00000000000..dd1cbed4dee --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RustServerCodegen.java @@ -0,0 +1,1099 @@ +package org.openapitools.codegen.languages; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import org.openapitools.codegen.*; +import org.openapitools.codegen.utils.*; +import org.openapitools.codegen.mustache.*; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.*; +import io.swagger.v3.oas.models.media.*; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.parameters.*; +import io.swagger.v3.oas.models.info.Info; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.net.URL; +import java.util.*; +import java.util.Map.Entry; + +import org.apache.commons.lang3.StringUtils; + +public class RustServerCodegen extends DefaultCodegen implements CodegenConfig { + + private static final Logger LOGGER = LoggerFactory.getLogger(RustServerCodegen.class); + + private HashMap modelXmlNames = new HashMap(); + + private static final String NO_FORMAT = "%%NO_FORMAT"; + + protected String apiVersion = "1.0.0"; + protected String serverHost = "localhost"; + protected int serverPort = 8080; + protected String projectName = "swagger-server"; + protected String apiPath = "rust-server"; + protected String packageName; + protected String packageVersion; + protected String externCrateName; + protected Map> pathSetMap = new HashMap>(); + + public RustServerCodegen() { + super(); + + // set the output folder here + outputFolder = "generated-code/rust-server"; + + /* + * Models. You can write model files using the modelTemplateFiles map. + * if you want to create one template for file, you can do so here. + * for multiple files for model, just put another entry in the `modelTemplateFiles` with + * a different extension + */ + modelTemplateFiles.clear(); + + /* + * Api classes. You can write classes for each Api file with the apiTemplateFiles map. + * as with models, add multiple entries with different extensions for multiple files per + * class + */ + apiTemplateFiles.clear(); + + /* + * Template Location. This is the location which templates will be read from. The generator + * will use the resource stream to attempt to read the templates. + */ + embeddedTemplateDir = templateDir = "rust-server"; + + /* + * Reserved words. Override this with reserved words specific to your language + */ + setReservedWordsLowerCase( + Arrays.asList( + // From https://doc.rust-lang.org/grammar.html#keywords + "abstract", "alignof", "as", "become", "box", "break", "const", + "continue", "crate", "do", "else", "enum", "extern", "false", + "final", "fn", "for", "if", "impl", "in", "let", "loop", "macro", + "match", "mod", "move", "mut", "offsetof", "override", "priv", + "proc", "pub", "pure", "ref", "return", "Self", "self", "sizeof", + "static", "struct", "super", "trait", "true", "type", "typeof", + "unsafe", "unsized", "use", "virtual", "where", "while", "yield" + ) + ); + + defaultIncludes = new HashSet( + Arrays.asList( + "map", + "array") + ); + + languageSpecificPrimitives = new HashSet( + Arrays.asList( + "bool", + "char", + "i8", + "i16", + "i32", + "i64", + "u8", + "u16", + "u32", + "u64", + "isize", + "usize", + "f32", + "f64", + "str") + ); + + instantiationTypes.clear(); + instantiationTypes.put("array", "Vec"); + instantiationTypes.put("map", "Map"); + + typeMapping.clear(); + typeMapping.put("number", "f64"); + typeMapping.put("integer", "i32"); + typeMapping.put("long", "i64"); + typeMapping.put("float", "f32"); + typeMapping.put("double", "f64"); + typeMapping.put("string", "String"); + typeMapping.put("UUID", "uuid::Uuid"); + typeMapping.put("byte", "u8"); + typeMapping.put("ByteArray", "swagger::ByteArray"); + typeMapping.put("binary", "swagger::ByteArray"); + typeMapping.put("boolean", "bool"); + typeMapping.put("date", "chrono::DateTime"); + typeMapping.put("DateTime", "chrono::DateTime"); + typeMapping.put("password", "String"); + typeMapping.put("File", "Box, Error=Error> + Send>"); + typeMapping.put("file", "Box, Error=Error> + Send>"); + typeMapping.put("array", "Vec"); + typeMapping.put("map", "HashMap"); + + importMapping = new HashMap(); + + cliOptions.clear(); + cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, + "Rust crate name (convention: snake_case).") + .defaultValue("swagger_client")); + cliOptions.add(new CliOption(CodegenConstants.PACKAGE_VERSION, + "Rust crate version.") + .defaultValue("1.0.0")); + + /* + * Additional Properties. These values can be passed to the templates and + * are available in models, apis, and supporting files + */ + additionalProperties.put("apiVersion", apiVersion); + additionalProperties.put("apiPath", apiPath); + + /* + * Supporting Files. You can write single files for the generator with the + * entire object tree available. If the input file has a suffix of `.mustache + * it will be processed by the template engine. Otherwise, it will be copied + */ + supportingFiles.add(new SupportingFile("swagger.mustache", "api", "swagger.yaml")); + supportingFiles.add(new SupportingFile("Cargo.mustache", "", "Cargo.toml")); + supportingFiles.add(new SupportingFile("cargo-config", ".cargo", "config")); + supportingFiles.add(new SupportingFile("gitignore", "", ".gitignore")); + supportingFiles.add(new SupportingFile("lib.mustache", "src", "lib.rs")); + supportingFiles.add(new SupportingFile("models.mustache", "src", "models.rs")); + supportingFiles.add(new SupportingFile("server-mod.mustache", "src/server", "mod.rs")); + supportingFiles.add(new SupportingFile("server-auth.mustache", "src/server", "auth.rs")); + supportingFiles.add(new SupportingFile("client-mod.mustache", "src/client", "mod.rs")); + supportingFiles.add(new SupportingFile("mimetypes.mustache", "src", "mimetypes.rs")); + supportingFiles.add(new SupportingFile("example-server.mustache", "examples", "server.rs")); + supportingFiles.add(new SupportingFile("example-client.mustache", "examples", "client.rs")); + supportingFiles.add(new SupportingFile("example-server_lib.mustache", "examples/server_lib", "mod.rs")); + supportingFiles.add(new SupportingFile("example-server_server.mustache", "examples/server_lib", "server.rs")); + supportingFiles.add(new SupportingFile("example-ca.pem", "examples", "ca.pem")); + supportingFiles.add(new SupportingFile("example-server-chain.pem", "examples", "server-chain.pem")); + supportingFiles.add(new SupportingFile("example-server-key.pem", "examples", "server-key.pem")); + writeOptional(outputFolder, new SupportingFile("README.mustache", "", "README.md")); + } + + @Override + public void processOpts() { + super.processOpts(); + + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) { + setPackageName((String) additionalProperties.get(CodegenConstants.PACKAGE_NAME)); + } else { + setPackageName("swagger_client"); + } + + if (additionalProperties.containsKey(CodegenConstants.PACKAGE_VERSION)) { + setPackageVersion((String) additionalProperties.get(CodegenConstants.PACKAGE_VERSION)); + } else { + setPackageVersion("1.0.0"); + } + + additionalProperties.put(CodegenConstants.PACKAGE_NAME, packageName); + additionalProperties.put(CodegenConstants.PACKAGE_VERSION, packageVersion); + additionalProperties.put("externCrateName", externCrateName); + } + + public void setPackageName(String packageName) { + this.packageName = packageName; + + // Also set the extern crate name, which has any '-' replace with a '_'. + this.externCrateName = packageName.replace('-', '_'); + } + + public void setPackageVersion(String packageVersion) { + this.packageVersion = packageVersion; + } + + @Override + public String apiPackage() { + return apiPath; + } + + /** + * Configures the type of generator. + * + * @return the CodegenType for this generator + * @see io.swagger.codegen.CodegenType + */ + @Override + public CodegenType getTag() { + return CodegenType.SERVER; + } + + /** + * Configures a friendly name for the generator. This will be used by the generator + * to select the library with the -l flag. + * + * @return the friendly name for the generator + */ + @Override + public String getName() { + return "rust-server"; + } + + /** + * Returns human-friendly help for the generator. Provide the consumer with help + * tips, parameters here + * + * @return A string value for the help message + */ + @Override + public String getHelp() { + return "Generates a Rust client/server library (beta) using the swagger-codegen project."; + } + + @Override + public void preprocessOpenAPI(OpenAPI openAPI) { + Info info = openAPI.getInfo(); + List versionComponents = new ArrayList(Arrays.asList(info.getVersion().split("[.]"))); + if (versionComponents.size() < 1) { + versionComponents.add("1"); + } + while (versionComponents.size() < 3) { + versionComponents.add("0"); + } + info.setVersion(StringUtils.join(versionComponents, ".")); + + URL url = URLPathUtil.getServerURL(openAPI); + additionalProperties.put("serverHost", url.getHost()); + additionalProperties.put("serverPort", url.getPort()); + } + + @Override + public String toApiName(String name) { + if (name.length() == 0) { + return "default"; + } + return underscore(name); + } + + /** + * Escapes a reserved word as defined in the `reservedWords` array. Handle escaping + * those terms here. This logic is only called if a variable matches the reserved words + * + * @return the escaped term + */ + @Override + public String escapeReservedWord(String name) { + if (this.reservedWordsMappings().containsKey(name)) { + return this.reservedWordsMappings().get(name); + } + return "_" + name; // add an underscore to the name + } + + /** + * Location to write api files. You can use the apiPackage() as defined when the class is + * instantiated + */ + @Override + public String apiFileFolder() { + return outputFolder + File.separator + apiPackage().replace('.', File.separatorChar); + } + + @Override + public String toModelName(String name) { + // camelize the model name + // phone_number => PhoneNumber + String camelizedName = camelize(toModelFilename(name)); + + // model name cannot use reserved keyword, e.g. return + if (isReservedWord(camelizedName)) { + camelizedName = "Model" + camelizedName; + LOGGER.warn(camelizedName + " (reserved word) cannot be used as model name. Renamed to " + camelizedName); + } + + // model name starts with number + else if (name.matches("^\\d.*")) { + // e.g. 200Response => Model200Response (after camelize) + camelizedName = "Model" + camelizedName; + LOGGER.warn(name + " (model name starts with number) cannot be used as model name. Renamed to " + camelizedName); + } + + return camelizedName; + + } + + @Override + public String toParamName(String name) { + // should be the same as variable name (stolen from RubyClientCodegen) + return toVarName(name); + } + + @Override + public String toVarName(String name) { + String sanitizedName = super.sanitizeName(name); + // for reserved word or word starting with number, append _ + if (isReservedWord(sanitizedName) || sanitizedName.matches("^\\d.*")) { + sanitizedName = escapeReservedWord(sanitizedName); + } + + return underscore(sanitizedName); + } + + @Override + public String toOperationId(String operationId) { + // method name cannot use reserved keyword, e.g. return + if (isReservedWord(operationId)) { + LOGGER.warn(operationId + " (reserved word) cannot be used as method name. Renamed to " + camelize(sanitizeName("call_" + operationId))); + operationId = "call_" + operationId; + } + + return camelize(operationId); + } + + @Override + public String toModelFilename(String name) { + if (!StringUtils.isEmpty(modelNamePrefix)) { + name = modelNamePrefix + "_" + name; + } + + if (!StringUtils.isEmpty(modelNameSuffix)) { + name = name + "_" + modelNameSuffix; + } + + name = sanitizeName(name); + + // model name cannot use reserved keyword, e.g. return + if (isReservedWord(name)) { + LOGGER.warn(name + " (reserved word) cannot be used as model name. Renamed to " + camelize("model_" + name)); + name = "model_" + name; // e.g. return => ModelReturn (after camelize) + } + + return underscore(name); + } + + @Override + public String toEnumName(CodegenProperty property) { + return sanitizeName(camelize(property.name)) + "Enum"; + } + + @Override + public String toEnumVarName(String value, String datatype) { + String var = null; + if (value.length() == 0) { + var = "EMPTY"; + } + + // for symbol, e.g. $, # + else if (getSymbolName(value) != null) { + var = getSymbolName(value).toUpperCase(); + } + + // number + else if ("Integer".equals(datatype) || "Long".equals(datatype) || + "Float".equals(datatype) || "Double".equals(datatype)) { + String varName = "NUMBER_" + value; + varName = varName.replaceAll("-", "MINUS_"); + varName = varName.replaceAll("\\+", "PLUS_"); + varName = varName.replaceAll("\\.", "_DOT_"); + var = varName; + } + + // string + var = value.replaceAll("\\W+", "_").toUpperCase(); + if (var.matches("\\d.*")) { + var = "_" + var; + } else { + var = sanitizeName(var); + } + return var; + } + + @Override + public String toEnumValue(String value, String datatype) { + if ("Integer".equals(datatype) || "Long".equals(datatype) || + "Float".equals(datatype) || "Double".equals(datatype)) { + return value; + } else { + return "\"" + escapeText(value) + "\""; + } + } + + @Override + public String toApiFilename(String name) { + // replace - with _ e.g. created-at => created_at + name = name.replaceAll("-", "_"); // FIXME: a parameter should not be assigned. Also declare the methods parameters as 'final'. + + // e.g. PetApi.go => pet_api.go + return underscore(name); + } + + @Override + public String escapeQuotationMark(String input) { + // remove " to avoid code injection + return input.replace("\"", ""); + } + + @Override + public String escapeUnsafeCharacters(String input) { + return input.replace("*/", "*_/").replace("/*", "/_*"); + } + + boolean isMimetypeXml(String mimetype) { + return mimetype.toLowerCase().startsWith("application/xml"); + } + + boolean isMimetypePlainText(String mimetype) { + return mimetype.toLowerCase().startsWith("text/plain"); + } + + boolean isMimetypeWwwFormUrlEncoded(String mimetype) { + return mimetype.toLowerCase().startsWith("application/x-www-form-urlencoded"); + } + + @Override + public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, Map definitions, OpenAPI openAPI) { + CodegenOperation op = super.fromOperation(path, httpMethod, operation, definitions, openAPI); + + // The Rust code will need to contain a series of regular expressions. + // For performance, we'll construct these at start-of-day and re-use + // them. That means we need labels for them. + // + // Construct a Rust constant (uppercase) token name, and ensure it's + // unique using a numeric tie-breaker if required. + String basePathId = sanitizeName(op.path.replace("/", "_").replace("{", "").replace("}", "").replaceAll("^_", "")).toUpperCase(); + String pathId = basePathId; + int pathIdTiebreaker = 2; + boolean found = false; + while (pathSetMap.containsKey(pathId)) { + Map pathSetEntry = pathSetMap.get(pathId); + if (pathSetEntry.get("path").equals(op.path)) { + found = true; + break; + } + pathId = basePathId + pathIdTiebreaker; + pathIdTiebreaker++; + } + + // Save off the regular expression and path details in the + // "pathSetMap", which we'll add to the source document that will be + // processed by the templates. + if (!found) { + Map pathSetEntry = new HashMap(); + pathSetEntry.put("path", op.path); + pathSetEntry.put("PATH_ID", pathId); + if (!op.pathParams.isEmpty()) { + pathSetEntry.put("hasPathParams", "true"); + } + // Don't prefix with '^' so that the templates can put the + // basePath on the front. + pathSetEntry.put("pathRegEx", op.path.replace("{", "(?P<").replace("}", ">[^/?#]*)") + "$"); + pathSetMap.put(pathId, pathSetEntry); + } + + op.vendorExtensions.put("operation_id", underscore(op.operationId)); + op.vendorExtensions.put("uppercase_operation_id", underscore(op.operationId).toUpperCase()); + op.vendorExtensions.put("path", op.path.replace("{", ":").replace("}", "")); + op.vendorExtensions.put("PATH_ID", pathId); + op.vendorExtensions.put("hasPathParams", !op.pathParams.isEmpty()); + op.vendorExtensions.put("HttpMethod", Character.toUpperCase(op.httpMethod.charAt(0)) + op.httpMethod.substring(1).toLowerCase()); + for (CodegenParameter param : op.allParams) { + processParam(param, op); + } + + List consumes = new ArrayList(); + + /* comment out the following logic as there's no consume in operation/global definition + if (consumes != null) { + if (!consumes.isEmpty()) { + // use consumes defined in the operation + consumes = operation.getConsumes(); + } + } else if (openAPI != null && openAPI.getConsumes() != null && swagger.getConsumes().size() > 0) { + // use consumes defined globally + consumes = swagger.getConsumes(); + LOGGER.debug("No consumes defined in operation. Using global consumes (" + swagger.getConsumes() + ") for " + op.operationId); + } + */ + + boolean consumesPlainText = false; + boolean consumesXml = false; + // if "consumes" is defined (per operation or using global definition) + if (consumes != null && !consumes.isEmpty()) { + consumes.addAll(getConsumesInfo(operation)); + List> c = new ArrayList>(); + for (String mimeType : consumes) { + Map mediaType = new HashMap(); + + if (isMimetypeXml(mimeType)) { + additionalProperties.put("usesXml", true); + consumesXml = true; + } else if (isMimetypePlainText(mimeType)) { + consumesPlainText = true; + } else if (isMimetypeWwwFormUrlEncoded(mimeType)) { + additionalProperties.put("usesUrlEncodedForm", true); + } + + mediaType.put("mediaType", mimeType); + c.add(mediaType); + } + op.consumes = c; + op.hasConsumes = true; + } + + + + List produces = new ArrayList(getProducesInfo(operation)); + // if "consumes" is defined (per operation or using global definition) + /* + if (operation.getProduces() != null) { + if (operation.getProduces().size() > 0) { + // use produces defined in the operation + produces = operation.getProduces(); + } + } else if (swagger != null && swagger.getProduces() != null && swagger.getProduces().size() > 0) { + // use produces defined globally + produces = swagger.getProduces(); + LOGGER.debug("No produces defined in operation. Using global produces (" + swagger.getProduces() + ") for " + op.operationId); + } + */ + + boolean producesXml = false; + boolean producesPlainText = false; + if (produces != null && !produces.isEmpty()) { + List> c = new ArrayList>(); + for (String mimeType : produces) { + Map mediaType = new HashMap(); + + if (isMimetypeXml(mimeType)) { + additionalProperties.put("usesXml", true); + producesXml = true; + } else if (isMimetypePlainText(mimeType)) { + producesPlainText = true; + } + + mediaType.put("mediaType", mimeType); + c.add(mediaType); + } + op.produces = c; + op.hasProduces = true; + } + + + /* TODO move the following logic to postProcessOperations as there's no body/form parameter in OAS 3.0 + if (op.bodyParam != null) { + if (paramHasXmlNamespace(op.bodyParam, definitions)) { + op.bodyParam.vendorExtensions.put("has_namespace", "true"); + } + for (String key : definitions.keySet()) { + op.bodyParam.vendorExtensions.put("model_key", key); + } + + // Default to consuming json + op.bodyParam.vendorExtensions.put("uppercase_operation_id", underscore(op.operationId).toUpperCase()); + if (consumesXml) { + op.bodyParam.vendorExtensions.put("consumesXml", true); + } else if (consumesPlainText) { + op.bodyParam.vendorExtensions.put("consumesPlainText", true); + } else { + op.bodyParam.vendorExtensions.put("consumesJson", true); + } + + } + for (CodegenParameter param : op.bodyParams) { + processParam(param, op); + + if (paramHasXmlNamespace(param, definitions)) { + param.vendorExtensions.put("has_namespace", "true"); + } + + param.vendorExtensions.put("uppercase_operation_id", underscore(op.operationId).toUpperCase()); + + // Default to producing json if nothing else is specified + if (consumesXml) { + param.vendorExtensions.put("consumesXml", true); + } else if (consumesPlainText) { + param.vendorExtensions.put("consumesPlainText", true); + } else { + param.vendorExtensions.put("consumesJson", true); + } + } + + for (CodegenParameter param : op.formParams) { + processParam(param, op); + } + */ + + for (CodegenParameter param : op.headerParams) { + // If a header uses UUIDs, we need to import the UUID package. + if (param.dataType.equals("uuid::Uuid")) { + additionalProperties.put("apiUsesUuid", true); + } + processParam(param, op); + + // Give header params a name in camel case. CodegenParameters don't have a nameInCamelCase property. + param.vendorExtensions.put("typeName", toModelName(param.baseName)); + } + + for (CodegenResponse rsp : op.responses) { + String[] words = rsp.message.split("[^A-Za-z ]"); + String responseId; + if (rsp.vendorExtensions.containsKey("x-responseId")) { + responseId = (String) rsp.vendorExtensions.get("x-responseId"); + } else if (words.length != 0) { + responseId = camelize(words[0].replace(" ", "_")); + } else { + responseId = "Status" + rsp.code; + } + rsp.vendorExtensions.put("x-responseId", responseId); + rsp.vendorExtensions.put("x-uppercaseResponseId", underscore(responseId).toUpperCase()); + rsp.vendorExtensions.put("uppercase_operation_id", underscore(op.operationId).toUpperCase()); + if (rsp.dataType != null) { + rsp.vendorExtensions.put("uppercase_data_type", (rsp.dataType.replace("models::", "")).toUpperCase()); + + // Default to producing json if nothing else is specified + if (producesXml) { + rsp.vendorExtensions.put("producesXml", true); + } else if (producesPlainText) { + rsp.vendorExtensions.put("producesPlainText", true); + } else { + rsp.vendorExtensions.put("producesJson", true); + } + + Schema response = (Schema) rsp.schema; + // Check whether we're returning an object with a defined XML namespace. + if (response != null && (!StringUtils.isEmpty(response.get$ref()))) { + Schema model = definitions.get(getSimpleRef(response.get$ref())); + if ((model != null)) { + XML xml = model.getXml(); + if ((xml != null) && (xml.getNamespace() != null)) { + rsp.vendorExtensions.put("has_namespace", "true"); + } + } + } + } + for (CodegenProperty header : rsp.headers) { + if (header.datatype.equals("uuid::Uuid")) { + additionalProperties.put("apiUsesUuid", true); + } + header.nameInCamelCase = toModelName(header.baseName); + } + } + for (CodegenProperty header : op.responseHeaders) { + if (header.datatype.equals("uuid::Uuid")) { + additionalProperties.put("apiUsesUuid", true); + } + header.nameInCamelCase = toModelName(header.baseName); + } + + return op; + } + + @Override + public boolean isDataTypeFile(final String dataType) { + return dataType != null && dataType.equals(typeMapping.get("File").toString()); + } + + @Override + public String getTypeDeclaration(Schema p) { + if (p instanceof ArraySchema) { + ArraySchema ap = (ArraySchema) p; + Schema inner = ap.getItems(); + String innerType = getTypeDeclaration(inner); + StringBuilder typeDeclaration = new StringBuilder(typeMapping.get("array")).append("<"); + if (!StringUtils.isEmpty(inner.get$ref())) { + typeDeclaration.append("models::"); + } + typeDeclaration.append(innerType).append(">"); + return typeDeclaration.toString(); + } else if (isMapSchema(p)) { + MapSchema mp = (MapSchema) p; + Schema inner = (Schema) mp.getAdditionalProperties(); + String innerType = getTypeDeclaration(inner); + StringBuilder typeDeclaration = new StringBuilder(typeMapping.get("map")).append("<").append(typeMapping.get("string")).append(", "); + if (!StringUtils.isEmpty(inner.get$ref())) { + typeDeclaration.append("models::"); + } + typeDeclaration.append(innerType).append(">"); + return typeDeclaration.toString(); + } else if (!StringUtils.isEmpty(p.get$ref())) { + String datatype; + try { + datatype = p.get$ref(); + if (datatype.indexOf("#/definitions/") == 0) { + datatype = toModelName(datatype.substring("#/definitions/".length())); + } + } catch (Exception e) { + LOGGER.warn("Error obtaining the datatype from schema (model):" + p + ". Datatype default to Object"); + datatype = "Object"; + LOGGER.error(e.getMessage(), e); + } + return datatype; + } else if (p instanceof FileSchema) { + return typeMapping.get("File").toString(); + } + return super.getTypeDeclaration(p); + } + + @Override + public CodegenParameter fromParameter(Parameter param, Set imports) { + CodegenParameter parameter = super.fromParameter(param, imports); + /* TODO need ot revise the logic below as there's no body parameter + if (param instanceof BodyParameter) { + BodyParameter bp = (BodyParameter) param; + Model model = bp.getSchema(); + if (model instanceof RefModel) { + String name = ((RefModel) model).getSimpleRef(); + name = toModelName(name); + // We need to be able to look up the model in the model definitions later. + parameter.vendorExtensions.put("uppercase_data_type", name.toUpperCase()); + + name = "models::" + getTypeDeclaration(name); + parameter.baseType = name; + parameter.dataType = name; + + String refName = ((RefModel) model).get$ref(); + if (refName.indexOf("#/definitions/") == 0) { + refName = refName.substring("#/definitions/".length()); + } + parameter.vendorExtensions.put("refName", refName); + + } else if (model instanceof ModelImpl) { + parameter.vendorExtensions.put("refName", ((ModelImpl) model).getName()); + } + } + */ + return parameter; + } + + @Override + public CodegenProperty fromProperty(String name, Schema p) { + CodegenProperty property = super.fromProperty(name, p); + + /* need to revise the logic below. Is this for alias? + if (p instanceof RefProperty) { + property.datatype = "models::" + property.datatype; + } + */ + return property; + } + + @Override + public String toInstantiationType(Schema p) { + if (p instanceof ArraySchema) { + ArraySchema ap = (ArraySchema) p; + Schema inner = ap.getItems(); + return instantiationTypes.get("array") + "<" + getSchemaType(inner) + ">"; + } else if (isMapSchema(p)) { + MapSchema mp = (MapSchema) p; + Schema inner = (Schema) mp.getAdditionalProperties(); + return instantiationTypes.get("map") + "<" + typeMapping.get("string") + ", " + getSchemaType(inner) + ">"; + } else { + return null; + } + } + + @Override + public CodegenModel fromModel(String name, Schema schema) { + return fromModel(name, schema, null); + } + + @Override + public CodegenModel fromModel(String name, Schema model, Map allDefinitions) { + CodegenModel mdl = super.fromModel(name, model, allDefinitions); + mdl.vendorExtensions.put("upperCaseName", name.toUpperCase()); + if (!StringUtils.isEmpty(model.get$ref())) { + Schema schema = allDefinitions.get(getSimpleRef(model.get$ref())); + mdl.dataType = typeMapping.get(schema.getType()); + } + if (model instanceof ArraySchema) { + ArraySchema am = (ArraySchema) model; + if ((am.getItems() != null) && + (am.getItems().getXml() != null)) { + + // If this model's items require wrapping in xml, squirrel + // away the xml name so we can insert it into the relevant model fields. + String xmlName = am.getItems().getXml().getName(); + if (xmlName != null) { + mdl.vendorExtensions.put("itemXmlName", xmlName); + modelXmlNames.put("models::" + mdl.classname, xmlName); + } + } + mdl.arrayModelType = toModelName(mdl.arrayModelType); + } + + if (mdl.xmlNamespace != null) { + additionalProperties.put("usesXmlNamespaces", true); + } + + return mdl; + } + + @Override + public Map postProcessAllModels(Map objs) { + Map newObjs = super.postProcessAllModels(objs); + + //Index all CodegenModels by model name. + HashMap allModels = new HashMap(); + for (Entry entry : objs.entrySet()) { + String modelName = toModelName(entry.getKey()); + Map inner = (Map) entry.getValue(); + List> models = (List>) inner.get("models"); + for (Map mo : models) { + CodegenModel cm = (CodegenModel) mo.get("model"); + allModels.put(modelName, cm); + } + } + + for (Entry entry : allModels.entrySet()) { + String modelName = entry.getKey(); + CodegenModel model = entry.getValue(); + + for (CodegenProperty prop : model.vars) { + String xmlName = modelXmlNames.get(prop.datatype); + if (xmlName != null) { + prop.vendorExtensions.put("itemXmlName", xmlName); + } + } + } + + return newObjs; + } + + @Override + public Map postProcessSupportingFileData(Map objs) { + generateYAMLSpecFile(objs); + + // We previously built a mapping from path to path ID and regular + // expression - see fromOperation for details. Sort it and add an + // index, and then add it to the objects that we're about to pass to + // the templates to process. + List>> pathSetEntryList = new ArrayList(pathSetMap.entrySet()); + Collections.sort(pathSetEntryList, new Comparator>>() { + public int compare(Map.Entry> a, Map.Entry> b) { + return a.getValue().get("path").compareTo(b.getValue().get("path")); + } + }); + List pathSet = new ArrayList>(); + int index = 0; + for (Map.Entry> pathSetEntry : pathSetEntryList) { + Map pathSetEntryValue = pathSetEntry.getValue(); + pathSetEntryValue.put("index", Integer.toString(index)); + index++; + pathSet.add(pathSetEntryValue); + } + objs.put("pathSet", pathSet); + + return super.postProcessSupportingFileData(objs); + } + + @Override + public String toDefaultValue(Schema p) { + if (p instanceof StringSchema) { + StringSchema dp = (StringSchema) p; + if (dp.getDefault() != null) { + return "\"" + dp.getDefault() + "\".to_string()"; + } + } else if (p instanceof BooleanSchema) { + BooleanSchema dp = (BooleanSchema) p; + if (dp.getDefault() != null) { + if (dp.getDefault().toString().equalsIgnoreCase("false")) + return "false"; + else + return "true"; + } + } else if (p instanceof NumberSchema) { + if (p.getDefault() != null) { + return p.getDefault().toString(); + } + } else if (p instanceof IntegerSchema) { + IntegerSchema dp = (IntegerSchema) p; + if (dp.getDefault() != null) { + return dp.getDefault().toString(); + } + } + + return null; + } + + @Override + public void postProcessModelProperty(CodegenModel model, CodegenProperty property) { + super.postProcessModelProperty(model, property); + if (!languageSpecificPrimitives.contains(property.datatype)) { + // If we use a more qualified model name, then only camelize the actual type, not the qualifier. + if (property.datatype.contains(":")) { + int position = property.datatype.lastIndexOf(":"); + property.datatype = property.datatype.substring(0, position) + camelize(property.datatype.substring(position)); + } else { + property.datatype = camelize(property.datatype, false); + } + } + + if ("integer".equals(property.baseType)) { + // custom integer formats (legacy) + if ("uint32".equals(property.dataFormat)) { + property.datatype = "u32"; + } else if ("uint64".equals(property.dataFormat)) { + property.datatype = "u64"; + + } else { + // match int type to schema constraints + Long inclusiveMinimum = property.minimum != null ? Long.parseLong(property.minimum) : null; + if (inclusiveMinimum != null && property.exclusiveMinimum) { + inclusiveMinimum++; + } + + // a signed int is required unless a minimum greater than zero is set + boolean unsigned = inclusiveMinimum != null && inclusiveMinimum >= 0; + + Long inclusiveMaximum = property.maximum != null ? Long.parseLong(property.maximum) : null; + if (inclusiveMaximum != null && property.exclusiveMaximum) { + inclusiveMaximum--; + } + + switch (property.dataFormat == null ? NO_FORMAT : property.dataFormat) { + // standard swagger formats + case "int32": + property.datatype = unsigned ? "u32" : "i32"; + break; + + case "int64": + property.datatype = unsigned ? "u64" : "i64"; + break; + + case NO_FORMAT: + property.datatype = matchingIntType(unsigned, inclusiveMinimum, inclusiveMaximum); + break; + + default: + // unknown format + LOGGER.warn("The integer format '{}' is not recognized and will be ignored.", property.dataFormat); + property.datatype = matchingIntType(unsigned, inclusiveMinimum, inclusiveMaximum); + } + } + } + + property.name = underscore(property.name); + + if (!property.required) { + property.defaultValue = (property.defaultValue != null) ? "Some(" + property.defaultValue + ")" : "None"; + } + } + + static long requiredBits(Long bound, boolean unsigned) { + if (bound == null) return 0; + + if (unsigned) { + if (bound < 0) { + throw new RuntimeException("Unsigned bound is negative: " + bound); + } + return 65 - Long.numberOfLeadingZeros(bound >> 1); + } + + return 65 - Long.numberOfLeadingZeros( + // signed bounds go from (-n) to (n - 1), i.e. i8 goes from -128 to 127 + bound < 0 ? Math.abs(bound) - 1 : bound); + } + + static String matchingIntType(boolean unsigned, Long inclusiveMin, Long inclusiveMax) { + long requiredMinBits = requiredBits(inclusiveMin, unsigned); + long requiredMaxBits = requiredBits(inclusiveMax, unsigned); + long requiredBits = Math.max(requiredMinBits, requiredMaxBits); + + if (requiredMaxBits == 0 && requiredMinBits <= 16) { + // rust 'size' types are arch-specific and thus somewhat loose + // so they are used when no format or maximum are specified + // and as long as minimum stays within plausible smallest ptr size (16 bits) + // this way all rust types are obtainable without defining custom formats + // this behavior (default int size) could also follow a generator flag + return unsigned ? "usize" : "isize"; + + } else if (requiredBits <= 8) { + return unsigned ? "u8" : "i8"; + + } else if (requiredBits <= 16) { + return unsigned ? "u16" : "i16"; + + } else if (requiredBits <= 32) { + return unsigned ? "u32" : "i32"; + } + return unsigned ? "u64" : "i64"; + } + + @Override + public Map postProcessModels(Map objs) { + return super.postProcessModelsEnum(objs); + + } + + private boolean paramHasXmlNamespace(CodegenParameter param, Map definitions) { + Object refName = param.vendorExtensions.get("refName"); + + if ((refName != null) && (refName instanceof String)) { + String name = (String) refName; + Schema model = definitions.get(getSimpleRef(name)); + + if (model != null) { + XML xml = model.getXml(); + if ((xml != null) && (xml.getNamespace() != null)) { + return true; + } + } + } + return false; + } + + private void processParam(CodegenParameter param, CodegenOperation op) { + String example = null; + + if (param.isString) { + if (param.dataFormat != null && param.dataFormat.equals("byte")) { + param.vendorExtensions.put("formatString", "\\\"{:?}\\\""); + example = "swagger::ByteArray(\"" + ((param.example != null) ? param.example : "") + "\".to_string().into_bytes())"; + } else { + param.vendorExtensions.put("formatString", "\\\"{}\\\""); + example = "\"" + ((param.example != null) ? param.example : "") + "\".to_string()"; + } + } else if (param.isPrimitiveType) { + if ((param.isByteArray) || + (param.isBinary)) { + // Binary primitive types don't implement `Display`. + param.vendorExtensions.put("formatString", "{:?}"); + example = "swagger::ByteArray(Vec::from(\"" + ((param.example != null) ? param.example : "") + "\"))"; + } else { + param.vendorExtensions.put("formatString", "{}"); + example = (param.example != null) ? param.example : ""; + } + } else if (param.isListContainer) { + param.vendorExtensions.put("formatString", "{:?}"); + example = (param.example != null) ? param.example : "&Vec::new()"; + } else if (param.isFile) { + param.vendorExtensions.put("formatString", "{:?}"); + op.vendorExtensions.put("hasFile", true); + additionalProperties.put("apiHasFile", true); + example = "Box::new(stream::once(Ok(b\"hello\".to_vec()))) as Box + Send>"; + } else { + param.vendorExtensions.put("formatString", "{:?}"); + if (param.example != null) { + example = "serde_json::from_str::<" + param.dataType + ">(\"" + param.example + "\").expect(\"Failed to parse JSON example\")"; + } + } + + if (param.required) { + if (example != null) { + param.vendorExtensions.put("example", example); + } else if (param.isListContainer) { + // Use the empty list if we don't have an example + param.vendorExtensions.put("example", "&Vec::new()"); + } else { + // If we don't have an example that we can provide, we need to disable the client example, as it won't build. + param.vendorExtensions.put("example", "???"); + op.vendorExtensions.put("noClientExample", Boolean.TRUE); + } + } else if ((param.dataFormat != null) && ((param.dataFormat.equals("date-time")) || (param.dataFormat.equals("date")))) { + param.vendorExtensions.put("formatString", "{:?}"); + param.vendorExtensions.put("example", "None"); + } else { + // Not required, so override the format string and example + param.vendorExtensions.put("formatString", "{:?}"); + if (param.isFile) { + // Optional file types are wrapped in a future + param.vendorExtensions.put("example", (example != null) ? "Box::new(future::ok(Some(" + example + "))) as Box + Send>" : "None"); + } else { + param.vendorExtensions.put("example", (example != null) ? "Some(" + example + ")" : "None"); + } + } + } +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/URLPathUtil.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/URLPathUtil.java index 5626c646d5d..9508136aead 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/URLPathUtil.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/URLPathUtil.java @@ -55,4 +55,5 @@ public class URLPathUtil { } return LOCAL_HOST; } + } \ No newline at end of file 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 465c429e959..499f0262553 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 @@ -49,6 +49,8 @@ org.openapitools.codegen.languages.RClientCodegen org.openapitools.codegen.languages.RubyClientCodegen org.openapitools.codegen.languages.RubyOnRailsServerCodegen org.openapitools.codegen.languages.RubySinatraServerCodegen +org.openapitools.codegen.languages.RustClientCodegen +org.openapitools.codegen.languages.RustServerCodegen org.openapitools.codegen.languages.ScalatraServerCodegen org.openapitools.codegen.languages.ScalaClientCodegen org.openapitools.codegen.languages.ScalaGatlingCodegen diff --git a/modules/openapi-generator/src/main/resources/rust-server/Cargo.mustache b/modules/openapi-generator/src/main/resources/rust-server/Cargo.mustache index 9e5b566b787..6735b7fdeff 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/Cargo.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/Cargo.mustache @@ -9,32 +9,37 @@ license = "Unlicense" [features] default = ["client", "server"] -client = ["serde_json", {{#usesXml}}"serde-xml-rs", {{/usesXml}}"serde_ignored", "hyper", "hyper-openssl", "uuid"{{#apiHasFile}}, "multipart"{{/apiHasFile}}] -server = ["serde_json", {{#usesXml}}"serde-xml-rs", {{/usesXml}}"serde_ignored", "hyper", "iron", "router", "bodyparser", "urlencoded", "uuid"{{#apiHasFile}}, "multipart"{{/apiHasFile}}] +client = ["serde_json", {{#usesUrlEncodedForm}}"serde_urlencoded", {{/usesUrlEncodedForm}} {{#usesXml}}"serde-xml-rs", {{/usesXml}}"serde_ignored", "hyper", "hyper-tls", "native-tls", "openssl", "tokio-core", "url", "uuid"{{#apiHasFile}}, "multipart"{{/apiHasFile}}] +server = ["serde_json", {{#usesXml}}"serde-xml-rs", {{/usesXml}}"serde_ignored", "hyper", "hyper-tls", "native-tls", "openssl", "tokio-core", "tokio-proto", "tokio-tls", "regex", "percent-encoding", "url", "uuid"{{#apiHasFile}}, "multipart"{{/apiHasFile}}] [dependencies] # Required by example server. # chrono = { version = "0.4", features = ["serde"] } futures = "0.1" -hyper = {version = "0.10", optional = true} -hyper-openssl = {version = "0.2", optional = true } -iron = {version = "0.5", optional = true} -swagger = "0.7" +hyper = {version = "0.11", optional = true} +hyper-tls = {version = "0.1.2", optional = true} +swagger = "0.9" # Not required by example server. # -bodyparser = {version = "0.7", optional = true} -url = "1.5" lazy_static = "0.2" log = "0.3.0" -multipart = {version = "0.13", optional = true} -router = {version = "0.5", optional = true} +mime = "0.3.3" +multipart = {version = "0.13.3", optional = true} +native-tls = {version = "0.1.4", optional = true} +openssl = {version = "0.9.14", optional = true} +percent-encoding = {version = "1.0.0", optional = true} +regex = {version = "0.2", optional = true} serde = "1.0" serde_derive = "1.0" serde_ignored = {version = "0.0.4", optional = true} serde_json = {version = "1.0", optional = true} -urlencoded = {version = "0.5", optional = true} +serde_urlencoded = {version = "0.5.1", optional = true} +tokio-core = {version = "0.1.6", optional = true} +tokio-proto = {version = "0.1.1", optional = true} +tokio-tls = {version = "0.1.3", optional = true, features = ["tokio-proto"]} +url = {version = "1.5", optional = true} uuid = {version = "0.5", optional = true, features = ["serde", "v4"]} # ToDo: this should be updated to point at the official crate once # https://github.com/RReverser/serde-xml-rs/pull/45 is accepted upstream diff --git a/modules/openapi-generator/src/main/resources/rust-server/client-mod.mustache b/modules/openapi-generator/src/main/resources/rust-server/client-mod.mustache new file mode 100644 index 00000000000..9ed53c0b807 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/rust-server/client-mod.mustache @@ -0,0 +1,437 @@ +#![allow(unused_extern_crates)] +extern crate tokio_core; +extern crate native_tls; +extern crate hyper_tls; +extern crate openssl; +extern crate mime; +extern crate chrono; +extern crate url; +{{#apiHasFile}}extern crate multipart;{{/apiHasFile}} +{{#usesUrlEncodedForm}}extern crate serde_urlencoded;{{/usesUrlEncodedForm}} + +{{#apiUsesUuid}}use uuid;{{/apiUsesUuid}} +{{#apiHasFile}}use self::multipart::client::lazy::Multipart;{{/apiHasFile}} +use hyper; +use hyper::header::{Headers, ContentType}; +use hyper::Uri; +use self::url::percent_encoding::{utf8_percent_encode, PATH_SEGMENT_ENCODE_SET, QUERY_ENCODE_SET}; +use futures; +use futures::{Future, Stream}; +use futures::{future, stream}; +use self::tokio_core::reactor::Handle; +use std::borrow::Cow; +use std::io::{Read, Error, ErrorKind}; +use std::error; +use std::fmt; +use std::path::Path; +use std::sync::Arc; +use std::str; +use std::str::FromStr; + +use mimetypes; + +use serde_json; +{{#usesXml}}use serde_xml_rs;{{/usesXml}} + +#[allow(unused_imports)] +use std::collections::{HashMap, BTreeMap}; +#[allow(unused_imports)] +use swagger; + +use swagger::{Context, ApiError, XSpanId}; + +use {Api{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}, + {{operationId}}Response{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} + }; +use models; + +/// Convert input into a base path, e.g. "http://example:123". Also checks the scheme as it goes. +fn into_base_path(input: &str, correct_scheme: Option<&'static str>) -> Result { + // First convert to Uri, since a base path is a subset of Uri. + let uri = Uri::from_str(input)?; + + let scheme = uri.scheme().ok_or(ClientInitError::InvalidScheme)?; + + // Check the scheme if necessary + if let Some(correct_scheme) = correct_scheme { + if scheme != correct_scheme { + return Err(ClientInitError::InvalidScheme); + } + } + + let host = uri.host().ok_or_else(|| ClientInitError::MissingHost)?; + let port = uri.port().map(|x| format!(":{}", x)).unwrap_or_default(); + Ok(format!("{}://{}{}", scheme, host, port)) +} + +/// A client that implements the API by making HTTP calls out to a server. +#[derive(Clone)] +pub struct Client { + hyper_client: Arc Box, Response=hyper::Response, Error=hyper::Error, Future=hyper::client::FutureResponse>> + Sync + Send>, + handle: Arc, + base_path: String, +} + +impl fmt::Debug for Client { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "Client {{ base_path: {} }}", self.base_path) + } +} + +impl Client { + + /// Create an HTTP client. + /// + /// # Arguments + /// * `handle` - tokio reactor handle to use for execution + /// * `base_path` - base path of the client API, i.e. "www.my-api-implementation.com" + pub fn try_new_http(handle: Handle, base_path: &str) -> Result { + let http_connector = swagger::http_connector(); + Self::try_new_with_connector::( + handle, + base_path, + Some("http"), + http_connector, + ) + } + + /// Create a client with a TLS connection to the server. + /// + /// # Arguments + /// * `handle` - tokio reactor handle to use for execution + /// * `base_path` - base path of the client API, i.e. "www.my-api-implementation.com" + /// * `ca_certificate` - Path to CA certificate used to authenticate the server + pub fn try_new_https( + handle: Handle, + base_path: &str, + ca_certificate: CA, + ) -> Result + where + CA: AsRef, + { + let https_connector = swagger::https_connector(ca_certificate); + Self::try_new_with_connector::>( + handle, + base_path, + Some("https"), + https_connector, + ) + } + + /// Create a client with a mutually authenticated TLS connection to the server. + /// + /// # Arguments + /// * `handle` - tokio reactor handle to use for execution + /// * `base_path` - base path of the client API, i.e. "www.my-api-implementation.com" + /// * `ca_certificate` - Path to CA certificate used to authenticate the server + /// * `client_key` - Path to the client private key + /// * `client_certificate` - Path to the client's public certificate associated with the private key + pub fn try_new_https_mutual( + handle: Handle, + base_path: &str, + ca_certificate: CA, + client_key: K, + client_certificate: C, + ) -> Result + where + CA: AsRef, + K: AsRef, + C: AsRef, + { + let https_connector = + swagger::https_mutual_connector(ca_certificate, client_key, client_certificate); + Self::try_new_with_connector::>( + handle, + base_path, + Some("https"), + https_connector, + ) + } + + /// Create a client with a custom implementation of hyper::client::Connect. + /// + /// Intended for use with custom implementations of connect for e.g. protocol logging + /// or similar functionality which requires wrapping the transport layer. When wrapping a TCP connection, + /// this function should be used in conjunction with + /// `swagger::{http_connector, https_connector, https_mutual_connector}`. + /// + /// For ordinary tcp connections, prefer the use of `try_new_http`, `try_new_https` + /// and `try_new_https_mutual`, to avoid introducing a dependency on the underlying transport layer. + /// + /// # Arguments + /// + /// * `handle` - tokio reactor handle to use for execution + /// * `base_path` - base path of the client API, i.e. "www.my-api-implementation.com" + /// * `protocol` - Which protocol to use when constructing the request url, e.g. `Some("http")` + /// * `connector_fn` - Function which returns an implementation of `hyper::client::Connect` + pub fn try_new_with_connector( + handle: Handle, + base_path: &str, + protocol: Option<&'static str>, + connector_fn: Box C + Send + Sync>, + ) -> Result + where + C: hyper::client::Connect + hyper::client::Service, + { + let hyper_client = { + move |handle: &Handle| -> Box< + hyper::client::Service< + Request = hyper::Request, + Response = hyper::Response, + Error = hyper::Error, + Future = hyper::client::FutureResponse, + >, + > { + let connector = connector_fn(handle); + Box::new(hyper::Client::configure().connector(connector).build( + handle, + )) + } + }; + + Ok(Client { + hyper_client: Arc::new(hyper_client), + handle: Arc::new(handle), + base_path: into_base_path(base_path, protocol)?, + }) + } + + /// Constructor for creating a `Client` by passing in a pre-made `hyper` client. + /// + /// One should avoid relying on this function if possible, since it adds a dependency on the underlying transport + /// implementation, which it would be better to abstract away. Therefore, using this function may lead to a loss of + /// code generality, which may make it harder to move the application to a serverless environment, for example. + /// + /// The reason for this function's existence is to support legacy test code, which did mocking at the hyper layer. + /// This is not a recommended way to write new tests. If other reasons are found for using this function, they + /// should be mentioned here. + pub fn try_new_with_hyper_client(hyper_client: Arc Box, Response=hyper::Response, Error=hyper::Error, Future=hyper::client::FutureResponse>> + Sync + Send>, + handle: Handle, + base_path: &str) + -> Result + { + Ok(Client { + hyper_client: hyper_client, + handle: Arc::new(handle), + base_path: into_base_path(base_path, None)?, + }) + } +} + +impl Api for Client { +{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} + fn {{#vendorExtensions}}{{operation_id}}{{/vendorExtensions}}(&self{{#allParams}}, param_{{paramName}}: {{^required}}{{#isFile}}Box{{#isFile}}, Error=Error> + Send>{{/isFile}}{{/required}}{{/allParams}}, context: &Context) -> Box> { +{{#queryParams}}{{#-first}} + // Query parameters +{{/-first}}{{#required}} let query_{{paramName}} = format!("{{baseName}}={{=<% %>=}}{<% paramName %>}<%={{ }}=%>&", {{paramName}}=param_{{paramName}}{{#isListContainer}}.join(","){{/isListContainer}}{{^isListContainer}}.to_string(){{/isListContainer}}); +{{/required}}{{^required}} let query_{{paramName}} = param_{{paramName}}.map_or_else(String::new, |query| format!("{{baseName}}={{=<% %>=}}{<% paramName %>}<%={{ }}=%>&", {{paramName}}=query{{#isListContainer}}.join(","){{/isListContainer}}{{^isListContainer}}.to_string(){{/isListContainer}})); +{{/required}}{{/queryParams}} + + let uri = format!( + "{}{{basePathWithoutHost}}{{path}}{{#queryParams}}{{#-first}}?{{/-first}}{{=<% %>=}}{<% paramName %>}<%={{ }}=%>{{/queryParams}}", + self.base_path{{#pathParams}}, {{baseName}}=utf8_percent_encode(¶m_{{paramName}}.to_string(), PATH_SEGMENT_ENCODE_SET){{/pathParams}}{{#queryParams}}, + {{paramName}}=utf8_percent_encode(&query_{{paramName}}, QUERY_ENCODE_SET){{/queryParams}} + ); + + let uri = match Uri::from_str(&uri) { + Ok(uri) => uri, + Err(err) => return Box::new(futures::done(Err(ApiError(format!("Unable to build URI: {}", err))))), + }; + + let mut request = hyper::Request::new(hyper::Method::{{#vendorExtensions}}{{HttpMethod}}{{/vendorExtensions}}, uri); + +{{#vendorExtensions}}{{#hasFile}} // Form data body + let mut multipart = Multipart::new(); + + // Helper function to convert a Stream into a String. The String can then be used to build the HTTP body. + fn convert_stream_to_string(stream: Box, Error=Error> + Send>) -> Result { + + stream.concat2() + .wait() + .map_err(|e| ApiError(format!("Unable to collect stream: {}", e))) + .and_then(|body| String::from_utf8(body) + .map_err(|e| ApiError(format!("Failed to convert utf8 stream to String: {}", e)))) + }{{/hasFile}}{{/vendorExtensions}}{{#formParams}}{{#isFile}} + +{{^required}} if let Ok(Some(param_{{paramName}})) = param_{{paramName}}.wait() { {{/required}} +{{^required}} {{/required}} match convert_stream_to_string(param_{{paramName}}) { +{{^required}} {{/required}} Ok(param_{{paramName}}) => { + // Add file to multipart form. + multipart.add_text("{{paramName}}", param_{{paramName}}); + }, +{{^required}} {{/required}} Err(err) => return Box::new(futures::done(Err(err))), +{{^required}} {{/required}} } + {{^required}}}{{/required}}{{/isFile}}{{/formParams}}{{#vendorExtensions}}{{#hasFile}} + + let mut fields = match multipart.prepare() { + Ok(fields) => fields, + Err(err) => return Box::new(futures::done(Err(ApiError(format!("Unable to build request: {}", err))))), + }; + + let mut body_string = String::new(); + let body = fields.to_body().read_to_string(&mut body_string); + let boundary = fields.boundary(); + let multipart_header = match mime::Mime::from_str(&format!("multipart/form-data;boundary={}", boundary)) { + Ok(multipart_header) => multipart_header, + Err(err) => return Box::new(futures::done(Err(ApiError(format!("Unable to build multipart header: {:?}", err))))), + };{{/hasFile}}{{^hasFile}}{{#formParams}}{{#-first}} let params = &[{{/-first}} + ("{{baseName}}", {{#vendorExtensions}}{{#required}}Some({{#isString}}param_{{paramName}}{{/isString}}{{^isString}}format!("{:?}", param_{{paramName}}){{/isString}}){{/required}}{{^required}}{{#isString}}param_{{paramName}}{{/isString}}{{^isString}}param_{{paramName}}.map(|param| format!("{:?}", param)){{/isString}}{{/required}}),{{/vendorExtensions}}{{#-last}} + ]; + let body = serde_urlencoded::to_string(params).expect("impossible to fail to serialize"); + + request.headers_mut().set(ContentType(mimetypes::requests::{{#vendorExtensions}}{{uppercase_operation_id}}{{/vendorExtensions}}.clone())); + request.set_body(body.into_bytes());{{/-last}}{{/formParams}}{{/hasFile}}{{/vendorExtensions}}{{#bodyParam}}{{#-first}} + // Body parameter +{{/-first}}{{#vendorExtensions}}{{#required}}{{#consumesPlainText}} let body = param_{{paramName}};{{/consumesPlainText}}{{#consumesXml}} +{{^has_namespace}} let body = serde_xml_rs::to_string(¶m_{{paramName}}).expect("impossible to fail to serialize");{{/has_namespace}}{{#has_namespace}} + let mut namespaces = BTreeMap::new(); + // An empty string is used to indicate a global namespace in xmltree. + namespaces.insert("".to_string(), models::namespaces::{{uppercase_data_type}}.clone()); + let body = serde_xml_rs::to_string_with_namespaces(¶m_{{paramName}}, namespaces).expect("impossible to fail to serialize");{{/has_namespace}}{{/consumesXml}}{{#consumesJson}} + let body = serde_json::to_string(¶m_{{paramName}}).expect("impossible to fail to serialize");{{/consumesJson}} +{{/required}}{{^required}}{{#consumesPlainText}} let body = param_{{paramName}}; +{{/consumesPlainText}}{{^consumesPlainText}} let body = param_{{paramName}}.map(|ref body| { +{{#consumesXml}} +{{^has_namespace}} serde_xml_rs::to_string(body).expect("impossible to fail to serialize"){{/has_namespace}}{{#has_namespace}} + let mut namespaces = BTreeMap::new(); + // An empty string is used to indicate a global namespace in xmltree. + namespaces.insert("".to_string(), models::namespaces::{{uppercase_data_type}}.clone()); + serde_xml_rs::to_string_with_namespaces(body, namespaces).expect("impossible to fail to serialize"){{/has_namespace}}{{/consumesXml}}{{#consumesJson}} + serde_json::to_string(body).expect("impossible to fail to serialize"){{/consumesJson}} + });{{/consumesPlainText}}{{/required}}{{/vendorExtensions}}{{/bodyParam}} + +{{#bodyParam}}{{^required}}if let Some(body) = body { + {{/required}} request.set_body(body.into_bytes()); +{{^required}} }{{/required}} + + request.headers_mut().set(ContentType(mimetypes::requests::{{#vendorExtensions}}{{uppercase_operation_id}}{{/vendorExtensions}}.clone())); +{{/bodyParam}} + context.x_span_id.as_ref().map(|header| request.headers_mut().set(XSpanId(header.clone()))); +{{#authMethods}}{{#isBasic}} context.auth_data.as_ref().map(|auth_data| { + if let &swagger::AuthData::Basic(ref basic_header) = auth_data { + request.headers_mut().set(hyper::header::Authorization( + basic_header.clone(), + )) + } + });{{/isBasic}}{{/authMethods}}{{#headerParams}}{{#-first}} + // Header parameters +{{/-first}}{{^isMapContainer}} header! { (Request{{vendorExtensions.typeName}}, "{{baseName}}") => {{#isListContainer}}({{{baseType}}})*{{/isListContainer}}{{^isListContainer}}[{{{dataType}}}]{{/isListContainer}} } +{{#required}} request.headers_mut().set(Request{{vendorExtensions.typeName}}(param_{{paramName}}{{#isListContainer}}.clone(){{/isListContainer}})); +{{/required}}{{^required}} param_{{paramName}}.map(|header| request.headers_mut().set(Request{{vendorExtensions.typeName}}(header{{#isListContainer}}.clone(){{/isListContainer}}))); +{{/required}}{{/isMapContainer}}{{#isMapContainer}} let param_{{paramName}}: Option<{{{dataType}}}> = None; +{{/isMapContainer}}{{/headerParams}} + +{{#vendorExtensions}}{{#hasFile}} + request.headers_mut().set(ContentType(multipart_header)); + request.set_body(body_string.into_bytes()); +{{/hasFile}}{{/vendorExtensions}} + + let hyper_client = (self.hyper_client)(&*self.handle); + Box::new(hyper_client.call(request) + .map_err(|e| ApiError(format!("No response received: {}", e))) + .and_then(|mut response| { + match response.status().as_u16() { +{{#responses}} + {{code}} => { +{{#headers}} header! { (Response{{nameInCamelCase}}, "{{baseName}}") => [{{{datatype}}}] } + let response_{{name}} = match response.headers().get::() { + Some(response_{{name}}) => response_{{name}}.0.clone(), + None => return Box::new(future::err(ApiError(String::from("Required response header {{baseName}} for response {{code}} was not found.")))) as Box>, + }; +{{/headers}} + {{^isFile}}let body = response.body();{{/isFile}}{{#isFile}}let body = Box::new(response.body() + .map(|chunk| chunk.to_vec()) + .map_err(|_| + Error::new(ErrorKind::Other, "Received error reading response.") + ));{{/isFile}} + Box::new( +{{#dataType}}{{^isFile}} + body + .concat2() + .map_err(|e| ApiError(format!("Failed to read response: {}", e))) + .and_then(|body| str::from_utf8(&body) + .map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e))) + .and_then(|body| +{{#vendorExtensions}}{{#producesXml}} + // ToDo: this will move to swagger-rs and become a standard From conversion trait + // once https://github.com/RReverser/serde-xml-rs/pull/45 is accepted upstream + serde_xml_rs::from_str::<{{{dataType}}}>(body) + .map_err(|e| ApiError(format!("Response body did not match the schema: {}", e))) +{{/producesXml}}{{#producesJson}} + serde_json::from_str::<{{{dataType}}}>(body) + .map_err(|e| e.into()) +{{/producesJson}}{{#producesPlainText}} + Ok(body.to_string()) +{{/producesPlainText}}{{/vendorExtensions}} + )) + .map(move |body| +{{/isFile}}{{#isFile}} + future::ok( +{{/isFile}} + {{operationId}}Response::{{#vendorExtensions}}{{x-responseId}}{{/vendorExtensions}}{{^headers}}(body){{/headers}}{{#headers}}{{#-first}}{ body: body, {{/-first}}{{name}}: response_{{name}}{{^-last}}, {{/-last}}{{#-last}} }{{/-last}}{{/headers}} + ) +{{/dataType}}{{^dataType}} + future::ok( + {{operationId}}Response::{{#vendorExtensions}}{{x-responseId}}{{/vendorExtensions}}{{#headers}}{{#-first}}{ {{/-first}}{{^-first}}, {{/-first}}{{name}}: response_{{name}}{{#-last}} }{{/-last}}{{/headers}} + ) +{{/dataType}} + ) as Box> + }, +{{/responses}} + code => { + let headers = response.headers().clone(); + Box::new(response.body() + .take(100) + .concat2() + .then(move |body| + future::err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", + code, + headers, + match body { + Ok(ref body) => match str::from_utf8(body) { + Ok(body) => Cow::from(body), + Err(e) => Cow::from(format!("", e)), + }, + Err(e) => Cow::from(format!("", e)), + }))) + ) + ) as Box> + } + } + })) + + } +{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} +} + +#[derive(Debug)] +pub enum ClientInitError { + InvalidScheme, + InvalidUri(hyper::error::UriError), + MissingHost, + SslError(openssl::error::ErrorStack) +} + +impl From for ClientInitError { + fn from(err: hyper::error::UriError) -> ClientInitError { + ClientInitError::InvalidUri(err) + } +} + +impl From for ClientInitError { + fn from(err: openssl::error::ErrorStack) -> ClientInitError { + ClientInitError::SslError(err) + } +} + +impl fmt::Display for ClientInitError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + (self as &fmt::Debug).fmt(f) + } +} + +impl error::Error for ClientInitError { + fn description(&self) -> &str { + "Failed to produce a hyper client." + } +} diff --git a/modules/openapi-generator/src/main/resources/rust-server/client.mustache b/modules/openapi-generator/src/main/resources/rust-server/client.mustache deleted file mode 100644 index 2d9e7b18d5e..00000000000 --- a/modules/openapi-generator/src/main/resources/rust-server/client.mustache +++ /dev/null @@ -1,342 +0,0 @@ -#![allow(unused_extern_crates)] -extern crate hyper_openssl; -extern crate chrono; -extern crate url; -{{#apiHasFile}}extern crate multipart;{{/apiHasFile}} - -{{#apiHasFile}}use multipart::client::lazy::Multipart;{{/apiHasFile}} -use hyper; -use hyper::client::IntoUrl; -use hyper::mime; -use hyper::header::{Headers, ContentType}; -use hyper::mime::{Mime, TopLevel, SubLevel, Attr, Value}; -use hyper::Url; -use self::hyper_openssl::openssl; -use self::url::percent_encoding::{utf8_percent_encode, PATH_SEGMENT_ENCODE_SET, QUERY_ENCODE_SET}; -use futures; -use futures::{Future, Stream}; -use futures::{future, stream}; -use std::borrow::Cow; -use std::io::{Read, Error}; -use std::error; -use std::fmt; -use std::path::Path; -use std::sync::Arc; -use std::str; - -use mimetypes; - -use serde_json; -{{#usesXml}}use serde_xml_rs;{{/usesXml}} - -#[allow(unused_imports)] -use std::collections::{HashMap, BTreeMap}; -#[allow(unused_imports)] -use swagger; - -use swagger::{Context, ApiError, XSpanId}; - -use {Api{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}, - {{operationId}}Response{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} - }; -use models; - -/// Convert input into a base path, e.g. "http://example:123". Also checks the scheme as it goes. -fn into_base_path(input: T, correct_scheme: Option<&'static str>) -> Result { - // First convert to Url, since a base path is a subset of Url. - let url = input.into_url()?; - - let scheme = url.scheme(); - - // Check the scheme if necessary - if let Some(correct_scheme) = correct_scheme { - if scheme != correct_scheme { - return Err(ClientInitError::InvalidScheme); - } - } - - let host = url.host().ok_or_else(|| ClientInitError::MissingHost)?; - let port = url.port().map(|x| format!(":{}", x)).unwrap_or_default(); - Ok(format!("{}://{}{}", scheme, host, port)) -} - -/// A client that implements the API by making HTTP calls out to a server. -#[derive(Clone)] -pub struct Client { - base_path: String, - hyper_client: Arc hyper::client::Client + Sync + Send>, -} - -impl fmt::Debug for Client { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "Client {{ base_path: {} }}", self.base_path) - } -} - -impl Client { - pub fn try_new_http(base_path: T) -> Result - where T: IntoUrl - { - Ok(Client { - base_path: into_base_path(base_path, Some("http"))?, - hyper_client: Arc::new(hyper::client::Client::new), - }) - } - - pub fn try_new_https(base_path: T, - ca_certificate: CA) - -> Result - where T: IntoUrl, - CA: AsRef - { - let ca_certificate = ca_certificate.as_ref().to_owned(); - - let https_hyper_client = move || { - // SSL implementation - let mut ssl = openssl::ssl::SslConnectorBuilder::new(openssl::ssl::SslMethod::tls()).unwrap(); - - // Server authentication - ssl.set_ca_file(ca_certificate.clone()).unwrap(); - - let ssl = hyper_openssl::OpensslClient::from(ssl.build()); - let connector = hyper::net::HttpsConnector::new(ssl); - hyper::client::Client::with_connector(connector) - }; - - Ok(Client { - base_path: into_base_path(base_path, Some("https"))?, - hyper_client: Arc::new(https_hyper_client), - }) - } - - pub fn try_new_https_mutual(base_path: T, - ca_certificate: CA, - client_key: K, - client_certificate: C) - -> Result - where T: IntoUrl, - CA: AsRef, - K: AsRef, - C: AsRef - { - let ca_certificate = ca_certificate.as_ref().to_owned(); - let client_key = client_key.as_ref().to_owned(); - let client_certificate = client_certificate.as_ref().to_owned(); - - let https_mutual_hyper_client = move || { - // SSL implementation - let mut ssl = openssl::ssl::SslConnectorBuilder::new(openssl::ssl::SslMethod::tls()).unwrap(); - - // Server authentication - ssl.set_ca_file(ca_certificate.clone()).unwrap(); - - // Client authentication - ssl.set_private_key_file(client_key.clone(), openssl::x509::X509_FILETYPE_PEM).unwrap(); - ssl.set_certificate_chain_file(client_certificate.clone()).unwrap(); - ssl.check_private_key().unwrap(); - - let ssl = hyper_openssl::OpensslClient::from(ssl.build()); - let connector = hyper::net::HttpsConnector::new(ssl); - hyper::client::Client::with_connector(connector) - }; - - Ok(Client { - base_path: into_base_path(base_path, Some("https"))?, - hyper_client: Arc::new(https_mutual_hyper_client) - }) - } - - /// Constructor for creating a `Client` by passing in a pre-made `hyper` client. - /// - /// One should avoid relying on this function if possible, since it adds a dependency on the underlying transport - /// implementation, which it would be better to abstract away. Therefore, using this function may lead to a loss of - /// code generality, which may make it harder to move the application to a serverless environment, for example. - /// - /// The reason for this function's existence is to support legacy test code, which did mocking at the hyper layer. - /// This is not a recommended way to write new tests. If other reasons are found for using this function, they - /// should be mentioned here. - pub fn try_new_with_hyper_client(base_path: T, - hyper_client: Arc hyper::client::Client + Sync + Send>) - -> Result - where T: IntoUrl - { - Ok(Client { - base_path: into_base_path(base_path, None)?, - hyper_client: hyper_client - }) - } -} - -impl Api for Client { -{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} - fn {{#vendorExtensions}}{{operation_id}}{{/vendorExtensions}}(&self{{#allParams}}, param_{{paramName}}: {{^required}}{{#isFile}}Box{{#isFile}}, Error=Error> + Send>{{/isFile}}{{/required}}{{/allParams}}, context: &Context) -> Box + Send> { -{{#queryParams}}{{#-first}} - // Query parameters -{{/-first}}{{#required}} let query_{{paramName}} = format!("{{baseName}}={{=<% %>=}}{<% paramName %>}<%={{ }}=%>&", {{paramName}}=param_{{paramName}}{{#isListContainer}}.join(","){{/isListContainer}}{{^isListContainer}}.to_string(){{/isListContainer}}); -{{/required}}{{^required}} let query_{{paramName}} = param_{{paramName}}.map_or_else(String::new, |query| format!("{{baseName}}={{=<% %>=}}{<% paramName %>}<%={{ }}=%>&", {{paramName}}=query{{#isListContainer}}.join(","){{/isListContainer}}{{^isListContainer}}.to_string(){{/isListContainer}})); -{{/required}}{{/queryParams}} - - let url = format!( - "{}{{basePathWithoutHost}}{{path}}{{#queryParams}}{{#-first}}?{{/-first}}{{=<% %>=}}{<% paramName %>}<%={{ }}=%>{{/queryParams}}", - self.base_path{{#pathParams}}, {{baseName}}=utf8_percent_encode(¶m_{{paramName}}.to_string(), PATH_SEGMENT_ENCODE_SET){{/pathParams}}{{#queryParams}}, - {{paramName}}=utf8_percent_encode(&query_{{paramName}}, QUERY_ENCODE_SET){{/queryParams}} - ); - -{{#vendorExtensions}}{{#hasFile}} // Form data body - let mut multipart = Multipart::new();{{/hasFile}}{{/vendorExtensions}}{{#formParams}}{{#isFile}} - -{{^required}} if let Ok(Some(param_{{paramName}})) = param_{{paramName}}.wait() { {{/required}} -{{^required}} {{/required}} match convert_stream_to_string(param_{{paramName}}) { -{{^required}} {{/required}} Ok(param_{{paramName}}) => { - // Add file to multipart form. - multipart.add_text("{{paramName}}", param_{{paramName}}); - }, -{{^required}} {{/required}} Err(err) => return Box::new(futures::done(Err(err))), -{{^required}} {{/required}} } - {{^required}}}{{/required}}{{/isFile}}{{/formParams}}{{#vendorExtensions}}{{#hasFile}} - - let mut fields = match multipart.prepare() { - Ok(fields) => fields, - Err(err) => return Box::new(futures::done(Err(ApiError(format!("Unable to build request: {}", err))))), - }; - - let mut body_string = String::new(); - let body = fields.to_body().read_to_string(&mut body_string); - let boundary = fields.boundary(); - let multipart_header = Mime(TopLevel::Multipart, SubLevel::FormData, vec![(Attr::Boundary, Value::Ext(boundary.to_string()))]);{{/hasFile}}{{/vendorExtensions}}{{#bodyParam}}{{#-first}} - // Body parameter -{{/-first}}{{#vendorExtensions}}{{#required}}{{#consumesPlainText}} let body = param_{{paramName}};{{/consumesPlainText}}{{#consumesXml}} -{{^has_namespace}} let body = serde_xml_rs::to_string(¶m_{{paramName}}).expect("impossible to fail to serialize");{{/has_namespace}}{{#has_namespace}} - let mut namespaces = BTreeMap::new(); - // An empty string is used to indicate a global namespace in xmltree. - namespaces.insert("".to_string(), models::namespaces::{{uppercase_data_type}}.clone()); - let body = serde_xml_rs::to_string_with_namespaces(¶m_{{paramName}}, namespaces).expect("impossible to fail to serialize");{{/has_namespace}}{{/consumesXml}}{{#consumesJson}} - let body = serde_json::to_string(¶m_{{paramName}}).expect("impossible to fail to serialize");{{/consumesJson}} -{{/required}}{{^required}}{{#consumesPlainText}} let body = param_{{paramName}}; -{{/consumesPlainText}}{{^consumesPlainText}} let body = param_{{paramName}}.map(|ref body| { -{{#consumesXml}} -{{^has_namespace}} serde_xml_rs::to_string(body).expect("impossible to fail to serialize"){{/has_namespace}}{{#has_namespace}} - let mut namespaces = BTreeMap::new(); - // An empty string is used to indicate a global namespace in xmltree. - namespaces.insert("".to_string(), models::namespaces::{{uppercase_data_type}}.clone()); - serde_xml_rs::to_string_with_namespaces(body, namespaces).expect("impossible to fail to serialize"){{/has_namespace}}{{/consumesXml}}{{#consumesJson}} - serde_json::to_string(body).expect("impossible to fail to serialize"){{/consumesJson}} - });{{/consumesPlainText}}{{/required}}{{/vendorExtensions}}{{/bodyParam}} - let hyper_client = (self.hyper_client)(); - let request = hyper_client.request(hyper::method::Method::{{#vendorExtensions}}{{HttpMethod}}{{/vendorExtensions}}, &url); - let mut custom_headers = hyper::header::Headers::new(); - -{{#bodyParam}}{{#required}} let request = request.body(&body); -{{/required}}{{^required}} let request = match body { - Some(ref body) => request.body(body), - None => request, - }; -{{/required}} - - custom_headers.set(ContentType(mimetypes::requests::{{#vendorExtensions}}{{uppercase_operation_id}}{{/vendorExtensions}}.clone())); -{{/bodyParam}} - context.x_span_id.as_ref().map(|header| custom_headers.set(XSpanId(header.clone()))); -{{#headerParams}}{{#-first}} - // Header parameters -{{/-first}}{{^isMapContainer}} header! { (Request{{vendorExtensions.typeName}}, "{{baseName}}") => {{#isListContainer}}({{{baseType}}})*{{/isListContainer}}{{^isListContainer}}[{{{dataType}}}]{{/isListContainer}} } -{{#required}} custom_headers.set(Request{{vendorExtensions.typeName}}(param_{{paramName}}{{#isListContainer}}.clone(){{/isListContainer}})); -{{/required}}{{^required}} param_{{paramName}}.map(|header| custom_headers.set(Request{{vendorExtensions.typeName}}(header{{#isListContainer}}.clone(){{/isListContainer}}))); -{{/required}}{{/isMapContainer}}{{#isMapContainer}} let param_{{paramName}}: Option<{{{dataType}}}> = None; -{{/isMapContainer}}{{/headerParams}} - - let request = request.headers(custom_headers);{{#vendorExtensions}}{{#hasFile}} - let request = request.header(ContentType(multipart_header)) - .body(&body_string); -{{/hasFile}}{{/vendorExtensions}} - - // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). - fn parse_response(mut response: hyper::client::response::Response) -> Result<{{operationId}}Response, ApiError> { - match response.status.to_u16() { -{{#responses}} - {{code}} => { -{{#dataType}}{{^isFile}} let mut buf = String::new(); - response.read_to_string(&mut buf).map_err(|e| ApiError(format!("Response was not valid UTF8: {}", e)))?;{{#vendorExtensions}}{{#producesXml}} - // ToDo: this will move to swagger-rs and become a standard From conversion trait - // once https://github.com/RReverser/serde-xml-rs/pull/45 is accepted upstream - let body = serde_xml_rs::from_str::<{{{dataType}}}>(&buf) - .map_err(|e| ApiError(format!("Response body did not match the schema: {}", e)))?;{{/producesXml}}{{#producesPlainText}} - let body = buf;{{/producesPlainText}}{{#producesJson}} - let body = serde_json::from_str::<{{{dataType}}}>(&buf)?;{{/producesJson}}{{/vendorExtensions}}{{/isFile}}{{#isFile}} - let mut buf = Vec::new(); - response.read_to_end(&mut buf).map_err(|e| ApiError(format!("Received error reading response: {}", e)))?; - let body = Box::new(stream::once(Ok(buf)));{{/isFile}}{{/dataType}} -{{#headers}} header! { (Response{{nameInCamelCase}}, "{{baseName}}") => [{{{datatype}}}] } - let response_{{name}} = response.headers.get::().ok_or_else(|| "Required response header {{baseName}} for response {{code}} was not found.")?; -{{/headers}} - -{{#dataType}} Ok({{operationId}}Response::{{#vendorExtensions}}{{x-responseId}}{{/vendorExtensions}}{{^headers}}(body){{/headers}}{{#headers}}{{#-first}}{ body: body, {{/-first}}{{name}}: response_{{name}}.0.clone(){{^-last}}, {{/-last}}{{#-last}} }{{/-last}}{{/headers}}) -{{/dataType}}{{^dataType}} Ok({{operationId}}Response::{{#vendorExtensions}}{{x-responseId}}{{/vendorExtensions}}{{#headers}}{{#-first}}{ {{/-first}}{{^-first}}, {{/-first}}{{name}}: response_{{name}}.0.clone(){{#-last}} }{{/-last}}{{/headers}}) -{{/dataType}} - }, -{{/responses}} - code => { - let mut buf = [0; 100]; - let debug_body = match response.read(&mut buf) { - Ok(len) => match str::from_utf8(&buf[..len]) { - Ok(body) => Cow::from(body), - Err(_) => Cow::from(format!("", &buf[..len].to_vec())), - }, - Err(e) => Cow::from(format!("", e)), - }; - Err(ApiError(format!("Unexpected response code {}:\n{:?}\n\n{}", - code, - response.headers, - debug_body))) - } - } - }{{#vendorExtensions}}{{#hasFile}} - - // Helper function to convert a Stream into a String. The String can then be used to build the HTTP body. - fn convert_stream_to_string(stream: Box, Error=Error> + Send>) -> Result { - - stream.fold(Vec::new(), |mut body, chunk| { - body.extend(chunk.iter()); - future::ok::,Error>(body) - }).wait() - .map_err(|e| ApiError(format!("Unable to fold stream: {}", e))) - .and_then(|body| String::from_utf8(body) - .map_err(|e| ApiError(format!("Failed to convert utf8 stream to String: {}", e)))) - }{{/hasFile}}{{/vendorExtensions}} - - let result = request.send().map_err(|e| ApiError(format!("No response received: {}", e))).and_then(parse_response); - Box::new(futures::done(result)) - } -{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} -} - -#[derive(Debug)] -pub enum ClientInitError { - InvalidScheme, - InvalidUrl(hyper::error::ParseError), - MissingHost, - SslError(openssl::error::ErrorStack) -} - -impl From for ClientInitError { - fn from(err: hyper::error::ParseError) -> ClientInitError { - ClientInitError::InvalidUrl(err) - } -} - -impl From for ClientInitError { - fn from(err: openssl::error::ErrorStack) -> ClientInitError { - ClientInitError::SslError(err) - } -} - -impl fmt::Display for ClientInitError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - (self as &fmt::Debug).fmt(f) - } -} - -impl error::Error for ClientInitError { - fn description(&self) -> &str { - "Failed to produce a hyper client." - } -} diff --git a/modules/openapi-generator/src/main/resources/rust-server/example-client.mustache b/modules/openapi-generator/src/main/resources/rust-server/example-client.mustache index e5b59e5c4f9..2245ae47562 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/example-client.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/example-client.mustache @@ -8,9 +8,11 @@ extern crate swagger; #[allow(unused_extern_crates)] extern crate uuid; extern crate clap; +extern crate tokio_core; #[allow(unused_imports)] use futures::{Future, future, Stream, stream}; +use tokio_core::reactor; #[allow(unused_imports)] use {{externCrateName}}::{ApiNoContext, ContextWrapperExt, ApiError{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}, @@ -42,18 +44,19 @@ fn main() { .help("Port to contact")) .get_matches(); + let mut core = reactor::Core::new().unwrap(); let is_https = matches.is_present("https"); let base_url = format!("{}://{}:{}", if is_https { "https" } else { "http" }, matches.value_of("host").unwrap(), matches.value_of("port").unwrap()); - let client = if is_https { + let client = if matches.is_present("https") { // Using Simple HTTPS - {{externCrateName}}::Client::try_new_https(&base_url, "examples/ca.pem") + {{externCrateName}}::Client::try_new_https(core.handle(), &base_url, "examples/ca.pem") .expect("Failed to create HTTPS client") } else { // Using HTTP - {{externCrateName}}::Client::try_new_http(&base_url) + {{externCrateName}}::Client::try_new_http(core.handle(), &base_url) .expect("Failed to create HTTP client") }; @@ -64,7 +67,7 @@ fn main() { {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} {{#vendorExtensions}}{{#noClientExample}}// Disabled because there's no example. // {{/noClientExample}}Some("{{operationId}}") => { - {{#noClientExample}}// {{/noClientExample}} let result = client.{{operation_id}}{{/vendorExtensions}}({{#allParams}}{{^-first}}, {{/-first}}{{#vendorExtensions}}{{{example}}}{{/vendorExtensions}}{{/allParams}}).wait(); + {{#noClientExample}}// {{/noClientExample}} let result = core.run(client.{{operation_id}}{{/vendorExtensions}}({{#allParams}}{{^-first}}, {{/-first}}{{#vendorExtensions}}{{{example}}}{{/vendorExtensions}}{{/allParams}})); {{#vendorExtensions}}{{#noClientExample}}// {{/noClientExample}}{{/vendorExtensions}} println!("{:?} (X-Span-ID: {:?})", result, client.context().x_span_id.clone().unwrap_or(String::from(""))); {{#vendorExtensions}}{{#noClientExample}}// {{/noClientExample}}{{/vendorExtensions}} }, {{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} diff --git a/modules/openapi-generator/src/main/resources/rust-server/example-server.mustache b/modules/openapi-generator/src/main/resources/rust-server/example-server.mustache index 007384b33fe..ee99d434bb3 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/example-server.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/example-server.mustache @@ -6,8 +6,11 @@ // extern crate ; extern crate {{externCrateName}}; extern crate swagger; -extern crate iron; -extern crate hyper_openssl; +extern crate hyper; +extern crate openssl; +extern crate native_tls; +extern crate tokio_proto; +extern crate tokio_tls; extern crate clap; // Imports required by server library. @@ -17,19 +20,20 @@ extern crate futures; extern crate chrono; #[macro_use] extern crate error_chain; +{{#apiUsesUuid}}extern crate uuid;{{/apiUsesUuid}} -use hyper_openssl::OpensslServer; -use hyper_openssl::openssl::x509::X509_FILETYPE_PEM; -use hyper_openssl::openssl::ssl::{SslAcceptorBuilder, SslMethod}; -use hyper_openssl::openssl::error::ErrorStack; +use openssl::x509::X509_FILETYPE_PEM; +use openssl::ssl::{SslAcceptorBuilder, SslMethod}; +use openssl::error::ErrorStack; +use hyper::server::Http; +use tokio_proto::TcpServer; use clap::{App, Arg}; -use iron::{Iron, Chain}; -use swagger::auth::AllowAllMiddleware; +use swagger::auth::AllowAllAuthenticator; mod server_lib; -/// Builds an SSL implementation for Simple HTTPS from some hard-coded file names -fn ssl() -> Result { +// Builds an SSL implementation for Simple HTTPS from some hard-coded file names +fn ssl() -> Result { let mut ssl = SslAcceptorBuilder::mozilla_intermediate_raw(SslMethod::tls())?; // Server authentication @@ -37,7 +41,7 @@ fn ssl() -> Result { ssl.set_certificate_chain_file("examples/server-chain.pem")?; ssl.check_private_key()?; - Ok(OpensslServer::from(ssl.build())) + Ok(ssl) } /// Create custom server, wire it to the autogenerated router, @@ -49,20 +53,22 @@ fn main() { .help("Whether to use HTTPS or not")) .get_matches(); - let server = server_lib::server().unwrap(); - let router = {{externCrateName}}::router(server); - - let mut chain = Chain::new(router); - chain.link_before({{externCrateName}}::server::ExtractAuthData); - // add authentication middlewares into the chain here - // for the purpose of this example, pretend we have authenticated a user - chain.link_before(AllowAllMiddleware::new("cosmo")); + let service_fn = + {{externCrateName}}::server::auth::NewService::new( + AllowAllAuthenticator::new( + server_lib::NewService, + "cosmo" + ) + ); + let addr = "127.0.0.1:{{serverPort}}".parse().expect("Failed to parse bind address"); if matches.is_present("https") { - // Using Simple HTTPS - Iron::new(chain).https("localhost:{{serverPort}}", ssl().expect("Failed to load SSL keys")).expect("Failed to start HTTPS server"); + let ssl = ssl().expect("Failed to load SSL keys"); + let builder: native_tls::TlsAcceptorBuilder = native_tls::backend::openssl::TlsAcceptorBuilderExt::from_openssl(ssl); + let tls_acceptor = builder.build().expect("Failed to build TLS acceptor"); + TcpServer::new(tokio_tls::proto::Server::new(Http::new(), tls_acceptor), addr).serve(service_fn); } else { // Using HTTP - Iron::new(chain).http("localhost:{{serverPort}}").expect("Failed to start HTTP server"); + TcpServer::new(Http::new(), addr).serve(service_fn); } } diff --git a/modules/openapi-generator/src/main/resources/rust-server/example-server_lib.mustache b/modules/openapi-generator/src/main/resources/rust-server/example-server_lib.mustache index 7e85ec86199..d02d0525b47 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/example-server_lib.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/example-server_lib.mustache @@ -7,8 +7,20 @@ mod errors { } pub use self::errors::*; +use std::io; +use hyper; +use {{externCrateName}}; -/// Instantiate a new server. -pub fn server() -> Result { - Ok(server::Server {}) +pub struct NewService; + +impl hyper::server::NewService for NewService { + type Request = (hyper::Request, {{externCrateName}}::Context); + type Response = hyper::Response; + type Error = hyper::Error; + type Instance = {{externCrateName}}::server::Service; + + /// Instantiate a new server. + fn new_service(&self) -> io::Result { + Ok({{externCrateName}}::server::Service::new(server::Server)) + } } diff --git a/modules/openapi-generator/src/main/resources/rust-server/example-server_server.mustache b/modules/openapi-generator/src/main/resources/rust-server/example-server_server.mustache index 8b8bf033927..fdb7ceaf541 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/example-server_server.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/example-server_server.mustache @@ -7,6 +7,7 @@ use chrono; {{#apiHasFile}}use futures::Stream;{{/apiHasFile}} use std::collections::HashMap; {{#apiHasFile}}use std::io::Error;{{/apiHasFile}} +{{#apiUsesUuid}}use uuid;{{/apiUsesUuid}} use swagger; use {{externCrateName}}::{Api, ApiError, Context{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}, @@ -20,7 +21,7 @@ pub struct Server; impl Api for Server { {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} {{#summary}} /// {{{summary}}}{{/summary}} - fn {{#vendorExtensions}}{{operation_id}}{{/vendorExtensions}}(&self{{#allParams}}, {{paramName}}: {{^required}}{{#isFile}}Box{{#isFile}}, Error=Error> + Send>{{/isFile}}{{/required}}{{/allParams}}, context: &Context) -> Box + Send> { + fn {{#vendorExtensions}}{{operation_id}}{{/vendorExtensions}}(&self{{#allParams}}, {{paramName}}: {{^required}}{{#isFile}}Box{{#isFile}}, Error=Error> + Send>{{/isFile}}{{/required}}{{/allParams}}, context: &Context) -> Box> { let context = context.clone(); println!("{{#vendorExtensions}}{{operation_id}}{{/vendorExtensions}}({{#allParams}}{{^isFile}}{{#vendorExtensions}}{{{formatString}}}{{/vendorExtensions}}{{#hasMore}}, {{/hasMore}}{{/isFile}}{{/allParams}}) - X-Span-ID: {:?}"{{#allParams}}{{^isFile}}, {{paramName}}{{/isFile}}{{/allParams}}, context.x_span_id.unwrap_or(String::from("")).clone());{{#allParams}}{{#isFile}} let _ = {{paramName}}; //Suppresses unused param warning{{/isFile}}{{/allParams}} diff --git a/modules/openapi-generator/src/main/resources/rust-server/lib.mustache b/modules/openapi-generator/src/main/resources/rust-server/lib.mustache index 0ab6543fcfc..e01f1496731 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/lib.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/lib.mustache @@ -3,10 +3,10 @@ extern crate serde; #[macro_use] extern crate serde_derive; extern crate serde_json; +{{#apiUsesUuid}}extern crate uuid;{{/apiUsesUuid}} {{#usesXml}}extern crate serde_xml_rs;{{/usesXml}} extern crate futures; extern crate chrono; -{{#apiHasFile}}extern crate multipart;{{/apiHasFile}} #[macro_use] extern crate lazy_static; #[macro_use] @@ -32,6 +32,8 @@ mod mimetypes; pub use swagger::{ApiError, Context, ContextWrapper}; +pub const BASE_PATH: &'static str = "{{basePathWithoutHost}}"; + {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} {{^isResponseFile}} #[derive(Debug, PartialEq)] @@ -48,7 +50,7 @@ pub enum {{operationId}}Response { pub trait Api { {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} {{#summary}} /// {{{summary}}}{{/summary}} - fn {{#vendorExtensions}}{{operation_id}}{{/vendorExtensions}}(&self{{#allParams}}, {{paramName}}: {{^required}}{{#isFile}}Box{{#isFile}}, Error=Error> + Send>{{/isFile}}{{/required}}{{/allParams}}, context: &Context) -> Box + Send>; + fn {{#vendorExtensions}}{{operation_id}}{{/vendorExtensions}}(&self{{#allParams}}, {{paramName}}: {{^required}}{{#isFile}}Box{{#isFile}}, Error=Error> + Send>{{/isFile}}{{/required}}{{/allParams}}, context: &Context) -> Box>; {{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} } @@ -56,7 +58,7 @@ pub trait Api { pub trait ApiNoContext { {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} {{#summary}} /// {{{summary}}}{{/summary}} - fn {{#vendorExtensions}}{{operation_id}}{{/vendorExtensions}}(&self{{#allParams}}, {{paramName}}: {{^required}}{{#isFile}}Box{{#isFile}}, Error=Error> + Send>{{/isFile}}{{/required}}{{/allParams}}) -> Box + Send>; + fn {{#vendorExtensions}}{{operation_id}}{{/vendorExtensions}}(&self{{#allParams}}, {{paramName}}: {{^required}}{{#isFile}}Box{{#isFile}}, Error=Error> + Send>{{/isFile}}{{/required}}{{/allParams}}) -> Box>; {{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} } @@ -75,7 +77,7 @@ impl<'a, T: Api + Sized> ContextWrapperExt<'a> for T { impl<'a, T: Api> ApiNoContext for ContextWrapper<'a, T> { {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} {{#summary}} /// {{{summary}}}{{/summary}} - fn {{#vendorExtensions}}{{operation_id}}{{/vendorExtensions}}(&self{{#allParams}}, {{paramName}}: {{^required}}{{#isFile}}Box{{#isFile}}, Error=Error> + Send>{{/isFile}}{{/required}}{{/allParams}}) -> Box + Send> { + fn {{#vendorExtensions}}{{operation_id}}{{/vendorExtensions}}(&self{{#allParams}}, {{paramName}}: {{^required}}{{#isFile}}Box{{#isFile}}, Error=Error> + Send>{{/isFile}}{{/required}}{{/allParams}}) -> Box> { self.api().{{#vendorExtensions}}{{operation_id}}{{/vendorExtensions}}({{#allParams}}{{paramName}}, {{/allParams}}&self.context()) } {{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} @@ -93,6 +95,6 @@ pub mod server; // Re-export router() as a top-level name #[cfg(feature = "server")] -pub use self::server::router; +pub use self::server::Service; pub mod models; diff --git a/modules/openapi-generator/src/main/resources/rust-server/mimetypes.mustache b/modules/openapi-generator/src/main/resources/rust-server/mimetypes.mustache index 744374ca995..047c7c7fbbc 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/mimetypes.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/mimetypes.mustache @@ -15,7 +15,11 @@ pub mod requests { use hyper::mime::*; {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}{{#bodyParam}} /// Create Mime objects for the request content types for {{operationId}} lazy_static! { - pub static ref {{#vendorExtensions}}{{uppercase_operation_id}}{{/vendorExtensions}}: Mime = "{{#consumes}}{{#-first}}{{{mediaType}}}{{/-first}}{{/consumes}}{{^consumes}}Application/Json{{/consumes}}".parse().unwrap(); + pub static ref {{#vendorExtensions}}{{uppercase_operation_id}}{{/vendorExtensions}}: Mime = "{{#consumes}}{{#-first}}{{{mediaType}}}{{/-first}}{{/consumes}}{{^consumes}}application/json{{/consumes}}".parse().unwrap(); } -{{/bodyParam}}{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} +{{/bodyParam}}{{^bodyParam}}{{#vendorExtensions}}{{#formParams}}{{#-first}}{{^hasFile}} /// Create Mime objects for the request content types for {{operationId}} + lazy_static! { + pub static ref {{#vendorExtensions}}{{uppercase_operation_id}}{{/vendorExtensions}}: Mime = "{{#consumes}}{{#-first}}{{{mediaType}}}{{/-first}}{{/consumes}}{{^consumes}}application/x-www-form-urlencoded{{/consumes}}".parse().unwrap(); + } +{{/hasFile}}{{/-first}}{{/formParams}}{{/vendorExtensions}}{{/bodyParam}}{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} } diff --git a/modules/openapi-generator/src/main/resources/rust-server/models.mustache b/modules/openapi-generator/src/main/resources/rust-server/models.mustache old mode 100755 new mode 100644 diff --git a/modules/openapi-generator/src/main/resources/rust-server/server-auth.mustache b/modules/openapi-generator/src/main/resources/rust-server/server-auth.mustache new file mode 100644 index 00000000000..5f2884cf8aa --- /dev/null +++ b/modules/openapi-generator/src/main/resources/rust-server/server-auth.mustache @@ -0,0 +1,95 @@ +use std::io; +use hyper; +use hyper::{Request, Response, Error, StatusCode}; +use server::url::form_urlencoded; +use swagger::auth::{Authorization, AuthData, Scopes}; +use Api; + +pub struct NewService where T: hyper::server::NewService), Response=Response, Error=Error> { + inner: T, +} + +impl NewService where T: hyper::server::NewService), Response=Response, Error=Error> + 'static { + pub fn new(inner: T) -> NewService { + NewService{inner} + } +} + +impl hyper::server::NewService for NewService where T: hyper::server::NewService), Response=Response, Error=Error> + 'static { + type Request = Request; + type Response = Response; + type Error = Error; + type Instance = Service; + + fn new_service(&self) -> Result { + self.inner.new_service().map(|s| Service::new(s)) + } +} + +/// Middleware to extract authentication data from request +pub struct Service where T: hyper::server::Service), Response=Response, Error=Error> { + inner: T, +} + +impl Service where T: hyper::server::Service), Response=Response, Error=Error> { + pub fn new(inner: T) -> Service { + Service{inner} + } +} + +impl hyper::server::Service for Service where T: hyper::server::Service), Response=Response, Error=Error> { + type Request = Request; + type Response = Response; + type Error = Error; + type Future = T::Future; + + fn call(&self, req: Self::Request) -> Self::Future { + {{#authMethods}} + {{#isBasic}} + { + use hyper::header::{Authorization, Basic, Bearer}; + use std::ops::Deref; + if let Some(basic) = req.headers().get::>().cloned() { + let auth_data = AuthData::Basic(basic.deref().clone()); + return self.inner.call((req, Some(auth_data))); + } + } + {{/isBasic}} + {{#isOAuth}} + { + use hyper::header::{Authorization, Basic, Bearer}; + use std::ops::Deref; + if let Some(bearer) = req.headers().get::>().cloned() { + let auth_data = AuthData::Bearer(bearer.deref().clone()); + return self.inner.call((req, Some(auth_data))); + } + } + {{/isOAuth}} + {{#isApiKey}} + {{#isKeyInHeader}} + { + header! { (ApiKey{{-index}}, "{{keyParamName}}") => [String] } + if let Some(header) = req.headers().get::().cloned() { + let auth_data = AuthData::ApiKey(header.0); + return self.inner.call((req, Some(auth_data))); + } + } + {{/isKeyInHeader}} + {{#isKeyInQuery}} + { + let key = form_urlencoded::parse(req.query().unwrap_or_default().as_bytes()) + .filter(|e| e.0 == "api_key_query") + .map(|e| e.1.clone().into_owned()) + .nth(0); + if let Some(key) = key { + let auth_data = AuthData::ApiKey(key); + return self.inner.call((req, Some(auth_data))); + } + } + {{/isKeyInQuery}} + {{/isApiKey}} + {{/authMethods}} + + return self.inner.call((req, None)); + } +} diff --git a/modules/openapi-generator/src/main/resources/rust-server/server-mod.mustache b/modules/openapi-generator/src/main/resources/rust-server/server-mod.mustache new file mode 100644 index 00000000000..6b564f05546 --- /dev/null +++ b/modules/openapi-generator/src/main/resources/rust-server/server-mod.mustache @@ -0,0 +1,449 @@ +#![allow(unused_extern_crates)] +extern crate serde_ignored; +extern crate tokio_core; +extern crate native_tls; +extern crate hyper_tls; +extern crate openssl; +extern crate mime; +{{^apiUsesUuid}}extern crate uuid;{{/apiUsesUuid}} +extern crate chrono; +{{#apiHasFile}}extern crate multipart;{{/apiHasFile}} +extern crate percent_encoding; +extern crate url; + +{{#apiUsesUuid}}use uuid;{{/apiUsesUuid}} +use std::sync::Arc; +use futures::{Future, future, Stream, stream}; +use hyper; +use hyper::{Request, Response, Error, StatusCode}; +use hyper::header::{Headers, ContentType}; +use self::url::form_urlencoded; +use mimetypes; +{{#apiHasFile}}use self::multipart::server::Multipart; +use self::multipart::server::save::SaveResult;{{/apiHasFile}} + +use serde_json; +{{#usesXml}}use serde_xml_rs;{{/usesXml}} + +#[allow(unused_imports)] +use std::collections::{HashMap, BTreeMap}; +#[allow(unused_imports)] +use swagger; +use std::io; + +#[allow(unused_imports)] +use std::collections::BTreeSet; + +pub use swagger::auth::Authorization; +use swagger::{ApiError, Context, XSpanId}; +use swagger::auth::Scopes; + +use {Api{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}, + {{operationId}}Response{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} + }; +#[allow(unused_imports)] +use models; + +pub mod auth; + +header! { (Warning, "Warning") => [String] } + +mod paths { + extern crate regex; + + lazy_static! { + pub static ref GLOBAL_REGEX_SET: regex::RegexSet = regex::RegexSet::new(&[ +{{#pathSet}} + r"^{{basePathWithoutHost}}{{{pathRegEx}}}"{{^-last}},{{/-last}} +{{/pathSet}} + ]).unwrap(); + } +{{#pathSet}} + pub static ID_{{PATH_ID}}: usize = {{index}}; +{{#hasPathParams}} + lazy_static! { + pub static ref REGEX_{{PATH_ID}}: regex::Regex = regex::Regex::new(r"^{{basePathWithoutHost}}{{{pathRegEx}}}").unwrap(); + } +{{/hasPathParams}} +{{/pathSet}} +} + +pub struct NewService { + api_impl: Arc, +} + +impl NewService where T: Api + Clone + 'static { + pub fn new>>(api_impl: U) -> NewService { + NewService{api_impl: api_impl.into()} + } +} + +impl hyper::server::NewService for NewService where T: Api + Clone + 'static { + type Request = (Request, Context); + type Response = Response; + type Error = Error; + type Instance = Service; + + fn new_service(&self) -> Result { + Ok(Service::new(self.api_impl.clone())) + } +} + +pub struct Service { + api_impl: Arc, +} + +impl Service where T: Api + Clone + 'static { + pub fn new>>(api_impl: U) -> Service { + Service{api_impl: api_impl.into()} + } +} + +impl hyper::server::Service for Service where T: Api + Clone + 'static { + type Request = (Request, Context); + type Response = Response; + type Error = Error; + type Future = Box>; + + fn call(&self, (req, mut context): Self::Request) -> Self::Future { + let api_impl = self.api_impl.clone(); + let (method, uri, _, headers, body) = req.deconstruct(); + let path = paths::GLOBAL_REGEX_SET.matches(uri.path()); + match &method { +{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} + // {{operationId}} - {{httpMethod}} {{path}} + &hyper::Method::{{vendorExtensions.HttpMethod}} if path.matched(paths::ID_{{vendorExtensions.PATH_ID}}) => { + if context.x_span_id.is_none() { + context.x_span_id = Some(headers.get::().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); + } +{{#hasAuthMethods}} + { + let authorization = match context.authorization.as_ref() { + Some(authorization) => authorization, + None => return Box::new(future::ok(Response::new() + .with_status(StatusCode::Forbidden) + .with_body("Unauthenticated"))), + }; + + {{#authMethods}} + {{#isOAuth}} + // Authorization + if let Scopes::Some(ref scopes) = authorization.scopes { + let required_scopes: BTreeSet = vec![ + {{#scopes}} + "{{scope}}".to_string(), // {{description}} + {{/scopes}} + ].into_iter().collect(); + + if !required_scopes.is_subset(scopes) { + let missing_scopes = required_scopes.difference(scopes); + return Box::new(future::ok(Response::new() + .with_status(StatusCode::Forbidden) + .with_body(missing_scopes.fold( + "Insufficient authorization, missing scopes".to_string(), + |s, scope| format!("{} {}", s, scope) + )) + )); + } + } + {{/isOAuth}} + {{/authMethods}} + } +{{/hasAuthMethods}} + +{{#vendorExtensions}}{{#hasPathParams}} + // Path parameters + let path = uri.path().to_string(); + let path_params = + paths::REGEX_{{PATH_ID}} + .captures(&path) + .unwrap_or_else(|| + panic!("Path {} matched RE {{PATH_ID}} in set but failed match against \"{}\"", path, paths::REGEX_{{PATH_ID}}.as_str()) + ); +{{/hasPathParams}}{{/vendorExtensions}} +{{#pathParams}} + let param_{{paramName}} = match percent_encoding::percent_decode(path_params["{{baseName}}"].as_bytes()).decode_utf8() { + Ok(param_{{paramName}}) => match param_{{paramName}}.parse::<{{{dataType}}}>() { + Ok(param_{{paramName}}) => param_{{paramName}}, + Err(e) => return Box::new(future::ok(Response::new().with_status(StatusCode::BadRequest).with_body(format!("Couldn't parse path parameter {{baseName}}: {}", e)))), + }, + Err(_) => return Box::new(future::ok(Response::new().with_status(StatusCode::BadRequest).with_body(format!("Couldn't percent-decode path parameter as UTF-8: {}", &path_params["{{baseName}}"])))) + }; +{{/pathParams}} +{{#headerParams}}{{#-first}} + // Header parameters +{{/-first}} + header! { (Request{{vendorExtensions.typeName}}, "{{baseName}}") => {{#isListContainer}}({{{baseType}}})*{{/isListContainer}}{{^isListContainer}}[{{{dataType}}}]{{/isListContainer}} } +{{#required}} + let param_{{paramName}} = match headers.get::() { + Some(param_{{paramName}}) => param_{{paramName}}.0.clone(), + None => return Box::new(future::ok(Response::new().with_status(StatusCode::BadRequest).with_body("Missing or invalid required header {{baseName}}"))), + }; +{{/required}} +{{^required}} + let param_{{paramName}} = headers.get::().map(|header| header.0.clone()); +{{/required}}{{/headerParams}} + +{{#queryParams}}{{#-first}} + // Query parameters (note that non-required or collection query parameters will ignore garbage values, rather than causing a 400 response) + let query_params = form_urlencoded::parse(uri.query().unwrap_or_default().as_bytes()).collect::>(); +{{/-first}} + let param_{{paramName}} = query_params.iter().filter(|e| e.0 == "{{baseName}}").map(|e| e.1.to_owned()) +{{#isListContainer}} + .filter_map(|param_{{paramName}}| param_{{paramName}}.parse::<{{{baseType}}}>().ok()) + .collect::>(); +{{^required}} + let param_{{paramName}} = if !param_{{paramName}}.is_empty() { + Some(param_{{paramName}}) + } else { + None + }; +{{/required}} +{{/isListContainer}}{{^isListContainer}} + .nth(0); +{{#required}} + let param_{{paramName}} = match param_{{paramName}} { + Some(param_{{paramName}}) => match param_{{paramName}}.parse::<{{{dataType}}}>() { + Ok(param_{{paramName}}) => param_{{paramName}}, + Err(e) => return Box::new(future::ok(Response::new().with_status(StatusCode::BadRequest).with_body(format!("Couldn't parse query parameter {{baseName}} - doesn't match schema: {}", e)))), + }, + None => return Box::new(future::ok(Response::new().with_status(StatusCode::BadRequest).with_body("Missing required query parameter {{baseName}}"))), + }; +{{/required}}{{^required}} + let param_{{paramName}} = param_{{paramName}}.and_then(|param_{{paramName}}| param_{{paramName}}.parse::<{{{baseType}}}>().ok()); +{{/required}} +{{/isListContainer}} +{{/queryParams}} + +{{#bodyParams}}{{#-first}} + // Body parameters (note that non-required body parameters will ignore garbage + // values, rather than causing a 400 response). Produce warning header and logs for + // any unused fields. + Box::new(body.concat2() + .then(move |result| -> Box> { + match result { + Ok(body) => { +{{#vendorExtensions}}{{^consumesPlainText}} + let mut unused_elements = Vec::new(); +{{/consumesPlainText}} + let param_{{paramName}}: Option<{{{dataType}}}> = if !body.is_empty() { +{{#consumesXml}} + let deserializer = &mut serde_xml_rs::de::Deserializer::new_from_reader(&*body); +{{/consumesXml}}{{#consumesJson}} + let deserializer = &mut serde_json::Deserializer::from_slice(&*body); +{{/consumesJson}}{{^consumesPlainText}} + match serde_ignored::deserialize(deserializer, |path| { + warn!("Ignoring unknown field in body: {}", path); + unused_elements.push(path.to_string()); + }) { + Ok(param_{{paramName}}) => param_{{paramName}}, +{{#required}} + Err(e) => return Box::new(future::ok(Response::new().with_status(StatusCode::BadRequest).with_body(format!("Couldn't parse body parameter {{baseName}} - doesn't match schema: {}", e)))), +{{/required}}{{^required}} + Err(_) => None, +{{/required}} + } +{{/consumesPlainText}}{{#consumesPlainText}} + match String::from_utf8(body.to_vec()) { + Ok(param_{{paramName}}) => Some(param_{{paramName}}), + Err(e) => return Box::new(future::ok(Response::new().with_status(StatusCode::BadRequest).with_body(format!("Couldn't parse body parameter {{baseName}} - not valid UTF-8: {}", e)))), + } +{{/consumesPlainText}}{{/vendorExtensions}} + } else { + None + }; +{{#required}} + let param_{{paramName}} = match param_{{paramName}} { + Some(param_{{paramName}}) => param_{{paramName}}, + None => return Box::new(future::ok(Response::new().with_status(StatusCode::BadRequest).with_body("Missing required body parameter {{baseName}}"))), + }; +{{/required}} +{{/-first}}{{/bodyParams}} +{{^bodyParams}}{{#vendorExtensions}}{{#hasFile}} + let boundary = match multipart_boundary(&headers) { + Some(boundary) => boundary.to_string(), + None => return Box::new(future::ok(Response::new().with_status(StatusCode::BadRequest).with_body("Couldn't find valid multipart body"))), + }; + + Box::new(body.concat2() + .then(move |result| -> Box> { + match result { + Ok(body) => { + let mut entries = match Multipart::with_body(&body.to_vec()[..], boundary).save().temp() { + SaveResult::Full(entries) => { + entries + }, + _ => { + return Box::new(future::ok(Response::new().with_status(StatusCode::BadRequest).with_body(format!("Unable to process all message parts")))) + }, + }; +{{#formParams}}{{#-first}} + // Form parameters +{{/-first}} + let param_{{paramName}} = entries.fields.remove("{{paramName}}"); + let param_{{paramName}} = match param_{{paramName}} { + Some(entry) => +{{#isFile}} + {{^required}}Some({{/required}}Box::new(stream::once(Ok(entry.as_bytes().to_vec()))) as Box, Error=io::Error> + Send>{{^required}}){{/required}}, +{{/isFile}}{{^isFile}} + match entry.parse::<{{{dataType}}}>() { + Ok(entry) => {{^required}}Some({{/required}}entry{{^required}}){{/required}}, +{{#required}} + Err(e) => return Box::new(future::ok(Response::new().with_status(StatusCode::BadRequest).with_body(format!("Couldn't parse form parameter {{baseName}} - doesn't match schema: {}", e)))), +{{/required}}{{^required}} + Err(_) => None, +{{/required}} + }, +{{/isFile}} +{{#required}} + None => return Box::new(future::ok(Response::new().with_status(StatusCode::BadRequest).with_body(format!("Missing required form parameter {{paramName}}")))), +{{/required}}{{^required}} + None => None, +{{/required}} + }; +{{^required}}{{#isFile}} let param_{{paramName}} = Box::new(future::ok(param_{{paramName}}));{{/isFile}}{{/required}} +{{/formParams}} +{{/hasFile}}{{^hasFile}} + Box::new(({ + {{ +{{#formParams}}{{#-first}} + // Form parameters +{{/-first}} + let param_{{paramName}} = {{^isContainer}}{{#vendorExtensions}}{{{example}}};{{/vendorExtensions}}{{/isContainer}}{{#isListContainer}}{{#required}}Vec::new();{{/required}}{{^required}}None;{{/required}}{{/isListContainer}}{{#isMapContainer}}None;{{/isMapContainer}} +{{/formParams}} +{{/hasFile}}{{/vendorExtensions}}{{/bodyParams}} + Box::new(api_impl.{{#vendorExtensions}}{{operation_id}}{{/vendorExtensions}}({{#allParams}}param_{{paramName}}{{#isListContainer}}.as_ref(){{/isListContainer}}, {{/allParams}}&context) + .then(move |result| { + let mut response = Response::new(); + context.x_span_id.as_ref().map(|header| response.headers_mut().set(XSpanId(header.clone()))); +{{#bodyParams}}{{#vendorExtensions}}{{^consumesPlainText}} + if !unused_elements.is_empty() { + response.headers_mut().set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); + } +{{/consumesPlainText}}{{/vendorExtensions}}{{/bodyParams}} + match result { + Ok(rsp) => match rsp { +{{#responses}} + {{operationId}}Response::{{#vendorExtensions}}{{x-responseId}}{{/vendorExtensions}} +{{#dataType}}{{^headers}} + (body) +{{/headers}}{{#headers}} +{{#-first}} + { + body, +{{/-first}} + {{name}}{{^-last}}, {{/-last}} +{{#-last}} + } +{{/-last}} +{{/headers}}{{/dataType}} +{{^dataType}}{{#headers}}{{#-first}} + { +{{/-first}} + {{name}}{{^-last}}, {{/-last}} +{{#-last}} + } +{{/-last}} +{{/headers}}{{/dataType}} + => { +{{^isFile}} response.set_status(StatusCode::try_from({{code}}).unwrap()); +{{#headers}} + header! { (Response{{nameInCamelCase}}, "{{baseName}}") => [{{{datatype}}}] } + response.headers_mut().set(Response{{nameInCamelCase}}({{name}})); +{{/headers}} +{{#produces}}{{#-first}}{{#dataType}} + response.headers_mut().set(ContentType(mimetypes::responses::{{#vendorExtensions}}{{uppercase_operation_id}}_{{x-uppercaseResponseId}}{{/vendorExtensions}}.clone())); +{{/dataType}}{{/-first}}{{/produces}} +{{#dataType}} +{{#vendorExtensions}}{{#producesXml}}{{^has_namespace}} + let body = serde_xml_rs::to_string(&body).expect("impossible to fail to serialize"); +{{/has_namespace}}{{#has_namespace}} + let mut namespaces = BTreeMap::new(); + // An empty string is used to indicate a global namespace in xmltree. + namespaces.insert("".to_string(), models::namespaces::{{uppercase_data_type}}.clone()); + let body = serde_xml_rs::to_string_with_namespaces(&body, namespaces).expect("impossible to fail to serialize"); +{{/has_namespace}}{{/producesXml}}{{#producesJson}} + let body = serde_json::to_string(&body).expect("impossible to fail to serialize"); +{{/producesJson}}{{/vendorExtensions}} + response.set_body(body); +{{/dataType}}{{/isFile}}{{#isFile}} + let body = body.fold(Vec::new(), | mut body, chunk| { + body.extend(chunk.iter()); + future::ok::, io::Error>(body) + }) + // Block whilst waiting for the stream to complete + .wait(); + + match body { + // If no error occurred then create successful hyper response + Ok(vec) => { + response.set_status(StatusCode::try_from({{code}}).unwrap()); + +{{#headers}} + header! { (Response{{nameInCamelCase}}, "{{baseName}}") => [{{{datatype}}}] } + response.headers_mut().set(Response{{nameInCamelCase}}({{name}})); +{{/headers}}{{#produces}}{{#-first}}{{#dataType}} + response.headers_mut().set(ContentType(mimetypes::responses::{{#vendorExtensions}}{{uppercase_operation_id}}_{{x-uppercaseResponseId}}{{/vendorExtensions}}.clone())); +{{/dataType}}{{/-first}}{{/produces}} + response.set_body(vec); + }, + // It's possible that errors were received in the stream, if this is the case then we can't return a success response to the client and instead must return an internal error. + Err(e) => { + response.set_status(StatusCode::InternalServerError); + response.set_body("An internal error occurred"); + } + } +{{/isFile}} + }, +{{/responses}} + }, + Err(_) => { + // Application code returned an error. This should not happen, as the implementation should + // return a valid response. + response.set_status(StatusCode::InternalServerError); + response.set_body("An internal error occurred"); + }, + } + + future::ok(response) + } + )) +{{^bodyParams}}{{#vendorExtensions}}{{^hasFile}} + }} + })) as Box> +{{/hasFile}}{{#hasFile}} + as Box> + }, + Err(e) => Box::new(future::ok(Response::new().with_status(StatusCode::BadRequest).with_body(format!("Couldn't read multipart body")))), + } + }) + ) +{{/hasFile}}{{/vendorExtensions}}{{/bodyParams}} +{{#bodyParams}}{{#-first}} + }, + Err(e) => Box::new(future::ok(Response::new().with_status(StatusCode::BadRequest).with_body(format!("Couldn't read body parameter {{baseName}}: {}", e)))), + } + }) + ) as Box> +{{/-first}}{{/bodyParams}} + }, + +{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} + _ => Box::new(future::ok(Response::new().with_status(StatusCode::NotFound))) as Box>, + } + } +} + +{{#apiHasFile}} +/// Utility function to get the multipart boundary marker (if any) from the Headers. +fn multipart_boundary<'a>(headers: &'a Headers) -> Option<&'a str> { + headers.get::().and_then(|content_type| { + let ContentType(ref mime) = *content_type; + if mime.type_() == mime::MULTIPART && mime.subtype() == mime::FORM_DATA { + mime.get_param(mime::BOUNDARY).map(|x| x.as_str()) + } else { + None + } + }) +} +{{/apiHasFile}} diff --git a/modules/openapi-generator/src/main/resources/rust-server/server.mustache b/modules/openapi-generator/src/main/resources/rust-server/server.mustache deleted file mode 100644 index edce03389f7..00000000000 --- a/modules/openapi-generator/src/main/resources/rust-server/server.mustache +++ /dev/null @@ -1,340 +0,0 @@ -#![allow(unused_extern_crates)] -extern crate serde_ignored; -extern crate iron; -extern crate router; -extern crate bodyparser; -extern crate urlencoded; -extern crate uuid; -extern crate chrono; -{{#apiHasFile}}extern crate multipart;{{/apiHasFile}} - -use futures::Future; -use futures::future; -use futures::{stream, Stream}; -use hyper; -use hyper::header::{Headers, ContentType}; -use self::iron::prelude::*; -use self::iron::{status, modifiers, BeforeMiddleware}; -use self::iron::url::percent_encoding::percent_decode; -use self::router::Router; -use self::urlencoded::UrlEncodedQuery; -use mimetypes; -{{#apiHasFile}}use multipart::server::{Multipart, SaveResult};{{/apiHasFile}} - -use serde_json; -{{#usesXml}}use serde_xml_rs;{{/usesXml}} - -#[allow(unused_imports)] -use std::collections::{HashMap, BTreeMap}; -#[allow(unused_imports)] -use swagger; -use std::io::Error; - -#[allow(unused_imports)] -use std::collections::BTreeSet; - -pub use swagger::auth::Authorization; -use swagger::auth::{AuthData, Scopes}; -use swagger::{ApiError, Context, XSpanId}; - -use {Api{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}, - {{operationId}}Response{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} - }; -#[allow(unused_imports)] -use models; - -header! { (Warning, "Warning") => [String] } - -/// Create a new router for `Api` -pub fn router(api: T) -> Router where T: Api + Send + Sync + Clone + 'static { - let mut router = Router::new(); - add_routes(&mut router, api); - router -} - -/// Add routes for `Api` to a provided router. -/// -/// Note that these routes are added straight onto the router. This means that if the router -/// already has a route for an endpoint which clashes with those provided by this API, then the -/// old route will be lost. -/// -/// It is generally a bad idea to add routes in this way to an existing router, which may have -/// routes on it for other APIs. Distinct APIs should be behind distinct paths to encourage -/// separation of interfaces, which this function does not enforce. APIs should not overlap. -/// -/// Alternative approaches include: -/// -/// - generate an `iron::middleware::Handler` (usually a `router::Router` or -/// `iron::middleware::chain`) for each interface, and add those handlers inside an existing -/// router, mounted at different paths - so the interfaces are separated by path -/// - use a different instance of `iron::Iron` for each interface - so the interfaces are -/// separated by the address/port they listen on -/// -/// This function exists to allow legacy code, which doesn't separate its APIs properly, to make -/// use of this crate. -#[deprecated(note="APIs should not overlap - only for use in legacy code.")] -pub fn route(router: &mut Router, api: T) where T: Api + Send + Sync + Clone + 'static { - add_routes(router, api) -} - -/// Add routes for `Api` to a provided router -fn add_routes(router: &mut Router, api: T) where T: Api + Send + Sync + Clone + 'static { -{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} - let api_clone = api.clone(); - router.{{#vendorExtensions}}{{httpmethod}}{{/vendorExtensions}}( - "{{#vendorExtensions}}{{basePathWithoutHost}}{{path}}{{/vendorExtensions}}", - move |req: &mut Request| { - let mut context = Context::default(); - - // Helper function to provide a code block to use `?` in (to be replaced by the `catch` block when it exists). - fn handle_request(req: &mut Request, api: &T, context: &mut Context) -> Result where T: Api { - - context.x_span_id = Some(req.headers.get::().map(XSpanId::to_string).unwrap_or_else(|| self::uuid::Uuid::new_v4().to_string())); - context.auth_data = req.extensions.remove::(); - context.authorization = req.extensions.remove::(); - - {{#hasAuthMethods}} - let authorization = context.authorization.as_ref().ok_or_else(|| { - Response::with(( - status::Forbidden, - "Unauthenticated".to_string() - )) - })?; - - {{#authMethods}} - {{#isOAuth}} - // Authorization - if let Scopes::Some(ref scopes) = authorization.scopes { - let required_scopes: BTreeSet = vec![ - {{#scopes}} - "{{scope}}".to_string(), // {{description}} - {{/scopes}} - ].into_iter().collect(); - - if !required_scopes.is_subset(scopes) { - let missing_scopes = required_scopes.difference(scopes); - return Err(Response::with(( - status::Forbidden, - missing_scopes.fold( - "Insufficient authorization, missing scopes".to_string(), - |s, scope| format!("{} {}", s, scope) - ) - ))); - } - } - {{/isOAuth}} - {{/authMethods}} - {{/hasAuthMethods}} - -{{#pathParams}}{{#-first}} - // Path parameters -{{/-first}} let param_{{paramName}} = { - let param = req.extensions.get::().ok_or_else(|| Response::with((status::InternalServerError, "An internal error occurred".to_string())))? - .find("{{baseName}}").ok_or_else(|| Response::with((status::BadRequest, "Missing path parameter {{baseName}}".to_string())))?; - percent_decode(param.as_bytes()).decode_utf8() - .map_err(|_| Response::with((status::BadRequest, format!("Couldn't percent-decode path parameter as UTF-8: {}", param))))? - .parse().map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse path parameter {{baseName}}: {}", e))))? - }; -{{/pathParams}} -{{#headerParams}}{{#-first}} - // Header parameters -{{/-first}} header! { (Request{{vendorExtensions.typeName}}, "{{baseName}}") => {{#isListContainer}}({{{baseType}}})*{{/isListContainer}}{{^isListContainer}}[{{{dataType}}}]{{/isListContainer}} } -{{#required}} let param_{{paramName}} = req.headers.get::().ok_or_else(|| Response::with((status::BadRequest, "Missing or invalid required header {{baseName}}".to_string())))?.0.clone(); -{{/required}}{{^required}} let param_{{paramName}} = req.headers.get::().map(|header| header.0.clone()); -{{/required}}{{/headerParams}} -{{#queryParams}}{{#-first}} - // Query parameters (note that non-required or collection query parameters will ignore garbage values, rather than causing a 400 response) - let query_params = req.get::().unwrap_or_default(); -{{/-first}} let param_{{paramName}} = query_params.get("{{baseName}}") -{{#required}} .ok_or_else(|| Response::with((status::BadRequest, "Missing required query parameter {{baseName}}".to_string())))? -{{#isListContainer}} .iter().flat_map(|x| x.parse::<{{{baseType}}}>()).collect::>(); -{{/isListContainer}}{{^isListContainer}} .first().ok_or_else(|| Response::with((status::BadRequest, "Required query parameter {{baseName}} was empty".to_string())))? - .parse::<{{{dataType}}}>().map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse query parameter {{baseName}} - doesn't match schema: {}", e))))?; -{{/isListContainer}}{{/required}}{{^required}}{{#isListContainer}} .map(|list| list.iter().flat_map(|x| x.parse::<{{{baseType}}}>()).collect::>()); -{{/isListContainer}}{{^isListContainer}} .and_then(|list| list.first()).and_then(|x| x.parse::<{{{dataType}}}>().ok()); -{{/isListContainer}}{{/required}}{{/queryParams}} -{{#bodyParams}}{{#-first}} // Body parameters (note that non-required body parameters will ignore garbage - // values, rather than causing a 400 response). Produce warning header and logs for - // any unused fields. -{{/-first}}{{#required}} - let param_{{paramName}} = req.get::().map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse body parameter {{baseName}} - not valid UTF-8: {}", e))))?; -{{/required}}{{^required}} - let param_{{paramName}} = req.get::().unwrap_or(None); -{{/required}}{{#vendorExtensions}}{{^consumesPlainText}} - let mut unused_elements = Vec::new(); - - let param_{{paramName}} = if let Some(param_{{paramName}}_raw) = param_{{paramName}} { {{#consumesXml}} - let deserializer = &mut serde_xml_rs::de::Deserializer::new_from_reader(param_{{paramName}}_raw.as_bytes());{{/consumesXml}}{{#consumesJson}} - let deserializer = &mut serde_json::Deserializer::from_str(¶m_{{paramName}}_raw);{{/consumesJson}} - - let param_{{paramName}}: Option<{{{dataType}}}> = serde_ignored::deserialize(deserializer, |path| { - warn!("Ignoring unknown field in body: {}", path); - unused_elements.push(path.to_string()); - }){{#required}}.map_err(|e| Response::with((status::BadRequest, format!("Couldn't parse body parameter {{baseName}} - doesn't match schema: {}", e))))?{{/required}}{{^required}}.unwrap_or(None){{/required}}; - - param_{{paramName}} - } else { - None - };{{/consumesPlainText}}{{/vendorExtensions}}{{#required}} - let param_{{paramName}} = param_{{paramName}}.ok_or_else(|| Response::with((status::BadRequest, "Missing required body parameter {{baseName}}".to_string())))?{{/required}}; - -{{/bodyParams}} -{{#formParams}} - {{#-first}} - // Form parameters -{{/-first}}{{/formParams}}{{#vendorExtensions}}{{#hasFile}} - // Expecting a multipart form, extract and parse it now. - let mut entries = match Multipart::from_request(req) { - Ok(mut multipart) => { - - match multipart.save().temp() { - SaveResult::Full(entries) => { - Ok(entries) - }, - _ => { - Err(Response::with((status::InternalServerError, format!("Unable to process all message parts")))) - }, - } - }, - Err(e) => { - // Unable to parse as multipart - Err(Response::with((status::BadRequest, format!("Couldn't parse body as multipart")))) - } - }?; - -{{/hasFile}}{{/vendorExtensions}}{{#allParams}}{{#isFormParam}}{{#isFile}} - - let param_{{paramName}} = entries.fields.remove("{{paramName}}"); - - let param_{{paramName}} = match param_{{paramName}} { - Some(body) => { - Ok({let bytes = body.as_bytes(); - {{^required}}Some({{/required}} - Box::new(stream::once(Ok(bytes.to_vec()))) as Box, Error=Error> + Send> - {{^required}}){{/required}}} - ) - } - None => {Err(Response::with((status::BadRequest, format!("Body part not found!"))))} - }?; -{{/isFile}} - let param_{{paramName}} = {{#isFile}}{{^required}}Box::new(future::ok({{/required}}param_{{paramName}}{{^required}})){{/required}};{{/isFile}}{{^isFile}}{{^isContainer}}{{#vendorExtensions}}{{{example}}};{{/vendorExtensions}}{{/isContainer}}{{#isListContainer}}{{#required}}Vec::new();{{/required}}{{^required}}None;{{/required}}{{/isListContainer}}{{#isMapContainer}}None;{{/isMapContainer}}{{/isFile}} - {{/isFormParam}} -{{/allParams}} - - match api.{{#vendorExtensions}}{{operation_id}}{{/vendorExtensions}}({{#allParams}}param_{{paramName}}{{#isListContainer}}.as_ref(){{/isListContainer}}, {{/allParams}}context).wait() { - Ok(rsp) => match rsp { -{{#responses}} - {{operationId}}Response::{{#vendorExtensions}}{{x-responseId}}{{/vendorExtensions}}{{#dataType}}{{^headers}}(body){{/headers}}{{#headers}}{{#-first}}{ body{{/-first}}{{/headers}}{{/dataType}}{{#headers}}{{#-first}}{{^dataType}}{ {{/dataType}}{{#dataType}}, {{/dataType}}{{/-first}}{{^-first}}, {{/-first}}{{name}}{{#-last}} }{{/-last}}{{/headers}} => { -{{^isFile}} -{{#dataType}}{{#vendorExtensions}}{{#producesPlainText}} let body_string = body; -{{/producesPlainText}}{{#producesXml}} -{{^has_namespace}} let body_string = serde_xml_rs::to_string(&body).expect("impossible to fail to serialize");{{/has_namespace}}{{#has_namespace}} - let mut namespaces = BTreeMap::new(); - // An empty string is used to indicate a global namespace in xmltree. - namespaces.insert("".to_string(), models::namespaces::{{uppercase_data_type}}.clone()); - let body_string = serde_xml_rs::to_string_with_namespaces(&body, namespaces).expect("impossible to fail to serialize");{{/has_namespace}}{{/producesXml}}{{#producesJson}} - let body_string = serde_json::to_string(&body).expect("impossible to fail to serialize");{{/producesJson}}{{/vendorExtensions}}{{/dataType}} - - let mut response = Response::with((status::Status::from_u16({{code}}){{#dataType}}, body_string{{/dataType}}));{{/isFile}}{{#isFile}} body.fold(Vec::new(), |mut body, chunk| { - body.extend(chunk.iter()); - future::ok::, Error>(body) - }) - - // Block whilst waiting for the stream to complete - .wait() - - // It's possible that errors were received in the stream, if this is the case then we can't return a success response to the client and instead must return an internal error. - .map_err(|_| Response::with((status::InternalServerError, "An internal error occurred".to_string()))) - - // Assuming no errors then create an Iron response. - .map(|rsp| { - let mut response = Response::new(); - response.status = Some(status::Status::from_u16({{code}})); - response.body = Some(Box::new(rsp)); -{{/isFile}} -{{#headers}}{{#isFile}} {{/isFile}} header! { (Response{{nameInCamelCase}}, "{{baseName}}") => [{{{datatype}}}] } -{{#isFile}} {{/isFile}} response.headers.set(Response{{nameInCamelCase}}({{name}})); -{{/headers}} - {{#produces}}{{#-first}} -{{#dataType}}{{#isFile}} {{/isFile}} response.headers.set(ContentType(mimetypes::responses::{{#vendorExtensions}}{{uppercase_operation_id}}_{{x-uppercaseResponseId}}{{/vendorExtensions}}.clone()));{{/dataType}} -{{/-first}}{{/produces}} -{{#isFile}} {{/isFile}} context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); -{{#bodyParams}}{{#vendorExtensions}}{{^consumesPlainText}} if !unused_elements.is_empty() { - response.headers.set(Warning(format!("Ignoring unknown fields in body: {:?}", unused_elements))); - }{{/consumesPlainText}}{{/vendorExtensions}}{{/bodyParams}} - {{^isFile}}Ok(response){{/isFile}}{{#isFile}} response - }){{/isFile}} - }, -{{/responses}} - }, - Err(_) => { - // Application code returned an error. This should not happen, as the implementation should - // return a valid response. - Err(Response::with((status::InternalServerError, "An internal error occurred".to_string()))) - } - } - } - - handle_request(req, &api_clone, &mut context).or_else(|mut response| { - context.x_span_id.as_ref().map(|header| response.headers.set(XSpanId(header.clone()))); - Ok(response) - }) - }, - "{{operationId}}"); -{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} -} - -/// Middleware to extract authentication data from request -pub struct ExtractAuthData; - -impl BeforeMiddleware for ExtractAuthData { - fn before(&self, req: &mut Request) -> IronResult<()> { - {{#authMethods}} - {{#isBasic}} - { - use hyper::header::{Authorization, Basic, Bearer}; - use std::ops::Deref; - if let Some(basic) = req.headers.get::>() { - req.extensions.insert::(AuthData::Basic(basic.deref().clone())); - return Ok(()); - } - } - {{/isBasic}} - {{#isOAuth}} - { - use hyper::header::{Authorization, Basic, Bearer}; - use std::ops::Deref; - if let Some(bearer) = req.headers.get::>() { - req.extensions.insert::(AuthData::Bearer(bearer.deref().clone())); - return Ok(()); - } - } - {{/isOAuth}} - {{#isApiKey}} - {{#isKeyInHeader}} - { - header! { (ApiKey{{-index}}, "{{keyParamName}}") => [String] } - if let Some(header) = req.headers.get::() { - req.extensions.insert::(AuthData::ApiKey(header.0.clone())); - return Ok(()); - } - } - {{/isKeyInHeader}} - {{#isKeyInQuery}} - { - let header = match req.get_ref::() { - Ok(query) => query.get("{{keyParamName}}").map(|v| v[0].clone()), - _ => None - }; - if let Some(key) = header { - req.extensions.insert::(AuthData::ApiKey(key)); - return Ok(()); - } - } - {{/isKeyInQuery}} - {{/isApiKey}} - {{/authMethods}} - - Ok(()) - } -} diff --git a/modules/openapi-generator/src/main/resources/rust-server/swagger.mustache b/modules/openapi-generator/src/main/resources/rust-server/swagger.mustache index 51ebafb0187..51560926bba 100644 --- a/modules/openapi-generator/src/main/resources/rust-server/swagger.mustache +++ b/modules/openapi-generator/src/main/resources/rust-server/swagger.mustache @@ -1 +1 @@ -{{{openapi-yaml}}} \ No newline at end of file +{{{swagger-yaml}}} \ No newline at end of file