feat(table): add quarkus-table-generator
All checks were successful
Build / build (pull_request) Successful in 2m7s
All checks were successful
Build / build (pull_request) Successful in 2m7s
This commit is contained in:
parent
1e9ab13c97
commit
3100279338
5 changed files with 239 additions and 0 deletions
47
quarkus-table-generator/pom.xml
Normal file
47
quarkus-table-generator/pom.xml
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?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>
|
||||
|
||||
<parent>
|
||||
<groupId>ch.phoenix.oss</groupId>
|
||||
<artifactId>quarkus-commons</artifactId>
|
||||
<version>1.0.9-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>quarkus-table-generator</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>${jacoco-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>jacoco-check</id>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
<phase>test</phase>
|
||||
<configuration>
|
||||
<dataFile>${project.build.directory}/jacoco-quarkus.exec</dataFile>
|
||||
<rules>
|
||||
<rule>
|
||||
<element>BUNDLE</element>
|
||||
<limits>
|
||||
<limit>
|
||||
<counter>INSTRUCTION</counter>
|
||||
<value>COVEREDRATIO</value>
|
||||
<minimum>1</minimum>
|
||||
</limit>
|
||||
</limits>
|
||||
</rule>
|
||||
</rules>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -0,0 +1,20 @@
|
|||
package ch.phoenix.oss.quarkus.commons.table;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TableGenerator {
|
||||
|
||||
/**
|
||||
* Generates a Jira-style table from a list of records.
|
||||
* Refer to <a href="https://jira.atlassian.com/secure/WikiRendererHelpAction.jspa?section=all">notation</a>
|
||||
*
|
||||
* @param records List of records to generate the table from
|
||||
* @param headers Optional custom headers, list must match record field order and count.
|
||||
* If null, field names are used as headers.
|
||||
* @return String representing the records as a table in Jira notation
|
||||
* @throws IllegalArgumentException If records are null or empty, if the type is not a Record.
|
||||
* Also throws if custom headers are provided but the size does not match the record's field count
|
||||
* @throws IllegalStateException if an error occurs while invoking a record's accessor
|
||||
*/
|
||||
String generateJiraTable(List<?> records, List<String> headers);
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package ch.phoenix.oss.quarkus.commons.table;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import java.lang.reflect.RecordComponent;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@ApplicationScoped
|
||||
class TableGeneratorImpl implements TableGenerator {
|
||||
|
||||
@Override
|
||||
public String generateJiraTable(List<?> records, List<String> headers) {
|
||||
if (records == null || records.isEmpty()) {
|
||||
throw new IllegalArgumentException("Records can't be null or empty");
|
||||
}
|
||||
|
||||
Class<?> recordClass = records.getFirst().getClass();
|
||||
if (!recordClass.isRecord()) {
|
||||
throw new IllegalArgumentException("Only records are supported at the moment");
|
||||
}
|
||||
|
||||
var components = recordClass.getRecordComponents();
|
||||
var columns = components.length;
|
||||
|
||||
List<String> headerRow;
|
||||
if (headers == null) {
|
||||
headerRow = Arrays.stream(components).map(RecordComponent::getName).toList();
|
||||
} else if (headers.size() == columns) {
|
||||
headerRow = headers;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Custom header count [%s] does not match actual number of columns [%s]"
|
||||
.formatted(headers.size(), columns));
|
||||
}
|
||||
|
||||
var dataRows = new ArrayList<List<String>>();
|
||||
for (var record : records) {
|
||||
var row = new ArrayList<String>(columns);
|
||||
for (var component : components) {
|
||||
try {
|
||||
var val = component.getAccessor().invoke(record);
|
||||
row.add(val == null ? "" : val.toString());
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(
|
||||
"Unable to invoke accessor for component [%s] of class [%s]"
|
||||
.formatted(component.getName(), recordClass.getName()),
|
||||
e);
|
||||
}
|
||||
}
|
||||
dataRows.add(row);
|
||||
}
|
||||
|
||||
var sb = new StringBuilder("||");
|
||||
headerRow.forEach(header -> sb.append(" ").append(header).append(" ||"));
|
||||
sb.append("\n");
|
||||
for (var row : dataRows) {
|
||||
sb.append("|");
|
||||
for (String cell : row) {
|
||||
sb.append(" ").append(cell).append(" |");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package ch.phoenix.oss.quarkus.commons.table;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||
|
||||
import io.quarkus.test.junit.QuarkusTest;
|
||||
import jakarta.inject.Inject;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
@QuarkusTest
|
||||
class TableGeneratorImplTest {
|
||||
|
||||
@Inject
|
||||
TableGenerator tableGenerator;
|
||||
|
||||
public record TestDto(String what, Instant instant, Double number) {}
|
||||
|
||||
static Stream<Arguments> generateJiraTable() {
|
||||
var dtos = List.of(
|
||||
new TestDto("What", Instant.ofEpochMilli(123).truncatedTo(ChronoUnit.MILLIS), 2.0d),
|
||||
new TestDto("SupDog", Instant.ofEpochMilli(124313425).truncatedTo(ChronoUnit.MILLIS), null),
|
||||
new TestDto(null, null, 214534.134d),
|
||||
new TestDto(null, null, null));
|
||||
return Stream.of(
|
||||
arguments(
|
||||
dtos,
|
||||
List.of("What", "Instant", "SomeNumber"),
|
||||
"""
|
||||
|| What || Instant || SomeNumber ||
|
||||
| What | 1970-01-01T00:00:00.123Z | 2.0 |
|
||||
| SupDog | 1970-01-02T10:31:53.425Z | |
|
||||
| | | 214534.134 |
|
||||
| | | |
|
||||
"""),
|
||||
arguments(
|
||||
dtos,
|
||||
null,
|
||||
"""
|
||||
|| what || instant || number ||
|
||||
| What | 1970-01-01T00:00:00.123Z | 2.0 |
|
||||
| SupDog | 1970-01-02T10:31:53.425Z | |
|
||||
| | | 214534.134 |
|
||||
| | | |
|
||||
"""));
|
||||
}
|
||||
|
||||
@MethodSource
|
||||
@ParameterizedTest
|
||||
void generateJiraTable(List<TestDto> testDtos, List<String> headers, String expected) {
|
||||
var actual = tableGenerator.generateJiraTable(testDtos, headers);
|
||||
assertThat(actual).as("Generated table should match expected value").isEqualTo(expected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateJiraTableWhenRecordsIsNull() {
|
||||
assertThatThrownBy(() -> tableGenerator.generateJiraTable(null, null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Records can't be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateJiraTableWhenRecordsIsEmpty() {
|
||||
assertThatThrownBy(() -> tableGenerator.generateJiraTable(List.of(), null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Records can't be null or empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateJiraTableWhenNotRecord() {
|
||||
var notARecord = List.of("just a string");
|
||||
assertThatThrownBy(() -> tableGenerator.generateJiraTable(notARecord, null))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Only records are supported");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateJiraTableWhenCustomHeaderWrongSize() {
|
||||
var dtos = List.of(new TestDto("foo", Instant.now(), 1.2d));
|
||||
assertThatThrownBy(() -> tableGenerator.generateJiraTable(dtos, List.of("One", "Two")))
|
||||
.isInstanceOf(IllegalArgumentException.class)
|
||||
.hasMessageContaining("Custom header count [2] does not match actual number of columns [3]");
|
||||
}
|
||||
|
||||
public record EvilRecord(String value) {
|
||||
@Override
|
||||
public String value() {
|
||||
throw new RuntimeException("Accessor failure!");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateJiraTableWhenAccessorThrows() {
|
||||
var list = List.of(new EvilRecord("hi"));
|
||||
assertThatThrownBy(() -> tableGenerator.generateJiraTable(list, null))
|
||||
.isInstanceOf(IllegalStateException.class)
|
||||
.hasMessageContaining("Unable to invoke accessor");
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue