Skip to content

Commit

Permalink
feat(core): group AsyncApi (springwolf#967)
Browse files Browse the repository at this point in the history
* feat(core): group AsyncApi

Co-authored-by: David Müller <[email protected]>

* feat(core): integrate grouping into DefaultAsyncApiService

Co-authored-by: Timon Back <[email protected]>

* feat(core): extend config properties to support grouping

Co-authored-by: Timon Back <[email protected]>

* feat(core): use patterns in group

Co-authored-by: David Müller <[email protected]>

* feat(core): mark messages (wip)

Co-authored-by: David Müller <[email protected]>

* feat(core): refactor DefaultAsyncApiService and add tests

* feat(core): filter messages

* test(core): add grouping integration test

* feat(core): include schemas in grouping

* refactor(core): include schemas in grouping

* test(core): integration test grouping

* refactor(core): update AsyncApiService default impl

* feat(core): expose group via controller (wip)

* feat(ui): show group in ui settings

* chore(core): cleanup

Co-authored-by: Timon Back <[email protected]>

---------

Co-authored-by: David Müller <[email protected]>
  • Loading branch information
2 people authored and ruskaof committed Nov 20, 2024
1 parent e32aa53 commit c00ecfd
Show file tree
Hide file tree
Showing 49 changed files with 1,762 additions and 102 deletions.
12 changes: 6 additions & 6 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -153,12 +153,12 @@ allprojects {

doLast { // ensure that the code is not executed as part of a gradle refresh
plugins.forEach {
project
.file('springwolf-examples/' + it + '-example/src/test/resources/asyncapi.actual.json')
.renameTo('springwolf-examples/' + it + '-example/src/test/resources/asyncapi.json')
project
.file('springwolf-examples/' + it + '-example/src/test/resources/asyncapi.actual.yaml')
.renameTo('springwolf-examples/' + it + '-example/src/test/resources/asyncapi.yaml')
project.fileTree(dir: project.projectDir, include: '**/src/test/resources/**/*.actual.json').forEach { file ->
file.renameTo(file.path.replace('.actual.json', '.json'))
}
project.fileTree(dir: project.projectDir, include: '**/src/test/resources/**/*.actual.yaml').forEach { file ->
file.renameTo(file.path.replace('.actual.yaml', '.yaml'))
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,8 @@ public class ReferenceUtil {
public static String toValidId(String name) {
return name.replaceAll(FORBIDDEN_ID_CHARACTER, "_");
}

public static String getLastSegment(String ref) {
return ref.substring(ref.lastIndexOf('/') + 1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
* @see <a href="https://www.asyncapi.com/docs/reference/specification/v3.0.0#channelObject">Channel Object</a>
*/
@Data
@Builder
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
* @see <a href="https://www.asyncapi.com/docs/reference/specification/v3.0.0#operationObject">Operation</a>
*/
@Data
@Builder
@Builder(toBuilder = true)
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.springwolf.asyncapi.v3.model;

import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -12,4 +13,28 @@ void shouldCorrectIllegalCharacter() {

assertThat(ReferenceUtil.toValidId(name)).isEqualTo("users_{userId}");
}

@Nested
class GetLastSegment {
@Test
void shouldExtractChannelId() {
String name = "users/{userId}";

assertThat(ReferenceUtil.getLastSegment(name)).isEqualTo("{userId}");
}

@Test
void shouldHandleEmptyString() {
String name = "";

assertThat(ReferenceUtil.getLastSegment(name)).isEqualTo("");
}

@Test
void shouldReturnOriginalStringWhenAlreadyExtracted() {
String name = "{userId}";

assertThat(ReferenceUtil.getLastSegment(name)).isEqualTo(name);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,18 @@

import io.github.springwolf.asyncapi.v3.model.AsyncAPI;

import java.util.Optional;

public interface AsyncApiService {

AsyncAPI getAsyncAPI();

/**
* Default implementation was added to avoid breaking (compiler) change.
*
* Maintainer note: remove default implementation
*/
default Optional<AsyncAPI> getForGroupName(String groupName) {
return Optional.ofNullable(getAsyncAPI());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,34 @@
import io.github.springwolf.asyncapi.v3.model.operation.Operation;
import io.github.springwolf.core.asyncapi.channels.ChannelsService;
import io.github.springwolf.core.asyncapi.components.ComponentsService;
import io.github.springwolf.core.asyncapi.grouping.AsyncApiGroupService;
import io.github.springwolf.core.asyncapi.operations.OperationsService;
import io.github.springwolf.core.configuration.docket.AsyncApiDocket;
import io.github.springwolf.core.configuration.docket.AsyncApiDocketService;
import jakarta.annotation.Nullable;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import java.util.List;
import java.util.Map;
import java.util.Optional;

@Slf4j
@RequiredArgsConstructor
public class DefaultAsyncApiService implements AsyncApiService {

/**
* Record holding the result of AsyncAPI creation.
*
* @param asyncAPI
* @param exception
*/
private record AsyncAPIResult(AsyncAPI asyncAPI, Throwable exception) {}
private record AsyncAPIResult(
@Nullable AsyncAPI asyncAPI, @Nullable Map<String, AsyncAPI> groupedApi, Throwable exception) {}

private final AsyncApiDocketService asyncApiDocketService;
private final ChannelsService channelsService;
private final OperationsService operationsService;
private final ComponentsService componentsService;
private final List<AsyncApiCustomizer> customizers;
private final AsyncApiGroupService groupService;

private volatile AsyncAPIResult asyncAPIResult = null;

Expand All @@ -45,7 +47,20 @@ public AsyncAPI getAsyncAPI() {
if (asyncAPIResult.asyncAPI != null) {
return asyncAPIResult.asyncAPI;
} else {
throw new RuntimeException("Error occured during creation of AsyncAPI", asyncAPIResult.exception);
throw new RuntimeException("Error occurred during creation of AsyncAPI", asyncAPIResult.exception);
}
}

@Override
public Optional<AsyncAPI> getForGroupName(String groupName) {
if (isNotInitialized()) {
initAsyncAPI();
}

if (asyncAPIResult.groupedApi != null) {
return Optional.ofNullable(asyncAPIResult.groupedApi.get(groupName));
} else {
throw new RuntimeException("Error occurred during creation of AsyncAPI", asyncAPIResult.exception);
}
}

Expand Down Expand Up @@ -89,17 +104,18 @@ protected synchronized void initAsyncAPI() {
log.debug("Starting customizer {}", customizer.getClass().getName());
customizer.customize(asyncAPI);
}
this.asyncAPIResult = new AsyncAPIResult(asyncAPI, null);
Map<String, AsyncAPI> groupedApi = groupService.group(asyncAPI);
this.asyncAPIResult = new AsyncAPIResult(asyncAPI, groupedApi, null);

log.debug("AsyncAPI document was built");
} catch (Throwable t) {
log.debug("Failed to build AsyncAPI document", t);
this.asyncAPIResult = new AsyncAPIResult(null, t);
this.asyncAPIResult = new AsyncAPIResult(null, null, t);
}
}

/**
* checks whether asyncApi has internally allready been initialized or not.
* checks whether asyncApi has internally already been initialized or not.
*
* @return true if asyncApi has not allready been created and initialized.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
* public void receiveMessage(MonetaryAmount payload) { ... }
* </pre>
*
* Maintainer node: move to io.github.springwolf.core.asyncapi.annotation
* Maintainer note: move to io.github.springwolf.core.asyncapi.annotation
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: Apache-2.0
package io.github.springwolf.core.asyncapi.grouping;

import io.github.springwolf.asyncapi.v3.model.AsyncAPI;
import io.github.springwolf.core.configuration.docket.AsyncApiGroup;
import io.github.springwolf.core.configuration.properties.SpringwolfConfigProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;

import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Slf4j
@RequiredArgsConstructor
public class AsyncApiGroupService {
private final SpringwolfConfigProperties springwolfConfigProperties;
private final GroupingService groupingService;

public Map<String, AsyncAPI> group(AsyncAPI asyncAPI) {
return getAsyncApiGroups()
.map(group -> Map.entry(group.getGroupName(), groupingService.groupAPI(asyncAPI, group)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

public Stream<AsyncApiGroup> getAsyncApiGroups() {
return springwolfConfigProperties.getDocket().getGroupConfigs().stream()
.map(AsyncApiGroupService::toGroupConfigAndValidate);
}

private static AsyncApiGroup toGroupConfigAndValidate(SpringwolfConfigProperties.ConfigDocket.Group group) {
String groupName = group.getGroup();
List<Pattern> channelNameToMatch =
group.getChannelNameToMatch().stream().map(Pattern::compile).toList();
List<Pattern> messageNameToMatch =
group.getMessageNameToMatch().stream().map(Pattern::compile).toList();

if (!StringUtils.hasText(groupName)) {
throw new IllegalArgumentException("AsyncApiGroup must have a name set in configuration");
}

int allItemCount = group.getActionToMatch().size()
+ group.getChannelNameToMatch().size()
+ group.getMessageNameToMatch().size();
if (allItemCount != 0
&& group.getActionToMatch().size() != allItemCount
&& channelNameToMatch.size() != allItemCount
&& messageNameToMatch.size() != allItemCount) {
throw new IllegalArgumentException(
"AsyncApiGroup %s must specify at most one filter criteria".formatted(groupName));
}

AsyncApiGroup asyncApiGroup = AsyncApiGroup.builder()
.groupName(groupName)
.operationActionsToKeep(group.getActionToMatch())
.channelNamesToKeep(channelNameToMatch)
.messageNamesToKeep(messageNameToMatch)
.build();
log.debug("Loaded AsyncApiGroup from configuration: {}", asyncApiGroup);
return asyncApiGroup;
}
}
Loading

0 comments on commit c00ecfd

Please sign in to comment.