mirror of https://github.com/halo-dev/halo
refactor: support chaining calls for flux and mono in javascript inline tag (#2715)
#### What type of PR is this? /kind improvement /area core /milestone 2.0 #### What this PR does / why we need it: 支持在 JavaScript 中对 Flux 或 Mono 的泛型类型属性链式调用 测试参考:pull/2718/head^27e9bdec492/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java (L131-L147)
期望7e9bdec492/src/test/java/run/halo/app/theme/ReactiveFinderExpressionParserTests.java (L64-L79)
#### Special notes for your reviewer: /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 支持在 JavaScript 中对 Flux 或 Mono 的泛型类型属性链式调用 ```
parent
eeb9c4dc7a
commit
c8bc96ffc3
|
@ -1,18 +1,16 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.springframework.expression.AccessException;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.PropertyAccessor;
|
||||
import org.springframework.expression.TypedValue;
|
||||
import org.springframework.expression.spel.ast.AstUtils;
|
||||
import org.springframework.integration.json.JsonPropertyAccessor;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* A SpEL PropertyAccessor that knows how to read properties from {@link Mono} or {@link Flux}
|
||||
|
@ -24,25 +22,25 @@ import run.halo.app.infra.utils.JsonUtils;
|
|||
* @since 2.0.0
|
||||
*/
|
||||
public class ReactivePropertyAccessor implements PropertyAccessor {
|
||||
private static final Class<?>[] SUPPORTED_CLASSES = {
|
||||
Mono.class,
|
||||
Flux.class
|
||||
};
|
||||
private final JsonPropertyAccessor jsonPropertyAccessor = new JsonPropertyAccessor();
|
||||
|
||||
@Override
|
||||
public Class<?>[] getSpecificTargetClasses() {
|
||||
return SUPPORTED_CLASSES;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRead(@NonNull EvaluationContext context, Object target, @NonNull String name)
|
||||
throws AccessException {
|
||||
if (target == null) {
|
||||
return false;
|
||||
if (isReactiveType(target)) {
|
||||
return true;
|
||||
}
|
||||
return Mono.class.isAssignableFrom(target.getClass())
|
||||
|| Flux.class.isAssignableFrom(target.getClass());
|
||||
List<PropertyAccessor> propertyAccessors = context.getPropertyAccessors();
|
||||
for (PropertyAccessor propertyAccessor : propertyAccessors) {
|
||||
if (propertyAccessor.canRead(context, target, name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -52,29 +50,44 @@ public class ReactivePropertyAccessor implements PropertyAccessor {
|
|||
if (target == null) {
|
||||
return TypedValue.NULL;
|
||||
}
|
||||
Class<?> clazz = target.getClass();
|
||||
Object value = null;
|
||||
if (Mono.class.isAssignableFrom(clazz)) {
|
||||
value = ((Mono<?>) target).block();
|
||||
} else if (Flux.class.isAssignableFrom(clazz)) {
|
||||
value = ((Flux<?>) target).toIterable();
|
||||
}
|
||||
|
||||
if (value == null) {
|
||||
return TypedValue.NULL;
|
||||
}
|
||||
Object value = blockingGetForReactive(target);
|
||||
|
||||
List<PropertyAccessor> propertyAccessorsToTry =
|
||||
getPropertyAccessorsToTry(value, context.getPropertyAccessors());
|
||||
for (PropertyAccessor propertyAccessor : propertyAccessorsToTry) {
|
||||
try {
|
||||
return propertyAccessor.read(context, target, name);
|
||||
TypedValue result = propertyAccessor.read(context, value, name);
|
||||
return new TypedValue(blockingGetForReactive(result.getValue()));
|
||||
} catch (AccessException e) {
|
||||
// ignore
|
||||
// ignore this
|
||||
}
|
||||
}
|
||||
JsonNode jsonNode = JsonUtils.DEFAULT_JSON_MAPPER.convertValue(value, JsonNode.class);
|
||||
return jsonPropertyAccessor.read(context, jsonNode, name);
|
||||
|
||||
throw new AccessException("Cannot read property '" + name + "' from [" + value + "]");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Object blockingGetForReactive(@Nullable Object target) {
|
||||
if (target == null) {
|
||||
return null;
|
||||
}
|
||||
Class<?> clazz = target.getClass();
|
||||
Object value = target;
|
||||
if (Mono.class.isAssignableFrom(clazz)) {
|
||||
value = ((Mono<?>) target).block();
|
||||
} else if (Flux.class.isAssignableFrom(clazz)) {
|
||||
value = ((Flux<?>) target).collectList().block();
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private boolean isReactiveType(Object target) {
|
||||
if (target == null) {
|
||||
return false;
|
||||
}
|
||||
Class<?> clazz = target.getClass();
|
||||
return Mono.class.isAssignableFrom(clazz)
|
||||
|| Flux.class.isAssignableFrom(clazz);
|
||||
}
|
||||
|
||||
private List<PropertyAccessor> getPropertyAccessorsToTry(
|
||||
|
@ -82,27 +95,10 @@ public class ReactivePropertyAccessor implements PropertyAccessor {
|
|||
|
||||
Class<?> targetType = (contextObject != null ? contextObject.getClass() : null);
|
||||
|
||||
List<PropertyAccessor> specificAccessors = new ArrayList<>();
|
||||
List<PropertyAccessor> generalAccessors = new ArrayList<>();
|
||||
for (PropertyAccessor resolver : propertyAccessors) {
|
||||
Class<?>[] targets = resolver.getSpecificTargetClasses();
|
||||
if (targets == null) {
|
||||
// generic resolver that says it can be used for any type
|
||||
generalAccessors.add(resolver);
|
||||
} else if (targetType != null) {
|
||||
for (Class<?> clazz : targets) {
|
||||
if (clazz == targetType) {
|
||||
specificAccessors.add(resolver);
|
||||
break;
|
||||
} else if (clazz.isAssignableFrom(targetType)) {
|
||||
generalAccessors.add(resolver);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
List<PropertyAccessor> resolvers = new ArrayList<>(specificAccessors);
|
||||
generalAccessors.removeAll(specificAccessors);
|
||||
resolvers.addAll(generalAccessors);
|
||||
List<PropertyAccessor> resolvers =
|
||||
AstUtils.getPropertyAccessorsToTry(targetType, propertyAccessors);
|
||||
// remove this resolver to avoid infinite loop
|
||||
resolvers.remove(this);
|
||||
return resolvers;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ public class ReactiveSpelVariableExpressionEvaluator
|
|||
return ((Mono<?>) returnValue).block();
|
||||
}
|
||||
if (Flux.class.isAssignableFrom(clazz)) {
|
||||
return ((Flux<?>) returnValue).toIterable();
|
||||
return ((Flux<?>) returnValue).collectList().block();
|
||||
}
|
||||
return returnValue;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
package run.halo.app.theme;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.thymeleaf.IEngineConfiguration;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.spring6.dialect.SpringStandardDialect;
|
||||
import org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;
|
||||
import org.thymeleaf.standard.expression.IStandardVariableExpressionEvaluator;
|
||||
import org.thymeleaf.templateresolver.StringTemplateResolver;
|
||||
import org.thymeleaf.templateresource.ITemplateResource;
|
||||
import org.thymeleaf.templateresource.StringTemplateResource;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.theme.dialect.HaloProcessorDialect;
|
||||
|
||||
/**
|
||||
* Tests expression parser for reactive return value.
|
||||
*
|
||||
* @author guqing
|
||||
* @see ReactivePropertyAccessor
|
||||
* @see ReactiveSpelVariableExpressionEvaluator
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ReactiveFinderExpressionParserTests {
|
||||
@Mock
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private TemplateEngine templateEngine;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();
|
||||
templateEngine = new TemplateEngine();
|
||||
templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect() {
|
||||
@Override
|
||||
public IStandardVariableExpressionEvaluator getVariableExpressionEvaluator() {
|
||||
return new ReactiveSpelVariableExpressionEvaluator();
|
||||
}
|
||||
}));
|
||||
templateEngine.addTemplateResolver(new TestTemplateResolver());
|
||||
}
|
||||
|
||||
@Test
|
||||
void javascriptInlineParser() {
|
||||
Context context = getContext();
|
||||
context.setVariable("target", new TestReactiveFinder());
|
||||
context.setVariable("genericMap", Map.of("key", "value"));
|
||||
String result = templateEngine.process("javascriptInline", context);
|
||||
assertThat(result).isEqualTo("""
|
||||
<p>value</p>
|
||||
<p>ruibaby</p>
|
||||
<p>guqing</p>
|
||||
<p>bar</p>
|
||||
<script>
|
||||
var genericValue = "value";
|
||||
var name = "guqing";
|
||||
var names = ["guqing","johnniang","ruibaby"];
|
||||
var users = [{"name":"guqing"},{"name":"ruibaby"},{"name":"johnniang"}];
|
||||
var userListItem = "guqing";
|
||||
var objectJsonNodeFlux = [{"name":"guqing"}];
|
||||
var objectJsonNodeFluxChain = "guqing";
|
||||
var mapMono = "bar";
|
||||
var arrayNodeMono = "bar";
|
||||
</script>
|
||||
""");
|
||||
}
|
||||
|
||||
static class TestReactiveFinder {
|
||||
public Mono<String> getName() {
|
||||
return Mono.just("guqing");
|
||||
}
|
||||
|
||||
public Flux<String> names() {
|
||||
return Flux.just("guqing", "johnniang", "ruibaby");
|
||||
}
|
||||
|
||||
public Flux<TestUser> users() {
|
||||
return Flux.just(
|
||||
new TestUser("guqing"), new TestUser("ruibaby"), new TestUser("johnniang")
|
||||
);
|
||||
}
|
||||
|
||||
public Flux<JsonNode> objectJsonNodeFlux() {
|
||||
ObjectNode objectNode = JsonUtils.DEFAULT_JSON_MAPPER.createObjectNode();
|
||||
objectNode.put("name", "guqing");
|
||||
return Flux.just(objectNode);
|
||||
}
|
||||
|
||||
public Mono<Map<String, Object>> mapMono() {
|
||||
return Mono.just(Map.of("foo", "bar"));
|
||||
}
|
||||
|
||||
public Mono<JsonNode> arrayNodeMono() {
|
||||
ArrayNode arrayNode = JsonUtils.DEFAULT_JSON_MAPPER.createArrayNode();
|
||||
arrayNode.add(arrayNode.objectNode().put("foo", "bar"));
|
||||
return Mono.just(arrayNode);
|
||||
}
|
||||
}
|
||||
|
||||
record TestUser(String name) {
|
||||
}
|
||||
|
||||
private Context getContext() {
|
||||
Context context = new Context();
|
||||
context.setVariable(
|
||||
ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME,
|
||||
new ThymeleafEvaluationContext(applicationContext, null));
|
||||
return context;
|
||||
}
|
||||
|
||||
static class TestTemplateResolver extends StringTemplateResolver {
|
||||
@Override
|
||||
protected ITemplateResource computeTemplateResource(IEngineConfiguration configuration,
|
||||
String ownerTemplate, String template,
|
||||
Map<String, Object> templateResolutionAttributes) {
|
||||
return new StringTemplateResource("""
|
||||
<p th:text="${genericMap.key}"></p>
|
||||
<p th:text="${target.users[1].name}"></p>
|
||||
<p th:text="${target.objectJsonNodeFlux[0].name}"></p>
|
||||
<p th:text="${target.arrayNodeMono.get(0).foo}"></p>
|
||||
<script th:inline="javascript">
|
||||
var genericValue = /*[[${genericMap.key}]]*/;
|
||||
var name = /*[[${target.getName()}]]*/;
|
||||
var names = /*[[${target.names()}]]*/;
|
||||
var users = /*[[${target.users()}]]*/;
|
||||
var userListItem = /*[[${target.users[0].name}]]*/;
|
||||
var objectJsonNodeFlux = /*[[${target.objectJsonNodeFlux()}]]*/;
|
||||
var objectJsonNodeFluxChain = /*[[${target.objectJsonNodeFlux[0].name}]]*/;
|
||||
var mapMono = /*[[${target.mapMono.foo}]]*/;
|
||||
var arrayNodeMono = /*[[${target.arrayNodeMono.get(0).foo}]]*/;
|
||||
</script>
|
||||
""");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue