From d814b12bd39172bc59fcefa5815bc3776b793177 Mon Sep 17 00:00:00 2001
From: Tavian Barnes <tavianator@tavianator.com>
Date: Wed, 7 May 2014 21:58:15 -0400
Subject: listbinder: Add an implementation of ListBinder<T>.

---
 .../listbinder/AnnotatedListBinderBuilder.java     |  28 ++
 .../tavianator/sangria/listbinder/ListBinder.java  | 427 +++++++++++++++++++++
 .../sangria/listbinder/ListBinderBuilder.java      |  24 ++
 .../sangria/listbinder/ListBinderErrors.java       |  43 +++
 .../tavianator/sangria/listbinder/ListElement.java |  44 +++
 .../sangria/listbinder/package-info.java           |  25 ++
 .../sangria/listbinder/ListBinderTest.java         | 210 ++++++++++
 7 files changed, 801 insertions(+)
 create mode 100644 sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/AnnotatedListBinderBuilder.java
 create mode 100644 sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinder.java
 create mode 100644 sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderBuilder.java
 create mode 100644 sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListBinderErrors.java
 create mode 100644 sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/ListElement.java
 create mode 100644 sangria-listbinder/src/main/java/com/tavianator/sangria/listbinder/package-info.java
 create mode 100644 sangria-listbinder/src/test/java/com/tavianator/sangria/listbinder/ListBinderTest.java

(limited to 'sangria-listbinder/src')

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..c28d758
--- /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<String> 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<String>}, which contains {@code "a"} followed by {@code "b"}. It also
+ * creates a binding for {@code List<Provider<String>>} &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<String> listBinder = ListBinder.build(binder(), String.class)
+ *         .annotatedWith(Names.named("name"))
+ *         .withDefaultPriority();
+ * </pre>
+ *
+ * <p>
+ * and the created binding will be {@code @Named("name") List<String>} 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<String> listBinder1 = ListBinder.build(binder(), String.class)
+ *         .withPriority(0);
+ * listBinder1.addBinding().toInstance("a");
+ * listBinder1.addBinding().toInstance("b");
+ *
+ * // ... some other module
+ * ListBinder<String> 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<Provider<T>>}.
+     */
+    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<T>}, in terms of {@code List<Provider<T>>}.
+     */
+    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]"));
+            }
+        });
+    }
+}
-- 
cgit v1.2.3