Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): group AsyncApi #967

Merged
merged 15 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading