mirror of https://github.com/halo-dev/halo
Refine menu item reconciler to sync permalinks of other refs (#2380)
#### What type of PR is this? /kind improvement /area core /milestone 2.0 #### What this PR does / why we need it: 1. Synchronize permalink and display name of category every 1min 2. Synchronize permalink and display name of tag every 1min 3. Synchronize permalink and display name of post every 1min Please note that we don't handle the synchronization of `Page` because we don't have the extension yet. #### Which issue(s) this PR fixes: See https://github.com/halo-dev/halo/pull/2303 for more. #### Special notes for your reviewer: **How to test?** 1. Create a Category/Tag/Post and check the permalink 2. Create a menu and a menu item 3. Set `spec.categoryRef.name` of menu item with the extension name we just created 5. Update the menu item and check the permalink 6. Update slug name of Category/Tag/Post and check the permalink 7. Wait for 1min and check the permalink of menu item #### Does this PR introduce a user-facing change? ```release-note None ```pull/2385/head
parent
703f697bc3
commit
5118434db2
|
@ -8,6 +8,7 @@ import lombok.EqualsAndHashCode;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
import run.halo.app.extension.AbstractExtension;
|
import run.halo.app.extension.AbstractExtension;
|
||||||
import run.halo.app.extension.GVK;
|
import run.halo.app.extension.GVK;
|
||||||
|
import run.halo.app.extension.Ref;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@ToString(callSuper = true)
|
@ToString(callSuper = true)
|
||||||
|
@ -40,24 +41,16 @@ public class MenuItem extends AbstractExtension {
|
||||||
private LinkedHashSet<String> children;
|
private LinkedHashSet<String> children;
|
||||||
|
|
||||||
@Schema(description = "Category reference.")
|
@Schema(description = "Category reference.")
|
||||||
private MenuItemRef categoryRef;
|
private Ref categoryRef;
|
||||||
|
|
||||||
@Schema(description = "Tag reference.")
|
@Schema(description = "Tag reference.")
|
||||||
private MenuItemRef tagRef;
|
private Ref tagRef;
|
||||||
|
|
||||||
@Schema(description = "Post reference.")
|
@Schema(description = "Post reference.")
|
||||||
private MenuItemRef postRef;
|
private Ref postRef;
|
||||||
|
|
||||||
@Schema(description = "Page reference.")
|
@Schema(description = "Page reference.")
|
||||||
private MenuItemRef pageRef;
|
private Ref pageRef;
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Data
|
|
||||||
public static class MenuItemRef {
|
|
||||||
|
|
||||||
@Schema(description = "Reference name.", required = true)
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,16 @@
|
||||||
package run.halo.app.core.extension.reconciler;
|
package run.halo.app.core.extension.reconciler;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Objects;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
import run.halo.app.core.extension.Category;
|
||||||
import run.halo.app.core.extension.MenuItem;
|
import run.halo.app.core.extension.MenuItem;
|
||||||
|
import run.halo.app.core.extension.MenuItem.MenuItemSpec;
|
||||||
import run.halo.app.core.extension.MenuItem.MenuItemStatus;
|
import run.halo.app.core.extension.MenuItem.MenuItemStatus;
|
||||||
|
import run.halo.app.core.extension.Post;
|
||||||
|
import run.halo.app.core.extension.Tag;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
|
import run.halo.app.extension.Ref;
|
||||||
import run.halo.app.extension.controller.Reconciler;
|
import run.halo.app.extension.controller.Reconciler;
|
||||||
import run.halo.app.extension.controller.Reconciler.Request;
|
import run.halo.app.extension.controller.Reconciler.Request;
|
||||||
|
|
||||||
|
@ -17,7 +24,7 @@ public class MenuItemReconciler implements Reconciler<Request> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Result reconcile(Request request) {
|
public Result reconcile(Request request) {
|
||||||
client.fetch(MenuItem.class, request.name()).ifPresent(menuItem -> {
|
return client.fetch(MenuItem.class, request.name()).map(menuItem -> {
|
||||||
final var spec = menuItem.getSpec();
|
final var spec = menuItem.getSpec();
|
||||||
|
|
||||||
if (menuItem.getStatus() == null) {
|
if (menuItem.getStatus() == null) {
|
||||||
|
@ -25,25 +32,69 @@ public class MenuItemReconciler implements Reconciler<Request> {
|
||||||
}
|
}
|
||||||
var status = menuItem.getStatus();
|
var status = menuItem.getStatus();
|
||||||
if (spec.getCategoryRef() != null) {
|
if (spec.getCategoryRef() != null) {
|
||||||
// TODO resolve permalink from category.
|
return handleCategoryRef(request.name(), status, spec.getCategoryRef());
|
||||||
} else if (spec.getTagRef() != null) {
|
} else if (spec.getTagRef() != null) {
|
||||||
// TODO resolve permalink from tag.
|
return handleTagRef(request.name(), status, spec.getTagRef());
|
||||||
} else if (spec.getPageRef() != null) {
|
} else if (spec.getPageRef() != null) {
|
||||||
// TODO resolve permalink from page.
|
// TODO resolve permalink from page. At present, we don't have Page extension.
|
||||||
|
return new Result(false, null);
|
||||||
} else if (spec.getPostRef() != null) {
|
} else if (spec.getPostRef() != null) {
|
||||||
// TODO resolve permalink from post.
|
return handlePostRef(request.name(), status, spec.getPostRef());
|
||||||
} else {
|
} else {
|
||||||
// at last, we resolve href and display name from spec.
|
return handleMenuSpec(request.name(), status, spec);
|
||||||
if (spec.getHref() == null || !StringUtils.hasText(spec.getDisplayName())) {
|
|
||||||
throw new IllegalArgumentException("Both href and displayName are required");
|
|
||||||
}
|
}
|
||||||
|
}).orElseGet(() -> new Result(false, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result handleCategoryRef(String menuItemName, MenuItemStatus status, Ref categoryRef) {
|
||||||
|
client.fetch(Category.class, categoryRef.getName())
|
||||||
|
.filter(category -> category.getStatus() != null)
|
||||||
|
.filter(category -> StringUtils.hasText(category.getStatus().getPermalink()))
|
||||||
|
.ifPresent(category -> {
|
||||||
|
status.setHref(category.getStatus().getPermalink());
|
||||||
|
status.setDisplayName(category.getSpec().getDisplayName());
|
||||||
|
updateStatus(menuItemName, status);
|
||||||
|
});
|
||||||
|
return new Result(true, Duration.ofMinutes(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result handleTagRef(String menuItemName, MenuItemStatus status, Ref tagRef) {
|
||||||
|
client.fetch(Tag.class, tagRef.getName()).filter(tag -> tag.getStatus() != null)
|
||||||
|
.filter(tag -> StringUtils.hasText(tag.getStatus().getPermalink())).ifPresent(tag -> {
|
||||||
|
status.setHref(tag.getStatus().getPermalink());
|
||||||
|
status.setDisplayName(tag.getSpec().getDisplayName());
|
||||||
|
updateStatus(menuItemName, status);
|
||||||
|
});
|
||||||
|
return new Result(true, Duration.ofMinutes(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result handlePostRef(String menuItemName, MenuItemStatus status, Ref postRef) {
|
||||||
|
client.fetch(Post.class, postRef.getName()).filter(post -> post.getStatus() != null)
|
||||||
|
.filter(post -> StringUtils.hasText(post.getStatus().getPermalink()))
|
||||||
|
.ifPresent(post -> {
|
||||||
|
status.setHref(post.getStatus().getPermalink());
|
||||||
|
status.setDisplayName(post.getSpec().getTitle());
|
||||||
|
updateStatus(menuItemName, status);
|
||||||
|
});
|
||||||
|
return new Result(true, Duration.ofMinutes(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result handleMenuSpec(String menuItemName, MenuItemStatus status, MenuItemSpec spec) {
|
||||||
|
if (spec.getHref() != null && StringUtils.hasText(spec.getDisplayName())) {
|
||||||
status.setHref(spec.getHref());
|
status.setHref(spec.getHref());
|
||||||
status.setDisplayName(spec.getDisplayName());
|
status.setDisplayName(spec.getDisplayName());
|
||||||
// update status
|
updateStatus(menuItemName, status);
|
||||||
client.update(menuItem);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
return new Result(false, null);
|
return new Result(false, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateStatus(String menuItemName, MenuItemStatus status) {
|
||||||
|
client.fetch(MenuItem.class, menuItemName)
|
||||||
|
.filter(menuItem -> !Objects.deepEquals(menuItem.getStatus(), status))
|
||||||
|
.ifPresent(menuItem -> {
|
||||||
|
menuItem.setStatus(status);
|
||||||
|
client.update(menuItem);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +1,31 @@
|
||||||
package run.halo.app.core.extension.reconciler;
|
package run.halo.app.core.extension.reconciler;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.argThat;
|
||||||
|
import static org.mockito.ArgumentMatchers.isA;
|
||||||
import static org.mockito.Mockito.never;
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.times;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Supplier;
|
||||||
import org.junit.jupiter.api.Nested;
|
import org.junit.jupiter.api.Nested;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.mockito.InjectMocks;
|
import org.mockito.InjectMocks;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import run.halo.app.core.extension.Category;
|
||||||
import run.halo.app.core.extension.MenuItem;
|
import run.halo.app.core.extension.MenuItem;
|
||||||
import run.halo.app.core.extension.MenuItem.MenuItemSpec;
|
import run.halo.app.core.extension.MenuItem.MenuItemSpec;
|
||||||
import run.halo.app.extension.ExtensionClient;
|
import run.halo.app.extension.ExtensionClient;
|
||||||
import run.halo.app.extension.Metadata;
|
import run.halo.app.extension.Metadata;
|
||||||
|
import run.halo.app.extension.Ref;
|
||||||
import run.halo.app.extension.controller.Reconciler.Request;
|
import run.halo.app.extension.controller.Reconciler.Request;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
@ -30,33 +38,96 @@ class MenuItemReconcilerTest {
|
||||||
MenuItemReconciler reconciler;
|
MenuItemReconciler reconciler;
|
||||||
|
|
||||||
@Nested
|
@Nested
|
||||||
class WhenOtherRefNotSet {
|
class WhenCategoryRefSet {
|
||||||
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReEnqueueIfHrefNotSet() {
|
void shouldNotUpdateMenuItemIfCategoryNotFound() {
|
||||||
|
Supplier<MenuItem> menuItemSupplier = () -> createMenuItem("fake-name", spec -> {
|
||||||
|
spec.setCategoryRef(Ref.of("fake-category"));
|
||||||
|
});
|
||||||
|
|
||||||
|
when(client.fetch(MenuItem.class, "fake-name"))
|
||||||
|
.thenReturn(Optional.of(menuItemSupplier.get()));
|
||||||
|
when(client.fetch(Category.class, "fake-category")).thenReturn(Optional.empty());
|
||||||
|
|
||||||
|
var result = reconciler.reconcile(new Request("fake-name"));
|
||||||
|
|
||||||
|
assertTrue(result.reEnqueue());
|
||||||
|
assertEquals(Duration.ofMinutes(1), result.retryAfter());
|
||||||
|
verify(client).fetch(MenuItem.class, "fake-name");
|
||||||
|
verify(client).fetch(Category.class, "fake-category");
|
||||||
|
verify(client, never()).update(isA(MenuItem.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldUpdateMenuItemIfCategoryFound() {
|
||||||
|
Supplier<MenuItem> menuItemSupplier = () -> createMenuItem("fake-name", spec -> {
|
||||||
|
spec.setCategoryRef(Ref.of("fake-category"));
|
||||||
|
});
|
||||||
|
|
||||||
|
when(client.fetch(MenuItem.class, "fake-name"))
|
||||||
|
.thenReturn(Optional.of(menuItemSupplier.get()))
|
||||||
|
.thenReturn(Optional.of(menuItemSupplier.get()));
|
||||||
|
when(client.fetch(Category.class, "fake-category"))
|
||||||
|
.thenReturn(Optional.of(createCategory()));
|
||||||
|
|
||||||
|
var result = reconciler.reconcile(new Request("fake-name"));
|
||||||
|
|
||||||
|
assertTrue(result.reEnqueue());
|
||||||
|
assertEquals(Duration.ofMinutes(1), result.retryAfter());
|
||||||
|
verify(client, times(2)).fetch(MenuItem.class, "fake-name");
|
||||||
|
verify(client).fetch(Category.class, "fake-category");
|
||||||
|
verify(client).<MenuItem>update(argThat(menuItem -> {
|
||||||
|
var status = menuItem.getStatus();
|
||||||
|
return status.getHref().equals("fake://permalink")
|
||||||
|
&& status.getDisplayName().equals("Fake Category");
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Category createCategory() {
|
||||||
|
var metadata = new Metadata();
|
||||||
|
metadata.setName("fake-category");
|
||||||
|
|
||||||
|
var spec = new Category.CategorySpec();
|
||||||
|
spec.setDisplayName("Fake Category");
|
||||||
|
var status = new Category.CategoryStatus();
|
||||||
|
status.setPermalink("fake://permalink");
|
||||||
|
|
||||||
|
var category = new Category();
|
||||||
|
category.setMetadata(metadata);
|
||||||
|
category.setSpec(spec);
|
||||||
|
category.setStatus(status);
|
||||||
|
return category;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nested
|
||||||
|
class WhenOtherRefsNotSet {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldNotRequeueIfHrefNotSet() {
|
||||||
var menuItem = createMenuItem("fake-name", spec -> {
|
var menuItem = createMenuItem("fake-name", spec -> {
|
||||||
spec.setHref(null);
|
spec.setHref(null);
|
||||||
spec.setDisplayName("Fake display name");
|
spec.setDisplayName("Fake display name");
|
||||||
});
|
});
|
||||||
when(client.fetch(MenuItem.class, "fake-name")).thenReturn(Optional.of(menuItem));
|
when(client.fetch(MenuItem.class, "fake-name")).thenReturn(Optional.of(menuItem));
|
||||||
assertThrows(IllegalArgumentException.class,
|
|
||||||
() -> reconciler.reconcile(new Request("fake-name")));
|
var result = reconciler.reconcile(new Request("fake-name"));
|
||||||
|
assertFalse(result.reEnqueue());
|
||||||
|
|
||||||
verify(client).fetch(MenuItem.class, "fake-name");
|
verify(client).fetch(MenuItem.class, "fake-name");
|
||||||
verify(client, never()).update(menuItem);
|
verify(client, never()).update(menuItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReEnqueueIfDisplayNameNotSet() {
|
void shouldNotRequeueIfDisplayNameNotSet() {
|
||||||
var menuItem = createMenuItem("fake-name", spec -> {
|
var menuItem = createMenuItem("fake-name", spec -> {
|
||||||
spec.setHref("/fake");
|
spec.setHref("/fake");
|
||||||
spec.setDisplayName(null);
|
spec.setDisplayName(null);
|
||||||
});
|
});
|
||||||
when(client.fetch(MenuItem.class, "fake-name")).thenReturn(
|
when(client.fetch(MenuItem.class, "fake-name")).thenReturn(Optional.of(menuItem));
|
||||||
Optional.of(menuItem));
|
var result = reconciler.reconcile(new Request("fake-name"));
|
||||||
|
assertFalse(result.reEnqueue());
|
||||||
assertThrows(IllegalArgumentException.class,
|
|
||||||
() -> reconciler.reconcile(new Request("fake-name")));
|
|
||||||
|
|
||||||
verify(client).fetch(MenuItem.class, "fake-name");
|
verify(client).fetch(MenuItem.class, "fake-name");
|
||||||
verify(client, never()).update(menuItem);
|
verify(client, never()).update(menuItem);
|
||||||
|
@ -64,19 +135,26 @@ class MenuItemReconcilerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void shouldReconcileIfHrefAndDisplayNameSet() {
|
void shouldReconcileIfHrefAndDisplayNameSet() {
|
||||||
var menuItem = createMenuItem("fake-name", spec -> {
|
Supplier<MenuItem> menuItemSupplier = () -> createMenuItem("fake-name", spec -> {
|
||||||
spec.setHref("/fake");
|
spec.setHref("/fake");
|
||||||
spec.setDisplayName("Fake display name");
|
spec.setDisplayName("Fake display name");
|
||||||
});
|
});
|
||||||
|
|
||||||
when(client.fetch(MenuItem.class, "fake-name")).thenReturn(
|
when(client.fetch(MenuItem.class, "fake-name"))
|
||||||
Optional.of(menuItem));
|
.thenReturn(Optional.of(menuItemSupplier.get()))
|
||||||
|
.thenReturn(Optional.of(menuItemSupplier.get()));
|
||||||
|
|
||||||
var result = reconciler.reconcile(new Request("fake-name"));
|
var result = reconciler.reconcile(new Request("fake-name"));
|
||||||
assertFalse(result.reEnqueue());
|
assertFalse(result.reEnqueue());
|
||||||
|
|
||||||
verify(client).fetch(MenuItem.class, "fake-name");
|
verify(client, times(2)).fetch(MenuItem.class, "fake-name");
|
||||||
verify(client).update(menuItem);
|
verify(client).update(argThat(ext -> {
|
||||||
|
if (!(ext instanceof MenuItem menuItem)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return menuItem.getStatus().getHref().equals("/fake")
|
||||||
|
&& menuItem.getStatus().getDisplayName().equals("Fake display name");
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue