diff --git a/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/CodegenModel.java b/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/CodegenModel.java index 169886da22e3..d1bfc90ba2a3 100644 --- a/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/CodegenModel.java +++ b/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/CodegenModel.java @@ -11,6 +11,6 @@ public class CodegenModel { public String defaultValue; public List vars = new ArrayList(); public Set imports = new HashSet(); - public Boolean hasVars, emptyVars, hasMoreModels; + public Boolean hasVars, emptyVars, hasMoreModels, hasEnums; public ExternalDocs externalDocs; } \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/CodegenResponse.java b/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/CodegenResponse.java index 15931c9fd271..b23c5f72120c 100644 --- a/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/CodegenResponse.java +++ b/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/CodegenResponse.java @@ -8,6 +8,7 @@ public class CodegenResponse { public List> examples; public final List headers = new ArrayList(); public String dataType, baseType, containerType; + public Boolean isDefault; public Boolean simpleType; public Boolean primitiveType; public Boolean isMapContainer; diff --git a/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/CodegenSecurity.java b/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/CodegenSecurity.java index 889a9419c64b..00d56ea213e1 100644 --- a/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/CodegenSecurity.java +++ b/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/CodegenSecurity.java @@ -1,11 +1,10 @@ package com.wordnik.swagger.codegen; public class CodegenSecurity { - String name; - String type; - Boolean hasMore, isBasic, isOAuth, isApiKey; + public String name; + public String type; + public Boolean hasMore, isBasic, isOAuth, isApiKey; // ApiKey specific - String keyParamName; - Boolean isKeyInQuery, isKeyInHeader; - + public String keyParamName; + public Boolean isKeyInQuery, isKeyInHeader; } diff --git a/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/DefaultCodegen.java b/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/DefaultCodegen.java index 0a6e955b9555..0055c968edcc 100644 --- a/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/DefaultCodegen.java +++ b/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/DefaultCodegen.java @@ -166,6 +166,9 @@ public class DefaultCodegen { return name; } + public String toEnumName(CodegenProperty property) { + return StringUtils.capitalize(property.name) + "Enum"; + } public String escapeReservedWord(String name) { throw new RuntimeException("reserved word " + name + " not allowed"); @@ -450,6 +453,7 @@ public class DefaultCodegen { } if(impl.getProperties() != null && impl.getProperties().size() > 0) { m.hasVars = true; + m.hasEnums = false; for(String key: impl.getProperties().keySet()) { Property prop = impl.getProperties().get(key); @@ -477,6 +481,8 @@ public class DefaultCodegen { } m.vars.add(cp); count += 1; + if (cp.isEnum) + m.hasEnums = true; if(count != impl.getProperties().keySet().size()) cp.hasMore = new Boolean(true); if(cp.isContainer != null) { @@ -569,7 +575,7 @@ public class DefaultCodegen { // this can cause issues for clients which don't support enums if(property.isEnum) - property.datatypeWithEnum = StringUtils.capitalize(property.name) + "Enum"; + property.datatypeWithEnum = toEnumName(property); else property.datatypeWithEnum = property.datatype; @@ -702,7 +708,6 @@ public class DefaultCodegen { if (operation.getResponses() != null && !operation.getResponses().isEmpty()) { Response methodResponse = findMethodResponse(operation.getResponses()); - CodegenResponse methodCodegenResponse = null; for (Map.Entry entry : operation.getResponses().entrySet()) { Response response = entry.getValue(); @@ -712,9 +717,7 @@ public class DefaultCodegen { !defaultIncludes.contains(r.baseType) && !languageSpecificPrimitives.contains(r.baseType)) imports.add(r.baseType); - - if (response == methodResponse) - methodCodegenResponse = r; + r.isDefault = response == methodResponse; op.responses.add(r); } op.responses.get(op.responses.size() - 1).hasMore = false; @@ -897,6 +900,19 @@ public class DefaultCodegen { collectionFormat = qp.getCollectionFormat(); CodegenProperty pr = fromProperty("inner", inner); p.baseType = pr.datatype; + p.isContainer = true; + imports.add(pr.baseType); + } + else if("object".equals(qp.getType())) { + Property inner = qp.getItems(); + if(inner == null) { + LOGGER.warn("warning! No inner type supplied for map parameter \"" + qp.getName() + "\", using String"); + inner = new StringProperty().description("//TODO automatically added by swagger-codegen"); + } + property = new MapProperty(inner); + collectionFormat = qp.getCollectionFormat(); + CodegenProperty pr = fromProperty("inner", inner); + p.baseType = pr.datatype; imports.add(pr.baseType); } else @@ -905,6 +921,7 @@ public class DefaultCodegen { LOGGER.warn("warning! Property type \"" + qp.getType() + "\" not found for parameter \"" + param.getName() + "\", using String"); property = new StringProperty().description("//TODO automatically added by swagger-codegen. Type was " + qp.getType() + " but not supported"); } + property.setRequired(param.getRequired()); CodegenProperty model = fromProperty(qp.getName(), property); p.collectionFormat = collectionFormat; p.dataType = model.datatype; @@ -928,6 +945,7 @@ public class DefaultCodegen { else { // TODO: missing format, so this will not always work Property prop = PropertyBuilder.build(impl.getType(), null, null); + prop.setRequired(bp.getRequired()); CodegenProperty cp = fromProperty("property", prop); if(cp != null) { p.dataType = cp.datatype; @@ -941,6 +959,7 @@ public class DefaultCodegen { CodegenModel cm = fromModel(bp.getName(), impl); // get the single property ArrayProperty ap = new ArrayProperty().items(impl.getItems()); + ap.setRequired(param.getRequired()); CodegenProperty cp = fromProperty("inner", ap); if(cp.complexType != null) { imports.add(cp.complexType); @@ -1148,4 +1167,4 @@ public class DefaultCodegen { } -} +} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/languages/AkkaScalaClientCodegen.java b/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/languages/AkkaScalaClientCodegen.java new file mode 100644 index 000000000000..ee2da7aa9213 --- /dev/null +++ b/modules/swagger-codegen/src/main/java/com/wordnik/swagger/codegen/languages/AkkaScalaClientCodegen.java @@ -0,0 +1,365 @@ +package com.wordnik.swagger.codegen.languages; + +import com.google.common.base.CaseFormat; +import com.samskivert.mustache.Mustache; +import com.samskivert.mustache.Template; +import com.wordnik.swagger.codegen.*; +import com.wordnik.swagger.models.auth.SecuritySchemeDefinition; +import com.wordnik.swagger.models.properties.*; +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.util.*; + +public class AkkaScalaClientCodegen extends DefaultCodegen implements CodegenConfig { + Logger LOGGER = LoggerFactory.getLogger(AkkaScalaClientCodegen.class); + + protected String mainPackage = "io.swagger.client"; + + protected String invokerPackage = mainPackage + ".core"; + protected String groupId = "com.wordnik"; + protected String artifactId = "swagger-client"; + protected String artifactVersion = "1.0.0"; + protected String sourceFolder = "src/main/scala"; + protected String resourcesFolder = "src/main/resources"; + protected String configKey = "apiRequest"; + protected int defaultTimeoutInMs = 5000; + protected String configKeyPath = mainPackage; + + protected boolean registerNonStandardStatusCodes = true; + protected boolean renderJavadoc = true; + protected boolean removeOAuthSecurities = true; + /** + * If set to true, only the default response (the one with le lowest 2XX code) will be considered as a success, and all + * others as ApiErrors. + * If set to false, all responses defined in the model will be considered as a success upon reception. Only http errors, + * unmarshalling problems and any other RuntimeException will be considered as ApiErrors. + */ + protected boolean onlyOneSuccess = true; + + public CodegenType getTag() { + return CodegenType.CLIENT; + } + + public String getName() { + return "akka-scala"; + } + + public String getHelp() { + return "Generates a Scala client library base on Akka/Spray."; + } + + public AkkaScalaClientCodegen() { + super(); + outputFolder = "generated-code/scala"; + modelTemplateFiles.put("model.mustache", ".scala"); + apiTemplateFiles.put("api.mustache", ".scala"); + templateDir = "akka-scala"; + apiPackage = mainPackage + ".api"; + modelPackage = mainPackage + ".model"; + + reservedWords = new HashSet( + Arrays.asList( + "abstract", "case", "catch", "class", "def", "do", "else", "extends", + "false", "final", "finally", "for", "forSome", "if", "implicit", + "import", "lazy", "match", "new", "null", "object", "override", "package", + "private", "protected", "return", "sealed", "super", "this", "throw", + "trait", "try", "true", "type", "val", "var", "while", "with", "yield") + ); + + additionalProperties.put("invokerPackage", invokerPackage); + additionalProperties.put("groupId", groupId); + additionalProperties.put("artifactId", artifactId); + additionalProperties.put("artifactVersion", artifactVersion); + additionalProperties.put("configKey", configKey); + additionalProperties.put("configKeyPath", configKeyPath); + additionalProperties.put("defaultTimeout", defaultTimeoutInMs); + if (renderJavadoc) + additionalProperties.put("javadocRenderer", new JavadocLambda()); + additionalProperties.put("fnCapitalize", new CapitalizeLambda()); + additionalProperties.put("fnCamelize", new CamelizeLambda(false)); + additionalProperties.put("fnEnumEntry", new EnumEntryLambda()); + additionalProperties.put("onlyOneSuccess", onlyOneSuccess); + + supportingFiles.add(new SupportingFile("pom.mustache", "", "pom.xml")); + supportingFiles.add(new SupportingFile("reference.mustache", resourcesFolder, "reference.conf")); + final String invokerFolder = (sourceFolder + File.separator + invokerPackage).replace(".", File.separator); + supportingFiles.add(new SupportingFile("apiRequest.mustache", invokerFolder, "ApiRequest.scala")); + supportingFiles.add(new SupportingFile("apiInvoker.mustache", invokerFolder, "ApiInvoker.scala")); + supportingFiles.add(new SupportingFile("requests.mustache", invokerFolder, "requests.scala")); + supportingFiles.add(new SupportingFile("apiSettings.mustache", invokerFolder, "ApiSettings.scala")); + final String apiFolder = (sourceFolder + File.separator + apiPackage).replace(".", File.separator); + supportingFiles.add(new SupportingFile("enumsSerializers.mustache", apiFolder, "EnumsSerializers.scala")); + + importMapping.remove("Seq"); + importMapping.remove("List"); + importMapping.remove("Set"); + importMapping.remove("Map"); + + importMapping.put("DateTime", "org.joda.time.DateTime"); + + typeMapping = new HashMap(); + typeMapping.put("array", "Seq"); + typeMapping.put("set", "Set"); + typeMapping.put("boolean", "Boolean"); + typeMapping.put("string", "String"); + typeMapping.put("int", "Int"); + typeMapping.put("integer", "Int"); + typeMapping.put("long", "Long"); + typeMapping.put("float", "Float"); + typeMapping.put("byte", "Byte"); + typeMapping.put("short", "Short"); + typeMapping.put("char", "Char"); + typeMapping.put("long", "Long"); + typeMapping.put("double", "Double"); + typeMapping.put("object", "Any"); + typeMapping.put("file", "File"); + typeMapping.put("number", "Double"); + + languageSpecificPrimitives = new HashSet( + Arrays.asList( + "String", + "boolean", + "Boolean", + "Double", + "Int", + "Long", + "Float", + "Object", + "List", + "Seq", + "Map") + ); + instantiationTypes.put("array", "ListBuffer"); + instantiationTypes.put("map", "Map"); + } + + @Override + public String escapeReservedWord(String name) { + return "`" + name + "`"; + } + + @Override + public String apiFileFolder() { + return outputFolder + "/" + sourceFolder + "/" + apiPackage().replace('.', File.separatorChar); + } + + public String modelFileFolder() { + return outputFolder + "/" + sourceFolder + "/" + modelPackage().replace('.', File.separatorChar); + } + + @Override + public Map postProcessOperations(Map objs) { + if (registerNonStandardStatusCodes) { + try { + @SuppressWarnings("unchecked") + Map> opsMap = (Map>) objs.get("operations"); + HashSet unknownCodes = new HashSet(); + for (CodegenOperation operation : opsMap.get("operation")) { + for (CodegenResponse response : operation.responses) { + if ("default".equals(response.code)) + continue; + try { + int code = Integer.parseInt(response.code); + if (code >= 600) { + unknownCodes.add(code); + } + } catch (NumberFormatException e) { + LOGGER.error("Status code is not an integer : response.code", e); + } + } + } + if (!unknownCodes.isEmpty()) { + additionalProperties.put("unknownStatusCodes", unknownCodes); + } + } catch (Exception e) { + LOGGER.error("Unable to find operations List", e); + } + } + return super.postProcessOperations(objs); + } + + @Override + public String getTypeDeclaration(Property p) { + if (p instanceof ArrayProperty) { + ArrayProperty ap = (ArrayProperty) p; + Property inner = ap.getItems(); + return getSwaggerType(p) + "[" + getTypeDeclaration(inner) + "]"; + } else if (p instanceof MapProperty) { + MapProperty mp = (MapProperty) p; + Property inner = mp.getAdditionalProperties(); + + return getSwaggerType(p) + "[String, " + getTypeDeclaration(inner) + "]"; + } + return super.getTypeDeclaration(p); + } + + @Override + public List fromSecurity(Map schemes) { + final List codegenSecurities = super.fromSecurity(schemes); + if (!removeOAuthSecurities) + return codegenSecurities; + + // Remove OAuth securities + Iterator it = codegenSecurities.iterator(); + while (it.hasNext()) { + final CodegenSecurity security = it.next(); + if (security.isOAuth) + it.remove(); + } + // Adapt 'hasMore' + it = codegenSecurities.iterator(); + while (it.hasNext()) { + final CodegenSecurity security = it.next(); + security.hasMore = it.hasNext(); + } + + if (codegenSecurities.isEmpty()) + return null; + return codegenSecurities; + } + + @Override + public String toOperationId(String operationId) { + return super.toOperationId(CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_CAMEL, operationId)); + } + + private String formatIdentifier(String name, boolean capitalized) { + String identifier = camelize(name); + if (capitalized) + identifier = StringUtils.capitalize(identifier); + if (identifier.matches("[a-zA-Z_$][\\w_$]+") && !reservedWords.contains(identifier)) + return identifier; + return escapeReservedWord(identifier); + } + + @Override + public String toParamName(String name) { return formatIdentifier(name, false); } + + @Override + public String toVarName(String name) { + return formatIdentifier(name, false); + } + + @Override + public String toEnumName(CodegenProperty property) + { + return formatIdentifier(property.baseName, true); + } + + @Override + public String getSwaggerType(Property p) { + String swaggerType = super.getSwaggerType(p); + String type; + if (typeMapping.containsKey(swaggerType)) { + type = typeMapping.get(swaggerType); + if (languageSpecificPrimitives.contains(type)) + return toModelName(type); + } else + type = swaggerType; + return toModelName(type); + } + + @Override + public String toInstantiationType(Property p) { + if (p instanceof MapProperty) { + MapProperty ap = (MapProperty) p; + String inner = getSwaggerType(ap.getAdditionalProperties()); + return instantiationTypes.get("map") + "[String, " + inner + "]"; + } else if (p instanceof ArrayProperty) { + ArrayProperty ap = (ArrayProperty) p; + String inner = getSwaggerType(ap.getItems()); + return instantiationTypes.get("array") + "[" + inner + "]"; + } else + return null; + } + + public String toDefaultValue(Property p) { + if (!p.getRequired()) + return "None"; + if (p instanceof StringProperty) + return "null"; + else if (p instanceof BooleanProperty) + return "null"; + else if (p instanceof DateProperty) + return "null"; + else if (p instanceof DateTimeProperty) + return "null"; + else if (p instanceof DoubleProperty) + return "null"; + else if (p instanceof FloatProperty) + return "null"; + else if (p instanceof IntegerProperty) + return "null"; + else if (p instanceof LongProperty) + return "null"; + else if (p instanceof MapProperty) { + MapProperty ap = (MapProperty) p; + String inner = getSwaggerType(ap.getAdditionalProperties()); + return "Map[String, " + inner + "].empty "; + } else if (p instanceof ArrayProperty) { + ArrayProperty ap = (ArrayProperty) p; + String inner = getSwaggerType(ap.getItems()); + return "Seq[" + inner + "].empty "; + } else + return "null"; + } + + private static abstract class CustomLambda implements Mustache.Lambda { + @Override + public void execute(Template.Fragment frag, Writer out) throws IOException { + final StringWriter tempWriter = new StringWriter(); + frag.execute(tempWriter); + out.write(formatFragment(tempWriter.toString())); + } + public abstract String formatFragment(String fragment); + } + + + private static class JavadocLambda extends CustomLambda { + @Override + public String formatFragment(String fragment) { + final String[] lines = fragment.split("\\r?\\n"); + final StringBuilder sb = new StringBuilder(); + sb.append(" /**\n"); + for (String line : lines) { + sb.append(" * ").append(line).append("\n"); + } + sb.append(" */\n"); + return sb.toString(); + } + } + + private static class CapitalizeLambda extends CustomLambda { + @Override + public String formatFragment(String fragment) { + return StringUtils.capitalize(fragment); + } + } + + private static class CamelizeLambda extends CustomLambda { + private final boolean capitalizeFirst; + + public CamelizeLambda(boolean capitalizeFirst) { + this.capitalizeFirst = capitalizeFirst; + } + + @Override + public String formatFragment(String fragment) { + return camelize(fragment, !capitalizeFirst); + } + } + + private class EnumEntryLambda extends CustomLambda { + @Override + public String formatFragment(String fragment) { + return formatIdentifier(fragment, true); + } + } + +} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/META-INF/services/com.wordnik.swagger.codegen.CodegenConfig b/modules/swagger-codegen/src/main/resources/META-INF/services/com.wordnik.swagger.codegen.CodegenConfig index 6535948ad88c..3bd4df773cd9 100644 --- a/modules/swagger-codegen/src/main/resources/META-INF/services/com.wordnik.swagger.codegen.CodegenConfig +++ b/modules/swagger-codegen/src/main/resources/META-INF/services/com.wordnik.swagger.codegen.CodegenConfig @@ -15,4 +15,5 @@ com.wordnik.swagger.codegen.languages.StaticDocCodegen com.wordnik.swagger.codegen.languages.StaticHtmlGenerator com.wordnik.swagger.codegen.languages.SwaggerGenerator com.wordnik.swagger.codegen.languages.SwaggerYamlGenerator -com.wordnik.swagger.codegen.languages.TizenClientCodegen \ No newline at end of file +com.wordnik.swagger.codegen.languages.TizenClientCodegen +com.wordnik.swagger.codegen.languages.AkkaScalaClientCodegen diff --git a/modules/swagger-codegen/src/main/resources/akka-scala/api.mustache b/modules/swagger-codegen/src/main/resources/akka-scala/api.mustache new file mode 100644 index 000000000000..34523b80353c --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/akka-scala/api.mustache @@ -0,0 +1,43 @@ +package {{package}} + +{{#imports}} +import {{import}} +{{/imports}} +import {{invokerPackage}}._ +import {{invokerPackage}}.CollectionFormats._ +import {{invokerPackage}}.ApiKeyLocations._ + +{{#operations}} +object {{classname}} { + +{{#operation}} +{{#javadocRenderer}} +{{>javadoc}} +{{/javadocRenderer}} + def {{operationId}}({{>methodParameters}}): ApiRequest[{{>operationReturnType}}] = + ApiRequest[{{>operationReturnType}}](ApiMethods.{{httpMethod.toUpperCase}}, "{{basePath}}", "{{path}}", {{#consumes.0}}"{{mediaType}}"{{/consumes.0}}{{^consumes}}"application/json"{{/consumes}}) + {{#authMethods}}{{#isApiKey}}.withApiKey(apiKey, "{{keyParamName}}", {{#isKeyInQuery}}QUERY{{/isKeyInQuery}}{{#isKeyInHeader}}HEADER{{/isKeyInHeader}}) + {{/isApiKey}}{{#isBasic}}.withCredentials(basicAuth) + {{/isBasic}}{{/authMethods}}{{#bodyParam}}.withBody({{paramName}}) + {{/bodyParam}}{{#formParams}}.withFormParam({{>paramCreation}}) + {{/formParams}}{{#queryParams}}.withQueryParam({{>paramCreation}}) + {{/queryParams}}{{#pathParams}}.withPathParam({{>paramCreation}}) + {{/pathParams}}{{#headerParams}}.withHeaderParam({{>paramCreation}}) + {{/headerParams}}{{#responses}}{{^isWildcard}}{{#dataType}}.with{{>responseState}}Response[{{dataType}}]({{code}}) + {{/dataType}}{{^dataType}}.with{{>responseState}}Response[Unit]({{code}}) + {{/dataType}}{{/isWildcard}}{{/responses}}{{#responses}}{{#isWildcard}}{{#dataType}}.withDefault{{>responseState}}Response[{{dataType}}]() + {{/dataType}}{{^dataType}}.withDefault{{>responseState}}Response[Unit]() + {{/dataType}}{{/isWildcard}}{{/responses}}{{^responseHeaders.isEmpty}} + object {{#fnCapitalize}}{{operationId}}{{/fnCapitalize}}Headers { {{#responseHeaders}} + def {{name}}(r: ApiReturnWithHeaders) = r.get{{^isContainer}}{{baseType}}{{/isContainer}}{{#isContainer}}String{{/isContainer}}Header("{{baseName}}"){{/responseHeaders}} + } + {{/responseHeaders.isEmpty}} +{{/operation}} + +{{#unknownStatusCodes}} + ApiInvoker.addCustomStatusCode({{value}}, isSuccess = false) +{{/unknownStatusCodes}} + +} + +{{/operations}} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/akka-scala/apiInvoker.mustache b/modules/swagger-codegen/src/main/resources/akka-scala/apiInvoker.mustache new file mode 100644 index 000000000000..0dde3673439c --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/akka-scala/apiInvoker.mustache @@ -0,0 +1,323 @@ +package {{invokerPackage}} + +import java.io.File +import java.security.cert.X509Certificate +import javax.net.ssl._ + +import akka.actor.ActorSystem +import akka.io.IO +import akka.pattern.ask +import akka.util.Timeout +import org.joda.time.DateTime +import org.joda.time.format.ISODateTimeFormat +import org.json4s.JsonAST.JString +import org.json4s._ +import org.json4s.jackson.JsonMethods._ +import org.json4s.jackson.Serialization +import spray.can.Http +import spray.can.Http.HostConnectorSetup +import spray.client.pipelining +import spray.client.pipelining._ +import spray.http.HttpEncodings._ +import spray.http.HttpHeaders.{RawHeader, `Accept-Encoding`} +import spray.http.Uri.Query +import spray.http._ +import spray.http.parser.HttpParser +import spray.httpx.encoding.{Deflate, Encoder, Gzip} +import spray.httpx.unmarshalling._ +import spray.io.ClientSSLEngineProvider + +import scala.concurrent.{ExecutionContext, Future} +import scala.reflect.ClassTag +import scala.util.control.NonFatal + +object ApiInvoker { + + def apply()(implicit system: ActorSystem): ApiInvoker = + apply(DefaultFormats + DateTimeSerializer) + def apply(serializers: Traversable[Serializer[_]])(implicit system: ActorSystem): ApiInvoker = + apply(DefaultFormats + DateTimeSerializer ++ serializers) + def apply(formats: Formats)(implicit system: ActorSystem): ApiInvoker = new ApiInvoker(formats) + + case class CustomStatusCode(value: Int, reason: String = "Application-defined status code", isSuccess: Boolean = true) + + def addCustomStatusCode(code: CustomStatusCode): Unit = addCustomStatusCode(code.value, code.reason, code.isSuccess) + + def addCustomStatusCode(code: Int, reason: String = "Application defined code", isSuccess: Boolean = true) = { + StatusCodes.getForKey(code) foreach { c => + StatusCodes.registerCustom(code, reason, reason, isSuccess, allowsEntity = true) + } + } + + /** + * Allows request execution without calling apiInvoker.execute(request) + * request.response can be used to get a future of the ApiResponse generated. + * request.result can be used to get a future of the expected ApiResponse content. If content doesn't match, a + * Future will failed with a ClassCastException + * @param request the apiRequest to be executed + */ + implicit class ApiRequestImprovements[T](request: ApiRequest[T]) { + + def response(invoker: ApiInvoker)(implicit ec: ExecutionContext, system: ActorSystem): Future[ApiResponse[T]] = + response(ec, system, invoker) + + def response(implicit ec: ExecutionContext, system: ActorSystem, invoker: ApiInvoker): Future[ApiResponse[T]] = + invoker.execute(request) + + def result[U <: T](implicit c: ClassTag[U], ec: ExecutionContext, system: ActorSystem, invoker: ApiInvoker): Future[U] = + invoker.execute(request).map(_.content).mapTo[U] + + } + + /** + * Allows transformation from ApiMethod to spray HttpMethods + * @param method the ApiMethod to be converted + */ + implicit class ApiMethodExtensions(val method: ApiMethod) { + def toSprayMethod: HttpMethod = HttpMethods.getForKey(method.value).getOrElse(HttpMethods.GET) + } + + case object DateTimeSerializer extends CustomSerializer[DateTime](format => ( { + case JString(s) => + ISODateTimeFormat.dateTimeParser().parseDateTime(s) + }, { + case d: DateTime => + JString(ISODateTimeFormat.dateTimeParser().print(d)) + })) +} + +class ApiInvoker(formats: Formats)(implicit system: ActorSystem) extends UntrustedSslContext with CustomContentTypes { + + import io.swagger.client.core.ApiInvoker._ + import io.swagger.client.core.ParametersMap._ + + implicit val ec = system.dispatcher + implicit val jsonFormats = formats + + def settings = ApiSettings(system) + + import spray.http.MessagePredicate._ + + val CompressionFilter = MessagePredicate({ _ => settings.compressionEnabled}) && + Encoder.DefaultFilter && + minEntitySize(settings.compressionSizeThreshold) + + settings.customCodes.foreach(addCustomStatusCode) + + private def addAuthentication(credentialsSeq: Seq[Credentials]): pipelining.RequestTransformer = + request => + credentialsSeq.foldLeft(request) { + case (req, BasicCredentials(login, password)) => + req ~> addCredentials(BasicHttpCredentials(login, password)) + case (req, ApiKeyCredentials(keyValue, keyName, ApiKeyLocations.HEADER)) => + req ~> addHeader(RawHeader(keyName, keyValue.value)) + case (req, _) => req + } + + private def addHeaders(headers: Map[String, Any]): pipelining.RequestTransformer = { request => + + val rawHeaders = for { + (name, value) <- headers.asFormattedParams + header = RawHeader(name, String.valueOf(value)) + } yield header + + request.withHeaders(rawHeaders.toList) + } + + private def bodyPart(name: String, value: Any): BodyPart = { + value match { + case f: File => + BodyPart(f, name) + case v: String => + BodyPart(HttpEntity(String.valueOf(v))) + case NumericValue(v) => + BodyPart(HttpEntity(String.valueOf(v))) + case m: ApiModel => + BodyPart(HttpEntity(Serialization.write(m))) + } + } + + private def formDataContent(request: ApiRequest[_]) = { + val params = request.formParams.asFormattedParams + if (params.isEmpty) + None + else + Some( + normalizedContentType(request.contentType).mediaType match { + case MediaTypes.`multipart/form-data` => + MultipartFormData(params.map { case (name, value) => (name, bodyPart(name, value))}) + case MediaTypes.`application/x-www-form-urlencoded` => + FormData(params.mapValues(String.valueOf)) + case m: MediaType => // Default : application/x-www-form-urlencoded. + FormData(params.mapValues(String.valueOf)) + } + ) + } + + private def bodyContent(request: ApiRequest[_]): Option[Any] = { + request.bodyParam.map(Extraction.decompose).map(compact) + } + + private def createRequest(uri: Uri, request: ApiRequest[_]): HttpRequest = { + + val builder = new RequestBuilder(request.method.toSprayMethod) + val httpRequest = request.method.toSprayMethod match { + case HttpMethods.GET | HttpMethods.DELETE => builder.apply(uri) + case HttpMethods.POST | HttpMethods.PUT => + formDataContent(request) orElse bodyContent(request) match { + case Some(c: FormData) => + builder.apply(uri, c) + case Some(c: MultipartFormData) => + builder.apply(uri, c) + case Some(c: String) => + builder.apply(uri, HttpEntity(normalizedContentType(request.contentType), c)) + case _ => + builder.apply(uri, HttpEntity(normalizedContentType(request.contentType), " ")) + } + case _ => builder.apply(uri) + } + + httpRequest ~> + addHeaders(request.headerParams) ~> + addAuthentication(request.credentials) ~> + encode(Gzip(CompressionFilter)) + } + + def makeQuery(r: ApiRequest[_]): Query = { + r.credentials.foldLeft(r.queryParams) { + case (params, ApiKeyCredentials(key, keyName, ApiKeyLocations.QUERY)) => + params + (keyName -> key.value) + case (params, _) => params + }.asFormattedParams + .mapValues(String.valueOf) + .foldRight[Query](Uri.Query.Empty) { + case ((name, value), acc) => acc.+:(name, value) + } + } + + def makeUri(r: ApiRequest[_]): Uri = { + val opPath = r.operationPath.replaceAll("\\{format\\}", "json") + val opPathWithParams = r.pathParams.asFormattedParams + .mapValues(String.valueOf) + .foldLeft(opPath) { + case (path, (name, value)) => path.replaceAll(s"\\{$name\\}", value) + } + val query = makeQuery(r) + + Uri(r.basePath + opPathWithParams).withQuery(query) + } + + def execute[T](r: ApiRequest[T]): Future[ApiResponse[T]] = { + try { + implicit val timeout: Timeout = settings.connectionTimeout + + val uri = makeUri(r) + + val connector = HostConnectorSetup( + uri.authority.host.toString, + uri.effectivePort, + sslEncryption = "https".equals(uri.scheme), + defaultHeaders = settings.defaultHeaders ++ List(`Accept-Encoding`(gzip, deflate))) + + val request = createRequest(uri, r) + + for { + Http.HostConnectorInfo(hostConnector, _) <- IO(Http) ? connector + response <- hostConnector.ask(request).mapTo[HttpResponse] + } yield { + response ~> decode(Deflate) ~> decode(Gzip) ~> unmarshallApiResponse(r) + } + } + catch { + case NonFatal(x) => Future.failed(x) + } + } + + def unmarshallApiResponse[T](request: ApiRequest[T])(response: HttpResponse): ApiResponse[T] = { + request.responseForCode(response.status.intValue) match { + case Some( (manifest: Manifest[T], state: ResponseState) ) => + entityUnmarshaller(manifest)(response.entity) match { + case Right(value) ⇒ + state match { + case ResponseState.Success => + ApiResponse(response.status.intValue, value, response.headers.map(header => (header.name, header.value)).toMap) + case ResponseState.Error => + throw new ApiError(response.status.intValue, "Error response received", + Some(value), + headers = response.headers.map(header => (header.name, header.value)).toMap) + } + + case Left(MalformedContent(error, Some(cause))) ⇒ + throw new ApiError(response.status.intValue, s"Unable to unmarshall content to [$manifest]", Some(response.entity.toString), cause) + + case Left(MalformedContent(error, None)) ⇒ + throw new ApiError(response.status.intValue, s"Unable to unmarshall content to [$manifest]", Some(response.entity.toString)) + + case Left(ContentExpected) ⇒ + throw new ApiError(response.status.intValue, s"Unable to unmarshall empty response to [$manifest]", Some(response.entity.toString)) + } + + case _ => throw new ApiError(response.status.intValue, "Unexpected response code", Some(response.entity.toString)) + } + } + + def entityUnmarshaller[T](implicit mf: Manifest[T]): Unmarshaller[T] = + Unmarshaller[T](MediaTypes.`application/json`) { + case x: HttpEntity.NonEmpty ⇒ + parse(x.asString(defaultCharset = HttpCharsets.`UTF-8`)) + .noNulls + .camelizeKeys + .extract[T] + } + +} + +sealed trait CustomContentTypes { + + def normalizedContentType(original: String): ContentType = + MediaTypes.forExtension(original) map (ContentType(_)) getOrElse parseContentType(original) + + def parseContentType(contentType: String): ContentType = { + val contentTypeAsRawHeader = HttpHeaders.RawHeader("Content-Type", contentType) + val parsedContentTypeHeader = HttpParser.parseHeader(contentTypeAsRawHeader) + (parsedContentTypeHeader: @unchecked) match { + case Right(ct: HttpHeaders.`Content-Type`) => + ct.contentType + case Left(error: ErrorInfo) => + throw new IllegalArgumentException( + s"Error converting '$contentType' to a ContentType header: '${error.summary}'") + } + } +} + +sealed trait UntrustedSslContext { + this: ApiInvoker => + + implicit lazy val trustfulSslContext: SSLContext = { + settings.alwaysTrustCertificates match { + case false => + SSLContext.getDefault + + case true => + class IgnoreX509TrustManager extends X509TrustManager { + def checkClientTrusted(chain: Array[X509Certificate], authType: String): Unit = {} + + def checkServerTrusted(chain: Array[X509Certificate], authType: String): Unit = {} + + def getAcceptedIssuers = null + } + + val context = SSLContext.getInstance("TLS") + context.init(null, Array(new IgnoreX509TrustManager), null) + context + } + } + + implicit val clientSSLEngineProvider = + ClientSSLEngineProvider { + _ => + val engine = trustfulSslContext.createSSLEngine() + engine.setUseClientMode(true) + engine + } +} diff --git a/modules/swagger-codegen/src/main/resources/akka-scala/apiRequest.mustache b/modules/swagger-codegen/src/main/resources/akka-scala/apiRequest.mustache new file mode 100644 index 000000000000..3016768f7ac0 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/akka-scala/apiRequest.mustache @@ -0,0 +1,50 @@ +package {{invokerPackage}} + +sealed trait ResponseState +object ResponseState { + case object Success extends ResponseState + case object Error extends ResponseState +} + +case class ApiRequest[U]( + // required fields + method: ApiMethod, + basePath: String, + operationPath: String, + contentType: String, + + // optional fields + responses: Map[Int, (Manifest[_], ResponseState)] = Map.empty, + bodyParam: Option[Any] = None, + formParams: Map[String, Any] = Map.empty, + pathParams: Map[String, Any] = Map.empty, + queryParams: Map[String, Any] = Map.empty, + headerParams: Map[String, Any] = Map.empty, + credentials: Seq[Credentials] = List.empty) { + + def withCredentials(cred: Credentials) = copy[U](credentials = credentials :+ cred) + + def withApiKey(key: ApiKeyValue, keyName: String, location: ApiKeyLocation) = withCredentials(ApiKeyCredentials(key, keyName, location)) + + def withSuccessResponse[T](code: Int)(implicit m: Manifest[T]) = copy[U](responses = responses + (code -> (m, ResponseState.Success))) + + def withErrorResponse[T](code: Int)(implicit m: Manifest[T]) = copy[U](responses = responses + (code -> (m, ResponseState.Error))) + + def withDefaultSuccessResponse[T](implicit m: Manifest[T]) = withSuccessResponse[T](0) + + def withDefaultErrorResponse[T](implicit m: Manifest[T]) = withErrorResponse[T](0) + + def responseForCode(statusCode: Int): Option[(Manifest[_], ResponseState)] = responses.get(statusCode) orElse responses.get(0) + + def withoutBody() = copy[U](bodyParam = None) + + def withBody(body: Any) = copy[U](bodyParam = Some(body)) + + def withFormParam(name: String, value: Any) = copy[U](formParams = formParams + (name -> value)) + + def withPathParam(name: String, value: Any) = copy[U](pathParams = pathParams + (name -> value)) + + def withQueryParam(name: String, value: Any) = copy[U](queryParams = queryParams + (name -> value)) + + def withHeaderParam(name: String, value: Any) = copy[U](headerParams = headerParams + (name -> value)) +} diff --git a/modules/swagger-codegen/src/main/resources/akka-scala/apiSettings.mustache b/modules/swagger-codegen/src/main/resources/akka-scala/apiSettings.mustache new file mode 100644 index 000000000000..d3ffe3613ae4 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/akka-scala/apiSettings.mustache @@ -0,0 +1,32 @@ +package {{invokerPackage}} + +import java.util.concurrent.TimeUnit + +import akka.actor.{ExtendedActorSystem, Extension, ExtensionKey} +import com.typesafe.config.Config +import io.swagger.client.core.ApiInvoker.CustomStatusCode +import spray.http.HttpHeaders.RawHeader + +import scala.collection.JavaConversions._ +import scala.concurrent.duration.FiniteDuration + +class ApiSettings(config: Config) extends Extension { + def this(system: ExtendedActorSystem) = this(system.settings.config) + + private def cfg = config.getConfig("io.swagger.client.apiRequest") + + val alwaysTrustCertificates = cfg.getBoolean("trust-certificates") + val defaultHeaders = cfg.getConfig("default-headers").entrySet.toList.map(c => RawHeader(c.getKey, c.getValue.render)) + val connectionTimeout = FiniteDuration(cfg.getDuration("connection-timeout", TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS) + val compressionEnabled = cfg.getBoolean("compression.enabled") + val compressionSizeThreshold = cfg.getBytes("compression.size-threshold").toInt + val customCodes = cfg.getConfigList("custom-codes").toList.map { c => CustomStatusCode( + c.getInt("code"), + c.getString("reason"), + c.getBoolean("success")) + } + + +} + +object ApiSettings extends ExtensionKey[ApiSettings] diff --git a/modules/swagger-codegen/src/main/resources/akka-scala/enumsSerializers.mustache b/modules/swagger-codegen/src/main/resources/akka-scala/enumsSerializers.mustache new file mode 100644 index 000000000000..52f1fd3e3303 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/akka-scala/enumsSerializers.mustache @@ -0,0 +1,42 @@ +package {{apiPackage}} + +import {{modelPackage}}._ +import org.json4s._ +import scala.reflect.ClassTag + +object EnumsSerializers { + + def all = Seq[Serializer[_]](){{#models}}{{#model}}{{#hasEnums}}{{#vars}}{{#isEnum}} :+ + new EnumNameSerializer({{classname}}Enums.{{datatypeWithEnum}}){{/isEnum}}{{/vars}}{{/hasEnums}}{{/model}}{{/models}} + + + + private class EnumNameSerializer[E <: Enumeration: ClassTag](enum: E) + extends Serializer[E#Value] { + import JsonDSL._ + + val EnumerationClass = classOf[E#Value] + + def deserialize(implicit format: Formats): + PartialFunction[(TypeInfo, JValue), E#Value] = { + case (t @ TypeInfo(EnumerationClass, _), json) if isValid(json) => { + json match { + case JString(value) => + enum.withName(value) + case value => + throw new MappingException(s"Can't convert $value to $EnumerationClass") + } + } + } + + private[this] def isValid(json: JValue) = json match { + case JString(value) if enum.values.exists(_.toString == value) => true + case _ => false + } + + def serialize(implicit format: Formats): PartialFunction[Any, JValue] = { + case i: E#Value => i.toString + } + } + +} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/akka-scala/javadoc.mustache b/modules/swagger-codegen/src/main/resources/akka-scala/javadoc.mustache new file mode 100644 index 000000000000..0d22fd62374b --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/akka-scala/javadoc.mustache @@ -0,0 +1,21 @@ +{{^notes.isEmpty}} +{{notes}} +{{/notes.isEmpty}} + +Expected answers: +{{#responses}} + code {{code}} : {{dataType}} {{^message.isEmpty}}({{message}}){{/message.isEmpty}}{{^headers.isEmpty}} + Headers :{{#headers}} + {{baseName}} - {{description}}{{/headers}}{{/headers.isEmpty}} +{{/responses}} +{{#authMethods.0}} + +Available security schemes: +{{#authMethods}} + {{name}} ({{type}}) +{{/authMethods}} +{{/authMethods.0}} + +{{#allParams}} +@param {{paramName}} {{description}} +{{/allParams}} diff --git a/modules/swagger-codegen/src/main/resources/akka-scala/methodParameters.mustache b/modules/swagger-codegen/src/main/resources/akka-scala/methodParameters.mustache new file mode 100644 index 000000000000..3e1ab33f6655 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/akka-scala/methodParameters.mustache @@ -0,0 +1 @@ +{{#allParams}}{{paramName}}: {{#required}}{{dataType}}{{/required}}{{^required}}{{#isContainer}}{{dataType}}{{/isContainer}}{{^isContainer}}Option[{{dataType}}]{{/isContainer}}{{/required}}{{#defaultValue}} = {{{defaultValue}}}{{/defaultValue}}{{^defaultValue}}{{^required}}{{^isContainer}} = None{{/isContainer}}{{/required}}{{/defaultValue}}{{#hasMore}}, {{/hasMore}}{{/allParams}}{{#authMethods.0}})(implicit {{#authMethods}}{{#isApiKey}}apiKey: ApiKeyValue{{/isApiKey}}{{#isBasic}}basicAuth: BasicCredentials{{/isBasic}}{{#hasMore}}, {{/hasMore}}{{/authMethods}}{{/authMethods.0}} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/akka-scala/model.mustache b/modules/swagger-codegen/src/main/resources/akka-scala/model.mustache new file mode 100644 index 000000000000..a7395b5f03a6 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/akka-scala/model.mustache @@ -0,0 +1,30 @@ +package {{package}} + +import {{invokerPackage}}.ApiModel +import org.joda.time.DateTime + +{{#models}} +{{#model}} + +case class {{classname}} ( + {{#vars}}{{#description}}/* {{{description}}} */ + {{/description}}{{name}}: {{^required}}Option[{{/required}}{{^isEnum}}{{datatype}}{{/isEnum}}{{#isEnum}}{{classname}}Enums.{{datatypeWithEnum}}{{/isEnum}}{{^required}}]{{/required}}{{#hasMore}},{{/hasMore}}{{^hasMore}}){{/hasMore}} + {{/vars}} extends ApiModel + +{{#hasEnums}} +object {{classname}}Enums { + + {{#vars}}{{#isEnum}}type {{datatypeWithEnum}} = {{datatypeWithEnum}}.Value + {{/isEnum}}{{/vars}} + {{#vars}}{{#isEnum}}object {{datatypeWithEnum}} extends Enumeration { +{{#_enum}} + val {{#fnEnumEntry}}{{.}}{{/fnEnumEntry}} = Value("{{.}}") +{{/_enum}} + } + + {{/isEnum}}{{/vars}} +} +{{/hasEnums}} +{{/model}} +{{/models}} + diff --git a/modules/swagger-codegen/src/main/resources/akka-scala/operationReturnType.mustache b/modules/swagger-codegen/src/main/resources/akka-scala/operationReturnType.mustache new file mode 100644 index 000000000000..e9d2be534011 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/akka-scala/operationReturnType.mustache @@ -0,0 +1 @@ +{{#onlyOneSuccess}}{{^defaultResponse}}Unit{{/defaultResponse}}{{#responses}}{{#isDefault}}{{#dataType}}{{dataType}}{{/dataType}}{{^dataType}}Unit{{/dataType}}{{/isDefault}}{{/responses}}{{/onlyOneSuccess}}{{^onlyOneSuccess}}{{#responses}}{{#-first}}{{^hasMore}}{{#dataType}}{{dataType}}{{/dataType}}{{^dataType}}Unit{{/dataType}}{{/hasMore}}{{#hasMore}}Any{{/hasMore}}{{/-first}}{{/responses}}{{/onlyOneSuccess}} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/akka-scala/paramCreation.mustache b/modules/swagger-codegen/src/main/resources/akka-scala/paramCreation.mustache new file mode 100644 index 000000000000..f93d3765038d --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/akka-scala/paramCreation.mustache @@ -0,0 +1 @@ +"{{baseName}}", {{#isContainer}}ArrayValues({{paramName}}{{#collectionFormat}}, {{collectionFormat.toUpperCase}}{{/collectionFormat}}){{/isContainer}}{{^isContainer}}{{paramName}}{{/isContainer}} \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/akka-scala/pom.mustache b/modules/swagger-codegen/src/main/resources/akka-scala/pom.mustache new file mode 100644 index 000000000000..0d65c135957f --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/akka-scala/pom.mustache @@ -0,0 +1,227 @@ + + 4.0.0 + {{groupId}} + {{artifactId}} + jar + {{artifactId}} + {{artifactVersion}} + + 2.2.0 + + + + + maven-mongodb-plugin-repo + maven mongodb plugin repository + http://maven-mongodb-plugin.googlecode.com/svn/maven/repo + default + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.12 + + + + loggerPath + conf/log4j.properties + + + -Xms512m -Xmx1500m + methods + pertest + + + + maven-dependency-plugin + + + package + + copy-dependencies + + + ${project.build.directory}/lib + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 2.2 + + + + jar + test-jar + + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + add_sources + generate-sources + + add-source + + + + + src/main/java + + + + + add_test_sources + generate-test-sources + + add-test-source + + + + + src/test/java + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 2.3.2 + + + 1.6 + 1.6 + + + + net.alchim31.maven + scala-maven-plugin + ${scala-maven-plugin-version} + + + scala-compile-first + process-resources + + add-source + compile + + + + scala-test-compile + process-test-resources + + testCompile + + + + + + -feature + + + -Xms128m + -Xmx1500m + + + + + + + + + org.scala-tools + maven-scala-plugin + + ${scala-version} + + + + + + + org.scala-lang + scala-library + ${scala-version} + + + com.wordnik + swagger-core + ${swagger-core-version} + + + org.scalatest + scalatest_2.10 + ${scala-test-version} + test + + + junit + junit + ${junit-version} + test + + + joda-time + joda-time + ${joda-time-version} + + + org.joda + joda-convert + ${joda-version} + + + com.typesafe + config + 1.2.1 + + + com.typesafe.akka + akka-actor_2.10 + ${akka-version} + + + io.spray + spray-client + ${spray-version} + + + org.json4s + json4s-jackson_2.10 + ${json4s-jackson-version} + + + + 2.10.4 + 3.2.11 + 3.2.11 + 1.3.1 + 2.3.9 + 1.2 + 2.2 + 1.5.0-M1 + 1.0.0 + + 4.8.1 + 3.1.5 + 2.1.3 + + diff --git a/modules/swagger-codegen/src/main/resources/akka-scala/reference.mustache b/modules/swagger-codegen/src/main/resources/akka-scala/reference.mustache new file mode 100644 index 000000000000..1a28a8962edc --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/akka-scala/reference.mustache @@ -0,0 +1,24 @@ +{{configKeyPath}} { + + {{configKey}} { + + compression { + enabled: false + size-threshold: 0 + } + + trust-certificates: true + + connection-timeout: {{defaultTimeout}}ms + + default-headers { + "userAgent": "{{artifactId}}_{{artifactVersion}}" + } + + // let you define custom http status code, as in : + // { code: 601, reason: "some custom http status code", success: false } + custom-codes : [] + } +} + +spray.can.host-connector.max-redirects = 10 \ No newline at end of file diff --git a/modules/swagger-codegen/src/main/resources/akka-scala/requests.mustache b/modules/swagger-codegen/src/main/resources/akka-scala/requests.mustache new file mode 100644 index 000000000000..145354203fb4 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/akka-scala/requests.mustache @@ -0,0 +1,166 @@ +package {{invokerPackage}} + +import java.io.File +import java.net.URLEncoder +import scala.util.Try + +sealed trait ApiReturnWithHeaders { + def headers: Map[String, String] + def header(name: String): Option[String] = headers.get(name) + def getStringHeader(name: String) = header(name) + def getIntHeader(name: String) = castedHeader(name, java.lang.Integer.parseInt) + def getLongHeader(name: String) = castedHeader(name, java.lang.Long.parseLong) + def getFloatHeader(name: String) = castedHeader(name, java.lang.Float.parseFloat) + def getDoubleHeader(name: String) = castedHeader(name, java.lang.Double.parseDouble) + def getBooleanHeader(name: String) = castedHeader(name, java.lang.Boolean.parseBoolean) + private def castedHeader[U](name: String, conversion: String => U): Option[U] = { Try { header(name).map( conversion ) }.get } +} + +sealed case class ApiResponse[T](code: Int, content: T, headers: Map[String, String] = Map.empty) + extends ApiReturnWithHeaders + +sealed case class ApiError[T](code: Int, message: String, responseContent: Option[T], cause: Throwable = null, headers: Map[String, String] = Map.empty) + extends Throwable(s"($code) $message.${responseContent.map(s => s" Content : $s").getOrElse("")}", cause) + with ApiReturnWithHeaders + +sealed case class ApiMethod(value: String) + +object ApiMethods { + val CONNECT = ApiMethod("CONNECT") + val DELETE = ApiMethod("DELETE") + val GET = ApiMethod("GET") + val HEAD = ApiMethod("HEAD") + val OPTIONS = ApiMethod("OPTIONS") + val PATCH = ApiMethod("PATCH") + val POST = ApiMethod("POST") + val PUT = ApiMethod("PUT") + val TRACE = ApiMethod("TRACE") +} + +/** + * This trait needs to be added to any model defined by the api. + */ +trait ApiModel + +/** + * Single trait defining a credential that can be transformed to a paramName / paramValue tupple + */ +sealed trait Credentials { + def asQueryParam: Option[(String, String)] = None +} + +sealed case class BasicCredentials(user: String, password: String) extends Credentials + +sealed case class ApiKeyCredentials(key: ApiKeyValue, keyName: String, location: ApiKeyLocation) extends Credentials { + override def asQueryParam: Option[(String, String)] = location match { + case ApiKeyLocations.QUERY => Some((keyName, key.value)) + case _ => None + } +} + +sealed case class ApiKeyValue(value: String) + +sealed trait ApiKeyLocation + +object ApiKeyLocations { + + case object QUERY extends ApiKeyLocation + + case object HEADER extends ApiKeyLocation +} + + +/** + * Case class used to unapply numeric values only in pattern matching + * @param value the string representation of the numeric value + */ +sealed case class NumericValue(value: String) { + override def toString = value +} + +object NumericValue { + def unapply(n: Any): Option[NumericValue] = n match { + case (_: Int | _: Long | _: Float | _: Double | _: Boolean | _: Byte) => Some(NumericValue(String.valueOf(n))) + case _ => None + } +} + +/** + * Used for params being arrays + */ +sealed case class ArrayValues(values: Seq[Any], format: CollectionFormat = CollectionFormats.CSV) + +object ArrayValues { + def apply(values: Option[Seq[Any]], format: CollectionFormat): ArrayValues = + ArrayValues(values.getOrElse(Seq.empty), format) + + def apply(values: Option[Seq[Any]]): ArrayValues = ArrayValues(values, CollectionFormats.CSV) +} + + +/** + * Defines how arrays should be rendered in query strings. + */ +sealed trait CollectionFormat + +trait MergedArrayFormat extends CollectionFormat { + def separator: String +} + +object CollectionFormats { + + case object CSV extends MergedArrayFormat { + override val separator = "," + } + + case object TSV extends MergedArrayFormat { + override val separator = "\t" + } + + case object SSV extends MergedArrayFormat { + override val separator = " " + } + + case object PIPES extends MergedArrayFormat { + override val separator = "|" + } + + case object MULTI extends CollectionFormat + +} + +object ParametersMap { + + /** + * Pimp parameters maps (Map[String, Any]) in order to transform them in a sequence of String -> Any tupples, + * with valid url-encoding, arrays handling, files preservation, ... + */ + implicit class ParametersMapImprovements(val m: Map[String, Any]) { + + def asFormattedParamsList = m.toList.flatMap(formattedParams) + + def asFormattedParams = m.flatMap(formattedParams) + + private def urlEncode(v: Any) = URLEncoder.encode(String.valueOf(v), "utf-8").replaceAll("\\+", "%20") + + private def formattedParams(tuple: (String, Any)): Seq[(String, Any)] = formattedParams(tuple._1, tuple._2) + + private def formattedParams(name: String, value: Any): Seq[(String, Any)] = value match { + case arr: ArrayValues => + arr.format match { + case CollectionFormats.MULTI => arr.values.flatMap(formattedParams(name, _)) + case format: MergedArrayFormat => Seq((name, arr.values.mkString(format.separator))) + } + case None => Seq.empty + case Some(opt) => + formattedParams(name, opt) + case s: Seq[Any] => + formattedParams(name, ArrayValues(s)) + case v: String => Seq((name, urlEncode(v))) + case NumericValue(v) => Seq((name, urlEncode(v))) + case f: File => Seq((name, f)) + case m: ApiModel => Seq((name, m)) + } + + } +} diff --git a/modules/swagger-codegen/src/main/resources/akka-scala/responseState.mustache b/modules/swagger-codegen/src/main/resources/akka-scala/responseState.mustache new file mode 100644 index 000000000000..c6d44b86d874 --- /dev/null +++ b/modules/swagger-codegen/src/main/resources/akka-scala/responseState.mustache @@ -0,0 +1 @@ +{{#onlyOneSuccess}}{{#isDefault}}Success{{/isDefault}}{{^isDefault}}Error{{/isDefault}}{{/onlyOneSuccess}}{{^onlyOneSuccess}}Success{{/onlyOneSuccess}} \ No newline at end of file diff --git a/modules/swagger-codegen/src/test/resources/2_0/requiredTest.json b/modules/swagger-codegen/src/test/resources/2_0/requiredTest.json new file mode 100644 index 000000000000..88106746d290 --- /dev/null +++ b/modules/swagger-codegen/src/test/resources/2_0/requiredTest.json @@ -0,0 +1,93 @@ +{ + "swagger": "2.0", + "info": { + "description": "This is a sample server Petstore server. You can find out more about Swagger at http://swagger.io or on irc.freenode.net, #swagger. For this sample, you can use the api key \"special-key\" to test the authorization filters", + "version": "1.0.0", + "title": "Swagger Petstore", + "termsOfService": "http://helloreverb.com/terms/", + "contact": { + "email": "apiteam@wordnik.com" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + } + }, + "host": "petstore.swagger.io", + "basePath": "/v2", + "schemes": [ + "http" + ], + "paths": { + "/tests/requiredParams": { + "get": { + "tags": [ + "tests" + ], + "summary": "Operation with required parameters", + "description": "", + "operationId": "requiredParams", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "param1", + "in": "formData", + "description": "Some required parameter", + "required": true, + "type": "integer", + "format": "int64" + }, + { + "name": "param2", + "in": "formData", + "description": "Some optional parameter", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation. Retuning a simple int.", + "schema": { + "type": "integer", + "format": "int64" + } + } + } + } + } + }, + "securityDefinitions": { + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + }, + "petstore_auth": { + "type": "oauth2", + "authorizationUrl": "http://petstore.swagger.io/api/oauth/dialog", + "flow": "implicit", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + }, + "definitions": { + "CustomModel": { + "required": ["id"], + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string", + "example": "doggie" + } + } + } + } +} \ No newline at end of file diff --git a/modules/swagger-codegen/src/test/scala/CodegenTest.scala b/modules/swagger-codegen/src/test/scala/CodegenTest.scala index 9e2a1d9fa9ed..5ce358251595 100644 --- a/modules/swagger-codegen/src/test/scala/CodegenTest.scala +++ b/modules/swagger-codegen/src/test/scala/CodegenTest.scala @@ -1,15 +1,9 @@ -import com.wordnik.swagger.models._ -import com.wordnik.swagger.util.Json -import io.swagger.parser._ - import com.wordnik.swagger.codegen.DefaultCodegen - +import com.wordnik.swagger.models.properties.Property +import io.swagger.parser._ import org.junit.runner.RunWith import org.scalatest.junit.JUnitRunner -import org.scalatest.FlatSpec -import org.scalatest.Matchers - -import scala.collection.JavaConverters._ +import org.scalatest.{FlatSpec, Matchers} @RunWith(classOf[JUnitRunner]) class CodegenTest extends FlatSpec with Matchers { @@ -95,6 +89,32 @@ class CodegenTest extends FlatSpec with Matchers { statusParam.hasMore should be (null) } + it should "handle required parameters from a 2.0 spec as required when figuring out Swagger types" in { + val model = new SwaggerParser() + .read("src/test/resources/2_0/requiredTest.json") + + val codegen = new DefaultCodegen() { + override def getSwaggerType(p: Property) = Option(p) match { + case Some(property) if !property.getRequired => + "Optional<" + super.getSwaggerType(p) + ">" + case other => super.getSwaggerType(p) + } + } + val path = "/tests/requiredParams" + val p = model.getPaths().get(path).getGet() + val op = codegen.fromOperation(path, "get", p, model.getDefinitions) + + val formParams = op.formParams + formParams.size should be(2) + val requiredParam = formParams.get(0) + requiredParam.dataType should be("Long") + + val optionalParam = formParams.get(1) + optionalParam.dataType should be("Optional") + + op.returnType should be("Long") + } + it should "select main response from a 2.0 spec using the lowest 2XX code" in { val model = new SwaggerParser() .read("src/test/resources/2_0/responseSelectionTest.json")