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 run.halo.app.extension.AbstractExtension;
|
||||
import run.halo.app.extension.GVK;
|
||||
import run.halo.app.extension.Ref;
|
||||
|
||||
@Data
|
||||
@ToString(callSuper = true)
|
||||
|
@ -40,24 +41,16 @@ public class MenuItem extends AbstractExtension {
|
|||
private LinkedHashSet<String> children;
|
||||
|
||||
@Schema(description = "Category reference.")
|
||||
private MenuItemRef categoryRef;
|
||||
private Ref categoryRef;
|
||||
|
||||
@Schema(description = "Tag reference.")
|
||||
private MenuItemRef tagRef;
|
||||
private Ref tagRef;
|
||||
|
||||
@Schema(description = "Post reference.")
|
||||
private MenuItemRef postRef;
|
||||
private Ref postRef;
|
||||
|
||||
@Schema(description = "Page reference.")
|
||||
private MenuItemRef pageRef;
|
||||
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class MenuItemRef {
|
||||
|
||||
@Schema(description = "Reference name.", required = true)
|
||||
private String name;
|
||||
private Ref pageRef;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
package run.halo.app.core.extension.reconciler;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
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.MenuItemSpec;
|
||||
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.Ref;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.extension.controller.Reconciler.Request;
|
||||
|
||||
|
@ -17,7 +24,7 @@ public class MenuItemReconciler implements Reconciler<Request> {
|
|||
|
||||
@Override
|
||||
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();
|
||||
|
||||
if (menuItem.getStatus() == null) {
|
||||
|
@ -25,25 +32,69 @@ public class MenuItemReconciler implements Reconciler<Request> {
|
|||
}
|
||||
var status = menuItem.getStatus();
|
||||
if (spec.getCategoryRef() != null) {
|
||||
// TODO resolve permalink from category.
|
||||
return handleCategoryRef(request.name(), status, spec.getCategoryRef());
|
||||
} else if (spec.getTagRef() != null) {
|
||||
// TODO resolve permalink from tag.
|
||||
return handleTagRef(request.name(), status, spec.getTagRef());
|
||||
} 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) {
|
||||
// TODO resolve permalink from post.
|
||||
return handlePostRef(request.name(), status, spec.getPostRef());
|
||||
} else {
|
||||
// at last, we resolve href and display name from spec.
|
||||
if (spec.getHref() == null || !StringUtils.hasText(spec.getDisplayName())) {
|
||||
throw new IllegalArgumentException("Both href and displayName are required");
|
||||
}
|
||||
status.setHref(spec.getHref());
|
||||
status.setDisplayName(spec.getDisplayName());
|
||||
// update status
|
||||
client.update(menuItem);
|
||||
return handleMenuSpec(request.name(), status, spec);
|
||||
}
|
||||
});
|
||||
}).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.setDisplayName(spec.getDisplayName());
|
||||
updateStatus(menuItemName, status);
|
||||
}
|
||||
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;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
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.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
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.MenuItemSpec;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.Ref;
|
||||
import run.halo.app.extension.controller.Reconciler.Request;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
|
@ -30,33 +38,96 @@ class MenuItemReconcilerTest {
|
|||
MenuItemReconciler reconciler;
|
||||
|
||||
@Nested
|
||||
class WhenOtherRefNotSet {
|
||||
|
||||
class WhenCategoryRefSet {
|
||||
|
||||
@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 -> {
|
||||
spec.setHref(null);
|
||||
spec.setDisplayName("Fake display name");
|
||||
});
|
||||
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, never()).update(menuItem);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReEnqueueIfDisplayNameNotSet() {
|
||||
void shouldNotRequeueIfDisplayNameNotSet() {
|
||||
var menuItem = createMenuItem("fake-name", spec -> {
|
||||
spec.setHref("/fake");
|
||||
spec.setDisplayName(null);
|
||||
});
|
||||
when(client.fetch(MenuItem.class, "fake-name")).thenReturn(
|
||||
Optional.of(menuItem));
|
||||
|
||||
assertThrows(IllegalArgumentException.class,
|
||||
() -> reconciler.reconcile(new Request("fake-name")));
|
||||
when(client.fetch(MenuItem.class, "fake-name")).thenReturn(Optional.of(menuItem));
|
||||
var result = reconciler.reconcile(new Request("fake-name"));
|
||||
assertFalse(result.reEnqueue());
|
||||
|
||||
verify(client).fetch(MenuItem.class, "fake-name");
|
||||
verify(client, never()).update(menuItem);
|
||||
|
@ -64,19 +135,26 @@ class MenuItemReconcilerTest {
|
|||
|
||||
@Test
|
||||
void shouldReconcileIfHrefAndDisplayNameSet() {
|
||||
var menuItem = createMenuItem("fake-name", spec -> {
|
||||
Supplier<MenuItem> menuItemSupplier = () -> createMenuItem("fake-name", spec -> {
|
||||
spec.setHref("/fake");
|
||||
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(menuItemSupplier.get()))
|
||||
.thenReturn(Optional.of(menuItemSupplier.get()));
|
||||
|
||||
var result = reconciler.reconcile(new Request("fake-name"));
|
||||
assertFalse(result.reEnqueue());
|
||||
|
||||
verify(client).fetch(MenuItem.class, "fake-name");
|
||||
verify(client).update(menuItem);
|
||||
verify(client, times(2)).fetch(MenuItem.class, "fake-name");
|
||||
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