diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSlim4ServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSlim4ServerCodegen.java index d45bf289e7f2..9105a4fe8de4 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSlim4ServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PhpSlim4ServerCodegen.java @@ -16,6 +16,14 @@ package org.openapitools.codegen.languages; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.servers.Server; +import org.apache.commons.lang3.StringEscapeUtils; +import org.apache.commons.lang3.StringUtils; +import org.openapitools.codegen.CodegenType; +import org.openapitools.codegen.CodegenOperation; +import org.openapitools.codegen.CodegenSecurity; import org.openapitools.codegen.CliOption; import org.openapitools.codegen.CodegenConstants; import org.openapitools.codegen.SupportingFile; @@ -26,12 +34,22 @@ import org.slf4j.LoggerFactory; import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.*; -public class PhpSlim4ServerCodegen extends PhpSlimServerCodegen { +import static org.openapitools.codegen.utils.StringUtils.camelize; + +public class PhpSlim4ServerCodegen extends AbstractPhpCodegen { private final Logger LOGGER = LoggerFactory.getLogger(PhpSlim4ServerCodegen.class); + public static final String USER_CLASSNAME_KEY = "userClassname"; public static final String PSR7_IMPLEMENTATION = "psr7Implementation"; + protected String groupId = "org.openapitools"; + protected String artifactId = "openapi-server"; + protected String authDirName = "Auth"; + protected String authPackage = ""; protected String psr7Implementation = "slim-psr7"; protected String interfacesDirName = "Interfaces"; protected String interfacesPackage = ""; @@ -40,6 +58,18 @@ public PhpSlim4ServerCodegen() { super(); modifyFeatureSet(features -> features + .includeDocumentationFeatures(DocumentationFeature.Readme) + .wireFormatFeatures(EnumSet.of(WireFormatFeature.JSON, WireFormatFeature.XML)) + .securityFeatures(EnumSet.noneOf(SecurityFeature.class)) + .excludeGlobalFeatures( + GlobalFeature.XMLStructureDefinitions, + GlobalFeature.Callbacks, + GlobalFeature.LinkObjects, + GlobalFeature.ParameterStyling + ) + .excludeSchemaSupportFeatures( + SchemaSupportFeature.Polymorphism + ) .includeClientModificationFeatures(ClientModificationFeature.MockServer) ); @@ -47,10 +77,29 @@ public PhpSlim4ServerCodegen() { .stability(Stability.STABLE) .build(); + // clear import mapping (from default generator) as slim does not use it + // at the moment + importMapping.clear(); + + variableNamingConvention = "camelCase"; + artifactVersion = "1.0.0"; + setInvokerPackage("OpenAPIServer"); + apiPackage = invokerPackage + "\\" + apiDirName; + modelPackage = invokerPackage + "\\" + modelDirName; + authPackage = invokerPackage + "\\" + authDirName; interfacesPackage = invokerPackage + "\\" + interfacesDirName; outputFolder = "generated-code" + File.separator + "slim4"; + + modelTestTemplateFiles.put("model_test.mustache", ".php"); + // no doc files + modelDocTemplateFiles.clear(); + apiDocTemplateFiles.clear(); + embeddedTemplateDir = templateDir = "php-slim4-server"; + additionalProperties.put(CodegenConstants.GROUP_ID, groupId); + additionalProperties.put(CodegenConstants.ARTIFACT_ID, artifactId); + // override cliOptions from AbstractPhpCodegen updateOption(AbstractPhpCodegen.VARIABLE_NAMING_CONVENTION, "camelCase"); @@ -68,6 +117,11 @@ public PhpSlim4ServerCodegen() { cliOptions.add(psr7Option); } + @Override + public CodegenType getTag() { + return CodegenType.SERVER; + } + @Override public String getName() { return "php-slim4"; @@ -78,15 +132,39 @@ public String getHelp() { return "Generates a PHP Slim 4 Framework server library(with Mock server)."; } + @Override + public String apiFileFolder() { + if (apiPackage.startsWith(invokerPackage + "\\")) { + // need to strip out invokerPackage from path + return (outputFolder + File.separator + toSrcPath(StringUtils.removeStart(apiPackage, invokerPackage + "\\"), srcBasePath)); + } + return (outputFolder + File.separator + toSrcPath(apiPackage, srcBasePath)); + } + + @Override + public String modelFileFolder() { + if (modelPackage.startsWith(invokerPackage + "\\")) { + // need to strip out invokerPackage from path + return (outputFolder + File.separator + toSrcPath(StringUtils.removeStart(modelPackage, invokerPackage + "\\"), srcBasePath)); + } + return (outputFolder + File.separator + toSrcPath(modelPackage, srcBasePath)); + } + @Override public void processOpts() { super.processOpts(); if (additionalProperties.containsKey(CodegenConstants.INVOKER_PACKAGE)) { + // Update the invokerPackage for the default authPackage + authPackage = invokerPackage + "\\" + authDirName; // Update interfacesPackage interfacesPackage = invokerPackage + "\\" + interfacesDirName; } + // make auth src path available in mustache template + additionalProperties.put("authPackage", authPackage); + additionalProperties.put("authSrcPath", "./" + toSrcPath(authPackage, srcBasePath)); + // same for interfaces package additionalProperties.put("interfacesPackage", interfacesPackage); additionalProperties.put("interfacesSrcPath", "./" + toSrcPath(interfacesPackage, srcBasePath)); @@ -121,6 +199,14 @@ public void processOpts() { additionalProperties.put("isSlimPsr7", Boolean.TRUE); } + supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); + supportingFiles.add(new SupportingFile("composer.mustache", "", "composer.json")); + supportingFiles.add(new SupportingFile("index.mustache", "", "index.php")); + supportingFiles.add(new SupportingFile(".htaccess", "", ".htaccess")); + supportingFiles.add(new SupportingFile("SlimRouter.mustache", toSrcPath(invokerPackage, srcBasePath), "SlimRouter.php")); + supportingFiles.add(new SupportingFile("phpunit.xml.mustache", "", "phpunit.xml.dist")); + supportingFiles.add(new SupportingFile("phpcs.xml.mustache", "", "phpcs.xml.dist")); + // Slim 4 doesn't parse JSON body anymore we need to add suggested middleware // ref: https://www.slimframework.com/docs/v4/objects/request.html#the-request-body supportingFiles.add(new SupportingFile("htaccess_deny_all", "config", ".htaccess")); @@ -131,6 +217,126 @@ public void processOpts() { supportingFiles.add(new SupportingFile("base_model_test.mustache", toSrcPath(invokerPackage, testBasePath), "BaseModelTest.php")); } + @Override + public Map postProcessOperationsWithModels(Map objs, List allModels) { + Map operations = (Map) objs.get("operations"); + List operationList = (List) operations.get("operation"); + addUserClassnameToOperations(operations); + escapeMediaType(operationList); + return objs; + } + + @Override + public Map postProcessSupportingFileData(Map objs) { + Map apiInfo = (Map) objs.get("apiInfo"); + List> apiList = (List>) apiInfo.get("apis"); + for (HashMap api : apiList) { + HashMap operations = (HashMap) api.get("operations"); + List operationList = (List) operations.get("operation"); + + // Sort operations to avoid static routes shadowing + // ref: https://github.com/nikic/FastRoute/blob/master/src/DataGenerator/RegexBasedAbstract.php#L92-L101 + Collections.sort(operationList, new Comparator() { + @Override + public int compare(CodegenOperation one, CodegenOperation another) { + if (one.getHasPathParams() && !another.getHasPathParams()) return 1; + if (!one.getHasPathParams() && another.getHasPathParams()) return -1; + return 0; + } + }); + } + return objs; + } + + @Override + public List fromSecurity(Map securitySchemeMap) { + List codegenSecurities = super.fromSecurity(securitySchemeMap); + if (Boolean.FALSE.equals(codegenSecurities.isEmpty())) { + supportingFiles.add(new SupportingFile("abstract_authenticator.mustache", toSrcPath(authPackage, srcBasePath), toAbstractName("Authenticator") + ".php")); + } + return codegenSecurities; + } + + @Override + public String toApiName(String name) { + if (name.length() == 0) { + return toAbstractName("DefaultApi"); + } + return toAbstractName(camelize(name) + "Api"); + } + + @Override + public String toApiTestFilename(String name) { + if (name.length() == 0) { + return "DefaultApiTest"; + } + return camelize(name) + "ApiTest"; + } + + /** + * Strips out abstract prefix and suffix from classname and puts it in "userClassname" property of operations object. + * + * @param operations codegen object with operations + */ + private void addUserClassnameToOperations(Map operations) { + String classname = (String) operations.get("classname"); + classname = classname.replaceAll("^" + abstractNamePrefix, ""); + classname = classname.replaceAll(abstractNameSuffix + "$", ""); + operations.put(USER_CLASSNAME_KEY, classname); + } + + @Override + public String encodePath(String input) { + if (input == null) { + return input; + } + + // from DefaultCodegen.java + // remove \t, \n, \r + // replace \ with \\ + // replace " with \" + // outter unescape to retain the original multi-byte characters + // finally escalate characters avoiding code injection + input = super.escapeUnsafeCharacters( + StringEscapeUtils.unescapeJava( + StringEscapeUtils.escapeJava(input) + .replace("\\/", "/")) + .replaceAll("[\\t\\n\\r]", " ") + .replace("\\", "\\\\")); + // .replace("\"", "\\\"")); + + // from AbstractPhpCodegen.java + // Trim the string to avoid leading and trailing spaces. + input = input.trim(); + try { + + input = URLEncoder.encode(input, "UTF-8") + .replaceAll("\\+", "%20") + .replaceAll("\\%2F", "/") + .replaceAll("\\%7B", "{") // keep { part of complex placeholders + .replaceAll("\\%7D", "}") // } part + .replaceAll("\\%5B", "[") // [ part + .replaceAll("\\%5D", "]") // ] part + .replaceAll("\\%3A", ":") // : part + .replaceAll("\\%2B", "+") // + part + .replaceAll("\\%5C\\%5Cd", "\\\\d"); // \d part + } catch (UnsupportedEncodingException e) { + // continue + LOGGER.error(e.getMessage(), e); + } + return input; + } + + @Override + public CodegenOperation fromOperation(String path, + String httpMethod, + Operation operation, + List servers) { + CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers); + op.path = encodePath(path); + return op; + } + /** * Set PSR-7 implementation package. * Ref: https://www.slimframework.com/docs/v4/concepts/value-objects.html