org.openapitools.codegen.languages.AbstractRustCodegen Maven / Gradle / Ivy
package org.openapitools.codegen.languages;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import io.swagger.v3.oas.models.media.FileSchema;
import io.swagger.v3.oas.models.media.Schema;
import org.openapitools.codegen.*;
import org.openapitools.codegen.utils.ModelUtils;
import org.openapitools.codegen.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
import java.math.BigInteger;
import java.util.*;
import java.util.function.Function;
import static org.openapitools.codegen.utils.StringUtils.*;
public abstract class AbstractRustCodegen extends DefaultCodegen implements CodegenConfig {
private final Logger LOGGER = LoggerFactory.getLogger(AbstractRustCodegen.class);
protected List charactersToAllow = Collections.singletonList("_");
protected Set keywordsThatDoNotSupportRawIdentifiers = new HashSet<>(
Arrays.asList("super", "self", "Self", "extern", "crate"));
protected String enumSuffix = "";
public AbstractRustCodegen() {
super();
// All 'Strict' and 'Reserved' keywords from https://doc.rust-lang.org/reference/keywords.html
// Note: These are case-sensitive
this.reservedWords = new HashSet<>(
Arrays.asList(
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for",
"if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return",
"self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where",
"while", "async", "await", "dyn", "abstract", "become", "box", "do", "final", "macro",
"override", "priv", "typeof", "unsized", "virtual", "yield", "try"
)
);
}
@Override
public GeneratorLanguage generatorLanguage() {
return GeneratorLanguage.RUST;
}
@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 boolean isReservedWord(String word) {
// This is overridden to take account of Rust reserved words being case-sensitive.
return word != null && reservedWords.contains(word);
}
/**
* Determine the best fitting Rust type for an integer property. This is intended for use when a specific format
* has not been defined in the specification. Where the minimum or maximum is not known then the returned type
* will default to having at least 32 bits.
*
* @param minimum The minimum value as set in the specification.
* @param exclusiveMinimum If the minimum value itself is excluded by the specification.
* @param maximum The maximum value as set in the specification.
* @param exclusiveMaximum If the maximum value itself is excluded by the specification.
* @param preferUnsigned Use unsigned types where the effective minimum is greater than or equal to zero.
* @return The Rust data type name.
*/
@VisibleForTesting
public String bestFittingIntegerType(@Nullable BigInteger minimum,
boolean exclusiveMinimum,
@Nullable BigInteger maximum,
boolean exclusiveMaximum,
boolean preferUnsigned) {
if (exclusiveMinimum) {
minimum = Optional.ofNullable(minimum).map(min -> min.add(BigInteger.ONE)).orElse(null);
}
if (exclusiveMaximum) {
maximum = Optional.ofNullable(maximum).map(max -> max.subtract(BigInteger.ONE)).orElse(null);
}
// If the minimum value is greater than or equal to zero, then it is safe to use an unsigned type
boolean guaranteedPositive = Optional.ofNullable(minimum).map(min -> min.signum() >= 0).orElse(false);
int requiredBits = Math.max(
Optional.ofNullable(minimum).map(BigInteger::bitLength).orElse(0),
Optional.ofNullable(maximum).map(BigInteger::bitLength).orElse(0)
);
// We will only enable the smaller types (less than 32 bits) if we know both the minimum and maximum
boolean knownRange = !(Objects.isNull(minimum) || Objects.isNull(maximum));
if (guaranteedPositive && preferUnsigned) {
if (requiredBits <= 8 && knownRange) {
return "u8";
} else if (requiredBits <= 16 && knownRange) {
return "u16";
} else if (requiredBits <= 32) {
return "u32";
} else if (requiredBits <= 64) {
return "u64";
} else if (requiredBits <= 128) {
return "u128";
}
} else {
if (requiredBits <= 7 && knownRange) {
return "i8";
} else if (requiredBits <= 15 && knownRange) {
return "i16";
} else if (requiredBits <= 31) {
return "i32";
} else if (requiredBits <= 63) {
return "i64";
} else if (requiredBits <= 127) {
return "i128";
}
}
throw new RuntimeException("Number is too large to fit into i128");
}
/**
* Determine if an integer property can be guaranteed to fit into an unsigned data type.
*
* @param minimum The minimum value as set in the specification.
* @param exclusiveMinimum If boundary values are excluded by the specification.
* @return True if the effective minimum is greater than or equal to zero.
*/
@VisibleForTesting
public boolean canFitIntoUnsigned(@Nullable BigInteger minimum, boolean exclusiveMinimum) {
return Optional.ofNullable(minimum).map(min -> {
if (exclusiveMinimum) {
min = min.add(BigInteger.ONE);
}
return min.signum() >= 0;
}).orElse(false);
}
public enum CasingType {CAMEL_CASE, SNAKE_CASE}
/**
* General purpose sanitizing function for Rust identifiers (fields, variables, structs, parameters, etc.).
* Rules for Rust are fairly simple:
*
* - Characters must belong to [A-Za-z0-9_]
*
- Cannot use reserved words (but can sometimes prefix with "r#")
*
- Cannot begin with a number
*
*
* @param name The input string
* @param casingType Which casing type to apply
* @param escapePrefix Prefix to escape words beginning with numbers or reserved words
* @param type The type of identifier (used for logging)
* @param allowRawIdentifiers Raw identifiers can't always be used, because of filename vs import mismatch.
* @return Sanitized string
*/
public String sanitizeIdentifier(String name, CasingType casingType, String escapePrefix, String type, boolean allowRawIdentifiers) {
String originalName = name;
Function casingFunction;
switch (casingType) {
case CAMEL_CASE:
// This probably seems odd, but it is necessary for two reasons
// Compatibility with rust-server, such that MyIDList => my_id_list => MyIdList
// Conversion from SCREAMING_SNAKE_CASE to ScreamingSnakeCase
casingFunction = (input) -> camelize(underscore(input));
break;
case SNAKE_CASE:
casingFunction = StringUtils::underscore;
break;
default:
throw new IllegalArgumentException("Unknown CasingType");
}
// Replace hyphens with underscores
name = name.replaceAll("-", "_");
// Apply special character escapes, e.g. "@type" => "At_type"
// Remove the trailing underscore if necessary
if (!Strings.isNullOrEmpty(name)) {
boolean endedWithUnderscore = name.endsWith("_");
name = escape(name, specialCharReplacements, charactersToAllow, "_");
if (!endedWithUnderscore && name.endsWith("_")) {
name = org.apache.commons.lang3.StringUtils.chop(name);
}
}
// Sanitize any other special characters that weren't replaced
name = sanitizeName(name);
// Keep track of modifications prior to casing
boolean nameWasModified = !originalName.equals(name);
// Convert casing
name = casingFunction.apply(name);
// If word starts with number add a prefix
// Note: this must be done after casing since CamelCase will strip leading underscores
if (name.matches("^\\d.*")) {
nameWasModified = true;
name = casingFunction.apply(escapePrefix + '_' + name);
}
// Escape reserved words - this is case-sensitive so must be done after casing
// There is currently a bug in Rust where this doesn't work for a few reserved words :(
// https://internals.rust-lang.org/t/raw-identifiers-dont-work-for-all-identifiers/9094
if (isReservedWord(name)) {
nameWasModified = true;
if (this.keywordsThatDoNotSupportRawIdentifiers.contains(name) || !allowRawIdentifiers) {
name = casingFunction.apply(escapePrefix + '_' + name);
} else {
name = "r#" + name;
}
}
// If the name had to be modified (not just because of casing), log the change
if (nameWasModified) {
LOGGER.warn("{} cannot be used as a {} name. Renamed to {}", casingFunction.apply(originalName), type, name);
}
return name;
}
@Override
public String getTypeDeclaration(Schema p) {
if (ModelUtils.isArraySchema(p)) {
Schema inner = ModelUtils.getSchemaItems(p);
String innerType = getTypeDeclaration(inner);
return typeMapping.get("array") + "<" + innerType + ">";
} else if (ModelUtils.isMapSchema(p)) {
Schema inner = ModelUtils.getAdditionalProperties(p);
String innerType = getTypeDeclaration(inner);
StringBuilder typeDeclaration = new StringBuilder(typeMapping.get("map")).append("<").append(typeMapping.get("string")).append(", ");
typeDeclaration.append(innerType).append(">");
return typeDeclaration.toString();
} else if (!org.apache.commons.lang3.StringUtils.isEmpty(p.get$ref())) {
String datatype;
try {
datatype = "models::" + toModelName(ModelUtils.getSimpleRef(p.get$ref()));
} catch (Exception e) {
LOGGER.warn("Error obtaining the datatype from schema (model):{}. Datatype default to Object", p);
datatype = "Object";
LOGGER.error(e.getMessage(), e);
}
return datatype;
} else if (p instanceof FileSchema) {
return typeMapping.get("file");
}
String oasType = getSchemaType(p);
if (typeMapping.containsKey(oasType)) {
return typeMapping.get(oasType);
}
if (typeMapping.containsValue(oasType)) {
return oasType;
}
if (languageSpecificPrimitives.contains(oasType)) {
return oasType;
}
return "models::" + toModelName(oasType);
}
@Override
public CodegenModel fromModel(String name, Schema model) {
LOGGER.trace("Creating model from schema: {}", model);
Map allDefinitions = ModelUtils.getSchemas(this.openAPI);
CodegenModel mdl = super.fromModel(name, model);
mdl.vendorExtensions.put("x-upper-case-name", name.toUpperCase(Locale.ROOT));
if (!org.apache.commons.lang3.StringUtils.isEmpty(model.get$ref())) {
Schema schema = allDefinitions.get(ModelUtils.getSimpleRef(model.get$ref()));
mdl.dataType = typeMapping.get(schema.getType());
}
if (ModelUtils.isArraySchema(model)) {
if (typeMapping.containsKey(mdl.arrayModelType)) {
mdl.arrayModelType = typeMapping.get(mdl.arrayModelType);
} else {
mdl.arrayModelType = toModelName(mdl.arrayModelType);
}
} else if ((!mdl.anyOf.isEmpty()) || (!mdl.oneOf.isEmpty())) {
mdl.dataType = getSchemaType(model);
}
Schema additionalProperties = ModelUtils.getAdditionalProperties(model);
if (additionalProperties != null) {
mdl.additionalPropertiesType = getTypeDeclaration(additionalProperties);
}
LOGGER.trace("Created model: {}", mdl);
return mdl;
}
@Override
public String toVarName(String name) {
// obtain the name from nameMapping directly if provided
if (nameMapping.containsKey(name)) {
return nameMapping.get(name);
}
return sanitizeIdentifier(name, CasingType.SNAKE_CASE, "param", "field/variable", true);
}
@Override
public String toParamName(String name) {
// obtain the name from parameterNameMapping directly if provided
if (parameterNameMapping.containsKey(name)) {
return parameterNameMapping.get(name);
}
return sanitizeIdentifier(name, CasingType.SNAKE_CASE, "param", "parameter", true);
}
@Override
public String toOperationId(String operationId) {
return sanitizeIdentifier(operationId, CasingType.SNAKE_CASE, "call", "method", true);
}
//// Model naming ////
protected String addModelNamePrefixAndSuffix(String name) {
if (!Strings.isNullOrEmpty(modelNamePrefix)) {
name = modelNamePrefix + "_" + name;
}
if (!Strings.isNullOrEmpty(modelNameSuffix)) {
name = name + "_" + modelNameSuffix;
}
return name;
}
@Override
public String toModelName(String name) {
return sanitizeIdentifier(addModelNamePrefixAndSuffix(name), CasingType.CAMEL_CASE, "model", "model", false);
}
@Override
public String toModelFilename(String name) {
return sanitizeIdentifier(addModelNamePrefixAndSuffix(name), CasingType.SNAKE_CASE, "model", "model file", false);
}
@Override
public String toModelDocFilename(String name) {
return toModelName(name);
}
//// Enum naming ////
@Override
public String toEnumVarName(String name, String datatype) {
if (enumNameMapping.containsKey(name)) {
return enumNameMapping.get(name);
}
// Empty strings need to be mapped to "Empty"
// https://github.com/OpenAPITools/openapi-generator/issues/13453
if (Strings.isNullOrEmpty(name)) {
return "Empty";
}
// Rust Enum variants should be camel cased
return sanitizeIdentifier(name, CasingType.CAMEL_CASE, "variant", "enum variant", true);
}
@Override
public String toEnumName(CodegenProperty property) {
// Note: Strangely this function is only used for inline enums, schema enums go through the toModelName function
String name = property.baseName;
if (!Strings.isNullOrEmpty(enumSuffix)) {
name = name + "_" + enumSuffix;
}
return sanitizeIdentifier(name, CasingType.CAMEL_CASE, "enum", "enum", false);
}
@Override
public String toEnumValue(String value, String datatype) {
// This is the representation of the enum that will be serialized / deserialized
// Note: generators currently only support string enums, so checking the type here is pointless
return escapeText(value);
}
@Override
public String toEnumDefaultValue(String value, String datatype) {
// TODO: Bug: currently the templates ignore this function and just use `Self::{{ enumVars.0.name }}`
// Return the Rust type name of the variant so we can `impl Default` with it
return toEnumVarName(value, datatype);
}
//// API naming ////
protected String addApiNamePrefixAndSuffix(String name) {
if (Strings.isNullOrEmpty(name)) {
name = "default";
}
if (!Strings.isNullOrEmpty(apiNamePrefix)) {
name = apiNamePrefix + "_" + name;
}
if (!Strings.isNullOrEmpty(apiNameSuffix)) {
name = name + "_" + apiNameSuffix;
}
return name;
}
@Override
public String toApiName(String name) {
return sanitizeIdentifier(addApiNamePrefixAndSuffix(name), CasingType.CAMEL_CASE, "api", "API", false);
}
@Override
public String toApiFilename(String name) {
return sanitizeIdentifier(addApiNamePrefixAndSuffix(name), CasingType.SNAKE_CASE, "api", "API file", false);
}
@Override
public String toApiDocFilename(String name) {
return toApiName(name);
}
@Override
public String addRegularExpressionDelimiter(String pattern) {
return pattern;
}
@Override
public String escapeReservedWord(String name) {
if (this.reservedWordsMappings().containsKey(name)) {
return this.reservedWordsMappings().get(name);
}
return "r#" + name;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy