Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-frak committed Jan 14, 2020
0 parents commit 0d0361f
Show file tree
Hide file tree
Showing 5 changed files with 332 additions and 0 deletions.
31 changes: 31 additions & 0 deletions .gitignore
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/
69 changes: 69 additions & 0 deletions pom.xml
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>
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;
}
}
}
2 changes: 2 additions & 0 deletions src/main/resources/META-INF/spring.factories
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.danielfrak.code.programmaticdocs.ModelDocumentation
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 {
}
}

0 comments on commit 0d0361f

Please sign in to comment.