Skip to content

Commit

Permalink
Add a validate callback (#184)
Browse files Browse the repository at this point in the history
BREAKING CHANGE: This removes an existing interface and replaces with a more evolvable api.

Add a `validate` callback to allow the caller to perform additional validate for a group of resource descriptors that all claim to describe the same resource.
  • Loading branch information
big-andy-coates authored Dec 23, 2023
1 parent f170f3a commit 5d1bbdb
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -58,42 +58,58 @@ public final class ResourceInitializer {
private static final StructuredLogger LOGGER =
StructuredLoggerFactory.internalLogger(ResourceInitializer.class);

private final ResourceCreator resourceCreator;
private final Callbacks callbacks;
private final ComponentValidator componentValidator;

/** Type for ensuring external resources exist */
@FunctionalInterface
public interface ResourceCreator {
/** Type for supplying callbacks to the resource initializer. */
public interface Callbacks {

/**
* Get the handler for a specific type.
* Callback to validate that all the supplied {@code resources} consistently represent the
* same resource.
*
* <p>There is often multiple resource descriptors describing the same resource present in
* the system. For example, both input and output descriptors for the same resource. Any
* inconsistencies in the details of the resource between these descriptors can lead to
* bugs.
*
* @param type the type of the {@code resources}.
* @param resources the set of resources that all share the same {@link
* ResourceDescriptor#id()}.
* @param <T> the resource descriptor type
* @throws RuntimeException with the details of any inconsistencies.
*/
<T extends ResourceDescriptor> void validate(Class<T> type, Collection<T> resources);

/**
* Callback to ensure the supplied {@code creatableResources} exist.
*
* <p>All {@code creatableResources} will be of the same type.
*
* @param type the type of the {@code creatableResources}.
* @param creatableResources the resource instances to ensure exists and are initialized.
* Resources passed will be {@link ResourceDescriptor#isCreatable creatable}.
* @param <T> the creatable resource descriptor type
* @throws RuntimeException on unknown resource type
*/
<T extends ResourceDescriptor & OwnedResource> void ensure(
Collection<T> creatableResources);
Class<T> type, Collection<T> creatableResources);
}

/**
* Create an initializer instance.
*
* @param resourceCreator callback used to ensure external resources exist, as exposed by Creek
* extensions.
* @param callbacks callbacks used to ensure external resources exist and are valid, as exposed
* by Creek extensions.
* @return an initializer instance.
*/
public static ResourceInitializer resourceInitializer(final ResourceCreator resourceCreator) {
return new ResourceInitializer(resourceCreator, new ComponentValidator());
public static ResourceInitializer resourceInitializer(final Callbacks callbacks) {
return new ResourceInitializer(callbacks, new ComponentValidator());
}

@VisibleForTesting
ResourceInitializer(
final ResourceCreator resourceCreator, final ComponentValidator componentValidator) {
this.resourceCreator = requireNonNull(resourceCreator, "resourceCreator");
ResourceInitializer(final Callbacks callbacks, final ComponentValidator componentValidator) {
this.callbacks = requireNonNull(callbacks, "callbacks");
this.componentValidator = requireNonNull(componentValidator, "componentValidator");
}

Expand All @@ -113,8 +129,7 @@ public void init(final Collection<? extends ComponentDescriptor> components) {
ensureResources(
groupById(
components,
resGroup -> resGroup.stream().anyMatch(ResourceDescriptor::isShared),
false));
resGroup -> resGroup.stream().anyMatch(ResourceDescriptor::isShared)));
}

/**
Expand All @@ -132,8 +147,7 @@ public void service(final Collection<? extends ComponentDescriptor> components)
ensureResources(
groupById(
components,
resGroup -> resGroup.stream().anyMatch(ResourceDescriptor::isOwned),
true));
resGroup -> resGroup.stream().anyMatch(ResourceDescriptor::isOwned)));
}

/**
Expand Down Expand Up @@ -163,18 +177,15 @@ public void test(
resGroup ->
resGroup.stream().anyMatch(ResourceDescriptor::isUnowned)
&& resGroup.stream()
.noneMatch(ResourceDescriptor::isOwned),
true)
.noneMatch(ResourceDescriptor::isOwned))
.collect(Collectors.toMap(group -> group.get(0).id(), Function.identity()));

groupById(
otherComponents,
resGroup -> resGroup.stream().anyMatch(r -> unowned.containsKey(r.id())),
false)
resGroup -> resGroup.stream().anyMatch(r -> unowned.containsKey(r.id())))
.forEach(resGroup -> unowned.get(resGroup.get(0).id()).addAll(resGroup));

final Stream<List<ResourceDescriptor>> stream = unowned.values().stream();
ensureResources(stream);
ensureResources(unowned.values().stream());
}

@SuppressWarnings({"unchecked", "rawtypes"})
Expand All @@ -184,7 +195,11 @@ private void ensureResources(final Stream<List<ResourceDescriptor>> resGroups) {
.map(this::creatableDescriptor)
.collect(groupingBy(Object::getClass))
.values()
.forEach(creatableResources -> resourceCreator.ensure((List) creatableResources));
.forEach(
creatableResources ->
callbacks.ensure(
(Class) creatableResources.get(0).getClass(),
(List) creatableResources));
}

private ResourceDescriptor creatableDescriptor(final List<ResourceDescriptor> resGroup) {
Expand All @@ -196,17 +211,12 @@ private ResourceDescriptor creatableDescriptor(final List<ResourceDescriptor> re

private Stream<List<ResourceDescriptor>> groupById(
final Collection<? extends ComponentDescriptor> components,
final Predicate<List<ResourceDescriptor>> groupPredicate,
final boolean validate) {
final Predicate<List<ResourceDescriptor>> groupPredicate) {
final Map<URI, List<ResourceDescriptor>> grouped =
components.stream()
.flatMap(this::getResources)
.collect(groupingBy(ResourceDescriptor::id));

if (validate) {
grouped.values().forEach(this::validateResourceGroup);
}

return grouped.values().stream().filter(groupPredicate);
}

Expand All @@ -221,14 +231,16 @@ private Stream<ResourceDescriptor> getResources(final ComponentDescriptor compon
*
* @param resourceGroup the group of descriptors that describe the same resource.
*/
@SuppressWarnings("unchecked")
private <T extends ResourceDescriptor> void validateResourceGroup(final List<T> resourceGroup) {
if (isShared(resourceGroup.get(0))) {
final T first = resourceGroup.get(0);
if (isShared(first)) {
// if shared, all should be shared.
if (resourceGroup.stream().anyMatch(r -> !isShared(r))) {
throw new ResourceDescriptorMismatchInitializationException(
"shared", resourceGroup);
}
} else if (isUnmanaged(resourceGroup.get(0))) {
} else if (isUnmanaged(first)) {
// if unmanaged, all should be unmanaged.
if (resourceGroup.stream().anyMatch(r -> !isUnmanaged(r))) {
throw new ResourceDescriptorMismatchInitializationException(
Expand All @@ -241,6 +253,8 @@ private <T extends ResourceDescriptor> void validateResourceGroup(final List<T>
"owned or unowned", resourceGroup);
}
}

callbacks.validate((Class<T>) first.getClass(), resourceGroup);
}

private static String formatResource(final List<? extends ResourceDescriptor> descriptors) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class ResourceInitializerTest {
@Mock private ComponentValidator validator;
@Mock private ComponentDescriptor component0;
@Mock private ComponentDescriptor component1;
@Mock private ResourceInitializer.ResourceCreator resourceCreator;
@Mock private ResourceInitializer.Callbacks callbacks;

@Mock(extraInterfaces = SharedResource.class)
private ResourceA sharedResource1;
Expand All @@ -74,7 +74,7 @@ class ResourceInitializerTest {

@BeforeEach
void setUp() {
initializer = new ResourceInitializer(resourceCreator, validator);
initializer = new ResourceInitializer(callbacks, validator);

when(sharedResource1.id()).thenReturn(A1_ID);
when(ownedResource1.id()).thenReturn(A1_ID);
Expand Down Expand Up @@ -224,6 +224,76 @@ void shouldThrowIfResourceGroupContainsUnownedAndOther() {
assertThat(e.getMessage(), containsString("sharedResource1"));
}

@SuppressWarnings("unchecked")
@Test
void shouldCallbackValidateEachResGroupOnInit() {
// Given:
final ResourceA sharedResource2 = resourceA(1, SharedResource.class);
when(component0.resources()).thenReturn(Stream.of(sharedResource1));
when(component1.resources()).thenReturn(Stream.of(sharedResource2));

// When:
initializer.init(List.of(component0, component1));

// Then:
verify(callbacks)
.validate(
(Class<ResourceA>) sharedResource1.getClass(),
List.of(sharedResource1, sharedResource2));
}

@SuppressWarnings("unchecked")
@Test
void shouldCallbackValidateEachResGroupOnService() {
// Given:
when(component0.resources()).thenReturn(Stream.of(ownedResource1));
when(component1.resources()).thenReturn(Stream.of(unownedResource1));

// When:
initializer.service(List.of(component0, component1));

// Then:
verify(callbacks)
.validate(
(Class<ResourceA>) ownedResource1.getClass(),
List.of(ownedResource1, unownedResource1));
}

@SuppressWarnings("unchecked")
@Test
void shouldCallbackValidateEachResGroupOnTest() {
// Given:
when(component0.resources()).thenReturn(Stream.of(unownedResource1));
when(component1.resources()).thenReturn(Stream.of(ownedResource1));

// When:
initializer.test(List.of(component0), List.of(component1));

// Then:
verify(callbacks)
.validate(
(Class<ResourceA>) unownedResource1.getClass(),
List.of(unownedResource1, ownedResource1));
}

@Test
void shouldThrowIfValidateCallbackThrows() {
// Given:
when(component0.resources()).thenReturn(Stream.of(ownedResource1));
when(component1.resources()).thenReturn(Stream.of(unownedResource1));
final RuntimeException expected = new RuntimeException("BIG BADA BOOM");
doThrow(expected).when(callbacks).validate(any(), any());

// When:
final Exception e =
assertThrows(
RuntimeException.class,
() -> initializer.service(List.of(component0, component1)));

// Then:
assertThat(e, is(expected));
}

@Test
void shouldNotInitializeAnyResourceOnInitIfNoSharedResources() {
// Given:
Expand All @@ -234,7 +304,7 @@ void shouldNotInitializeAnyResourceOnInitIfNoSharedResources() {
initializer.init(List.of(component0, component1));

// Then:
verify(resourceCreator, never()).ensure(any());
verify(callbacks, never()).ensure(any(), any());
}

@Test
Expand All @@ -249,7 +319,7 @@ void shouldNotInitializeAnyResourcesOnServiceIfNoOwnedResources() {
initializer.service(List.of(component0, component1));

// Then:
verify(resourceCreator, never()).ensure(any());
verify(callbacks, never()).ensure(any(), any());
}

@Test
Expand All @@ -266,7 +336,7 @@ void shouldNotInitializeAnyResourcesOnTestIfNoUnownedResources() {
initializer.test(List.of(component0), List.of(component1));

// Then:
verify(resourceCreator, never()).ensure(any());
verify(callbacks, never()).ensure(any(), any());
}

@Test
Expand All @@ -280,7 +350,7 @@ void shouldNotInitializeAnyResourcesOnTestIfNoUnownedResources() {
initializer.test(List.of(component0, component1), List.of());

// Then:
verify(resourceCreator, never()).ensure(any());
verify(callbacks, never()).ensure(any(), any());
}

@Test
Expand All @@ -296,7 +366,7 @@ void shouldNotInitializeUnmanagedGroups() {
initializer.init(List.of(component0, component1));

// Then:
verify(resourceCreator, never()).ensure(any());
verify(callbacks, never()).ensure(any(), any());
}

@Test
Expand Down Expand Up @@ -332,7 +402,10 @@ void shouldEnsureSharedResource() {
initializer.init(List.of(component0, component1));

// Then:
verify(resourceCreator).ensure((List) List.of(sharedResource1, sharedResource2));
verify(callbacks)
.ensure(
(Class) sharedResource1.getClass(),
(List) List.of(sharedResource1, sharedResource2));
}

@SuppressWarnings({"unchecked", "rawtypes"})
Expand All @@ -347,7 +420,10 @@ void shouldEnsureOwnedResource() {
initializer.service(List.of(component0, component1));

// Then:
verify(resourceCreator).ensure((List) List.of(ownedResource1, ownedResource2));
verify(callbacks)
.ensure(
(Class) ownedResource1.getClass(),
(List) List.of(ownedResource1, ownedResource2));
}

@SuppressWarnings({"unchecked", "rawtypes"})
Expand All @@ -363,14 +439,14 @@ void shouldEnsureUnownedResource() {
initializer.test(List.of(component0), List.of(component1));

// Then:
verify(resourceCreator).ensure((List) List.of(ownedResource2));
verify(callbacks).ensure((Class) ownedResource2.getClass(), (List) List.of(ownedResource2));
}

@Test
void shouldThrowIfEnsureThrows() {
// Given:
final RuntimeException expected = new RuntimeException("boom");
doThrow(expected).when(resourceCreator).ensure(any());
doThrow(expected).when(callbacks).ensure(any(), any());
when(component0.resources()).thenReturn(Stream.of(ownedResource1));

// When:
Expand All @@ -393,15 +469,15 @@ void shouldEnsureGroupingByHandler() {
initializer.service(List.of(component0));

// Then:
verify(resourceCreator).ensure((List) List.of(ownedResource1));
verify(resourceCreator).ensure((List) List.of(ownedResourceB));
verify(callbacks).ensure((Class) ownedResource1.getClass(), (List) List.of(ownedResource1));
verify(callbacks).ensure((Class) ownedResourceB.getClass(), (List) List.of(ownedResourceB));
}

@Test
void shouldThrowOnUnknownResourceType() {
// Given:
final NullPointerException expected = new NullPointerException("unknown");
doThrow(expected).when(resourceCreator).ensure(any());
doThrow(expected).when(callbacks).ensure(any(), any());
when(component0.resources()).thenReturn(Stream.of(sharedResource1));

// When:
Expand All @@ -416,7 +492,7 @@ void shouldThrowOnUnknownResourceType() {
@Test
void shouldThrowOnInvalidComponentUsingActualValidator() {
// Given:
initializer = ResourceInitializer.resourceInitializer(resourceCreator);
initializer = ResourceInitializer.resourceInitializer(callbacks);

// Then:
assertThrows(RuntimeException.class, () -> initializer.init(List.of(component0)));
Expand Down

0 comments on commit 5d1bbdb

Please sign in to comment.