Initial support for composable functions
There's a test for each of the supported types. No error handling yet. Fixes gh-122
This commit is contained in:
@@ -33,6 +33,7 @@ import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
@@ -66,6 +67,7 @@ import org.springframework.messaging.Message;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@@ -94,8 +96,10 @@ public class ContextFunctionCatalogAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
public FunctionCatalog functionCatalog(ContextFunctionPostProcessor processor) {
|
||||
return new InMemoryFunctionCatalog(
|
||||
processor.merge(registrations, consumers, suppliers, functions));
|
||||
return new BeanFactoryFunctionCatalog(
|
||||
new InMemoryFunctionCatalog(
|
||||
processor.merge(registrations, consumers, suppliers, functions)),
|
||||
processor);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -103,6 +107,76 @@ public class ContextFunctionCatalogAutoConfiguration {
|
||||
return new BeanFactoryFunctionInspector(processor);
|
||||
}
|
||||
|
||||
protected class BeanFactoryFunctionCatalog implements FunctionCatalog {
|
||||
|
||||
private final FunctionCatalog delegate;
|
||||
private final ContextFunctionPostProcessor processor;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Supplier<T> lookupSupplier(String name) {
|
||||
Supplier<T> result = this.delegate.lookupSupplier(name);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
if (name.contains(",")) {
|
||||
Object composed = processor.compose(name);
|
||||
if (composed instanceof Supplier) {
|
||||
result = (Supplier<T>) composed;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T, R> Function<T, R> lookupFunction(String name) {
|
||||
Function<T, R> result = this.delegate.lookupFunction(name);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
if (name.contains(",")) {
|
||||
Object composed = processor.compose(name);
|
||||
if (composed instanceof Function) {
|
||||
result = (Function<T, R>) composed;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Consumer<T> lookupConsumer(String name) {
|
||||
Consumer<T> result = this.delegate.lookupConsumer(name);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
if (name.contains(",")) {
|
||||
Object composed = processor.compose(name);
|
||||
if (composed instanceof Consumer) {
|
||||
result = (Consumer<T>) composed;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public Set<String> getSupplierNames() {
|
||||
return this.delegate.getSupplierNames();
|
||||
}
|
||||
|
||||
public Set<String> getFunctionNames() {
|
||||
return this.delegate.getFunctionNames();
|
||||
}
|
||||
|
||||
public Set<String> getConsumerNames() {
|
||||
return this.delegate.getConsumerNames();
|
||||
}
|
||||
|
||||
public BeanFactoryFunctionCatalog(FunctionCatalog delegate,
|
||||
ContextFunctionPostProcessor processor) {
|
||||
this.delegate = delegate;
|
||||
this.processor = processor;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected class BeanFactoryFunctionInspector implements FunctionInspector {
|
||||
|
||||
private ContextFunctionPostProcessor processor;
|
||||
@@ -159,6 +233,7 @@ public class ContextFunctionCatalogAutoConfiguration {
|
||||
private BeanDefinitionRegistry registry;
|
||||
private ConversionService conversionService;
|
||||
private Map<Object, String> registrations = new HashMap<>();
|
||||
private Map<String, Object> lookup = new HashMap<>();
|
||||
private Map<String, Map<ParamType, Class<?>>> types = new HashMap<>();
|
||||
|
||||
@Override
|
||||
@@ -166,6 +241,67 @@ public class ContextFunctionCatalogAutoConfiguration {
|
||||
this.registry = registry;
|
||||
}
|
||||
|
||||
public Object compose(String name) {
|
||||
if (lookup.containsKey(name)) {
|
||||
return lookup.get(name);
|
||||
}
|
||||
String[] stages = StringUtils.tokenizeToStringArray(name, ",");
|
||||
Object function = Stream.of(stages).map(key -> lookup(key)).sequential()
|
||||
.reduce(this::compose).get();
|
||||
lookup.computeIfAbsent(name, key -> function);
|
||||
Map<ParamType, Class<?>> values = types.computeIfAbsent(name,
|
||||
key -> new HashMap<>());
|
||||
for (ParamType type : ParamType.values()) {
|
||||
if (!values.containsKey(type)) {
|
||||
if (type.isInput()) {
|
||||
values.put(type, types.get(stages[0]).get(type));
|
||||
}
|
||||
else {
|
||||
values.put(type, types.get(stages[stages.length - 1]).get(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
registrations.put(function, name);
|
||||
return function;
|
||||
}
|
||||
|
||||
private Object lookup(String name) {
|
||||
Object result = lookup.get(name);
|
||||
if (result != null) {
|
||||
Map<ParamType, Class<?>> values = types.computeIfAbsent(name,
|
||||
key -> new HashMap<>());
|
||||
for (ParamType type : ParamType.values()) {
|
||||
if (!values.containsKey(type)) {
|
||||
values.put(type, findType(result, type));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Object compose(Object a, Object b) {
|
||||
if (a instanceof Supplier && b instanceof Function) {
|
||||
Supplier<Object> supplier = (Supplier<Object>) a;
|
||||
Function<Object, Object> function = (Function<Object, Object>) b;
|
||||
return (Supplier<Object>) () -> function.apply(supplier.get());
|
||||
}
|
||||
else if (a instanceof Function && b instanceof Function) {
|
||||
Function<Object, Object> function1 = (Function<Object, Object>) a;
|
||||
Function<Object, Object> function2 = (Function<Object, Object>) b;
|
||||
return function1.andThen(function2);
|
||||
}
|
||||
else if (a instanceof Function && b instanceof Consumer) {
|
||||
Function<Object, Object> function = (Function<Object, Object>) a;
|
||||
Consumer<Object> consumer = (Consumer<Object>) b;
|
||||
return (Consumer<Object>) v -> consumer.accept(function.apply(v));
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException(String.format(
|
||||
"Could not compose %s and %s", a.getClass(), b.getClass()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory factory)
|
||||
throws BeansException {
|
||||
@@ -275,6 +411,7 @@ public class ContextFunctionCatalogAutoConfiguration {
|
||||
registration.target(target((Function<?, ?>) target, key));
|
||||
}
|
||||
registrations.remove(target);
|
||||
this.lookup.put(key, registration.getTarget());
|
||||
this.registrations.put(registration.getTarget(), key);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,12 +66,9 @@ public class InMemoryFunctionCatalog implements FunctionCatalog {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T, R> Function<T, R> lookupFunction(String name) {
|
||||
return (Function<T, R>) Stream.of(StringUtils.tokenizeToStringArray(name, ","))
|
||||
.map(functions::get)
|
||||
.filter(f -> f != null)
|
||||
.reduce(null, (f1, f2) -> f1 == null ? f2 : f1.andThen((Function)f2));
|
||||
return (Function<T, R>) functions.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -37,6 +37,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.function.compiler.CompiledFunctionFactory;
|
||||
import org.springframework.cloud.function.compiler.FunctionCompiler;
|
||||
import org.springframework.cloud.function.core.FunctionCatalog;
|
||||
import org.springframework.cloud.function.scan.ScannedFunction;
|
||||
import org.springframework.cloud.function.test.GenericFunction;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
@@ -63,7 +64,7 @@ import reactor.core.publisher.Flux;
|
||||
public class ContextFunctionCatalogAutoConfigurationTests {
|
||||
|
||||
private ConfigurableApplicationContext context;
|
||||
private InMemoryFunctionCatalog catalog;
|
||||
private FunctionCatalog catalog;
|
||||
private FunctionInspector inspector;
|
||||
private static String value;
|
||||
|
||||
@@ -81,10 +82,13 @@ public class ContextFunctionCatalogAutoConfigurationTests {
|
||||
assertThat(context.getBean("function")).isInstanceOf(Function.class);
|
||||
assertThat(catalog.lookupFunction("function")).isInstanceOf(Function.class);
|
||||
assertThat(context.getBean("function2")).isInstanceOf(Function.class);
|
||||
assertThat(catalog.lookupFunction("function,function2")).isInstanceOf(Function.class);
|
||||
Function<Flux<String>,Flux<String>> f = catalog.lookupFunction("function,function2,function3");
|
||||
assertThat(catalog.lookupFunction("function,function2"))
|
||||
.isInstanceOf(Function.class);
|
||||
Function<Flux<String>, Flux<String>> f = catalog
|
||||
.lookupFunction("function,function2,function3");
|
||||
assertThat(f).isInstanceOf(Function.class);
|
||||
assertThat(f.apply(Flux.just("hello")).blockFirst()).isEqualTo("HELLOfunction2function3");
|
||||
assertThat(f.apply(Flux.just("hello")).blockFirst())
|
||||
.isEqualTo("HELLOfunction2function3");
|
||||
assertThat(context.getBean("supplierFoo")).isInstanceOf(Supplier.class);
|
||||
assertThat(catalog.lookupSupplier("supplierFoo")).isInstanceOf(Supplier.class);
|
||||
assertThat(context.getBean("supplier_Foo")).isInstanceOf(Supplier.class);
|
||||
@@ -104,6 +108,41 @@ public class ContextFunctionCatalogAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void composedFunction() {
|
||||
create(MultipleConfiguration.class);
|
||||
assertThat(catalog.lookupFunction("foos,bars")).isInstanceOf(Function.class);
|
||||
assertThat(catalog.lookupFunction("names,foos")).isNull();
|
||||
assertThat(inspector.getInputType(catalog.lookupFunction("foos,bars")))
|
||||
.isAssignableFrom(String.class);
|
||||
assertThat(inspector.getOutputType(catalog.lookupFunction("foos,bars")))
|
||||
.isAssignableFrom(Bar.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void composedSupplier() {
|
||||
create(MultipleConfiguration.class);
|
||||
assertThat(catalog.lookupSupplier("names,foos")).isInstanceOf(Supplier.class);
|
||||
assertThat(catalog.lookupFunction("names,foos")).isNull();
|
||||
assertThat(inspector.getOutputType(catalog.lookupSupplier("names,foos")))
|
||||
.isAssignableFrom(Foo.class);
|
||||
// The input type is the same as the output type of the first element in the chain
|
||||
assertThat(inspector.getInputType(catalog.lookupSupplier("names,foos")))
|
||||
.isAssignableFrom(String.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void composedConsumer() {
|
||||
create(MultipleConfiguration.class);
|
||||
assertThat(catalog.lookupConsumer("foos,print")).isInstanceOf(Consumer.class);
|
||||
assertThat(catalog.lookupFunction("foos,print")).isNull();
|
||||
assertThat(inspector.getInputType(catalog.lookupConsumer("foos,print")))
|
||||
.isAssignableFrom(String.class);
|
||||
// The output type is the same as the input type of the last element in the chain
|
||||
assertThat(inspector.getOutputType(catalog.lookupConsumer("foos,print")))
|
||||
.isAssignableFrom(Foo.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void genericFunction() {
|
||||
create(GenericConfiguration.class);
|
||||
@@ -331,7 +370,7 @@ public class ContextFunctionCatalogAutoConfigurationTests {
|
||||
|
||||
private void create(Class<?>[] types, String... props) {
|
||||
context = new SpringApplicationBuilder((Object[]) types).properties(props).run();
|
||||
catalog = context.getBean(InMemoryFunctionCatalog.class);
|
||||
catalog = context.getBean(FunctionCatalog.class);
|
||||
inspector = context.getBean(FunctionInspector.class);
|
||||
}
|
||||
|
||||
@@ -369,7 +408,7 @@ public class ContextFunctionCatalogAutoConfigurationTests {
|
||||
return () -> "hello";
|
||||
}
|
||||
|
||||
@Bean(name={"supplierFoo", "supplier_Foo"})
|
||||
@Bean(name = { "supplierFoo", "supplier_Foo" })
|
||||
public Supplier<String> foo() {
|
||||
return () -> "hello";
|
||||
}
|
||||
@@ -397,6 +436,30 @@ public class ContextFunctionCatalogAutoConfigurationTests {
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@Configuration
|
||||
protected static class MultipleConfiguration {
|
||||
@Bean
|
||||
public Function<String, Foo> foos() {
|
||||
return value -> new Foo(value.toUpperCase());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Function<Foo, Bar> bars() {
|
||||
return value -> new Bar(value.getValue());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Consumer<Foo> print() {
|
||||
return System.out::println;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Supplier<String> names() {
|
||||
return () -> "Mark";
|
||||
}
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@Configuration
|
||||
protected static class GenericConfiguration {
|
||||
@@ -423,14 +486,14 @@ public class ContextFunctionCatalogAutoConfigurationTests {
|
||||
beanFactory.registerSingleton("function", new SingletonFunction());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected static class SingletonFunction implements Function<Integer, String> {
|
||||
|
||||
@Override
|
||||
public String apply(Integer input) {
|
||||
return "value=" + input;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@@ -532,4 +595,24 @@ public class ContextFunctionCatalogAutoConfigurationTests {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Bar {
|
||||
private String message;
|
||||
|
||||
public Bar(String value) {
|
||||
this.message = value;
|
||||
}
|
||||
|
||||
Bar() {
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user