Skip to content

Commit

Permalink
Replaced thread context with Validators.getContext(), putContext(), r…
Browse files Browse the repository at this point in the history
…emoveContext().
  • Loading branch information
cowwoc committed Mar 14, 2024
1 parent 2c7b2ba commit 3b38f9c
Show file tree
Hide file tree
Showing 65 changed files with 1,038 additions and 2,287 deletions.
48 changes: 20 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,24 @@ To get started, add this Maven dependency:
Designed for discovery using your favorite IDE's auto-complete feature.
The main entry points are:

* [requireThat(value, name)](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/com.github.cowwoc.requirements/com/github/cowwoc/requirements/DefaultJavaValidators.html#requireThat(T,java.lang.String)) for preconditions
* [assumeThat(value, name)](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/com.github.cowwoc.requirements/com/github/cowwoc/requirements/DefaultJavaValidators.html#assumeThat(T,java.lang.String)) for postconditions and class invariants
* [checkIfThat(value, name)](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/com.github.cowwoc.requirements/com/github/cowwoc/requirements/DefaultJavaValidators.html#checkIf(T,java.lang.String)) for multiple failures and everything else
* [JavaValidators](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/com.github.cowwoc.requirements/com/github/cowwoc/requirements/JavaValidators.html) for custom configurations
* [requireThat(value, name)](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/com.github.cowwoc.requirements/com/github/cowwoc/requirements/java/DefaultJavaValidators.html#requireThat(T,java.lang.String)) for method preconditions.
* [assumeThat(value, name)](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/com.github.cowwoc.requirements/com/github/cowwoc/requirements/java/DefaultJavaValidators.html#assumeThat(T,java.lang.String)) for class invariants, method postconditions and private methods.
* [checkIfThat(value, name)](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/com.github.cowwoc.requirements/com/github/cowwoc/requirements/java/DefaultJavaValidators.html#checkIf(T,java.lang.String)) for multiple failures and customized error handling.
* [JavaValidators](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/com.github.cowwoc.requirements/com/github/cowwoc/requirements/java/JavaValidators.html) for custom configurations.

The first three methods use a shared configuration, while `JavaValidators` allows you to create an independent
configuration.

`requireThat()` and `assumeThat()` throw an exception on the first validation failure,
while `checkIf()` collects multiple validation failures before throwing an exception at the end.
`checkIf()` is more flexible than the others, but its syntax is more verbose.

Exceptions that are thrown in response to invalid method arguments (e.g. `isGreaterThan(null, value)`) are
thrown by all validators and cannot be configured. Exceptions that are thrown in response to the value
failing a validation check, e.g. `isGreaterThan(5)` on a value of 0, are thrown by `requireThat()` and
`assumeThat()` but are recorded by `checkIf()` without being thrown. The type of thrown exceptions is
configurable using [ConfigurationUpdater#exceptionTransformer(Function)](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/com.github.cowwoc.requirements/com/github/cowwoc/requirements/java/ConfigurationUpdater.html#exceptionTransformer(java.util.function.Function)).

See the [API documentation](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/) for more details.

## Usage Example
Expand Down Expand Up @@ -105,31 +115,13 @@ This library offers the following features:
* [String diff](docs/Features.md#string-diff) that shows the differences between two strings
* [Performant and robust](docs/Performance.md)

## Getting Started

The best way to learn about the API is using your IDE's auto-complete engine.
The main entry points you should be aware of are:

* [requireThat(value, name)](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/com.github.cowwoc.requirements/com/github/cowwoc/requirements/DefaultJavaValidators.html#requireThat(T,java.lang.String))
* [assumeThat(value, name)](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/com.github.cowwoc.requirements/com/github/cowwoc/requirements/DefaultJavaValidators.html#assumeThat(T,java.lang.String))
* [checkIfThat(value, name)](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/com.github.cowwoc.requirements/com/github/cowwoc/requirements/DefaultJavaValidators.html#checkIf(T,java.lang.String))

The three static methods share the same configuration.
To create an independent configuration, use [JavaValidators](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/com.github.cowwoc.requirements/com/github/cowwoc/requirements/JavaValidators.html).

See the [API documentation](https://cowwoc.github.io/requirements.java/9.0.0/docs/api/) for more details.
## Tips

## Best practices

* Use `requireThat()` to verify the preconditions of public APIs.
* Use `assert assumeThat()` to verify class invariants, method post-conditions, and the preconditions of
private methods.
The JVM will remove these checks if assertions are disabled.
* Use `checkIf()` to return multiple failures at once.
* Use `checkIf().elseGetMessages()` to return failures without throwing an exception.
This is the best-performing approach, ideal for web services.
* To enhance the clarity of failure messages, you should provide parameter names, even though they are
optional.
* Use `assert` with `assumeThat().elseThrow()` for sanity checks. When assertions are disabled, they will get
disabled.
* Use `checkIf().elseGetMessages()` to return failure messages without throwing an exception.
This is the fastest validation approach, ideal for web services.
* To enhance the clarity of failure messages, you should provide parameter names, even when they are optional.

## Third-party libraries and tools

Expand Down
1 change: 1 addition & 0 deletions docs/Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ See https://github.com/cowwoc/requirements.java/commits/master for a full list.
14. Dropped the `isOneOf()` and `isNotOneOf()` functionality yet again. I haven't figured out a good
design for this yet.
15. Added `ObjectValidator.isX()` methods to downcast to known types.
16. Replaced thread context with `Validators.getContext()`, `putContext()`, `removeContext()`.
* Bugfixes:
* `StringValidator/Verifier.asShort()`, `asInteger()` and `asLong()` were not handling the case where a
string could not be converted to a number.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
import com.github.cowwoc.requirements.java.Configuration;
import com.github.cowwoc.requirements.java.ConfigurationUpdater;
import com.github.cowwoc.requirements.java.GlobalConfiguration;
import com.github.cowwoc.requirements.java.ScopedContext;
import com.github.cowwoc.requirements.java.internal.scope.MainApplicationScope;
import com.github.cowwoc.requirements.java.internal.util.CloseableLock;
import com.github.cowwoc.requirements.java.internal.util.ReentrantStampedLock;
import com.github.cowwoc.requirements.java.type.part.Validator;
import com.google.common.collect.Multimap;

import java.util.Map;
import java.util.function.Function;

/**
Expand All @@ -36,13 +38,16 @@
* of 0, are thrown by {@code requireThat()} and {@code assumeThat()} but are recorded by {@code checkIf()}
* without being thrown. The type of thrown exceptions is configurable using
* {@link ConfigurationUpdater#exceptionTransformer(Function)}.
* <p>
* <b>Thread Safety</b>: This class is thread-safe.
*
* @see GuavaValidators#newInstance() Creating a new instance with an independent configuration
*/
public final class DefaultGuavaValidators
{
private static final GuavaValidatorsImpl delegate = new GuavaValidatorsImpl(MainApplicationScope.INSTANCE,
Configuration.DEFAULT);
private static final ReentrantStampedLock contextLock = new ReentrantStampedLock();

/**
* Validates the state of a {@code Multimap}. Any exceptions thrown due to validation failure are
Expand All @@ -54,7 +59,7 @@ public final class DefaultGuavaValidators
* @param value the value
* @param name the name of the value
* @return a validator for the value
* @throws NullPointerException if {@code name} is null
* @throws NullPointerException if any of the mandatory arguments are null
* @throws IllegalArgumentException if {@code name} contains leading or trailing whitespace, or is empty
*/
public static <K, V, T extends Multimap<K, V>> MultimapValidator<K, V, T> assumeThat(T value, String name)
Expand Down Expand Up @@ -86,7 +91,7 @@ public static <K, V, T extends Multimap<K, V>> MultimapValidator<K, V, T> assume
* @param value the value
* @param name the name of the value
* @return a validator for the value
* @throws NullPointerException if {@code name} is null
* @throws NullPointerException if any of the mandatory arguments are null
* @throws IllegalArgumentException if {@code name} contains leading or trailing whitespace, or is empty
*/
public static <K, V, T extends Multimap<K, V>> MultimapValidator<K, V, T> checkIf(T value, String name)
Expand Down Expand Up @@ -117,7 +122,7 @@ public static <K, V, T extends Multimap<K, V>> MultimapValidator<K, V, T> checkI
* @param value the value
* @param name the name of the value
* @return a validator for the value
* @throws NullPointerException if {@code name} is null
* @throws NullPointerException if any of the mandatory arguments are null
* @throws IllegalArgumentException if {@code name} contains leading or trailing whitespace, or is empty
*/
public static <K, V, T extends Multimap<K, V>> MultimapValidator<K, V, T> requireThat(T value, String name)
Expand Down Expand Up @@ -150,26 +155,56 @@ public static ConfigurationUpdater updateConfiguration()
}

/**
* Returns the contextual information for validations performed by this thread using any validator. The
* contextual information is a map of key-value pairs that can provide more details about validation
* failures. For example, if the message is "Password may not be empty" and the map contains the key-value
* pair {@code {"username": "john.smith"}}, the exception message would be:
* Returns the contextual information for validators created out by this factory. The contextual information
* is a map of key-value pairs that can provide more details about validation failures. For example, if the
* message is "Password may not be empty" and the map contains the key-value pair
* {@code {"username": "john.smith"}}, the exception message would be:
* <p>
* {@snippet lang = output:
* Password may not be empty
* username: john.smith}
*
* @return an unmodifiable map from each entry's name to its value
*/
public static Map<String, Object> getContext()
{
return contextLock.optimisticRead(delegate::getContext);
}

/**
* Sets the contextual information for validators created by this factory.
* <p>
* Values set by this method may be overridden by {@link Validator#putContext(Object, String)}}.
* <p>
* <b>NOTE</b>: This method affects existing and new validators used by current thread. Changes are
* reversed once {@link ScopedContext#close()} is invoked.
* This method adds contextual information to exception messages. The contextual information is stored as
* key-value pairs in a map. Values set by this method may be overridden by
* {@link Validator#putContext(Object, String)}}.
*
* @return the thread context updater
* @param value the value of the entry
* @param name the name of an entry
* @return the underlying validator factory
* @throws NullPointerException if {@code name} is null
*/
@CheckReturnValue
public static ScopedContext threadContext()
public static GuavaValidators putContext(Object value, String name)
{
try (CloseableLock unused = contextLock.write())
{
return delegate.putContext(value, name);
}
}

/**
* Removes the contextual information of validators created by this factory.
*
* @param name the parameter name
* @return the underlying validator factory
* @throws NullPointerException if {@code name} is null
* @throws IllegalArgumentException if {@code name} contains leading or trailing whitespace, or is empty
*/
public static GuavaValidators removeContext(String name)
{
return delegate.threadContext();
try (CloseableLock unused = contextLock.write())
{
return delegate.removeContext(name);
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/
package com.github.cowwoc.requirements.guava;

import com.github.cowwoc.requirements.annotation.CheckReturnValue;
import com.github.cowwoc.requirements.guava.internal.implementation.GuavaValidatorsImpl;
import com.github.cowwoc.requirements.java.Configuration;
import com.github.cowwoc.requirements.java.ConfigurationUpdater;
Expand All @@ -27,13 +28,14 @@
* {@code checkIf()} is more flexible than the others, but its syntax is more verbose.
* <p>
* Exceptions that are thrown in response to invalid method arguments (e.g.
* {@code isGreaterThan(null, value)} are thrown by all validators and cannot be configured. Exceptions that
* {@code isGreaterThan(null, value)}) are thrown by all validators and cannot be configured. Exceptions that
* are thrown in response to the value failing a validation check, e.g. {@code isGreaterThan(5)} on a value
* of 0, are thrown by {@code requireThat()} and {@code assumeThat()} but are recorded by {@code checkIf()}
* without being thrown. The type of thrown exceptions is configurable using
* {@link ConfigurationUpdater#exceptionTransformer(Function)}.
*/
public interface GuavaValidators extends Validators, GuavaRequireThat, GuavaAssumeThat, GuavaCheckIf
public interface GuavaValidators
extends Validators<GuavaValidators>, GuavaRequireThat, GuavaAssumeThat, GuavaCheckIf
{
/**
* Creates a new instance using the default configuration.
Expand All @@ -57,4 +59,20 @@ static GuavaValidators newInstance(Configuration configuration)
{
return new GuavaValidatorsImpl(MainApplicationScope.INSTANCE, configuration);
}

/**
* Returns a new factory instance with an independent configuration. This method is commonly used to inherit
* and update contextual information from the original factory before passing it into a nested operation.
* For example:
* <p>
* {@snippet :
* GuavaValidators copy = validators.copy();
* copy.context().put(json.toString(), "json");
* nestedOperation(copy);
*}
*
* @return a copy of this factory
*/
@CheckReturnValue
GuavaValidators copy();
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@
import com.github.cowwoc.requirements.guava.GuavaValidators;
import com.github.cowwoc.requirements.guava.MultimapValidator;
import com.github.cowwoc.requirements.java.Configuration;
import com.github.cowwoc.requirements.java.ValidationFailure;
import com.github.cowwoc.requirements.java.internal.implementation.AbstractValidator;
import com.github.cowwoc.requirements.java.internal.implementation.AbstractValidators;
import com.github.cowwoc.requirements.java.internal.scope.ApplicationScope;
import com.google.common.collect.Multimap;

public class GuavaValidatorsImpl extends AbstractValidators<GuavaValidatorsImpl>
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class GuavaValidatorsImpl extends AbstractValidators<GuavaValidators>
implements GuavaValidators
{
/**
Expand All @@ -23,10 +29,22 @@ public GuavaValidatorsImpl(ApplicationScope scope, Configuration configuration)
super(scope, configuration);
}

/**
* Creates a copy of an existing validator factory.
*
* @param other the factory to copy
* @throws NullPointerException if {@code other} is null
*/
public GuavaValidatorsImpl(GuavaValidatorsImpl other)
{
this(other.scope, other.configuration());
this.context.putAll(other.context);
}

@Override
public <K, V, T extends Multimap<K, V>> MultimapValidator<K, V, T> requireThat(T value, String name)
{
return newInstance(value, name, getRequireThatConfiguration());
return newInstance(value, name, configuration());
}

@Override
Expand Down Expand Up @@ -56,6 +74,39 @@ public <K, V, T extends Multimap<K, V>> MultimapValidator<K, V, T> checkIf(T val
private <K, V, T extends Multimap<K, V>> MultimapValidator<K, V, T> newInstance(T value, String name,
Configuration configuration)
{
return new MultimapValidatorImpl<>(scope, configuration, name, value);
return new MultimapValidatorImpl<>(scope, configuration, name, value, newValidatorContext(),
newValidatorFailures());
}

private Map<String, Object> newValidatorContext()
{
HashMap<String, Object> context = HashMap.newHashMap(this.context.size() + 2);
context.putAll(this.context);
return context;
}

private List<ValidationFailure> newValidatorFailures()
{
return new ArrayList<>(2);
}

@Override
public GuavaValidatorsImpl copy()
{
return new GuavaValidatorsImpl(this);
}

@Override
public GuavaValidatorsImpl putContext(Object value, String name)
{
context.put(name, value);
return this;
}

@Override
public GuavaValidatorsImpl removeContext(String name)
{
context.remove(name);
return this;
}
}
Loading

0 comments on commit 3b38f9c

Please sign in to comment.