package ru.touchin.codegen; import io.swagger.codegen.v3.*; import io.swagger.codegen.v3.generators.DefaultCodegenConfig; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.media.*; import io.swagger.v3.oas.models.parameters.RequestBody; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.text.WordUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; public class TINetworkingCodegen extends DefaultCodegenConfig { protected static final Logger LOGGER = LoggerFactory.getLogger(TINetworkingCodegen.class); public static final String PROJECT_NAME = "projectName"; public static final String RESPONSE_AS = "responseAs"; protected String projectName = "SwaggerAPI"; private String[] responseAs = new String[0]; protected String sourceFolder = "Classes" + File.separator + "Swaggers"; private Map allCustomDateFormats = new HashMap<>(); @Override public CodegenType getTag() { return CodegenType.CLIENT; } @Override public String getName() { return "TINetworking"; } @Override public String getHelp() { return "Generates a TINetworking client library."; } @Override protected void addAdditionPropertiesToCodeGenModel(CodegenModel codegenModel, Schema swaggerModel) { final Object additionalProperties = swaggerModel.getAdditionalProperties(); if (additionalProperties != null && additionalProperties instanceof Schema) { codegenModel.additionalPropertiesType = getSchemaType((Schema) additionalProperties); } } /** * Constructor for the TINetworking language codegen module. */ public TINetworkingCodegen() { super(); outputFolder = "generated-code" + File.separator + "swift"; modelTemplateFiles.put("model.mustache", ".swift"); apiTemplateFiles.put("api.mustache", ".swift"); apiPackage = File.separator + "APIs"; languageSpecificPrimitives = new HashSet<>( Arrays.asList( "Int", "Int32", "Int64", "Float", "Double", "Bool", "Void", "String", "Character", "AnyObject", "Any") ); defaultIncludes = new HashSet<>( Arrays.asList( "Data", "Date", "URL", // for file "UUID", "Array", "Dictionary", "Set", "Any", "Empty", "AnyObject", "Any") ); reservedWords = new HashSet<>( Arrays.asList( // // Swift keywords. This list is taken from here: // https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/LexicalStructure.html#//apple_ref/doc/uid/TP40014097-CH30-ID410 // // Keywords used in declarations "associatedtype", "class", "deinit", "enum", "extension", "fileprivate", "func", "import", "init", "inout", "internal", "let", "open", "operator", "private", "protocol", "public", "static", "struct", "subscript", "typealias", "var", // Keywords uses in statements "break", "case", "continue", "default", "defer", "do", "else", "fallthrough", "for", "guard", "if", "in", "repeat", "return", "switch", "where", "while", // Keywords used in expressions and types "as", "Any", "catch", "false", "is", "nil", "rethrows", "super", "self", "Self", "throw", "throws", "true", "try", // Keywords used in patterns "_", // Keywords that begin with a number sign "#available", "#colorLiteral", "#column", "#else", "#elseif", "#endif", "#file", "#fileLiteral", "#function", "#if", "#imageLiteral", "#line", "#selector", "#sourceLocation", // Keywords reserved in particular contexts "associativity", "convenience", "dynamic", "didSet", "final", "get", "infix", "indirect", "lazy", "left", "mutating", "none", "nonmutating", "optional", "override", "postfix", "precedence", "prefix", "Protocol", "required", "right", "set", "Type", "unowned", "weak", "willSet", // // Swift Standard Library types // https://developer.apple.com/documentation/swift // // Numbers and Basic Values "Bool", "Int", "Double", "Float", "Range", "ClosedRange", "Error", "Optional", // Special-Use Numeric Types "UInt", "UInt8", "UInt16", "UInt32", "UInt64", "Int8", "Int16", "Int32", "Int64", "Float80", "Float32", "Float64", // Strings and Text "String", "Character", "Unicode", "StaticString", // Collections "Array", "Dictionary", "Set", "OptionSet", "CountableRange", "CountableClosedRange", // The following are commonly-used Foundation types "URL", "Data", "Date", "TimeInterval", "Codable", "Encodable", "Decodable", // The following are other words we want to reserve "Void", "AnyObject", "Class", "dynamicType", "COLUMN", "FILE", "FUNCTION", "LINE" ) ); typeMapping = new HashMap<>(); typeMapping.put("array", "Array"); typeMapping.put("List", "Array"); typeMapping.put("map", "Dictionary"); typeMapping.put("date", "Date"); typeMapping.put("Date", "Date"); typeMapping.put("DateTime", "Date"); typeMapping.put("boolean", "Bool"); typeMapping.put("string", "String"); typeMapping.put("char", "Character"); typeMapping.put("short", "Int"); typeMapping.put("int", "Int"); typeMapping.put("long", "Int64"); typeMapping.put("integer", "Int"); typeMapping.put("Integer", "Int"); typeMapping.put("float", "Float"); typeMapping.put("number", "Double"); typeMapping.put("double", "Double"); typeMapping.put("object", "Any"); typeMapping.put("Object", "Any"); typeMapping.put("file", "URL"); typeMapping.put("binary", "Data"); typeMapping.put("ByteArray", "Data"); typeMapping.put("UUID", "UUID"); typeMapping.put("URI", "String"); typeMapping.put("BigDecimal", "Decimal"); importMapping = new HashMap<>(); cliOptions.add(new CliOption(PROJECT_NAME, "Project name in Xcode")); cliOptions.add(new CliOption(CodegenConstants.HIDE_GENERATION_TIMESTAMP, CodegenConstants.HIDE_GENERATION_TIMESTAMP_DESC) .defaultValue(Boolean.TRUE.toString())); } @Override public void processOpts() { super.processOpts(); /* * 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. */ if (StringUtils.isBlank(templateDir)) { embeddedTemplateDir = templateDir = getTemplateDir(); } // Setup project name if (additionalProperties.containsKey(PROJECT_NAME)) { setProjectName((String) additionalProperties.get(PROJECT_NAME)); } else { additionalProperties.put(PROJECT_NAME, projectName); } sourceFolder = projectName + File.separator + sourceFolder; // Setup unwrapRequired option, which makes all the properties with "required" non-optional if (additionalProperties.containsKey(RESPONSE_AS)) { Object responseAsObject = additionalProperties.get(RESPONSE_AS); if (responseAsObject instanceof String) { setResponseAs(((String) responseAsObject).split(",")); } else { setResponseAs((String[]) responseAsObject); } } additionalProperties.put(RESPONSE_AS, responseAs); supportingFiles.add(new SupportingFile("Servers.mustache", sourceFolder, projectName + "+Servers.swift")); supportingFiles.add(new SupportingFile("Plugins.mustache", sourceFolder, "Plugins.swift")); copyFistAllOfProperties = true; } @Override public List supportingFiles() { List supportingFiles = super.supportingFiles(); if (!allCustomDateFormats.isEmpty()) { supportingFiles.add(new SupportingFile("APIDateFormat.mustache", sourceFolder, "APIDateFormat.swift")); } return supportingFiles; } @Override protected boolean isReservedWord(String word) { return word != null && reservedWords.contains(word); //don't lowercase as super does } @Override public String getDefaultTemplateDir() { return "TINetworking"; } @Override public String escapeReservedWord(String name) { if (this.reservedWordsMappings().containsKey(name)) { return this.reservedWordsMappings().get(name); } return "_" + name; // add an underscore to the name } @Override public String modelFileFolder() { return outputFolder + File.separator + sourceFolder + modelPackage().replace('.', File.separatorChar); } @Override public String apiFileFolder() { return outputFolder + File.separator + sourceFolder + apiPackage().replace('.', File.separatorChar); } @Override public String getTypeDeclaration(Schema prop) { if (prop instanceof ArraySchema) { ArraySchema arraySchema = (ArraySchema) prop; Schema inner = arraySchema.getItems(); return "[" + getTypeDeclaration(inner) + "]"; } else if (prop instanceof MapSchema) { MapSchema mp = (MapSchema) prop; Object inner = mp.getAdditionalProperties(); if (inner instanceof Schema) { return "[String:" + getTypeDeclaration((Schema) inner) + "]"; } } return super.getTypeDeclaration(prop); } @Override public String getSchemaType(Schema prop) { String schemaType = super.getSchemaType(prop); String type; if (typeMapping.containsKey(schemaType)) { type = typeMapping.get(schemaType); if (languageSpecificPrimitives.contains(type) || defaultIncludes.contains(type)) { return type; } } else { type = schemaType; } return toModelName(type); } @Override public CodegenParameter fromRequestBody(RequestBody body, String name, Schema schema, Map schemas, Set imports) { CodegenParameter codegenParameter = super.fromRequestBody(body, name, schema, schemas, imports); codegenParameter.description = codegenParameter.getDescription() == null ? schema.getDescription() : codegenParameter.getDescription(); return codegenParameter; } @Override public boolean isDataTypeFile(String dataType) { return dataType != null && dataType.equals("URL"); } @Override public boolean isDataTypeBinary(final String dataType) { return dataType != null && dataType.equals("Data"); } /** * Output the proper model name (capitalized). * * @param name the name of the model * @return capitalized model name */ @Override public String toModelName(String name) { // FIXME parameter should not be assigned. Also declare it as "final" name = sanitizeName(name); if (!StringUtils.isEmpty(modelNameSuffix)) { // set model suffix name = name + "_" + modelNameSuffix; } if (!StringUtils.isEmpty(modelNamePrefix)) { // set model prefix name = modelNamePrefix + "_" + name; } // camelize the model name // phone_number => PhoneNumber name = camelize(name); // model name cannot use reserved keyword, e.g. return if (isReservedWord(name)) { String modelName = escapeReservedTypeDeclaration(name); LOGGER.warn(name + " (reserved word) cannot be used as model name. Renamed to " + modelName); return modelName; } // model name starts with number if (name.matches("^\\d.*")) { // e.g. 200Response => Model200Response (after camelize) String modelName = "Model" + name; LOGGER.warn(name + " (model name starts with number) cannot be used as model name." + " Renamed to " + modelName); return modelName; } return name; } /** * Return the capitalized file name of the model. * * @param name the model name * @return the file name of the model */ @Override public String toModelFilename(String name) { // should be the same as the model name return toModelName(name); } @Override public String toDefaultValue(Schema prop) { // nil return null; } @Override public String toInstantiationType(Schema prop) { if (prop instanceof MapSchema) { MapSchema mapSchema = (MapSchema) prop; if (mapSchema.getAdditionalProperties() != null && mapSchema.getAdditionalProperties() instanceof Schema) { return getSchemaType((Schema) mapSchema.getAdditionalProperties()); } } else if (prop instanceof ArraySchema) { ArraySchema ap = (ArraySchema) prop; String inner = getSchemaType(ap.getItems()); return "[" + inner + "]"; } return null; } @Override public String toApiName(String name) { if (name.length() == 0) { return "DefaultAPI"; } return initialCaps(name) + "API"; } @Override public String toOperationId(String operationId) { operationId = camelize(sanitizeName(operationId), true); // Throw exception if method name is empty. // This should not happen but keep the check just in case if (StringUtils.isEmpty(operationId)) { throw new RuntimeException("Empty method name (operationId) not allowed"); } // method name cannot use reserved keyword, e.g. return if (isReservedWord(operationId)) { String newOperationId = camelize(("call_" + operationId), true); LOGGER.warn(operationId + " (reserved word) cannot be used as method name." + " Renamed to " + newOperationId); return newOperationId; } return operationId; } @Override public String toVarName(String name) { // sanitize name name = sanitizeName(name); // if it's all uppper case, do nothing if (name.matches("^[A-Z_]*$")) { return name; } // camelize the variable name // pet_id => petId name = camelize(name, true); // for reserved word or word starting with number, append _ if (isReservedWord(name) || name.matches("^\\d.*")) { name = escapeReservedWord(name); } return name; } @Override public String toParamName(String name) { // sanitize name name = sanitizeName(name); // replace - with _ e.g. created-at => created_at name = name.replaceAll("-", "_"); // if it's all uppper case, do nothing if (name.matches("^[A-Z_]*$")) { return name; } // camelize(lower) the variable name // pet_id => petId name = camelize(name, true); // for reserved word or word starting with number, append _ if (isReservedWord(name) || name.matches("^\\d.*")) { name = escapeReservedWord(name); } return name; } @Override public CodegenModel fromModel(String name, Schema model, Map allDefinitions) { CodegenModel codegenModel = super.fromModel(name, model, allDefinitions); if (codegenModel.description != null) { codegenModel.imports.add("ApiModel"); } if (allDefinitions != null) { String parentSchema = codegenModel.parentSchema; // multilevel inheritance: reconcile properties of all the parents while (parentSchema != null) { final Schema parentModel = allDefinitions.get(parentSchema); final CodegenModel parentCodegenModel = super.fromModel(codegenModel.parent, parentModel, allDefinitions); reconcileProperties(codegenModel, parentCodegenModel); // get the next parent parentSchema = parentCodegenModel.parentSchema; } } return codegenModel; } @Override public Map postProcessSupportingFileData(Map objs) { Map supportingFileData = super.postProcessSupportingFileData(objs); supportingFileData.put("apiDateFormats", allCustomDateFormats); return supportingFileData; } protected void updateCodegenModelEnumVars(CodegenModel codegenModel) { super.updateCodegenModelEnumVars(codegenModel); for (CodegenProperty var : codegenModel.allVars) { updateCodegenPropertyEnum(var); } } @Override public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, Map definitions, OpenAPI openAPI) { CodegenOperation codegenOperation = super.fromOperation(path, httpMethod, operation, definitions, openAPI); if (codegenOperation.returnType != null && codegenOperation.returnType.equals("Any")) { codegenOperation.returnType = null; } if (operation.getResponses() != null && !operation.getResponses().isEmpty()) { for (CodegenContent content : codegenOperation.getContents()) { ArrayList contentStatusCodes = new ArrayList<>(); for (CodegenResponse codegenResponse : codegenOperation.responses) { contentStatusCodes.add(codegenResponse.code); } content.getContentExtensions() .put("x-codegen-acceptable-status-codes", String.join(", ", contentStatusCodes)); } } return codegenOperation; } public void setProjectName(String projectName) { this.projectName = projectName; } public void setResponseAs(String[] responseAs) { this.responseAs = responseAs; } @Override public String toEnumValue(String value, String datatype) { return String.valueOf(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"; } Pattern startWithNumberPattern = Pattern.compile("^\\d+"); Matcher startWithNumberMatcher = startWithNumberPattern.matcher(name); if (startWithNumberMatcher.find()) { String startingNumbers = startWithNumberMatcher.group(0); String nameWithoutStartingNumbers = name.substring(startingNumbers.length()); return "_" + startingNumbers + camelize(nameWithoutStartingNumbers, true); } // for symbol, e.g. $, # if (getSymbolName(name) != null) { return camelize(WordUtils.capitalizeFully(getSymbolName(name).toUpperCase()), true); } // Camelize only when we have a structure defined below boolean camelized = false; if (name.matches("[A-Z][a-z0-9]+[a-zA-Z0-9]*")) { name = camelize(name, true); camelized = true; } else if (name.matches("[a-z]+((\\d)|([A-Z0-9][a-z0-9]+))*([A-Z])?")) { // already camelized: https://stackoverflow.com/a/47591707 camelized = true; } // Reserved Name String nameLowercase = StringUtils.lowerCase(name); if (isReservedWord(nameLowercase)) { return escapeReservedWord(nameLowercase); } // Check for numerical conversions if ("Int".equals(datatype) || "Int32".equals(datatype) || "Int64".equals(datatype) || "Float".equals(datatype) || "Double".equals(datatype)) { String varName = "number" + camelize(name); varName = varName.replaceAll("-", "minus"); varName = varName.replaceAll("\\+", "plus"); varName = varName.replaceAll("\\.", "dot"); return varName; } // If we have already camelized the word, don't progress // any further if (camelized) { return name; } char[] separators = {'-', '_', ' ', ':', '(', ')'}; return camelize(WordUtils.capitalizeFully(StringUtils.lowerCase(name), separators) .replaceAll("[-_ :()]", ""), true); } @Override public String toEnumName(CodegenProperty property) { String enumName = toModelName(property.name); // Ensure that the enum type doesn't match a reserved word or // the variable name doesn't match the generated enum type or the // Swift compiler will generate an error if (isReservedWord(property.datatypeWithEnum) || toVarName(property.name).equals(property.datatypeWithEnum)) { enumName = property.datatypeWithEnum + "Enum"; } // TODO: toModelName already does something for names starting with number, // so this code is probably never called if (enumName.matches("\\d.*")) { // starts with number return "_" + enumName; } else { return enumName; } } @Override public Map postProcessModels(Map objs) { Map postProcessedModelsEnum = postProcessModelsEnum(objs); // We iterate through the list of models, and also iterate through each of the // properties for each model. For each property, if: // // CodegenProperty.name != CodegenProperty.baseName // // then we set // // CodegenProperty.vendorExtensions["x-codegen-escaped-property-name"] = true // // Also, if any property in the model has x-codegen-escaped-property-name=true, then we mark: // // CodegenModel.vendorExtensions["x-codegen-has-escaped-property-names"] = true // List models = (List) postProcessedModelsEnum.get("models"); for (Object _mo : models) { Map mo = (Map) _mo; CodegenModel cm = (CodegenModel) mo.get("model"); boolean modelHasPropertyWithEscapedName = false; for (CodegenProperty prop : cm.allVars) { if (!prop.name.equals(prop.baseName)) { prop.vendorExtensions.put("x-codegen-escaped-property-name", true); modelHasPropertyWithEscapedName = true; } } if (modelHasPropertyWithEscapedName) { cm.vendorExtensions.put("x-codegen-has-escaped-property-names", true); } } return postProcessedModelsEnum; } @Override public void postProcessModelProperty(CodegenModel model, CodegenProperty property) { super.postProcessModelProperty(model, property); if (typeAliases.containsKey(property.baseType)) { Schema resolvedPropertySchema = getOpenAPI().getComponents().getSchemas().get(property.datatype); postProcessAliasProperty(property, resolvedPropertySchema); } updateVendorExtensionsForProperty(property); if (property.getIsListContainer()) { updateVendorExtensionsForProperty(property.items); } if (property.getIsObject() && isReservedWord(property.getDatatype())) { property.datatype = escapeReservedTypeDeclaration(property.getDatatype()); property.datatypeWithEnum = property.datatype; } } private void updateVendorExtensionsForProperty(CodegenProperty property) { Map vendorExtensions = property.getVendorExtensions(); if (isISO8601DateProperty(property)) { vendorExtensions.put(TINetworkingCodegenConstants.IS_ISO8601_DATE, true); } else if (isCustomDateFormatProperty(property)) { String customDateFormat = (String) vendorExtensions.get(TINetworkingCodegenConstants.DATE_FORMAT); String dateFormatName = customDateFormat.replace(".", "_") .replaceAll("[^A-Za-z0-9_]", ""); vendorExtensions.put(TINetworkingCodegenConstants.DATE_FORMAT_NAME, dateFormatName); allCustomDateFormats.put(dateFormatName, customDateFormat); } } @Override public String escapeQuotationMark(String input) { // remove " to avoid code injection return input.replace("\"", ""); } @Override public String escapeUnsafeCharacters(String input) { return input.replace("*/", "*_/").replace("/*", "/_*"); } private static void reconcileProperties(CodegenModel codegenModel, CodegenModel parentCodegenModel) { // To support inheritance in this generator, we will analyze // the parent and child models, look for properties that match, and remove // them from the child models and leave them in the parent. // Because the child models extend the parents, the properties // will be available via the parent. // Get the properties for the parent and child models final List parentModelCodegenProperties = parentCodegenModel.vars; List codegenProperties = codegenModel.vars; codegenModel.allVars = new ArrayList<>(codegenProperties); codegenModel.parentVars = parentCodegenModel.allVars; // Iterate over all of the parent model properties boolean removedChildProperty = false; for (CodegenProperty parentModelCodegenProperty : parentModelCodegenProperties) { // Now that we have found a prop in the parent class, // and search the child class for the same prop. Iterator iterator = codegenProperties.iterator(); while (iterator.hasNext()) { CodegenProperty codegenProperty = iterator.next(); if (codegenProperty.baseName.equals(parentModelCodegenProperty.baseName)) { // We found a property in the child class that is // a duplicate of the one in the parent, so remove it. iterator.remove(); removedChildProperty = true; } } } if (removedChildProperty) { // If we removed an entry from this model's vars, we need to ensure hasMore is updated int count = 0; int numVars = codegenProperties.size(); for (CodegenProperty codegenProperty : codegenProperties) { count += 1; codegenProperty.getVendorExtensions().put(CodegenConstants.HAS_MORE_EXT_NAME, count < numVars); } codegenModel.vars = codegenProperties; } } private String escapeReservedTypeDeclaration(String name) { return "Model" + name; } private void postProcessAliasProperty(CodegenProperty codegenProperty, Schema resolvedPropertySchema) { Map propertyExtensions = codegenProperty.getVendorExtensions(); propertyExtensions.put(CodegenConstants.IS_ALIAS_EXT_NAME, Boolean.TRUE); codegenProperty.setDescription(codegenProperty.getDescription() == null ? resolvedPropertySchema.getDescription() : codegenProperty.getDescription()); if (resolvedPropertySchema.getExtensions() != null) { propertyExtensions.putAll(resolvedPropertySchema.getExtensions()); } if (resolvedPropertySchema instanceof DateSchema) { propertyExtensions.put(CodegenConstants.IS_DATE_EXT_NAME, Boolean.TRUE); } if (resolvedPropertySchema instanceof DateTimeSchema) { propertyExtensions.put(CodegenConstants.IS_DATE_TIME_EXT_NAME, Boolean.TRUE); } } private boolean isISO8601DateProperty(CodegenProperty property) { return property.getIsDate() || property.getIsDateTime() && !isCustomDateFormatProperty(property); } private boolean isCustomDateFormatProperty(CodegenProperty property) { return property.getVendorExtensions().containsKey(TINetworkingCodegenConstants.DATE_FORMAT); } }