-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 0d0361f
Showing
5 changed files
with
332 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
HELP.md | ||
target/ | ||
!.mvn/wrapper/maven-wrapper.jar | ||
!**/src/main/** | ||
!**/src/test/** | ||
|
||
### STS ### | ||
.apt_generated | ||
.classpath | ||
.factorypath | ||
.project | ||
.settings | ||
.springBeans | ||
.sts4-cache | ||
|
||
### IntelliJ IDEA ### | ||
.idea | ||
*.iws | ||
*.iml | ||
*.ipr | ||
|
||
### NetBeans ### | ||
/nbproject/private/ | ||
/nbbuild/ | ||
/dist/ | ||
/nbdist/ | ||
/.nb-gradle/ | ||
build/ | ||
|
||
### VS Code ### | ||
.vscode/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<groupId>com.danielfrak.code</groupId> | ||
<artifactId>springdoc-openapi-programmatic-documentation</artifactId> | ||
<version>1.0</version> | ||
|
||
<name>Springdoc OpenAPI Programmatic Documenation</name> | ||
<description>Allows for defining OpenAPI documentation using code</description> | ||
<url>https://www.code.danielfrak.com</url> | ||
|
||
<licenses> | ||
<license> | ||
<name>MIT License</name> | ||
<url>http://www.opensource.org/licenses/mit-license.php</url> | ||
<distribution>repo</distribution> | ||
</license> | ||
</licenses> | ||
|
||
<properties> | ||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
<maven.compiler.source>1.8</maven.compiler.source> | ||
<maven.compiler.target>1.8</maven.compiler.target> | ||
|
||
<spring.boot.version>2.1.0.RELEASE</spring.boot.version> | ||
<swagger.core.version>2.1.0</swagger.core.version> | ||
<junit.version>5.5.0</junit.version> | ||
<maven-surefire-plugin.version>2.22.0</maven-surefire-plugin.version> | ||
<mockito.version>3.2.4</mockito.version> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>org.springframework.boot</groupId> | ||
<artifactId>spring-boot-starter</artifactId> | ||
<version>${spring.boot.version}</version> | ||
<scope>provided</scope> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>io.swagger.core.v3</groupId> | ||
<artifactId>swagger-core</artifactId> | ||
<version>${swagger.core.version}</version> | ||
<scope>provided</scope> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.junit.jupiter</groupId> | ||
<artifactId>junit-jupiter-engine</artifactId> | ||
<version>${junit.version}</version> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.mockito</groupId> | ||
<artifactId>mockito-core</artifactId> | ||
<version>${mockito.version}</version> | ||
<scope>test</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>org.mockito</groupId> | ||
<artifactId>mockito-junit-jupiter</artifactId> | ||
<version>${mockito.version}</version> | ||
<scope>test</scope> | ||
</dependency> | ||
</dependencies> | ||
</project> |
125 changes: 125 additions & 0 deletions
125
src/main/java/com/danielfrak/code/programmaticdocs/ModelDocumentation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,125 @@ | ||
package com.danielfrak.code.programmaticdocs; | ||
|
||
import com.fasterxml.jackson.databind.type.TypeFactory; | ||
import io.swagger.v3.core.converter.AnnotatedType; | ||
import io.swagger.v3.core.converter.ModelConverter; | ||
import io.swagger.v3.core.converter.ModelConverterContext; | ||
import io.swagger.v3.oas.models.media.Schema; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.springframework.stereotype.Component; | ||
|
||
import java.lang.reflect.Field; | ||
import java.util.HashMap; | ||
import java.util.Iterator; | ||
import java.util.Map; | ||
|
||
/** | ||
* Enables programmatic configuration of OpenAPI schema. | ||
* <p> | ||
* Example usage: | ||
* <p> | ||
* <pre>{@code | ||
* externalModelDocumentation | ||
* .add(new ExternalModel() | ||
* .source(MyValueObject.class) | ||
* .implementation(String.class) | ||
* .schema(new Schema<>() | ||
* .example("Some example") | ||
* .description("Some description"))); | ||
* }</pre> | ||
*/ | ||
@Component | ||
public class ModelDocumentation implements ModelConverter { | ||
|
||
private Map<Class<?>, Class<?>> classMap = new HashMap<>(); | ||
private Map<Class<?>, Schema<?>> schemaMap = new HashMap<>(); | ||
|
||
public ModelDocumentation add(DocumentedModel model) { | ||
if(model.sourceClass == null) { | ||
throw new IllegalArgumentException("Source class cannot be NULL"); | ||
} | ||
if(model.implementation != null) { | ||
classMap.put(model.sourceClass, model.implementation); | ||
} | ||
if(model.schema != null) { | ||
schemaMap.put(model.sourceClass, model.schema); | ||
} | ||
return this; | ||
} | ||
|
||
@Override | ||
public Schema<?> resolve(AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) { | ||
Class<?> typeClass = TypeFactory.rawClass(type.getType()); | ||
|
||
if (classMap.containsKey(typeClass)) { | ||
type.setType(classMap.get(typeClass)); | ||
} | ||
|
||
try { | ||
return overrideSchema(typeClass, resolveChain(type, context, chain)); | ||
} catch (IllegalAccessException e) { | ||
throw new RuntimeException("Could not convert model", e); | ||
} | ||
} | ||
|
||
private Schema<?> resolveChain(AnnotatedType type, ModelConverterContext context, Iterator<ModelConverter> chain) { | ||
if (chain.hasNext()) { | ||
return chain.next().resolve(type, context, chain); | ||
} else { | ||
return null; | ||
} | ||
} | ||
|
||
private Schema<?> overrideSchema(Class<?> typeName, Schema<?> model) throws IllegalAccessException { | ||
if (model == null || !schemaMap.containsKey(typeName)) { | ||
return model; | ||
} | ||
|
||
Schema<?> schema = schemaMap.get(typeName); | ||
|
||
for(Field field: schema.getClass().getDeclaredFields()) { | ||
field.setAccessible(true); | ||
|
||
if (isBlank(field.get(model))) { | ||
field.set(model, field.get(schema)); | ||
} | ||
} | ||
|
||
return model; | ||
} | ||
|
||
private boolean isBlank(Object modelValue) { | ||
return modelValue == null || (modelValue instanceof String && StringUtils.isBlank((String) modelValue)); | ||
} | ||
|
||
public static class DocumentedModel { | ||
|
||
private Class<?> sourceClass; | ||
private Class<?> implementation; | ||
private Schema<?> schema; | ||
|
||
/** | ||
* The class to document | ||
*/ | ||
public DocumentedModel source(Class<?> sourceClass) { | ||
this.sourceClass = sourceClass; | ||
return this; | ||
} | ||
|
||
/** | ||
* Programmatic alternative to {@code @Schema(implementation = X)} | ||
*/ | ||
public DocumentedModel implementation(Class<?> implementation) { | ||
this.implementation = implementation; | ||
return this; | ||
} | ||
|
||
/** | ||
* A base schema for the class. Its values may be overridden by annotations. | ||
*/ | ||
public DocumentedModel schema(Schema<?> schema) { | ||
this.schema = schema; | ||
return this; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ | ||
com.danielfrak.code.programmaticdocs.ModelDocumentation |
105 changes: 105 additions & 0 deletions
105
src/test/java/com/danielfrak/code/programmaticdocs/ModelDocumentationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package com.danielfrak.code.programmaticdocs; | ||
|
||
import com.danielfrak.code.programmaticdocs.ModelDocumentation.DocumentedModel; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import io.swagger.v3.core.converter.AnnotatedType; | ||
import io.swagger.v3.core.converter.ModelConverter; | ||
import io.swagger.v3.core.converter.ModelConverterContext; | ||
import io.swagger.v3.core.jackson.ModelResolver; | ||
import io.swagger.v3.oas.models.media.Schema; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.mockito.Mock; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
|
||
import java.util.Iterator; | ||
|
||
import static java.util.Collections.singletonList; | ||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||
import static org.mockito.ArgumentMatchers.any; | ||
import static org.mockito.Mockito.mock; | ||
import static org.mockito.Mockito.when; | ||
|
||
@ExtendWith(MockitoExtension.class) | ||
class ModelDocumentationTest { | ||
|
||
private ModelDocumentation modelDocumentation; | ||
|
||
@Mock | ||
private ModelConverterContext modelConverterContext; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
modelDocumentation = new ModelDocumentation(); | ||
} | ||
|
||
@Test | ||
void throwsExceptionWhenSourceClassNotProvided() { | ||
assertThrows(IllegalArgumentException.class, () -> modelDocumentation.add(new DocumentedModel())); | ||
} | ||
|
||
@Test | ||
void changesImplementation() { | ||
Iterator<ModelConverter> chain = | ||
singletonList((ModelConverter) new ModelResolver(new ObjectMapper())).iterator(); | ||
|
||
modelDocumentation.add(new DocumentedModel() | ||
.source(DummyObject.class) | ||
.implementation(String.class)); | ||
|
||
Schema<?> schema = modelDocumentation.resolve(new AnnotatedType(DummyObject.class), modelConverterContext, | ||
chain); | ||
|
||
assertEquals("string", schema.getType()); | ||
} | ||
|
||
@Test | ||
void changesSchema() { | ||
Iterator<ModelConverter> chain = | ||
singletonList((ModelConverter) new ModelResolver(new ObjectMapper())).iterator(); | ||
|
||
final String descriptionValue = "Some description"; | ||
final String exampleValue = "Some example"; | ||
|
||
modelDocumentation.add(new DocumentedModel() | ||
.source(DummyObject.class) | ||
.schema(new Schema<>() | ||
.description(descriptionValue) | ||
.example(exampleValue))); | ||
|
||
Schema<?> schema = modelDocumentation.resolve(new AnnotatedType(DummyObject.class), modelConverterContext, | ||
chain); | ||
|
||
assertEquals(descriptionValue, schema.getDescription()); | ||
assertEquals(exampleValue, schema.getExample()); | ||
} | ||
|
||
@Test | ||
void willNotChangeValueSchemaWhenAlreadyDefined() { | ||
ModelConverter mockConverter = mock(ModelConverter.class); | ||
Iterator<ModelConverter> chain = singletonList(mockConverter).iterator(); | ||
|
||
final String exampleValue = "Some example"; | ||
final String existingDescriptionValue = "Existing description"; | ||
|
||
modelDocumentation.add(new DocumentedModel() | ||
.source(DummyObject.class) | ||
.schema(new Schema<>() | ||
.description("Some description") | ||
.example(exampleValue))); | ||
|
||
when(mockConverter.resolve(any(), any(), any())) | ||
.thenReturn(new Schema<>().description(existingDescriptionValue)); | ||
|
||
Schema<?> schema = modelDocumentation.resolve(new AnnotatedType(DummyObject.class), modelConverterContext, | ||
chain); | ||
|
||
assertEquals(existingDescriptionValue, schema.getDescription()); | ||
assertEquals(exampleValue, schema.getExample()); | ||
} | ||
|
||
private static class DummyObject { | ||
} | ||
} |