Contract.java
package io.github.jonloucks.contracts.api;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import static io.github.jonloucks.contracts.api.Checks.*;
/**
* The API agreement for a Contract between the claimee and the claimant.
* <p>
* Each Contract instance is a unique key for establishing a binding between
* an implementation and those that use it.
* </p>
* <ul>
* <li>Create by custom config {@link #create(Config)}</li>
* <li>Creation by automatic config. {@link #create(String, Object[])}</li>
* <li>Used by {@link GlobalContracts#claimContract(Contract)}</li>
* <li>Used by {@link GlobalContracts#bindContract(Contract, Promisor)}</li>
* <li>Used by {@link Contracts#claim(Contract)}</li>
* <li>Used by {@link Contracts#bind(Contract, Promisor)}</li>
* </ul>
* <h2>A simple factory</h2>
* Each call to {@link Contracts#claim(Contract)} will return a new instance.
* <pre class="code">
* <code class="java">
* // Save Contract at some agreed upon place
* public static final Contract<Contracts> SERVICE = Contract.create("You Choose");
* // The promisor needs to be bound before it is claimed.
* GlobalContracts.bindContract(SERVICE, () -> new ServiceImpl());
* // Ready to be claimed
* Contracts service = GlobalContracts.claimContract(SERVICE);
* </code></pre>
* If you need a thread specific
*
* @param <T> The type of deliverable
*/
public final class Contract<T> {
/**
* Create a contract with a given name, the rest is automatic.
* For custom configuration see {@link #create(Config)}
*
* @param name the name for the contract, null is not allowed
* @param reifiedArray null is not allowed
* @param <T> the type of deliverable for this Contract
* @return the new Contract
*/
@SafeVarargs // Required to safely determine the checked type of T
public static <T> Contract<T> create(String name, T... reifiedArray) {
return create(detectDeliverableType(reifiedArray), b -> b.name(name));
}
/**
* Create a contract with a given name, the rest is automatic.
* For custom configuration see {@link #create(Config)}
*
* @param type the deliverable class for the Contract
* @param <T> the type of deliverable for this Contract
* @return the new Contract
*/
public static <T> Contract<T> create(Class<T> type) {
return create(type, b -> {});
}
/**
* Create a contract derived from the given configuration
*
* @param config the name for the contract, null is not allowed
* @param <T> the type of deliverable for this Contract
* @return the new Contract
*/
public static <T> Contract<T> create(Config<T> config) {
return new Contract<>(config);
}
/**
* Create a contract from a class type and an optional builder callback
* @param type the type of deliverable for this Contract
* @param builderConsumer the builder callback
* @return the new Contract
* @param <T> the type of deliverable for this Contract
*/
public static <T> Contract<T> create(Class<T> type, Consumer<Config.Builder<T>> builderConsumer) {
final ContractBuilderImpl<T> builder = new ContractBuilderImpl<>(typeCheck(type));
builderConsumerCheck(builderConsumer).accept(builder);
return create(builder);
}
/**
* Casts the given object to the return type for this Contract
* This is used to make sure the value is a checked value and does not sneak passed during erasure
*
* @param value the value to cast
* @return the checked value. Note: null is possible. The Promisor is allowed to return null
* @throws ClassCastException iif the value can't be cast to the return type.
*/
public T cast(Object value) {
return config.cast(value);
}
/**
* @return the contract name
*/
public String getName() {
return config.name();
}
/**
* Note: Do not rely on this being a java class name
* Note: The actual class is never exposed and is by design.
*
* @return the type of deliverable for this contract.
*/
public String getTypeName() {
return config.typeName();
}
/**
* When replaceable a new binding can replace in an existing one
* The default is false
*
* @return true if replaceable
*/
public boolean isReplaceable() {
return config.isReplaceable();
}
@Override
public String toString() {
return "Contract[id=" + id + ", name=" + getName() + ", type=" + getTypeName() + "]";
}
/**
* The configuration for creating a custom Contract.
* The required function is {@link Config#cast(Object)} which is plays
* a key role in ensuring unchecked or unsafe instances do not escape
*
* @param <T> The Contract deliverable type
*/
@FunctionalInterface
public interface Config<T> {
/**
* Ensure an instance is of type T (or descendant)
*
* @param instance the value to cast to type T
* @return the value, null is allowed
* @throws ClassCastException when type of instance is not correct
*/
T cast(Object instance);
/**
* User defined name for this contract.
* Note: Do not rely on this being a java class name
*
* @return the type name
*/
default String name() {
return "";
}
/**
* The type of deliverable for this contract.
* Note: Do not rely on this being a java class name
*
* @return the type name, null is illegal
*/
default String typeName() {
return "";
}
/**
* When replaceable a new binding can replace in an existing one
* The default is false
*
* @return true if replaceable
*/
default boolean isReplaceable() {
return false;
}
/**
* Partial builder to streamline custom contracts
* @param <T> The Contract deliverable type
*/
interface Builder<T> extends Config<T> {
/**
* Set the Contract name
* @param name the new name
* @return this builder
*/
Builder<T> name(String name);
/**
* Set the Contract typeName
* @param typeName the new type name
* @return this builder
*/
Builder<T> typeName(String typeName);
/**
* Set if the Contract binding can be replaced
* @param replaceable true allows a Contract binding to be replaced
* @return this builder
*/
Builder<T> replaceable(boolean replaceable);
}
}
private static final AtomicInteger ID_GENERATOR = new AtomicInteger(1);
private final int id = ID_GENERATOR.getAndIncrement();
private final Config<T> config;
private Contract(Config<T> config) {
this.config = configCheck(config);
nameCheck(config.name());
nullCheck(config.typeName(), "Config type must be present.");
}
//Since arrays are reified the following is safe and checked
@SuppressWarnings("unchecked")
private static <T> Class<T> detectDeliverableType(T ... reifiedArray) {
final T[] validReifiedArray = nullCheck(reifiedArray, "Reified array must be present.");
return (Class<T>) validReifiedArray.getClass().getComponentType();
}
}