Skip to content

Commit

Permalink
feat(core): refine AsyncGenericOperationBinding
Browse files Browse the repository at this point in the history
  • Loading branch information
timonback committed Oct 1, 2023
1 parent ff5b399 commit f0b5893
Show file tree
Hide file tree
Showing 14 changed files with 343 additions and 61 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.annotation;

import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.AsyncOperationBinding;

import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// TODO: move classes to new bindings package

/**
* Springwolf cannot support all available protocol bindings that exist.
* To allow users to manually define them, {@link AsyncGenericOperationBinding} can be used.
* <p>
* Use the {@link AsyncGenericOperationBinding#fields()} to define the attributes
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Repeatable(AsyncGenericOperationBindings.class)
@AsyncOperationBinding
public @interface AsyncGenericOperationBinding {
/**
* The name of the binding
*/
String type();

/**
* All binding fields
*/
String[] fields() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.annotation;

import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.AsyncOperationBinding;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@AsyncOperationBinding
public @interface AsyncGenericOperationBindings {
AsyncGenericOperationBinding[] value();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.annotation.processor;

import com.asyncapi.v2.binding.operation.OperationBinding;
import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.annotation.AsyncGenericOperationBinding;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.AbstractOperationBindingProcessor;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.ProcessedOperationBinding;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

@Component
@Slf4j
// TODO: Add bindingPriority
public class AsyncGenericOperationBindingProcessor
extends AbstractOperationBindingProcessor<AsyncGenericOperationBinding> {

@Override
protected ProcessedOperationBinding mapToOperationBinding(AsyncGenericOperationBinding bindingAnnotation) {
Map<String, Object> bindingData = PropertiesUtil.stringsToMap(bindingAnnotation.fields());

return new ProcessedOperationBinding(
bindingAnnotation.type(), new DefaultAsyncGenerialOperationBinding(bindingData));
}

public static class DefaultAsyncGenerialOperationBinding extends OperationBinding {

public DefaultAsyncGenerialOperationBinding(Map<String, Object> properties) {
this.extensionFields = new HashMap<>(properties);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.annotation.processor;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.io.StringReader;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.Properties;

@Slf4j
public class PropertiesUtil {

public static Map<String, Object> stringsToMap(String[] propertyStrings) {
return convertPropertiesToNestedMap(buildProperties((propertyStrings)));
}

private static Properties buildProperties(String[] propertyStrings) {
Properties properties = new Properties();
for (String bindingProperty : propertyStrings) {
try {
properties.load(new StringReader(bindingProperty));
} catch (IOException e) {
log.warn("Unable to parse property %s".formatted(bindingProperty), e);
}
}
return properties;
}

private static Map<String, Object> convertPropertiesToNestedMap(Properties properties) {
Map<String, Object> bindingData = new HashMap<>();
for (String propertyName : properties.stringPropertyNames()) {
LinkedList<String> path = new LinkedList<>(Arrays.asList(propertyName.split("\\.")));
Map<String, Object> mapIterator = bindingData;
while (path.size() > 1) {
if (!mapIterator.containsKey(path.get(0))) {
mapIterator.put(path.get(0), new HashMap<>());
}
mapIterator = (Map<String, Object>) mapIterator.get(path.get(0));
path.pop();
}
mapIterator.put(path.get(0), properties.get(propertyName));
}
return bindingData;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,24 @@
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.annotation.AsyncOperationBinding;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.ProcessedOperationBinding;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.annotation.OperationBindingProcessor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.Stream;

@Slf4j
public abstract class AbstractOperationBindingProcessor<A>
implements OperationBindingProcessor, EmbeddedValueResolverAware {

private final Class<A> specificAnnotationClazz =
(Class<A>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
private StringValueResolver resolver;

@Override
Expand All @@ -23,17 +30,28 @@ public void setEmbeddedValueResolver(StringValueResolver resolver) {

@Override
public Optional<ProcessedOperationBinding> process(Method method) {
final Class<A> clazz = getGenericAnnotationClass();

return Arrays.stream(method.getAnnotations())
.filter(annotation -> annotation.annotationType().isAnnotationPresent(AsyncOperationBinding.class))
.map(clazz::cast)
.flatMap(this::tryCast)
.findAny()
.map(this::mapToOperationBinding);
}

private Class<A> getGenericAnnotationClass() {
return (Class<A>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
/**
* Attempt to cast the annotation to the specific annotation
*
* Casting might fail, when multiple, different binding annotations are used,
* which results in an (expected) exception.
*
* If there is an option to previously test casting without casting, then lets change the code here.
*/
private Stream<A> tryCast(Annotation obj) {
try {
return Stream.of(specificAnnotationClazz.cast(obj));
} catch (ClassCastException ex) {
log.trace("Method has multiple bindings defined.", ex);
}
return Stream.empty();
}

protected abstract ProcessedOperationBinding mapToOperationBinding(A bindingAnnotation);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public static Map<String, OperationBinding> processOperationBindingFromAnnotatio
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toMap(ProcessedOperationBinding::getType, ProcessedOperationBinding::getBinding));
// TODO: collect toMap -> handle duplicate bindings (bindingPriority, take first)
}

public static Map<String, MessageBinding> processMessageBindingFromAnnotation(
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,4 @@
* Mapped to {@link OperationData}
*/
AsyncOperation operation();

AsyncGenericBinding[] bindings() default {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,4 @@
* Mapped to {@link OperationData}
*/
AsyncOperation operation();

AsyncGenericBinding[] bindings() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package io.github.stavshamir.springwolf.asyncapi.scanners.bindings.annotation.processor;

import io.github.stavshamir.springwolf.asyncapi.scanners.bindings.annotation.AsyncGenericOperationBinding;
import io.github.stavshamir.springwolf.asyncapi.scanners.channels.operationdata.ProcessedOperationBinding;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

class AsyncGenericOperationBindingProcessorTest {

private final AsyncGenericOperationBindingProcessor processor = new AsyncGenericOperationBindingProcessor();

@Nested
class MapToOperationBinding {

@Test
void testClassWithoutAnnotation() {
// when
List<ProcessedOperationBinding> result = getProcessedOperationBindings(ClassWithoutAnnotation.class);

// then
assertThat(result).hasSize(0);
}

@Test
void testClassWithAnnotationHasABinding() {
// when
List<ProcessedOperationBinding> result = getProcessedOperationBindings(ClassWithAnnotation.class);

// then
assertThat(result).hasSize(1);
ProcessedOperationBinding processedOperationBinding = result.get(0);
assertThat(processedOperationBinding.getType()).isEqualTo("test-binding");
AsyncGenericOperationBindingProcessor.DefaultAsyncGenerialOperationBinding binding =
(AsyncGenericOperationBindingProcessor.DefaultAsyncGenerialOperationBinding)
processedOperationBinding.getBinding();
assertThat(binding.getExtensionFields())
.isEqualTo(Map.of("binding", Map.of("field", "1"), "field", "true"));
}

@Test
void testClassWithMultipleAnnotationHasABinding() {
// when
List<ProcessedOperationBinding> result = getProcessedOperationBindings(ClassWithMultipleAnnotation.class);

// then
assertThat(result).hasSize(2);

ProcessedOperationBinding processedOperationBinding = result.get(0);
assertThat(processedOperationBinding.getType()).isEqualTo("test-binding");
AsyncGenericOperationBindingProcessor.DefaultAsyncGenerialOperationBinding binding =
(AsyncGenericOperationBindingProcessor.DefaultAsyncGenerialOperationBinding)
processedOperationBinding.getBinding();
assertThat(binding.getExtensionFields())
.isEqualTo(Map.of("binding", Map.of("field", "1"), "field", "true"));

ProcessedOperationBinding processedOperationBinding2 = result.get(1);
assertThat(processedOperationBinding2.getType()).isEqualTo("another-binding");
assertThat(processedOperationBinding2.getBinding())
.isEqualTo(
new AsyncGenericOperationBindingProcessor.DefaultAsyncGenerialOperationBinding(Map.of()));
}

private List<ProcessedOperationBinding> getProcessedOperationBindings(Class<?> testClass) {
List<ProcessedOperationBinding> result = Arrays.stream(testClass.getDeclaredMethods())
.map((m) -> m.getAnnotationsByType(AsyncGenericOperationBinding.class))
.flatMap(Arrays::stream)
.map(processor::mapToOperationBinding)
.toList();
return result;
}

private static class ClassWithoutAnnotation {
private void methodWithoutAnnotation() {}
}

private static class ClassWithAnnotation {
@AsyncGenericOperationBinding(
type = "test-binding",
fields = {"binding.field=1", "field=true"})
private void methodWithAnnotation() {}

private void methodWithoutAnnotation() {}
}

private static class ClassWithMultipleAnnotation {
@AsyncGenericOperationBinding(
type = "test-binding",
fields = {"binding.field=1", "field=true"})
@AsyncGenericOperationBinding(type = "another-binding")
private void methodWithAnnotation() {}

private void methodWithoutAnnotation() {}
}
}
}
Loading

0 comments on commit f0b5893

Please sign in to comment.