com.reprezen.genflow.rapidml.swagger.XGenerateSwagger.xtend Maven / Gradle / Ivy
The newest version!
/*******************************************************************************
* Copyright © 2013, 2016 Modelsolv, Inc.
* All Rights Reserved.
*
* NOTICE: All information contained herein is, and remains the property
* of ModelSolv, Inc. See the file license.html in the root directory of
* this project for further information.
*******************************************************************************/
package com.reprezen.genflow.rapidml.swagger
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.google.common.base.Splitter
import com.google.common.base.Strings
import com.google.common.collect.Lists
import com.reprezen.genflow.api.template.IGenTemplateContext
import com.reprezen.genflow.api.zenmodel.ZenModelOutputItem
import com.reprezen.genflow.common.jsonschema.JsonSchemaHelper
import com.reprezen.genflow.common.xtend.ExtensionsHelper
import com.reprezen.genflow.common.xtend.ZenModelHelper
import com.reprezen.rapidml.AuthenticationFlows
import com.reprezen.rapidml.AuthenticationTypes
import com.reprezen.rapidml.Constraint
import com.reprezen.rapidml.Extensible
import com.reprezen.rapidml.HasSecurityValue
import com.reprezen.rapidml.HttpMessageParameterLocation
import com.reprezen.rapidml.LengthConstraint
import com.reprezen.rapidml.MessageParameter
import com.reprezen.rapidml.Method
import com.reprezen.rapidml.PropertyReference
import com.reprezen.rapidml.RapidmlFactory
import com.reprezen.rapidml.RegExConstraint
import com.reprezen.rapidml.ResourceDefinition
import com.reprezen.rapidml.SecurityScheme
import com.reprezen.rapidml.ServiceDataResource
import com.reprezen.rapidml.SourceReference
import com.reprezen.rapidml.Structure
import com.reprezen.rapidml.TemplateParameter
import com.reprezen.rapidml.TypedMessage
import com.reprezen.rapidml.TypedResponse
import com.reprezen.rapidml.ValueRangeConstraint
import com.reprezen.rapidml.ZenModel
import com.reprezen.rapidml.util.OAuth2Parameters
import io.swagger.models.Info
import io.swagger.models.Model
import io.swagger.models.Operation
import io.swagger.models.Path
import io.swagger.models.RefModel
import io.swagger.models.Response
import io.swagger.models.Scheme
import io.swagger.models.Swagger
import io.swagger.models.Tag
import io.swagger.models.auth.ApiKeyAuthDefinition
import io.swagger.models.auth.BasicAuthDefinition
import io.swagger.models.auth.In
import io.swagger.models.auth.OAuth2Definition
import io.swagger.models.auth.SecuritySchemeDefinition
import io.swagger.models.parameters.AbstractParameter
import io.swagger.models.parameters.AbstractSerializableParameter
import io.swagger.models.parameters.BodyParameter
import io.swagger.models.parameters.HeaderParameter
import io.swagger.models.parameters.PathParameter
import io.swagger.models.parameters.QueryParameter
import io.swagger.models.properties.AbstractNumericProperty
import io.swagger.models.properties.ArrayProperty
import io.swagger.models.properties.BooleanProperty
import io.swagger.models.properties.DateProperty
import io.swagger.models.properties.DateTimeProperty
import io.swagger.models.properties.DecimalProperty
import io.swagger.models.properties.DoubleProperty
import io.swagger.models.properties.FloatProperty
import io.swagger.models.properties.IntegerProperty
import io.swagger.models.properties.LongProperty
import io.swagger.models.properties.Property
import io.swagger.models.properties.RefProperty
import io.swagger.models.properties.StringProperty
import io.swagger.util.Json
import io.swagger.util.Yaml
import java.io.IOException
import java.math.BigDecimal
import java.net.URI
import java.util.Collections
import java.util.HashMap
import java.util.List
import org.yaml.snakeyaml.DumperOptions
import org.yaml.snakeyaml.DumperOptions.FlowStyle
class XGenerateSwagger extends ZenModelOutputItem {
val public static String OUTPUT_FORMAT_PARAM = 'format'
val public static String OPTION_RETAIN_EMPTY_PARAMATERS = 'retainEmptyParameters'
extension ZenModelHelper = new ZenModelHelper
extension JsonSchemaHelper = new JsonSchemaHelper
extension ExtensionsHelper = new ExtensionsHelper
val String defaultFormat;
val JsonSchemaForSwaggerGenerator jsonSchemaGenerator
new() {
this(SwaggerOutputFormat.JSON);
}
new(SwaggerOutputFormat format) {
this(format, new JsonSchemaForSwaggerGenerator())
}
new(SwaggerOutputFormat format, JsonSchemaForSwaggerGenerator jsonSchemaGenerator) {
defaultFormat = format.toString;
this.jsonSchemaGenerator = jsonSchemaGenerator
}
override init(IGenTemplateContext context) {
super.init(context)
}
override generate(ZenModel model) {
val templateParam = context.genTargetParameters
val swagger = getSwagger(model)
val mapper = Json.pretty()
var content = mapper.writeValueAsString(swagger)
// workaround to use JSON Schema generator for Swagger
val tempMapper = new ObjectMapper()
val jsonSchemasDefinitionsNode = jsonSchemaGenerator.generateDefinitionsNode(model, templateParam)
val swaggerNode = (tempMapper.readTree(content) as ObjectNode).set("definitions", jsonSchemasDefinitionsNode)
val format = if (templateParam?.get(OUTPUT_FORMAT_PARAM) !== null)
templateParam.get(OUTPUT_FORMAT_PARAM)
else
defaultFormat;
if (SwaggerOutputFormat.YAML.toString == format) {
val ObjectMapper swaggerYamlMapper = Yaml::mapper();
val foldMultiline = templateParam?.get(XSwaggerGenTemplate::FOLD_MULTILINE)
if (foldMultiline instanceof Boolean && (foldMultiline as Boolean)) {
val DumperOptions opts = new DumperOptions();
opts.setDefaultFlowStyle(FlowStyle::BLOCK);
val yaml = new org.yaml.snakeyaml.Yaml(opts);
// Note that we are using SnakeYAML directly here. See https://github.com/ModelSolv/RepreZen/pull/1348
return yaml.dump(new ObjectMapper().convertValue(swaggerNode, typeof(Object)));
}
return swaggerYamlMapper.writeValueAsString(swaggerNode)
} else {
return tempMapper.writerWithDefaultPrettyPrinter.writeValueAsString(swaggerNode)
}
}
def Swagger getSwagger(ZenModel model) {
val Swagger swagger = new Swagger
// casting to Extensible, otherwise Xtend does not recognize inherited methods
val groups = (model as Extensible).extensions.filter[it.name.startsWith("openAPI.tags.")]
if (!groups.isEmpty) {
groups.forEach [
val tag = new Tag
tag.name = it.name.substring(13) // 13 == "openAPI.tags.".length()
val description = it.value
tag.description = description
swagger.addTag(tag)
]
}
setVendorExtensions(model, swagger)
if (model.resourceAPIs.empty) {
addDefaultResourceAPI(model)
}
val resourceAPI = model.resourceAPIs.get(0)
val uri = URI.create(resourceAPI.baseURI ?: 'http://localhost')
swagger.basePath = if(uri.path !== null && !uri.path.empty) uri.path else '/'
swagger.host = uri.host + if(uri.port >= 0) ':' + uri.port else ''
if (uri.scheme !== null) {
swagger.addScheme(Scheme.forValue(uri.scheme))
}
swagger.info = new Info()
swagger.info.version = if(!Strings::isNullOrEmpty(resourceAPI.version)) resourceAPI.version else '1.0.0'
swagger.info.title = resourceAPI.name
swagger.info.description = getDocumentation(resourceAPI)
// Is it a right mapping?
// setVendorExtensions(resourceAPI, swagger.info)
// global media types
resourceAPI.definedMediaTypes.forEach [
{
swagger.addConsumes(it.name)
swagger.addProduces(it.name)
}
]
// resources
model.resourceAPIs.map[ownedResourceDefinitions].flatten.forEach [
val tag = new Tag
tag.name = it.name
tag.description = getDocumentation(it)
swagger.addTag(tag)
// make uri absolute because it is required by Swagger UI
val pathUri = '/' + if(it.URI !== null) it.URI.toString else ''
swagger.path(pathUri, createSwaggerPath(it))
]
// ZEN-2962 - we should output paths: {} when there are no paths objects
if (swagger.paths === null) {
swagger.paths = Collections.emptyMap()
}
getAllUsedSecuritySchemes(model).forEach[swagger.addSecurityDefinition(it.name, createSecurityScheme(it))]
return swagger
}
def getAllUsedSecuritySchemes(ZenModel model) {
val result = Lists::newArrayList
if (model.securitySchemesLibrary !== null) {
result.addAll(model.securitySchemesLibrary.securitySchemes)
}
model.eAllContents.filter(typeof(HasSecurityValue)).toList.map[securedBy].flatten.forEach[result.add(it.scheme)]
return result
}
def SecuritySchemeDefinition createSecurityScheme(SecurityScheme scheme) {
switch (scheme.type) {
case AuthenticationTypes::OAUTH2: {
val result = new OAuth2Definition
setVendorExtensions(scheme, result)
result.flow = switch (scheme.flow) {
case AuthenticationFlows::IMPLICIT: "implicit"
case AuthenticationFlows::PASSWORD: "password"
case AuthenticationFlows::APPLICATION: "application"
case AuthenticationFlows::ACCESS_CODE: "accessCode"
}
val authorizationUrlSetting = scheme.settings.findFirst[it.name == OAuth2Parameters::AUTHORIZATION_URL]
result.authorizationUrl = authorizationUrlSetting?.value
// Use tokenUrl if it's defined, otherwise try using accessTokenUrl
val tokenUrlSetting = scheme.settings.findFirst[it.name == OAuth2Parameters::TOKEN_URL]
if (tokenUrlSetting !== null) {
result.tokenUrl = tokenUrlSetting.value
} else {
val accessTokenUrlSetting = scheme.settings.findFirst[it.name == OAuth2Parameters::ACCESS_TOKEN_URL]
result.tokenUrl = accessTokenUrlSetting?.value
}
scheme.scopes.forEach[result.addScope(it.name, getDocumentation(it))]
result.type = "oauth2"
return result;
}
case AuthenticationTypes::BASIC: {
val result = new BasicAuthDefinition
setVendorExtensions(scheme, result)
result.type = "basic"
return result;
}
case AuthenticationTypes::CUSTOM: {
val result = new ApiKeyAuthDefinition
setVendorExtensions(scheme, result)
// TODO Swagger requires exactly one param for ApiKey auth
if (!scheme.parameters.empty) {
val param = scheme.parameters.get(0)
result.name = param.name
val in = if(param.type == HttpMessageParameterLocation::HEADER) In::HEADER else In::QUERY
result.setIn(in)
}
result.type = "apiKey"
return result;
}
}
}
def Path createSwaggerPath(ResourceDefinition res) {
val Path path = new Path()
// Xtend not recognizing supertype again:(
setVendorExtensions(res as Extensible, path)
if ((res as ServiceDataResource).URI !== null) {
(res as ServiceDataResource).URI.uriParameters.forEach [
// use matrix parameters as QueryParameter because Swagger doesnot support Matrix parameters
val AbstractSerializableParameter> param = if(it instanceof TemplateParameter) new PathParameter else new QueryParameter
param.description = getDocumentation(it)
param.name = it.name
param.type = it.sourceReference.type.JSONSchemaTypeName
if (it.sourceReference.type.JSONSchemaTypeFormat !== null) {
param.format = it.sourceReference.type.JSONSchemaTypeFormat
}
setVendorExtensions(it, param)
switch (param) {
PathParameter: {
// path parameter should be always true according Swagger spec
param.required = true
param.defaultValue = it.^default
}
QueryParameter: {
param.required = it.required
param.defaultValue = it.^default
}
}
path.addParameter(param)
]
}
res.methods.forEach[path.createSwaggerOperation(it)]
return path
}
def createSwaggerOperation(Path path, Method method) {
val Operation operation = new Operation()
setVendorExtensions(method, operation)
operation.operationId = method.id
operation.description = getDocumentation(method)
// casting to Extensible, otherwise Xtend does not recognize inherited methods
val groups = (method as Extensible).extensions.filter[it.name == "openAPI.tags"]
if (!groups.isEmpty) {
groups.forEach [
Splitter.on(",").split(it.value).forEach[tag|operation.addTag(tag.trim)]
]
} else {
operation.addTag(method.containingResourceDefinition.name)
}
method.responses.forEach [
operation.addResponse(if(it.statusCode > 0) it.statusCode.toString else '200', getResponse(it))
]
method.responses.map[mediaTypes].flatten.map[name].toSet.forEach[operation.safeAddProduces(it)]
method.request.mediaTypes.map[name].toSet.forEach[operation.safeAddConsumes(it)]
method.request.parameters.filter[it.httpLocation != HttpMessageParameterLocation::QUERY].forEach [
operation.addParameter(messageHeaderParameter)
]
method.request.parameters.filter[it.httpLocation == HttpMessageParameterLocation::QUERY].forEach [
operation.addParameter(messageQueryParameter)
]
if (!shouldRetainEmptyParameters) {
if (operation.parameters.isEmpty) {
operation.parameters = null
}
}
val requestTypeName = getDefinitionName(method.request)
if (requestTypeName !== null) {
val param = new BodyParameter()
operation.addParameter(param)
param.name = requestTypeName
param.description = "Request body" // TODO
param.description = getMessageDocumentation(method.request)
param.schema = getReferenceToType(requestTypeName)
setVendorExtensions(method.request, param)
param.required = true
}
switch (method.httpMethod.getName()) {
case "GET": path.get = operation
case "POST": path.post = operation
case "PUT": path.put = operation
case "DELETE": path.delete = operation
case "OPTIONS": path.options = operation
case "PATCH": path.patch = operation
}
method.securedBy.forEach[operation.addSecurity(it.scheme.name, it.scopes.map[it.name])]
}
/**
* @returns creates Swagger Response from ZEN TypedResponse
*/
def getResponse(TypedResponse rapidResponse) {
val swaggerResponse = new Response()
// Casting because Xtend editor does not recognize Extensible as supertype
setVendorExtensions(rapidResponse as Extensible, swaggerResponse)
val example = rapidResponse.allExamples.head
if (example !== null && example.body !== null) {
val type = rapidResponse.mediaTypes.head
if (type !== null) {
swaggerResponse.example(rapidResponse.mediaTypes.head.name, example.body.renderExample(type.name))
}
}
val typeName = getDefinitionName(rapidResponse)
swaggerResponse.description = getMessageDocumentation(rapidResponse)
if (typeName !== null) {
val property = new RefProperty
property.$ref = "#/definitions/" + typeName
swaggerResponse.schema = property
}
rapidResponse.parameters.forEach [
if (swaggerResponse.headers === null) {
swaggerResponse.headers = new HashMap
}
if (it.arrayProperty) {
val prop = new ArrayProperty
prop.items = it.createSwaggerPropertyWithConstraints()
prop.description = getDocumentation(it)
swaggerResponse.headers.put(it.name, prop)
} else {
val prop = it.createSwaggerPropertyWithConstraints()
prop.description = getDocumentation(it)
swaggerResponse.headers.put(it.name, prop)
}
]
return swaggerResponse
}
def private renderExample(String example, String type) {
switch (type) {
case "application/json":
example.jsonExample
default:
example
}
}
val static jsonMapper = new ObjectMapper()
val static yamlMapper = new ObjectMapper(new YAMLFactory())
def private getJsonExample(String example) {
try {
return jsonMapper.readTree(example)
} catch (IOException exception) {
}
try {
return yamlMapper.readTree(example.removeIndentation)
} catch (Exception exception) {
}
return example
}
// We first remove any initial or final lines that contain only whitespace, then compute the indentation of the first
// line among those that remain. If all lines begin with that exact indentation, we strip it from all lines to yield
// the final example text. Otherwise, we just return the example text as-is, which will almost certainly fail YAML
// parse and will therefore be copied as a string-valued swagger example
def private removeIndentation(String text) {
var lines = text.split("(?m)^")
lines = lines.dropWhile[it.trim.isEmpty]
lines = lines.reverse.dropWhile[it.trim.isEmpty].toList.reverse
val indent = findIndent(lines.head)
if (lines.forall[it.startsWith(indent)]) {
return lines.map[it.substring(indent.length())].join("\n")
} else {
return text
}
}
def private findIndent(String line) {
if (line !== null) {
for (i : 0 ..< line.length) {
if (" \t".indexOf(line.charAt(i)) < 0) {
return line.substring(0, i)
}
}
}
return line
}
def getMessageDocumentation(TypedMessage message) {
var result = getDocumentation(message)
if (result.nullOrEmpty) {
val Structure dataType = if (message.actualType !== null) {
message.actualType
} else {
if (message.resourceType instanceof ServiceDataResource) {
(message.resourceType as ServiceDataResource).dataType
}
};
if (dataType !== null) {
result = getDocumentation(dataType)
}
}
return result
}
def String getDefinitionName(TypedMessage message) {
if (message === null) {
return null;
}
var String typeName = if (message.actualType !== null) {
jsonSchemaGenerator.getDefinitionName(message)
} else if (message.resourceType !== null) {
jsonSchemaGenerator.getDefinitionName(message.resourceType as ServiceDataResource)
} else {
null
}
return typeName
}
def Model getReferenceToType(String typeName) {
val ref = new RefModel
ref.$ref = "#/definitions/" + typeName
ref
}
def Property createSwaggerPropertyWithConstraints(MessageParameter parameter) {
val SourceReference sourceReference = parameter.sourceReference
val prop = getSwaggerProperty(sourceReference.type.name)
prop.setConstraints(parameter.messageParameterConstraints)
prop
}
def Property getSwaggerProperty(String typeName) {
switch (typeName) {
case #["anyURI", "duration", "gMonth", "gMonthDay", "gDay", "gYearMonth", "gYear", "QName", "time",
"string", "NCName"].contains(typeName):
new StringProperty
case "boolean":
new BooleanProperty
case "date":
new DateProperty
case "dateTime":
new DateTimeProperty
case "decimal":
new DecimalProperty
case "double":
new DoubleProperty
case "float":
new FloatProperty
case "integer":
new IntegerProperty
case "int":
new IntegerProperty
case "long":
new LongProperty
default: {
val prop = new RefProperty
prop.$ref = "#/definitions/" + typeName
prop
}
}
}
def setConstraints(Property property, List constraints) {
if (constraints === null || constraints.empty) {
return
}
switch (property) {
StringProperty: {
constraints.filter(LengthConstraint).forEach [
if (it.setMinLength) {
property.minLength = it.minLength
}
if (it.setMaxLength) {
property.maxLength = it.maxLength
}
]
constraints.filter(RegExConstraint).forEach [
property.pattern = it.pattern
]
}
AbstractNumericProperty: {
constraints.filter(ValueRangeConstraint).forEach [
if (it.minValue !== null) {
property.minimum = new BigDecimal(it.minValue)
property.exclusiveMinimum = it.minValueExclusive
}
if (it.maxValue !== null) {
property.maximum = new BigDecimal(it.maxValue)
property.exclusiveMaximum = it.maxValueExclusive
}
]
}
}
}
def getMessageHeaderParameter(MessageParameter parameter) {
val swaggerParameter = new HeaderParameter
if (parameter.arrayProperty) {
// TODO - Make sure this is the proper way to handle arrays in new swagger model. Formerly:
// swaggerParameter.array = true
// swaggerParameter.items = parameter.createSwaggerPropertyWithConstraints()
val items = parameter.createSwaggerPropertyWithConstraints
swaggerParameter.property = new ArrayProperty(items)
} else {
swaggerParameter.property = parameter.createSwaggerPropertyWithConstraints()
}
swaggerParameter.description = getDocumentation(parameter)
swaggerParameter.name = parameter.name
swaggerParameter.required = parameter.required
setVendorExtensions(parameter, swaggerParameter)
return swaggerParameter
}
def private setVendorExtensions(Extensible rapidElement, Swagger swaggerObj) {
val extensions = getRapidExtensions(rapidElement)
extensions.forEach[swaggerObj.setVendorExtension(it.name, it.value)]
}
def private setVendorExtensions(Extensible rapidElement, Path swaggerObj) {
val extensions = getRapidExtensions(rapidElement)
extensions.forEach[swaggerObj.setVendorExtension(it.name, it.value)]
}
def private setVendorExtensions(Extensible rapidElement, Operation swaggerObj) {
val extensions = getRapidExtensions(rapidElement)
extensions.forEach[swaggerObj.setVendorExtension(it.name, it.value)]
}
def private setVendorExtensions(Extensible rapidElement, AbstractParameter swaggerObj) {
val extensions = getRapidExtensions(rapidElement)
extensions.forEach[swaggerObj.setVendorExtension(it.name, it.value)]
}
def private setVendorExtensions(Extensible rapidElement, Response swaggerObj) {
val extensions = getRapidExtensions(rapidElement)
extensions.forEach[swaggerObj.setVendorExtension(it.name, it.value)]
}
def private setVendorExtensions(Extensible rapidElement, SecuritySchemeDefinition swaggerObj) {
val extensions = getRapidExtensions(rapidElement)
extensions.forEach[swaggerObj.setVendorExtension(it.name, it.value)]
}
def getMessageQueryParameter(MessageParameter parameter) {
val swaggerParameter = new QueryParameter
if (parameter.arrayProperty) {
// TOOD is this the right way to handle arrays in V2? Formerly was:
// swaggerParameter.array = true
// swaggerParameter.items = parameter.createSwaggerPropertyWithConstraints()
val items = parameter.createSwaggerPropertyWithConstraints()
swaggerParameter.property = new ArrayProperty(items);
} else {
swaggerParameter.property = parameter.createSwaggerPropertyWithConstraints()
}
swaggerParameter.description = getDocumentation(parameter)
swaggerParameter.name = parameter.name
swaggerParameter.required = parameter.required
setVendorExtensions(parameter, swaggerParameter)
return swaggerParameter
}
def List getMessageParameterConstraints(MessageParameter parameter) {
val ref = parameter.sourceReference
switch (ref) {
PropertyReference: {
var property = parameter.containingMessage.includedProperties.findFirst [
it.baseProperty == ref.conceptualFeature
]
if (property === null) {
return ref.conceptualFeature.allConstraints
}
return property.allConstraints
}
}
}
def addDefaultResourceAPI(ZenModel model) {
val resourceAPI = RapidmlFactory.eINSTANCE.createResourceAPI
resourceAPI.baseURI = 'http://localhost'
resourceAPI.name = model.name
model.resourceAPIs.add(resourceAPI)
}
private def boolean shouldRetainEmptyParameters() {
val option = context?.genTargetParameters?.get(OPTION_RETAIN_EMPTY_PARAMATERS)
return if (option instanceof Boolean) {
option as Boolean
} else if (option === null) {
false
} else
throw new IllegalArgumentException(
String.format("Only boolean values are allowed for '%s', current value is of type %s",
OPTION_RETAIN_EMPTY_PARAMATERS, option.class.simpleName))
}
private static def safeAddConsumes(Operation op, String mediaType) {
if(op.consumes === null || !op.consumes.contains(mediaType)) op.addConsumes(mediaType)
}
private static def safeAddProduces(Operation op, String mediaType) {
if(op.produces === null || !op.produces.contains(mediaType)) op.addProduces(mediaType)
}
}