summaryrefslogtreecommitdiffstats
path: root/sangria-listbinder
diff options
context:
space:
mode:
Diffstat (limited to 'sangria-listbinder')
-rw-r--r--sangria-listbinder/pom.xml63
-rw-r--r--sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/AnnotatedListBinderBuilder.java28
-rw-r--r--sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinder.java427
-rw-r--r--sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderBuilder.java24
-rw-r--r--sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderErrors.java43
-rw-r--r--sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListElement.java44
-rw-r--r--sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/package-info.java25
-rw-r--r--sangria-listbinder/src/test/java/com/tavianator/sangria/listbinder/ListBinderTest.java210
8 files changed, 864 insertions, 0 deletions
diff --git a/sangria-listbinder/pom.xml b/sangria-listbinder/pom.xml
new file mode 100644
index 0000000..5b04e5e
--- /dev/null
+++ b/sangria-listbinder/pom.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <parent>
+ <groupId>com.tavianator.sangria</groupId>
+ <artifactId>sangria</artifactId>
+ <version>1.1-SNAPSHOT</version>
+ </parent>
+
+ <artifactId>sangria-listbinder</artifactId>
+ <packaging>jar</packaging>
+ <name>Sangria ListBinder</name>
+ <description>A multi-binder with ordering guarantees</description>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.tavianator.sangria</groupId>
+ <artifactId>sangria-core</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.inject</groupId>
+ <artifactId>guice</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.inject.extensions</groupId>
+ <artifactId>guice-multibindings</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.code.findbugs</groupId>
+ <artifactId>jsr305</artifactId>
+ <optional>true</optional>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-integration</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/AnnotatedListBinderBuilder.java b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/AnnotatedListBinderBuilder.java
new file mode 100644
index 0000000..594971c
--- /dev/null
+++ b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/AnnotatedListBinderBuilder.java
@@ -0,0 +1,28 @@
+package com.tavianator.sangria.listbinder;
+
+import java.lang.annotation.Annotation;
+
+/**
+ * Fluent builder interface.
+ *
+ * @author Tavian Barnes (tavianator@tavianator.com)
+ * @version 1.1
+ * @since 1.1
+ */
+public interface AnnotatedListBinderBuilder<T> extends ListBinderBuilder<T> {
+ /**
+ * Make a binder for an annotated list type.
+ *
+ * @param annotationType The annotation type for the list.
+ * @return A fluent builder.
+ */
+ ListBinderBuilder<T> annotatedWith(Class<? extends Annotation> annotationType);
+
+ /**
+ * Make a binder for an annotated list type.
+ *
+ * @param annotation The annotation instance for the list.
+ * @return A fluent builder.
+ */
+ ListBinderBuilder<T> annotatedWith(Annotation annotation);
+}
diff --git a/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinder.java b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinder.java
new file mode 100644
index 0000000..d813b7a
--- /dev/null
+++ b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinder.java
@@ -0,0 +1,427 @@
+/****************************************************************************
+ * Sangria *
+ * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> *
+ * *
+ * 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.tavianator.sangria.listbinder;
+
+import java.lang.annotation.Annotation;
+import java.util.*;
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import com.google.common.base.Function;
+import com.google.common.base.Objects;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ListMultimap;
+import com.google.inject.AbstractModule;
+import com.google.inject.Binder;
+import com.google.inject.CreationException;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.multibindings.Multibinder;
+import com.google.inject.spi.Message;
+import com.google.inject.util.Types;
+
+import com.tavianator.sangria.core.PotentialAnnotation;
+import com.tavianator.sangria.core.PrettyTypes;
+import com.tavianator.sangria.core.Priority;
+import com.tavianator.sangria.core.TypeLiterals;
+import com.tavianator.sangria.core.UniqueAnnotations;
+
+/**
+ * A multi-binder with guaranteed order.
+ *
+ * <p>
+ * {@link ListBinder} is much like {@link Multibinder}, except it provides a guaranteed iteration order, and binds a
+ * {@link List} instead of a {@link Set}. For example:
+ * </p>
+ *
+ * <pre>
+ * ListBinder&lt;String&gt; listBinder = ListBinder.build(binder(), String.class)
+ * .withDefaultPriority();
+ * listBinder.addBinding().toInstance("a");
+ * listBinder.addBinding().toInstance("b");
+ * </pre>
+ *
+ * <p>
+ * This will create a binding for a {@code List&lt;String&gt;}, which contains {@code "a"} followed by {@code "b"}. It
+ * also creates a binding for {@code List&lt;Provider&lt;String&gt;&gt;} &mdash; this may be useful in more advanced
+ * cases to allow list elements to be lazily loaded.
+ * </p>
+ *
+ * <p>To add an annotation to the list binding, simply write this:</p>
+ *
+ * <pre>
+ * ListBinder&lt;String&gt; listBinder = ListBinder.build(binder(), String.class)
+ * .annotatedWith(Names.named("name"))
+ * .withDefaultPriority();
+ * </pre>
+ *
+ * <p>
+ * and the created binding will be {@code @Named("name") List&lt;String&gt;} instead.
+ * </p>
+ *
+ * <p>
+ * For large lists, it may be helpful to split up their specification across different modules. This is accomplished by
+ * specifying <em>priorities</em> for the {@link ListBinder}s when they are created. For example:
+ * </p>
+ *
+ * <pre>
+ * // In some module
+ * ListBinder&lt;String&gt; listBinder1 = ListBinder.build(binder(), String.class)
+ * .withPriority(0);
+ * listBinder1.addBinding().toInstance("a");
+ * listBinder1.addBinding().toInstance("b");
+ *
+ * // ... some other module
+ * ListBinder&lt;String&gt; listBinder2 = ListBinder.build(binder(), String.class)
+ * .withPriority(1);
+ * listBinder2.addBinding().toInstance("c");
+ * listBinder2.addBinding().toInstance("d");
+ * </pre>
+ *
+ * <p>
+ * The generated list will contain {@code "a"}, {@code "b"}, {@code "c"}, {@code "d"}, in order. This happens because
+ * the first {@link ListBinder} had a smaller priority, so its entries come first. For more information about the
+ * priority system, see {@link Priority}.
+ * </p>
+ *
+ * @param <T> The type of the list element.
+ * @author Tavian Barnes (tavianator@tavianator.com)
+ * @version 1.1
+ * @since 1.1
+ */
+public class ListBinder<T> {
+ private static final Class<?>[] SKIPPED_SOURCES = {
+ ListBinder.class,
+ BuilderImpl.class,
+ };
+
+ private final Binder binder;
+ private final Multibinder<ListElement<T>> multibinder;
+ private final Multibinder<ListBinderErrors<T>> errorMultibinder;
+ private final TypeLiteral<T> entryType;
+ private final Key<List<T>> listKey;
+ private final Key<List<Provider<T>>> listOfProvidersKey;
+ private final Key<Set<ListElement<T>>> setKey;
+ private final Key<Set<ListBinderErrors<T>>> errorSetKey;
+ private final PotentialAnnotation potentialAnnotation;
+ private final Priority initialPriority;
+ private Priority priority;
+
+ private ListBinder(
+ Binder binder,
+ TypeLiteral<T> entryType,
+ PotentialAnnotation potentialAnnotation,
+ Priority initialPriority) {
+ this.binder = binder;
+ this.entryType = entryType;
+
+ TypeLiteral<ListElement<T>> elementType = listElementOf(entryType);
+ TypeLiteral<ListBinderErrors<T>> errorsType = listBinderErrorsOf(entryType);
+ this.listKey = potentialAnnotation.getKey(TypeLiterals.listOf(entryType));
+ this.listOfProvidersKey = potentialAnnotation.getKey(TypeLiterals.listOf(TypeLiterals.providerOf(entryType)));
+ this.setKey = potentialAnnotation.getKey(TypeLiterals.setOf(elementType));
+ this.errorSetKey = potentialAnnotation.getKey(TypeLiterals.setOf(errorsType));
+ this.multibinder = potentialAnnotation.accept(new MultibinderMaker<>(binder, elementType));
+ this.errorMultibinder = potentialAnnotation.accept(new MultibinderMaker<>(binder, errorsType));
+
+ this.potentialAnnotation = potentialAnnotation;
+ this.priority = this.initialPriority = initialPriority;
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T> TypeLiteral<ListElement<T>> listElementOf(TypeLiteral<T> type) {
+ return (TypeLiteral<ListElement<T>>)TypeLiteral.get(Types.newParameterizedType(ListElement.class, type.getType()));
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <T> TypeLiteral<ListBinderErrors<T>> listBinderErrorsOf(TypeLiteral<T> type) {
+ return (TypeLiteral<ListBinderErrors<T>>)TypeLiteral.get(Types.newParameterizedType(ListBinderErrors.class, type.getType()));
+ }
+
+ /**
+ * {@link PotentialAnnotation.Visitor} that makes {@link Multibinder}s with the given annotation.
+ */
+ private static class MultibinderMaker<T> implements PotentialAnnotation.Visitor<Multibinder<T>> {
+ private final Binder binder;
+ private final TypeLiteral<T> type;
+
+ MultibinderMaker(Binder binder, TypeLiteral<T> type) {
+ this.binder = binder;
+ this.type = type;
+ }
+
+ @Override
+ public Multibinder<T> visitNoAnnotation() {
+ return Multibinder.newSetBinder(binder, type);
+ }
+
+ @Override
+ public Multibinder<T> visitAnnotationType(Class<? extends Annotation> annotationType) {
+ return Multibinder.newSetBinder(binder, type, annotationType);
+ }
+
+ @Override
+ public Multibinder<T> visitAnnotationInstance(Annotation annotation) {
+ return Multibinder.newSetBinder(binder, type, annotation);
+ }
+ }
+
+ /**
+ * Start building a {@link ListBinder}.
+ *
+ * @param binder The current binder, usually {@link AbstractModule#binder()}.
+ * @param type The type of the list element.
+ * @param <T> The type of the list element.
+ * @return A fluent builder.
+ */
+ public static <T> AnnotatedListBinderBuilder<T> build(Binder binder, Class<T> type) {
+ return build(binder, TypeLiteral.get(type));
+ }
+
+ /**
+ * Start building a {@link ListBinder}.
+ *
+ * @param binder The current binder, usually {@link AbstractModule#binder()}.
+ * @param type The type of the list element.
+ * @param <T> The type of the list element.
+ * @return A fluent builder.
+ */
+ public static <T> AnnotatedListBinderBuilder<T> build(Binder binder, TypeLiteral<T> type) {
+ return new BuilderImpl<>(binder.skipSources(SKIPPED_SOURCES), type, PotentialAnnotation.none());
+ }
+
+ private static class BuilderImpl<T> implements AnnotatedListBinderBuilder<T> {
+ private final Binder binder;
+ private final TypeLiteral<T> entryType;
+ private final PotentialAnnotation potentialAnnotation;
+
+ BuilderImpl(Binder binder, TypeLiteral<T> type, PotentialAnnotation potentialAnnotation) {
+ this.binder = binder;
+ this.entryType = type;
+ this.potentialAnnotation = potentialAnnotation;
+ }
+
+ @Override
+ public ListBinderBuilder<T> annotatedWith(Class<? extends Annotation> annotationType) {
+ return new BuilderImpl<>(binder, entryType, potentialAnnotation.annotatedWith(annotationType));
+ }
+
+ @Override
+ public ListBinderBuilder<T> annotatedWith(Annotation annotation) {
+ return new BuilderImpl<>(binder, entryType, potentialAnnotation.annotatedWith(annotation));
+ }
+
+ @Override
+ public ListBinder<T> withDefaultPriority() {
+ return create(Priority.getDefault());
+ }
+
+ @Override
+ public ListBinder<T> withPriority(int weight, int... weights) {
+ return create(Priority.create(weight, weights));
+ }
+
+ private ListBinder<T> create(Priority priority) {
+ ListBinder<T> listBinder = new ListBinder<>(binder, entryType, potentialAnnotation, priority);
+
+ // Add the delayed errors
+ Message duplicateBindersError = new Message(PrettyTypes.format("Duplicate %s", listBinder));
+ Message conflictingDefaultExplicitError;
+ if (priority.isDefault()) {
+ conflictingDefaultExplicitError = new Message(PrettyTypes.format("%s conflicts with ListBinder with explicit priority", listBinder));
+ } else {
+ conflictingDefaultExplicitError = new Message(PrettyTypes.format("%s conflicts with ListBinder with default priority", listBinder));
+ }
+ listBinder.errorMultibinder.addBinding().toInstance(new ListBinderErrors<T>(
+ priority,
+ duplicateBindersError,
+ conflictingDefaultExplicitError));
+
+ // Set up the exposed bindings
+ binder.bind(listBinder.listOfProvidersKey)
+ .toProvider(new ListOfProvidersProvider<>(listBinder));
+ binder.bind(listBinder.listKey)
+ .toProvider(new ListOfProvidersAdapter<>(listBinder.listOfProvidersKey));
+
+ return listBinder;
+ }
+ }
+
+ /**
+ * Provider implementation for {@code List&lt;Provider&lt;T&gt;&gt;}.
+ */
+ private static class ListOfProvidersProvider<T> implements Provider<List<Provider<T>>> {
+ private final Key<Set<ListElement<T>>> setKey;
+ private final Key<Set<ListBinderErrors<T>>> errorSetKey;
+ private final Priority priority;
+ private List<Provider<T>> providers;
+
+ ListOfProvidersProvider(ListBinder<T> listBinder) {
+ this.setKey = listBinder.setKey;
+ this.errorSetKey = listBinder.errorSetKey;
+ this.priority = listBinder.initialPriority;
+ }
+
+ @Inject
+ void inject(Injector injector) {
+ validate(injector);
+ initialize(injector);
+ }
+
+ private void validate(Injector injector) {
+ // Note that here we don't report all errors at once, correctness relies on Guice injecting even providers
+ // that get de-duplicated. This way, all errors are attached to the right source.
+
+ List<Message> messages = new ArrayList<>();
+
+ // Get the errors into a multimap by priority
+ Set<ListBinderErrors<T>> errorSet = injector.getInstance(errorSetKey);
+ ListMultimap<Priority, ListBinderErrors<T>> errorMap = ArrayListMultimap.create();
+ for (ListBinderErrors<T> errors : errorSet) {
+ errorMap.put(errors.priority, errors);
+ }
+
+ // Check for duplicate priorities
+ List<ListBinderErrors<T>> ourPriorityErrors = errorMap.get(priority);
+ ListBinderErrors<T> ourErrors = ourPriorityErrors.get(0);
+ if (ourPriorityErrors.size() > 1) {
+ messages.add(ourErrors.duplicateBindersError);
+ }
+
+ // Check for default and non-default priorities
+ if (errorMap.containsKey(Priority.getDefault()) && errorMap.keySet().size() > 1) {
+ messages.add(ourErrors.conflictingDefaultExplicitError);
+ }
+
+ if (!messages.isEmpty()) {
+ throw new CreationException(messages);
+ }
+ }
+
+ private void initialize(final Injector injector) {
+ Set<ListElement<T>> set = injector.getInstance(setKey);
+ List<ListElement<T>> elements = new ArrayList<>(set);
+ Collections.sort(elements);
+
+ this.providers = FluentIterable.from(elements)
+ .transform(new Function<ListElement<T>, Provider<T>>() {
+ @Override
+ public Provider<T> apply(ListElement<T> input) {
+ return injector.getProvider(input.key);
+ }
+ })
+ .toList();
+ }
+
+ @Override
+ public List<Provider<T>> get() {
+ return providers;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ } else if (!(obj instanceof ListOfProvidersProvider)) {
+ return false;
+ }
+
+ ListOfProvidersProvider<?> other = (ListOfProvidersProvider<?>)obj;
+ return setKey.equals(other.setKey);
+ }
+
+ @Override
+ public int hashCode() {
+ return setKey.hashCode();
+ }
+ }
+
+ /**
+ * Provider implementation for {@code List&lt;T&gt;}, in terms of {@code List&lt;Provider&lt;T&gt;&gt;}.
+ */
+ private static class ListOfProvidersAdapter<T> implements Provider<List<T>> {
+ private final Key<List<Provider<T>>> providerListKey;
+ private Provider<List<Provider<T>>> provider;
+
+ ListOfProvidersAdapter(Key<List<Provider<T>>> providerListKey) {
+ this.providerListKey = providerListKey;
+ }
+
+ @Inject
+ void inject(final Injector injector) {
+ this.provider = injector.getProvider(providerListKey);
+ }
+
+ @Override
+ public List<T> get() {
+ return FluentIterable.from(provider.get())
+ .transform(new Function<Provider<T>, T>() {
+ @Override
+ public T apply(Provider<T> input) {
+ return input.get();
+ }
+ })
+ .toList();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ } else if (!(obj instanceof ListOfProvidersAdapter)) {
+ return false;
+ }
+
+ ListOfProvidersAdapter<?> other = (ListOfProvidersAdapter<?>)obj;
+ return providerListKey.equals(other.providerListKey);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hashCode(providerListKey);
+ }
+ }
+
+ /**
+ * Add an entry to the list.
+ *
+ * <p>
+ * The entry will be added in order for this {@link ListBinder} instance. Between different {@link ListBinder}s, the
+ * order is determined by the {@link ListBinder}'s {@link Priority}.
+ * </p>
+ *
+ * @return A fluent binding builder.
+ */
+ public LinkedBindingBuilder<T> addBinding() {
+ Key<T> key = Key.get(entryType, UniqueAnnotations.create());
+ multibinder.addBinding().toInstance(new ListElement<>(key, priority));
+ priority = priority.next();
+ return binder.bind(key);
+ }
+
+ @Override
+ public String toString() {
+ return PrettyTypes.format("ListBinder<%s>%s with %s",
+ entryType,
+ (potentialAnnotation.hasAnnotation() ? " annotated with " + potentialAnnotation : ""),
+ initialPriority);
+ }
+}
diff --git a/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderBuilder.java b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderBuilder.java
new file mode 100644
index 0000000..5dae594
--- /dev/null
+++ b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderBuilder.java
@@ -0,0 +1,24 @@
+package com.tavianator.sangria.listbinder;
+
+import com.tavianator.sangria.core.Priority;
+
+/**
+ * Fluent builder interface.
+ *
+ * @author Tavian Barnes (tavianator@tavianator.com)
+ * @version 1.1
+ * @since 1.1
+ */
+public interface ListBinderBuilder<T> {
+ /**
+ * @return A {@link ListBinder} with the default priority.
+ * @see Priority
+ */
+ ListBinder<T> withDefaultPriority();
+
+ /**
+ * @return A {@link ListBinder} with the given priority.
+ * @see Priority
+ */
+ ListBinder<T> withPriority(int weight, int... weights);
+}
diff --git a/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderErrors.java b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderErrors.java
new file mode 100644
index 0000000..dfd0c64
--- /dev/null
+++ b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderErrors.java
@@ -0,0 +1,43 @@
+/****************************************************************************
+ * Sangria *
+ * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> *
+ * *
+ * 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.tavianator.sangria.listbinder;
+
+import com.google.inject.Key;
+import com.google.inject.spi.Message;
+
+import com.tavianator.sangria.core.Priority;
+
+/**
+ * Error holder for {@link ListBinder}s.
+ *
+ * @param <T> Only used to allow different {@link Key}s.
+ * @author Tavian Barnes (tavianator@tavianator.com)
+ * @version 1.1
+ * @since 1.1
+ */
+class ListBinderErrors<T> {
+ final Priority priority;
+ final Message duplicateBindersError;
+ final Message conflictingDefaultExplicitError;
+
+ ListBinderErrors(Priority priority, Message duplicateBindersError, Message conflictingDefaultExplicitError) {
+ this.priority = priority;
+ this.duplicateBindersError = duplicateBindersError;
+ this.conflictingDefaultExplicitError = conflictingDefaultExplicitError;
+ }
+}
diff --git a/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListElement.java b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListElement.java
new file mode 100644
index 0000000..bb95674
--- /dev/null
+++ b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListElement.java
@@ -0,0 +1,44 @@
+/****************************************************************************
+ * Sangria *
+ * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> *
+ * *
+ * 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.tavianator.sangria.listbinder;
+
+import com.google.inject.Key;
+
+import com.tavianator.sangria.core.Priority;
+
+/**
+ * An individual element in a ListBinder.
+ *
+ * @author Tavian Barnes (tavianator@tavianator.com)
+ * @version 1.1
+ * @since 1.1
+ */
+class ListElement<T> implements Comparable<ListElement<T>> {
+ final Key<T> key;
+ final Priority priority;
+
+ ListElement(Key<T> key, Priority priority) {
+ this.key = key;
+ this.priority = priority;
+ }
+
+ @Override
+ public int compareTo(ListElement<T> o) {
+ return priority.compareTo(o.priority);
+ }
+}
diff --git a/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/package-info.java b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/package-info.java
new file mode 100644
index 0000000..1ed1af4
--- /dev/null
+++ b/sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/package-info.java
@@ -0,0 +1,25 @@
+/****************************************************************************
+ * Sangria *
+ * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> *
+ * *
+ * 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. *
+ ****************************************************************************/
+
+/**
+ * {@code sangria-listbinder}: A multi-binder with guaranteed order.
+ *
+ * @author Tavian Barnes (tavianator@tavianator.com)
+ * @version 1.1
+ * @since 1.1
+ */
+package com.tavianator.sangria.listbinder;
diff --git a/sangria-listbinder/src/test/java/com/tavianator/sangria/listbinder/ListBinderTest.java b/sangria-listbinder/src/test/java/com/tavianator/sangria/listbinder/ListBinderTest.java
new file mode 100644
index 0000000..4c7c86b
--- /dev/null
+++ b/sangria-listbinder/src/test/java/com/tavianator/sangria/listbinder/ListBinderTest.java
@@ -0,0 +1,210 @@
+/****************************************************************************
+ * Sangria *
+ * Copyright (C) 2014 Tavian Barnes <tavianator@tavianator.com> *
+ * *
+ * 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.tavianator.sangria.listbinder;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.*;
+import javax.inject.Provider;
+import javax.inject.Qualifier;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.CreationException;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Names;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import com.tavianator.sangria.core.TypeLiterals;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@link ListBinder}.
+ *
+ * @author Tavian Barnes (tavianator@tavianator.com)
+ * @version 1.1
+ * @since 1.1
+ */
+public class ListBinderTest {
+ public @Rule ExpectedException thrown = ExpectedException.none();
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Qualifier
+ private @interface Simple {
+ }
+
+ private static final TypeLiteral<List<String>> LIST_OF_STRINGS = TypeLiterals.listOf(String.class);
+ private static final TypeLiteral<List<Provider<String>>> LIST_OF_STRING_PROVIDERS = TypeLiterals.listOf(TypeLiterals.providerOf(String.class));
+
+ @Test
+ public void testBasicLists() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ ListBinder<String> listBinder = ListBinder.build(binder(), String.class)
+ .withDefaultPriority();
+ listBinder.addBinding().toInstance("a");
+ listBinder.addBinding().toInstance("b");
+ listBinder.addBinding().toInstance("c");
+
+ listBinder = ListBinder.build(binder(), String.class)
+ .annotatedWith(Simple.class)
+ .withDefaultPriority();
+ listBinder.addBinding().toInstance("d");
+ listBinder.addBinding().toInstance("e");
+ listBinder.addBinding().toInstance("f");
+
+ listBinder = ListBinder.build(binder(), String.class)
+ .annotatedWith(Names.named("name"))
+ .withDefaultPriority();
+ listBinder.addBinding().toInstance("g");
+ listBinder.addBinding().toInstance("h");
+ listBinder.addBinding().toInstance("i");
+ }
+ });
+ List<String> list = injector.getInstance(Key.get(LIST_OF_STRINGS));
+ assertThat(list, contains("a", "b", "c"));
+
+ List<Provider<String>> providers = injector.getInstance(Key.get(LIST_OF_STRING_PROVIDERS));
+ assertThat(providers, hasSize(3));
+ assertThat(providers.get(0).get(), equalTo("a"));
+ assertThat(providers.get(1).get(), equalTo("b"));
+ assertThat(providers.get(2).get(), equalTo("c"));
+
+ list = injector.getInstance(Key.get(LIST_OF_STRINGS, Simple.class));
+ assertThat(list, contains("d", "e", "f"));
+
+ list = injector.getInstance(Key.get(LIST_OF_STRINGS, Names.named("name")));
+ assertThat(list, contains("g", "h", "i"));
+ }
+
+ @Test
+ public void testSplitBinders() {
+ Injector injector = Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ ListBinder<String> listBinder = ListBinder.build(binder(), String.class)
+ .withPriority(1);
+ listBinder.addBinding().toInstance("c");
+ listBinder.addBinding().toInstance("d");
+ }
+ }, new AbstractModule() {
+ @Override
+ protected void configure() {
+ ListBinder<String> listBinder = ListBinder.build(binder(), String.class)
+ .withPriority(0);
+ listBinder.addBinding().toInstance("a");
+ listBinder.addBinding().toInstance("b");
+ }
+ });
+ List<String> list = injector.getInstance(Key.get(LIST_OF_STRINGS));
+ assertThat(list, contains("a", "b", "c", "d"));
+ }
+
+ @Test
+ public void testConflictingDefaultPriorities() {
+ thrown.expect(CreationException.class);
+ thrown.expectMessage(containsString("2 errors"));
+ thrown.expectMessage(containsString("1) Duplicate ListBinder<java.lang.String> with default priority"));
+ thrown.expectMessage(containsString("2) Duplicate ListBinder<java.lang.String> with default priority"));
+ thrown.expectMessage(containsString("at com.tavianator.sangria.listbinder.ListBinderTest"));
+
+ Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ ListBinder.build(binder(), String.class)
+ .withDefaultPriority();
+ }
+ }, new AbstractModule() {
+ @Override
+ protected void configure() {
+ ListBinder.build(binder(), String.class)
+ .withDefaultPriority();
+ }
+ });
+ }
+
+ @Test
+ public void testConflictingExplicitPriorities() {
+ thrown.expect(CreationException.class);
+ thrown.expectMessage(containsString("2 errors"));
+ thrown.expectMessage(containsString("1) Duplicate ListBinder<java.lang.String> with priority [1]"));
+ thrown.expectMessage(containsString("2) Duplicate ListBinder<java.lang.String> with priority [1]"));
+ thrown.expectMessage(containsString("at com.tavianator.sangria.listbinder.ListBinderTest"));
+
+ Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ ListBinder.build(binder(), String.class)
+ .withPriority(1);
+ }
+ }, new AbstractModule() {
+ @Override
+ protected void configure() {
+ ListBinder.build(binder(), String.class)
+ .withPriority(1);
+ }
+ });
+ }
+
+ @Test
+ public void testConflictingDefaultAndExplicitPriorities() {
+ thrown.expect(CreationException.class);
+ thrown.expectMessage(containsString("2 errors"));
+ thrown.expectMessage(containsString(") ListBinder<java.lang.String> with default priority conflicts with ListBinder with explicit priority"));
+ thrown.expectMessage(containsString(") ListBinder<java.lang.String> with priority [1] conflicts with ListBinder with default priority"));
+ thrown.expectMessage(containsString("at com.tavianator.sangria.listbinder.ListBinderTest"));
+
+ Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ ListBinder.build(binder(), String.class)
+ .withDefaultPriority();
+ }
+ }, new AbstractModule() {
+ @Override
+ protected void configure() {
+ ListBinder.build(binder(), String.class)
+ .withPriority(1);
+ }
+ });
+ }
+
+ @Test
+ public void testToString() {
+ Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ ListBinder<String> listBinder = ListBinder.build(binder(), String.class)
+ .annotatedWith(Names.named("name"))
+ .withDefaultPriority();
+ assertThat(listBinder.toString(), equalTo("ListBinder<java.lang.String> annotated with @com.google.inject.name.Named(value=name) with default priority"));
+
+ ListBinder<Object> objectListBinder = ListBinder.build(binder(), Object.class)
+ .withPriority(1, 2);
+ assertThat(objectListBinder.toString(), equalTo("ListBinder<java.lang.Object> with priority [1, 2]"));
+ }
+ });
+ }
+}