Skip to content

Add an extension point to ComponentRuntime. #3967

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package com.google.firebase.components;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
Expand Down Expand Up @@ -76,6 +78,7 @@ public final class Component<T> {
int SET = 1;
}

private final String name;
private final Set<Class<? super T>> providedInterfaces;
private final Set<Dependency> dependencies;
private final @Instantiation int instantiation;
Expand All @@ -84,12 +87,14 @@ public final class Component<T> {
private final Set<Class<?>> publishedEvents;

private Component(
@Nullable String name,
Set<Class<? super T>> providedInterfaces,
Set<Dependency> dependencies,
@Instantiation int instantiation,
@ComponentType int type,
ComponentFactory<T> factory,
Set<Class<?>> publishedEvents) {
this.name = name;
this.providedInterfaces = Collections.unmodifiableSet(providedInterfaces);
this.dependencies = Collections.unmodifiableSet(dependencies);
this.instantiation = instantiation;
Expand All @@ -98,6 +103,16 @@ private Component(
this.publishedEvents = Collections.unmodifiableSet(publishedEvents);
}

/**
* Optional name of the component.
*
* <p>Used for debug purposes only.
*/
@Nullable
public String getName() {
return name;
}

/**
* Returns all interfaces this component provides.
*
Expand Down Expand Up @@ -153,6 +168,12 @@ public boolean isValue() {
return type == ComponentType.VALUE;
}

/** Creates a copy of the component with {@link ComponentFactory} replaced. */
public Component<T> withFactory(ComponentFactory<T> factory) {
return new Component<>(
name, providedInterfaces, dependencies, instantiation, type, factory, publishedEvents);
}

Comment on lines +171 to +176
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this for testing only? Looks like the purpose of ComponentFactory is only for product teams to implement. Once a product defined its factory in its registrars (and supply it via the component builder), it never needs to change?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's intended to be used outside of tests, for existing use example see:

cmp.withFactory(
wrap(
libraryVersion.getLibraryName(),
libraryVersion.getVersion(),
cmp.getFactory()));

@Override
public String toString() {
StringBuilder sb =
Expand Down Expand Up @@ -219,6 +240,7 @@ public static <T> Component<T> intoSet(T value, Class<T> anInterface) {

/** FirebaseComponent builder. */
public static class Builder<T> {
private String name = null;
private final Set<Class<? super T>> providedInterfaces = new HashSet<>();
private final Set<Dependency> dependencies = new HashSet<>();
private @Instantiation int instantiation = Instantiation.LAZY;
Expand All @@ -236,6 +258,12 @@ private Builder(Class<T> anInterface, Class<? super T>... additionalInterfaces)
Collections.addAll(providedInterfaces, additionalInterfaces);
}

/** Set a name for the {@link Component} being built. */
public Builder<T> name(@NonNull String name) {
this.name = name;
return this;
}

/** Add a {@link Dependency} to the {@link Component} being built. */
public Builder<T> add(Dependency dependency) {
Preconditions.checkNotNull(dependency, "Null dependency");
Expand Down Expand Up @@ -288,6 +316,7 @@ private Builder<T> intoSet() {
public Component<T> build() {
Preconditions.checkState(factory != null, "Missing required property: factory.");
return new Component<>(
name,
new HashSet<>(providedInterfaces),
new HashSet<>(dependencies),
instantiation,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2022 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.firebase.components;

import java.util.List;

/**
* Provides the ability to customize/decorate components as they are requested by {@link
* ComponentRuntime}.
*
* <p>This makes it possible to do validation, change/add dependencies, or customize components'
* initialization logic by decorating their {@link ComponentFactory factories}.
*/
public interface ComponentRegistrarProcessor {

/** Default "noop" processor. */
ComponentRegistrarProcessor NOOP = ComponentRegistrar::getComponents;

List<Component<?>> processRegistrar(ComponentRegistrar registrar);
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class ComponentRuntime extends AbstractComponentContainer implements Comp
private final List<Provider<ComponentRegistrar>> unprocessedRegistrarProviders;
private final EventBus eventBus;
private final AtomicReference<Boolean> eagerComponentsInitializedWith = new AtomicReference<>();
private final ComponentRegistrarProcessor componentRegistrarProcessor;

/**
* Creates an instance of {@link ComponentRuntime} for the provided {@link ComponentRegistrar}s
Expand All @@ -62,7 +63,11 @@ public ComponentRuntime(
Executor defaultEventExecutor,
Iterable<ComponentRegistrar> registrars,
Component<?>... additionalComponents) {
this(defaultEventExecutor, toProviders(registrars), Arrays.asList(additionalComponents));
this(
defaultEventExecutor,
toProviders(registrars),
Arrays.asList(additionalComponents),
ComponentRegistrarProcessor.NOOP);
}

/** A builder for creating {@link ComponentRuntime} instances. */
Expand All @@ -73,8 +78,10 @@ public static Builder builder(Executor defaultEventExecutor) {
private ComponentRuntime(
Executor defaultEventExecutor,
Iterable<Provider<ComponentRegistrar>> registrars,
Collection<Component<?>> additionalComponents) {
Collection<Component<?>> additionalComponents,
ComponentRegistrarProcessor componentRegistrarProcessor) {
eventBus = new EventBus(defaultEventExecutor);
this.componentRegistrarProcessor = componentRegistrarProcessor;

List<Component<?>> componentsToAdd = new ArrayList<>();

Expand Down Expand Up @@ -106,7 +113,7 @@ private void discoverComponents(List<Component<?>> componentsToAdd) {
try {
ComponentRegistrar registrar = provider.get();
if (registrar != null) {
componentsToAdd.addAll(registrar.getComponents());
componentsToAdd.addAll(componentRegistrarProcessor.processRegistrar(registrar));
iterator.remove();
}
} catch (InvalidRegistrarException ex) {
Expand Down Expand Up @@ -218,9 +225,12 @@ private List<Runnable> processSetComponents() {
if (!lazySetMap.containsKey(entry.getKey())) {
lazySetMap.put(entry.getKey(), LazySet.fromCollection(entry.getValue()));
} else {
@SuppressWarnings("unchecked")
LazySet<Object> existingSet = (LazySet<Object>) lazySetMap.get(entry.getKey());
for (Provider<?> provider : entry.getValue()) {
runnables.add(() -> existingSet.add((Provider<Object>) provider));
@SuppressWarnings("unchecked")
Provider<Object> castedProvider = (Provider<Object>) provider;
runnables.add(() -> existingSet.add(castedProvider));
}
}
}
Expand Down Expand Up @@ -332,10 +342,17 @@ private void processDependencies() {
}
}

@VisibleForTesting
Collection<Component<?>> getAllComponentsForTest() {
return components.keySet();
}

public static final class Builder {
private final Executor defaultExecutor;
private final List<Provider<ComponentRegistrar>> lazyRegistrars = new ArrayList<>();
private final List<Component<?>> additionalComponents = new ArrayList<>();
private ComponentRegistrarProcessor componentRegistrarProcessor =
ComponentRegistrarProcessor.NOOP;

Builder(Executor defaultExecutor) {
this.defaultExecutor = defaultExecutor;
Expand All @@ -356,8 +373,14 @@ public Builder addComponent(Component<?> component) {
return this;
}

public Builder setProcessor(ComponentRegistrarProcessor processor) {
this.componentRegistrarProcessor = processor;
return this;
}

public ComponentRuntime build() {
return new ComponentRuntime(defaultExecutor, lazyRegistrars, additionalComponents);
return new ComponentRuntime(
defaultExecutor, lazyRegistrars, additionalComponents, componentRegistrarProcessor);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import java.util.List;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
Expand Down Expand Up @@ -583,4 +584,32 @@ public void newlyDiscoveredComponent_shouldBecomeAvailableThroughItsDeferred() {
componentLoader.discoverComponents();
assertThat(dependsOnDeferredString.value).isEqualTo("hello");
}

@Test
public void container_withComponentProcessor_shouldDelegateToItForEachComponentRegistrar() {
InitTracker initTracker = new InitTracker();

ComponentFactory<Object> replacedFactory = c -> null;
ComponentRegistrarProcessor processor =
r ->
r.getComponents().stream()
.map(c -> ((Component<Object>) c).withFactory(replacedFactory))
.collect(Collectors.toList());

ComponentRuntime runtime =
ComponentRuntime.builder(EXECUTOR)
.addComponentRegistrar(new ComponentRegistrarImpl(Eagerness.ALWAYS))
.addComponent(Component.of(initTracker, InitTracker.class))
.setProcessor(processor)
.build();

assertThat(
runtime.getAllComponentsForTest().stream()
.filter(
c ->
(c.getProvidedInterfaces().contains(ComponentOne.class)
|| c.getProvidedInterfaces().contains(ComponentTwo.class)))
.allMatch(c -> c.getFactory() == replacedFactory))
.isTrue();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -195,4 +195,30 @@ public void getFactory_shouldReturnFactorySetInBuilder() {
Component.builder(TestClass.class).factory(nullFactory).build();
assertThat(component.getFactory()).isSameInstanceAs(nullFactory);
}

@Test
public void withFactory_shouldReplaceTheFactorySetInBuilder() {
Component<TestClass> component =
Component.builder(TestClass.class).factory(nullFactory).build();

ComponentFactory<TestClass> newFactory = c -> null;
assertThat(component.withFactory(newFactory).getFactory()).isSameInstanceAs(newFactory);
}

@Test
public void name_shouldDefaultToNull() {
Component<TestClass> component =
Component.builder(TestClass.class).factory(nullFactory).build();

assertThat(component.getName()).isNull();
}

@Test
public void name_shouldReturnNameSetInBuilder() {
String name = "myName";
Component<TestClass> component =
Component.builder(TestClass.class).name(name).factory(nullFactory).build();

assertThat(component.getName()).isEqualTo(name);
}
}