diff --git a/bin/openapi3/plantuml-documentation-petstore.sh b/bin/openapi3/plantuml-documentation-petstore.sh new file mode 100644 index 00000000000..61bdfbbcb4a --- /dev/null +++ b/bin/openapi3/plantuml-documentation-petstore.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +SCRIPT="$0" + +while [ -h "$SCRIPT" ] ; do + ls=$(ls -ld "$SCRIPT") + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' > /dev/null; then + SCRIPT="$link" + else + SCRIPT=$(dirname "$SCRIPT")/"$link" + fi +done + +if [ ! -d "${APP_DIR}" ]; then + APP_DIR=$(dirname "$SCRIPT")/.. + APP_DIR=$(cd "${APP_DIR}"; pwd) +fi + +executable="./modules/openapi-generator-cli/target/openapi-generator-cli.jar" + +if [ ! -f "$executable" ] +then + mvn clean package +fi + +# if you've executed sbt assembly previously it will use that instead. +export JAVA_OPTS="${JAVA_OPTS} -Xmx1024M -DloggerPath=conf/log4j.properties" +ags="$@ generate -i modules/openapi-generator/src/test/resources/3_0/petstore.yaml -g plantuml -o samples/documentation/petstore/plantuml" + +java ${JAVA_OPTS} -jar ${executable} ${ags} diff --git a/bin/openapi3/windows/plantuml-documentation-petstore.bat b/bin/openapi3/windows/plantuml-documentation-petstore.bat new file mode 100644 index 00000000000..8928568143e --- /dev/null +++ b/bin/openapi3/windows/plantuml-documentation-petstore.bat @@ -0,0 +1,10 @@ +set executable=.\modules\openapi-generator-cli\target\openapi-generator-cli.jar + +If Not Exist %executable% ( + mvn clean package +) + +REM set JAVA_OPTS=%JAVA_OPTS% -Xmx1024M -DloggerPath=conf/log4j.properties +set ags=generate --artifact-id "plantuml-petstore-documentation" -i modules\openapi-generator\src\test\resources\3_0\petstore.yaml -g plantuml -o samples\documentation\petstore\plantuml + +java %JAVA_OPTS% -jar %executable% %ags% diff --git a/bin/plantuml-documentation-petstore.sh b/bin/plantuml-documentation-petstore.sh new file mode 100644 index 00000000000..39993e7212f --- /dev/null +++ b/bin/plantuml-documentation-petstore.sh @@ -0,0 +1,31 @@ +#!/bin/sh + +SCRIPT="$0" + +while [ -h "$SCRIPT" ] ; do + ls=$(ls -ld "$SCRIPT") + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' > /dev/null; then + SCRIPT="$link" + else + SCRIPT=$(dirname "$SCRIPT")/"$link" + fi +done + +if [ ! -d "${APP_DIR}" ]; then + APP_DIR=$(dirname "$SCRIPT")/.. + APP_DIR=$(cd "${APP_DIR}"; pwd) +fi + +executable="./modules/openapi-generator-cli/target/openapi-generator-cli.jar" + +if [ ! -f "$executable" ] +then + mvn clean package +fi + +# if you've executed sbt assembly previously it will use that instead. +export JAVA_OPTS="${JAVA_OPTS} -Xmx1024M -DloggerPath=conf/log4j.properties" +ags="$@ generate -i modules/openapi-generator/src/test/resources/2_0/petstore.yaml -g plantuml -o samples/documentation/petstore/plantuml" + +java ${JAVA_OPTS} -jar ${executable} ${ags} diff --git a/bin/windows/plantuml-documentation-petstore.bat b/bin/windows/plantuml-documentation-petstore.bat new file mode 100644 index 00000000000..0ea5b918e23 --- /dev/null +++ b/bin/windows/plantuml-documentation-petstore.bat @@ -0,0 +1,10 @@ +set executable=.\modules\openapi-generator-cli\target\openapi-generator-cli.jar + +If Not Exist %executable% ( + mvn clean package +) + +REM set JAVA_OPTS=%JAVA_OPTS% -Xmx1024M -DloggerPath=conf/log4j.properties +set ags=generate --artifact-id "plantuml-petstore-documentation" -i modules\openapi-generator\src\test\resources\2_0\petstore.yaml -g plantuml -o samples\documentation\petstore\plantuml + +java %JAVA_OPTS% -jar %executable% %ags% diff --git a/docs/generators.md b/docs/generators.md index 8f48e2543bc..a74d15a276c 100644 --- a/docs/generators.md +++ b/docs/generators.md @@ -136,6 +136,7 @@ The following generators are available: * [markdown (beta)](generators/markdown.md) * [openapi](generators/openapi.md) * [openapi-yaml](generators/openapi-yaml.md) +* [plantuml](generators/plantuml.md) ## SCHEMA generators diff --git a/docs/generators/plantuml.md b/docs/generators/plantuml.md new file mode 100644 index 00000000000..9f64e821834 --- /dev/null +++ b/docs/generators/plantuml.md @@ -0,0 +1,159 @@ +--- +title: Config Options for plantuml +sidebar_label: plantuml +--- + +| Option | Description | Values | Default | +| ------ | ----------- | ------ | ------- | +|allowUnicodeIdentifiers|boolean, toggles whether unicode identifiers are allowed in names or not, default is false| |false| +|ensureUniqueParams|Whether to ensure parameter names are unique in an operation (rename parameters that are not).| |true| +|legacyDiscriminatorBehavior|This flag is used by OpenAPITools codegen to influence the processing of the discriminator attribute in OpenAPI documents. This flag has no impact if the OAS document does not use the discriminator attribute. The default value of this flag is set in each language-specific code generator (e.g. Python, Java, go...)using the method toModelName. Note to developers supporting a language generator in OpenAPITools; to fully support the discriminator attribute as defined in the OAS specification 3.x, language generators should set this flag to true by default; however this requires updating the mustache templates to generate a language-specific discriminator lookup function that iterates over {{#mappedModels}} and does not iterate over {{children}}, {{#anyOf}}, or {{#oneOf}}.|
**true**
The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.
**false**
The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.
|true| +|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false| +|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true| +|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true| + +## IMPORT MAPPING + +| Type/Alias | Imports | +| ---------- | ------- | +|Array|java.util.List| +|ArrayList|java.util.ArrayList| +|BigDecimal|java.math.BigDecimal| +|Date|java.util.Date| +|DateTime|org.joda.time.*| +|File|java.io.File| +|HashMap|java.util.HashMap| +|List|java.util.*| +|LocalDate|org.joda.time.*| +|LocalDateTime|org.joda.time.*| +|LocalTime|org.joda.time.*| +|Map|java.util.Map| +|Set|java.util.*| +|Timestamp|java.sql.Timestamp| +|URI|java.net.URI| +|UUID|java.util.UUID| + + +## INSTANTIATION TYPES + +| Type/Alias | Instantiated By | +| ---------- | --------------- | + + +## LANGUAGE PRIMITIVES + + + +## RESERVED WORDS + + + +## FEATURE SET + + +### Client Modification Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|BasePath|✗|ToolingExtension +|Authorizations|✗|ToolingExtension +|UserAgent|✗|ToolingExtension + +### Data Type Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Custom|✗|OAS2,OAS3 +|Int32|✓|OAS2,OAS3 +|Int64|✓|OAS2,OAS3 +|Float|✓|OAS2,OAS3 +|Double|✓|OAS2,OAS3 +|Decimal|✓|ToolingExtension +|String|✓|OAS2,OAS3 +|Byte|✓|OAS2,OAS3 +|Binary|✓|OAS2,OAS3 +|Boolean|✓|OAS2,OAS3 +|Date|✓|OAS2,OAS3 +|DateTime|✓|OAS2,OAS3 +|Password|✓|OAS2,OAS3 +|File|✓|OAS2 +|Array|✓|OAS2,OAS3 +|Maps|✓|ToolingExtension +|CollectionFormat|✓|OAS2 +|CollectionFormatMulti|✓|OAS2 +|Enum|✓|OAS2,OAS3 +|ArrayOfEnum|✓|ToolingExtension +|ArrayOfModel|✓|ToolingExtension +|ArrayOfCollectionOfPrimitives|✓|ToolingExtension +|ArrayOfCollectionOfModel|✓|ToolingExtension +|ArrayOfCollectionOfEnum|✓|ToolingExtension +|MapOfEnum|✓|ToolingExtension +|MapOfModel|✓|ToolingExtension +|MapOfCollectionOfPrimitives|✓|ToolingExtension +|MapOfCollectionOfModel|✓|ToolingExtension +|MapOfCollectionOfEnum|✓|ToolingExtension + +### Documentation Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Readme|✗|ToolingExtension +|Model|✓|ToolingExtension +|Api|✓|ToolingExtension + +### Global Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Host|✓|OAS2,OAS3 +|BasePath|✓|OAS2,OAS3 +|Info|✓|OAS2,OAS3 +|Schemes|✗|OAS2,OAS3 +|PartialSchemes|✓|OAS2,OAS3 +|Consumes|✓|OAS2 +|Produces|✓|OAS2 +|ExternalDocumentation|✓|OAS2,OAS3 +|Examples|✓|OAS2,OAS3 +|XMLStructureDefinitions|✗|OAS2,OAS3 +|MultiServer|✗|OAS3 +|ParameterizedServer|✗|OAS3 +|ParameterStyling|✗|OAS3 +|Callbacks|✓|OAS3 +|LinkObjects|✗|OAS3 + +### Parameter Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Path|✓|OAS2,OAS3 +|Query|✓|OAS2,OAS3 +|Header|✓|OAS2,OAS3 +|Body|✓|OAS2 +|FormUnencoded|✓|OAS2 +|FormMultipart|✓|OAS2 +|Cookie|✓|OAS3 + +### Schema Support Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|Simple|✓|OAS2,OAS3 +|Composite|✓|OAS2,OAS3 +|Polymorphism|✓|OAS2,OAS3 +|Union|✗|OAS3 + +### Security Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|BasicAuth|✓|OAS2,OAS3 +|ApiKey|✓|OAS2,OAS3 +|OpenIDConnect|✗|OAS3 +|BearerToken|✓|OAS3 +|OAuth2_Implicit|✓|OAS2,OAS3 +|OAuth2_Password|✓|OAS2,OAS3 +|OAuth2_ClientCredentials|✓|OAS2,OAS3 +|OAuth2_AuthorizationCode|✓|OAS2,OAS3 + +### Wire Format Feature +| Name | Supported | Defined By | +| ---- | --------- | ---------- | +|JSON|✓|OAS2,OAS3 +|XML|✓|OAS2,OAS3 +|PROTOBUF|✗|ToolingExtension +|Custom|✗|OAS2,OAS3 diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PlantumlDocumentationCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PlantumlDocumentationCodegen.java new file mode 100644 index 00000000000..295cdd817e5 --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PlantumlDocumentationCodegen.java @@ -0,0 +1,185 @@ +package org.openapitools.codegen.languages; + +import org.openapitools.codegen.*; + +import java.io.File; +import java.util.*; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class PlantumlDocumentationCodegen extends DefaultCodegen implements CodegenConfig { + public static final String ALL_OF_SUFFIX = "AllOf"; + + static Logger LOGGER = LoggerFactory.getLogger(PlantumlDocumentationCodegen.class); + + public CodegenType getTag() { + return CodegenType.DOCUMENTATION; + } + + public String getName() { + return "plantuml"; + } + + public String getHelp() { + return "Generates a plantuml documentation."; + } + + public PlantumlDocumentationCodegen() { + super(); + + outputFolder = "generated-code" + File.separator + "plantuml"; + embeddedTemplateDir = templateDir = "plantuml-documentation"; + supportingFiles.add(new SupportingFile("schemas.mustache", "", "schemas.plantuml")); + } + + @SuppressWarnings("unchecked") + @Override + public Map postProcessSupportingFileData(Map objs) { + Object models = objs.get("models"); + List modelsList = (List) models; + List codegenModelList = modelsList.stream() + .filter(listItem -> listItem instanceof HashMap) + .map(listItem -> (CodegenModel)((HashMap)listItem).get("model")) + .collect(Collectors.toList()); + + List inlineAllOfCodegenModelList = codegenModelList.stream() + .filter(codegenModel -> codegenModel.getClassname().endsWith(ALL_OF_SUFFIX)) + .collect(Collectors.toList()); + + List nonInlineAllOfCodegenModelList = codegenModelList.stream() + .filter(codegenModel -> !codegenModel.getClassname().endsWith(ALL_OF_SUFFIX)) + .collect(Collectors.toList()); + + List subtypeCodegenModelList = codegenModelList.stream() + .filter(codegenModel -> !codegenModel.allOf.isEmpty()) + .collect(Collectors.toList()); + + + List> entities = calculateEntities(nonInlineAllOfCodegenModelList, inlineAllOfCodegenModelList); + objs.put("entities", entities); + + List> relationships = calculateCompositionRelationshipsFrom(entities); + objs.put("relationships", relationships); + + List inheritances = calculateInheritanceRelationships(subtypeCodegenModelList); + objs.put("inheritances", inheritances); + + return super.postProcessSupportingFileData(objs); + } + + private List> calculateEntities(List nonInlineAllOfCodegenModelList, List inlineAllOfCodegenModelList) { + Map inlineAllOfCodegenModelMap = inlineAllOfCodegenModelList.stream().collect(Collectors.toMap(cm -> cm.getClassname(), cm -> cm)); + + return nonInlineAllOfCodegenModelList.stream().map(codegenModel -> createEntityFor(codegenModel, inlineAllOfCodegenModelMap)).collect(Collectors.toList()); + } + + private Map createEntityFor(CodegenModel nonInlineAllOfCodegenModel, Map inlineAllOfCodegenModelMap) { + if (nonInlineAllOfCodegenModel.allOf.isEmpty()) { + return createEntityFor(nonInlineAllOfCodegenModel, nonInlineAllOfCodegenModel.getAllVars()); + } else { + Optional inheritingInlineAllOfName = nonInlineAllOfCodegenModel.allOf.stream().filter(allOfName -> allOfName.endsWith(ALL_OF_SUFFIX)).findFirst(); + if (inheritingInlineAllOfName.isPresent()) { + if (inlineAllOfCodegenModelMap.containsKey(inheritingInlineAllOfName.get())) { + CodegenModel inheritingInlineAllOfModel = inlineAllOfCodegenModelMap.get(inheritingInlineAllOfName.get()); + return createEntityFor(nonInlineAllOfCodegenModel, inheritingInlineAllOfModel.getAllVars()); + } + } + + return createEntityFor(nonInlineAllOfCodegenModel, Collections.emptyList()); + } + } + + private Map createEntityFor(CodegenModel codegenModel, List properties) { + Map entity = new HashMap<>(); + entity.put("name", removeSuffix(codegenModel.getClassname(), ALL_OF_SUFFIX)); + + List fields = properties.stream() + .map(var -> createFieldFor(var)) + .collect(Collectors.toList()); + entity.put("fields", fields); + + return entity; + } + + private Object createFieldFor(CodegenProperty codegenProperty) { + Map field = new HashMap<>(); + field.put("name", codegenProperty.getBaseName()); + field.put("isRequired", codegenProperty.getRequired()); + field.put("isList", codegenProperty.isListContainer); + field.put("complexDataType", getComplexDataTypeFor(codegenProperty)); + + String dataType = codegenProperty.isListContainer && codegenProperty.getItems() != null ? "List<" + toModelName(codegenProperty.getItems().getDataType()) + ">" : toModelName(codegenProperty.getDataType()); + field.put("dataType", dataType); + + return field; + } + + @SuppressWarnings("unchecked") + private List> calculateCompositionRelationshipsFrom(List> entities) { + Map>> entityFieldsMap = entities.stream() + .collect(Collectors.toMap(entity -> (String)entity.get("name"), entity -> (List>)entity.get("fields"))); + + return entityFieldsMap.entrySet().stream() + .map(entry -> createRelationshipsFor(entry.getKey(), entry.getValue())) + .flatMap(relationshipList -> relationshipList.stream()) + .collect(Collectors.toList()); + } + + private List> createRelationshipsFor(String entityName, List> fields) { + return fields.stream() + .filter(field -> field.get("complexDataType") != null) + .map(complexField -> createRelationshipFor(complexField, entityName)) + .collect(Collectors.toList()); + } + + private Map createRelationshipFor(Map complexField, String entityName) { + Map relationship = new HashMap<>(); + relationship.put("parent", entityName); + relationship.put("child", complexField.get("complexDataType")); + relationship.put("name", complexField.get("name")); + relationship.put("isList", complexField.get("isList")); + + return relationship; + } + + private String getComplexDataTypeFor(CodegenProperty codegenProperty) { + if (codegenProperty.isModel) { + return toModelName(codegenProperty.getDataType()); + } else if (codegenProperty.isListContainer && codegenProperty.getItems().isModel) { + return toModelName((codegenProperty.getItems().getDataType())); + } + + return null; + } + + private List calculateInheritanceRelationships(List subtypeCodegenModelList) { + return subtypeCodegenModelList.stream() + .map(subtypeModel -> getInterfacesForSubtype(subtypeModel)) + .flatMap(subTypeInterfaces -> subTypeInterfaces.getValue().stream().map(parent -> createInheritance(parent, subTypeInterfaces.getKey()))) + .collect(Collectors.toList()); + } + + private Map.Entry> getInterfacesForSubtype(CodegenModel subtypeModel) { + List interfaceList = subtypeModel.getInterfaces().stream() + .filter(inf -> !inf.endsWith(ALL_OF_SUFFIX)) + .collect(Collectors.toList()); + return new AbstractMap.SimpleEntry<>(subtypeModel.getClassname(), interfaceList); + } + + private Object createInheritance(String parentName, String childName) { + Map inheritance = new HashMap<>(); + inheritance.put("parent", parentName); + inheritance.put("child", childName); + return inheritance; + } + + private String removeSuffix(final String value, final String suffix) { + if (value != null && suffix != null && value.endsWith(suffix)) { + return value.substring(0, value.length() - suffix.length()); + } + + return value; + } +} diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index 9b3b68c28d7..0ad7647b9a7 100644 --- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -130,3 +130,5 @@ org.openapitools.codegen.languages.MarkdownDocumentationCodegen org.openapitools.codegen.languages.ScalaSttpClientCodegen org.openapitools.codegen.languages.ScalaAkkaHttpServerCodegen + +org.openapitools.codegen.languages.PlantumlDocumentationCodegen diff --git a/modules/openapi-generator/src/main/resources/plantuml-documentation/schemas.mustache b/modules/openapi-generator/src/main/resources/plantuml-documentation/schemas.mustache new file mode 100644 index 00000000000..bdcb455f6ea --- /dev/null +++ b/modules/openapi-generator/src/main/resources/plantuml-documentation/schemas.mustache @@ -0,0 +1,21 @@ +@startuml + +title {{appName}} Schemas Diagram + +{{#entities}} +entity {{{name}}} { +{{#fields}} + {{#isRequired}}* {{/isRequired}}{{{name}}}: {{{dataType}}} +{{/fields}} +} + +{{/entities}} +{{#inheritances}} +{{{parent}}} <|--- {{{child}}} +{{/inheritances}} + +{{#relationships}} +{{{parent}}} -- {{#isList}}"0..*" {{/isList}}{{{child}}} : {{{name}}} +{{/relationships}} + +@enduml \ No newline at end of file diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/plantuml/PlantumlDocumentationCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/plantuml/PlantumlDocumentationCodegenTest.java new file mode 100644 index 00000000000..ff4b4648b84 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/plantuml/PlantumlDocumentationCodegenTest.java @@ -0,0 +1,307 @@ +package org.openapitools.codegen.plantuml; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.media.*; +import io.swagger.v3.parser.util.SchemaTypeUtil; +import org.openapitools.codegen.CodegenModel; +import org.openapitools.codegen.TestUtils; +import org.openapitools.codegen.languages.PlantumlDocumentationCodegen; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.*; + +public class PlantumlDocumentationCodegenTest { + private PlantumlDocumentationCodegen plantumlDocumentationCodegen = new PlantumlDocumentationCodegen(); + + @Test + public void simpleEntityTest() { + final Schema model = new Schema() + .description("a sample model") + .addProperties("id", new IntegerSchema().format(SchemaTypeUtil.INTEGER64_FORMAT)) + .addProperties("name", new StringSchema()) + .addRequiredItem("id") + .addRequiredItem("name"); + OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", model); + plantumlDocumentationCodegen.setOpenAPI(openAPI); + final CodegenModel cm = plantumlDocumentationCodegen.fromModel("sample", model); + + Map objs = createObjectsMapFor(cm); + + plantumlDocumentationCodegen.postProcessSupportingFileData(objs); + + List entityList = getList(objs, "entities"); + Assert.assertFalse(entityList.isEmpty(), "empty entity list"); + + Map firstEntity = getEntityFromList("Sample", entityList); + + List fieldList = getList(firstEntity, "fields"); + Assert.assertEquals(fieldList.size(), 2, "size of field list"); + + Map firstField = (Map)fieldList.get(0); + + Assert.assertEquals((String)firstField.get("name"), "id"); + Assert.assertTrue((boolean)firstField.get("isRequired")); + Assert.assertEquals((String)firstField.get("dataType"), "Long"); + } + + @Test + public void listFieldTest() { + final Schema model = new Schema() + .description("a sample model") + .addProperties("tags", new ArraySchema().items(new StringSchema())); + OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", model); + plantumlDocumentationCodegen.setOpenAPI(openAPI); + final CodegenModel cm = plantumlDocumentationCodegen.fromModel("sample", model); + + Map objs = createObjectsMapFor(cm); + + plantumlDocumentationCodegen.postProcessSupportingFileData(objs); + + List entityList = getList(objs, "entities"); + + Map sampleEntity = getEntityFromList("Sample", entityList); + Map tagsField = getFieldFromEntity("tags", sampleEntity); + Assert.assertEquals((String)tagsField.get("dataType"), "List"); + } + + @Test + public void inheritedEntitiesTest() { + OpenAPI openAPI = TestUtils.createOpenAPI(); + final Schema parentSchema = new Schema() + .description("a parent model") + .addProperties("id", new StringSchema()) + .addRequiredItem("id"); + + openAPI.getComponents().addSchemas("parent", parentSchema); + + final Schema childAllOfInlineSchema = new Schema() + .description("an inline model") + .addProperties("name", new StringSchema()); + + openAPI.getComponents().addSchemas("child_allOf", childAllOfInlineSchema); + + final ComposedSchema composedSchema = new ComposedSchema(); + composedSchema.setDescription("a composed child model"); + composedSchema.addAllOfItem(new Schema().$ref("#/components/schemas/parent")); + composedSchema.addAllOfItem(new Schema().$ref("#/components/schemas/child_allOf")); + openAPI.getComponents().addSchemas("child", composedSchema); + + plantumlDocumentationCodegen.setOpenAPI(openAPI); + final CodegenModel parentModel = plantumlDocumentationCodegen.fromModel("parent", parentSchema); + final CodegenModel childAllOfModel = plantumlDocumentationCodegen.fromModel("child_allOf", childAllOfInlineSchema); + final CodegenModel childComposedModel = plantumlDocumentationCodegen.fromModel("child", composedSchema); + + Map objs = createObjectsMapFor(parentModel, childComposedModel, childAllOfModel); + + plantumlDocumentationCodegen.postProcessSupportingFileData(objs); + + List entityList = getList(objs, "entities"); + Assert.assertEquals(entityList.size(), 2, "size of entity list"); + + assertEntityDoesNotExistsInList("ChildAllOf", entityList); + + Map parentEntity = getEntityFromList("Parent", entityList); + getFieldFromEntity("id", parentEntity); + + Map childEntity = getEntityFromList("Child", entityList); + assertFieldDoesNotExistsInEntity("id", childEntity); + getFieldFromEntity("name", childEntity); + + List inheritanceList = getList(objs, "inheritances"); + Assert.assertEquals(inheritanceList.size(), 1, "size of inheritance list"); + + Map firstInheritance = (Map)inheritanceList.get(0); + Assert.assertEquals(firstInheritance.get("parent"), "Parent"); + Assert.assertEquals(firstInheritance.get("child"), "Child"); + } + + @Test + public void aggregatedEntitiesTest() { + OpenAPI openAPI = TestUtils.createOpenAPI(); + final Schema simpleDataTypeSchema = new Schema() + .description("a simple model") + .addProperties("name", new StringSchema()); + + openAPI.getComponents().addSchemas("simple", simpleDataTypeSchema); + + final Schema tagDataTypeSchema = new Schema() + .description("a tag model") + .addProperties("name", new StringSchema()); + + openAPI.getComponents().addSchemas("tag", tagDataTypeSchema); + + final Schema parentSchema = new Schema() + .description("a parent model") + .addProperties("id", new StringSchema()) + .addProperties("name", new Schema().$ref("#/components/schemas/simple")) + .addProperties("tags", new ArraySchema().items(new Schema().$ref("#/components/schemas/tag"))) + .addRequiredItem("id"); + + openAPI.getComponents().addSchemas("parent", parentSchema); + + plantumlDocumentationCodegen.setOpenAPI(openAPI); + final CodegenModel simpleModel = plantumlDocumentationCodegen.fromModel("simple", simpleDataTypeSchema); + final CodegenModel tagModel = plantumlDocumentationCodegen.fromModel("tag", tagDataTypeSchema); + final CodegenModel parentModel = plantumlDocumentationCodegen.fromModel("parent", parentSchema); + + Map objs = createObjectsMapFor(parentModel, simpleModel, tagModel); + + plantumlDocumentationCodegen.postProcessSupportingFileData(objs); + + List entityList = getList(objs, "entities"); + Assert.assertEquals(entityList.size(), 3, "size of entity list"); + + Map parentEntity = getEntityFromList("Parent", entityList); + + Map nameField = getFieldFromEntity("name", parentEntity); + Assert.assertEquals((String)nameField.get("dataType"), "Simple"); + + Map tagsField = getFieldFromEntity("tags", parentEntity); + Assert.assertEquals((String)tagsField.get("dataType"), "List"); + + List relationshipList = getList(objs, "relationships"); + Assert.assertEquals(relationshipList.size(), 2, "size of relationship list"); + + Map firstRelationship = (Map)relationshipList.get(0); + Assert.assertEquals((String)firstRelationship.get("parent"), "Parent"); + Assert.assertEquals((String)firstRelationship.get("child"), "Simple"); + Assert.assertEquals((String)firstRelationship.get("name"), "name"); + Assert.assertFalse((boolean)firstRelationship.get("isList")); + + Map secondRelationship = (Map)relationshipList.get(1); + Assert.assertEquals((String)secondRelationship.get("parent"), "Parent"); + Assert.assertEquals((String)secondRelationship.get("child"), "Tag"); + Assert.assertEquals((String)secondRelationship.get("name"), "tags"); + Assert.assertTrue((boolean)secondRelationship.get("isList")); + } + + @Test + public void sharedIdenticalInlineAllOfTest() { + OpenAPI openAPI = TestUtils.createOpenAPI(); + final Schema parentSchema = new Schema() + .description("a parent model") + .addProperties("id", new StringSchema()); + + openAPI.getComponents().addSchemas("parent", parentSchema); + + final Schema tagDataTypeSchema = new Schema() + .description("a tag model") + .addProperties("name", new StringSchema()); + + openAPI.getComponents().addSchemas("tag", tagDataTypeSchema); + + final Schema anotherAllOfInlineSchema = new Schema() + .description("an identical inline model used elsewhere") + .addProperties("tag", new Schema().$ref("#/components/schemas/tag")); + + openAPI.getComponents().addSchemas("another_allOf", anotherAllOfInlineSchema); + + final ComposedSchema composedSchema = new ComposedSchema(); + composedSchema.setDescription("a composed child model"); + composedSchema.addAllOfItem(new Schema().$ref("#/components/schemas/parent")); + composedSchema.addAllOfItem(new Schema().$ref("#/components/schemas/another_allOf")); + openAPI.getComponents().addSchemas("child", composedSchema); + + plantumlDocumentationCodegen.setOpenAPI(openAPI); + final CodegenModel parentModel = plantumlDocumentationCodegen.fromModel("parent", parentSchema); + final CodegenModel childAllOfModel = plantumlDocumentationCodegen.fromModel("another_allOf", anotherAllOfInlineSchema); + final CodegenModel childComposedModel = plantumlDocumentationCodegen.fromModel("child", composedSchema); + + Map objs = createObjectsMapFor(parentModel, childComposedModel, childAllOfModel); + + plantumlDocumentationCodegen.postProcessSupportingFileData(objs); + + List entityList = getList(objs, "entities"); + Assert.assertEquals(entityList.size(), 2, "size of entity list"); + + Map parentEntity = getEntityFromList("Parent", entityList); + Map childEntity = getEntityFromList("Child", entityList); + + List inheritanceList = getList(objs, "inheritances"); + Assert.assertEquals(inheritanceList.size(), 1, "size of inheritance list"); + + Map firstInheritance = (Map)inheritanceList.get(0); + Assert.assertEquals(firstInheritance.get("parent"), "Parent"); + Assert.assertEquals(firstInheritance.get("child"), "Child"); + + List relationshipList = getList(objs, "relationships"); + Assert.assertEquals(relationshipList.size(), 1, "size of relationship list"); + + Map firstRelationship = (Map)relationshipList.get(0); + Assert.assertEquals((String)firstRelationship.get("parent"), "Child"); + Assert.assertEquals((String)firstRelationship.get("child"), "Tag"); + } + + private Map createObjectsMapFor(CodegenModel... codegenModels) { + List> modelsList = new ArrayList(); + + for (CodegenModel codegenModel: codegenModels) { + Map modelMap = new HashMap<>(); + modelMap.put("model", codegenModel); + modelsList.add(modelMap); + } + + Map objs = new HashMap<>(); + objs.put("models", modelsList); + return objs; + } + + private Map toMap(Object entityItem) { + return (Map)entityItem; + } + + private boolean hasName(String name, Map map) { + return (map.get("name")).equals(name); + } + + private void assertEntityDoesNotExistsInList(String name, List entityList) { + long count = entityList.stream() + .map(entityItem -> toMap(entityItem)) + .filter(entityMap -> hasName(name, entityMap)) + .count(); + + Assert.assertEquals(count, 0, "entries with name " + name); + } + + private Map getEntityFromList(String name, List entityList) { + Optional> entity = entityList.stream() + .map(entityItem -> toMap(entityItem)) + .filter(entityMap -> hasName(name, entityMap)) + .findFirst(); + + Assert.assertTrue(entity.isPresent(), "entity with name '" + name + "' found in list"); + + return entity.get(); + } + + private void assertFieldDoesNotExistsInEntity(String name, Map entity) { + List fieldList = (List)entity.get("fields"); + long count = fieldList.stream() + .map(fieldItem -> toMap(fieldItem)) + .filter(fieldMap -> hasName(name, fieldMap)) + .count(); + + Assert.assertEquals(count, 0, "fields with name " + name); + } + + private Map getFieldFromEntity(String name, Map entity) { + List fieldList = (List)entity.get("fields"); + Optional> field = fieldList.stream() + .map(fieldItem -> toMap(fieldItem)) + .filter(fieldMap -> hasName(name, fieldMap)) + .findFirst(); + + Assert.assertTrue(field.isPresent(), "field with name '" + name + "' found in list"); + + return field.get(); + } + + private List getList(Map objs, String listName) { + Object list = objs.get(listName); + Assert.assertNotNull(list, "object with name '" + listName + "' in objs map"); + + Assert.assertTrue(list instanceof List, "object with name '" + listName + "' in objs map is a list"); + return (List)list; + } +} diff --git a/samples/documentation/petstore/plantuml/.openapi-generator-ignore b/samples/documentation/petstore/plantuml/.openapi-generator-ignore new file mode 100644 index 00000000000..08b85d1c878 --- /dev/null +++ b/samples/documentation/petstore/plantuml/.openapi-generator-ignore @@ -0,0 +1,25 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md + +#*.plantuml \ No newline at end of file diff --git a/samples/documentation/petstore/plantuml/.openapi-generator/VERSION b/samples/documentation/petstore/plantuml/.openapi-generator/VERSION new file mode 100644 index 00000000000..d99e7162d01 --- /dev/null +++ b/samples/documentation/petstore/plantuml/.openapi-generator/VERSION @@ -0,0 +1 @@ +5.0.0-SNAPSHOT \ No newline at end of file diff --git a/samples/documentation/petstore/plantuml/schemas.plantuml b/samples/documentation/petstore/plantuml/schemas.plantuml new file mode 100644 index 00000000000..f4d1ddf5e7d --- /dev/null +++ b/samples/documentation/petstore/plantuml/schemas.plantuml @@ -0,0 +1,54 @@ +@startuml + +title OpenAPI Petstore Schemas Diagram + +entity ApiResponse { + code: Integer + type: String + message: String +} + +entity Category { + id: Long + name: String +} + +entity Order { + id: Long + petId: Long + quantity: Integer + shipDate: Date + status: String + complete: Boolean +} + +entity Pet { + id: Long + category: Category + * name: String + * photoUrls: List + tags: List + status: String +} + +entity Tag { + id: Long + name: String +} + +entity User { + id: Long + username: String + firstName: String + lastName: String + email: String + password: String + phone: String + userStatus: Integer +} + + +Pet -- Category : category +Pet -- "0..*" Tag : tags + +@enduml \ No newline at end of file