Merge pull request #23 from TouchInstinct/feature/validation-comments

Feature/validation comments
This commit is contained in:
Gavriil 2017-02-06 19:26:00 +03:00 committed by GitHub
commit fbad7275ea
10 changed files with 716 additions and 0 deletions

View File

@ -0,0 +1,26 @@
package ru.touchin.templates.validation;
import android.support.annotation.NonNull;
import java.io.Serializable;
/**
* Created by Ilia Kurtov on 30/01/2017.
* Simple interface that gets one parameter with {@link TInput} type as input and returns other type {@link TReturn} as a result.
* Interface extends {@link Serializable} to survive after {@link ru.touchin.roboswag.components.navigation.AbstractState} recreation.
* Created as a replace for {@link rx.functions.Func1} because it needed to be {@link Serializable}
* @param <TInput> input type.
* @param <TReturn> return type.
*/
public interface ValidationFunc<TInput, TReturn> extends Serializable {
/**
* The method maps some {@link TInput} type argument into a {@link TReturn} type.
* @param input data;
* @return mapped data into a {@link TReturn} type.
* @throws Throwable for catching conversion errors.
*/
@NonNull
TReturn call(@NonNull final TInput input) throws Throwable;
}

View File

@ -0,0 +1,82 @@
/*
* Copyright (c) 2016 Touch Instinct
*
* This file is part of RoboSwag library.
*
* 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 ru.touchin.templates.validation;
import java.io.Serializable;
/**
* Created by Ilia Kurtov on 24/01/2017.
* Basic class for validation states. If you need to have more states with more data in it -
* create class that extends this class and don't forget to redefine {@link #equals(Object)} and {@link #hashCode()} methods.
* Don't use same {@link #code} for different states.
*/
public class ValidationState implements Serializable {
private static final long serialVersionUID = 1L;
/**
* Initial state of validation. It indicates that no validation rules applied yet.
*/
public static final ValidationState INITIAL = new ValidationState(-1);
/**
* Valid state.
*/
public static final ValidationState VALID = new ValidationState(-2);
/**
* Error shows when model (e.g. DateTime) is failing on conversion from raw data (e.g. from String) for validation.
*/
public static final ValidationState ERROR_CONVERSION = new ValidationState(-3);
/**
* Error shows when we don't need to show any description of error (e.g. just highlight input field with red color).
*/
public static final ValidationState ERROR_NO_DESCRIPTION = new ValidationState(-4);
private final int code;
public ValidationState(final int code) {
this.code = code;
}
/**
* Returns unique code of the {@link ValidationState}.
* @return code or the ValidationState.
*/
public int getCode() {
return code;
}
/**
* Don't forget to override this method!
* @param object that you want to compare.
* @return true if objects equals and false otherwise.
*/
@Override
public boolean equals(final Object object) {
return this == object
|| !(object == null || getClass() != object.getClass()) && code == ((ValidationState) object).code;
}
@Override
public int hashCode() {
return 31 * code;
}
}

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) 2016 Touch Instinct
*
* This file is part of RoboSwag library.
*
* 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 ru.touchin.templates.validation;
import android.support.annotation.NonNull;
/**
* Created by Ilia Kurtov on 24/01/2017.
* Interface for views that must be validated. Have two states - show error or hide error.
* You can provide your own Validation State to provide, eg string resource.
* In this case use instanceOf to define what state do you have.
*/
public interface ViewWithError {
/**
* Hides the error when validation passes successful.
*/
void hideError();
/**
* Shows error
* Pass here error state.
* It is not correct to pass here {@link ValidationState#VALID} or {@link ValidationState#INITIAL}
* @param validationState error state. Can be other than {@link ValidationState} if you have successor of base {@link ValidationState}.
*/
void showError(@NonNull final ValidationState validationState);
}

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2016 Touch Instinct
*
* This file is part of RoboSwag library.
*
* 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 ru.touchin.templates.validation.validationcontrollers;
import android.support.annotation.NonNull;
import ru.touchin.templates.validation.ValidationState;
import ru.touchin.templates.validation.validators.SameTypeValidator;
import rx.Observable;
/**
* Created by Ilia Kurtov on 24/01/2017.
* {@link ValidationController} for {@link Boolean} models. Eg if you have some flag that should be bounded to checkbox.
*/
public class BooleanValidationController extends ValidationController<Boolean, Boolean, SameTypeValidator<Boolean>> {
public BooleanValidationController(@NonNull final SameTypeValidator<Boolean> validator) {
super(validator);
}
/**
* This method validates bounded view.
* @param activatedObservable emits true when we need to show error on empty fields. Eg when user clicks on Done button but he missed some
* necessary fields to fill.
* @return observable without any concrete type. Simply subscribe to this method to make it works.
*/
@NonNull
public Observable<?> validation(@NonNull final Observable<Boolean> activatedObservable) {
return Observable.combineLatest(activatedObservable, getValidator().getWrapperModel().observe(),
(activated, flag) -> {
final boolean selected = flag == null ? false : flag;
if (activated && !selected) {
return ValidationState.ERROR_NO_DESCRIPTION;
} else if (!activated && !selected) {
return ValidationState.INITIAL;
}
return ValidationState.VALID;
})
.doOnNext(validationState -> getValidator().getValidationState().set(validationState));
}
}

View File

@ -0,0 +1,96 @@
/*
* Copyright (c) 2016 Touch Instinct
*
* This file is part of RoboSwag library.
*
* 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 ru.touchin.templates.validation.validationcontrollers;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import java.io.Serializable;
import ru.touchin.roboswag.core.utils.pairs.NonNullPair;
import ru.touchin.templates.validation.ValidationState;
import ru.touchin.templates.validation.validators.EditTextValidator;
import rx.Observable;
/**
* Created by Ilia Kurtov on 24/01/2017.
* ValidationController for {@link android.widget.EditText} views. It has method {@link #validation} that response
* for validating view. To use this class properly, you need to subscribe to the {@link #modelAndViewUpdating}
* and to the {@link #validation} methods.
*/
public class EditTextValidationController<TModel extends Serializable>
extends ValidationController<String, TModel, EditTextValidator<TModel>> {
public EditTextValidationController(@NonNull final EditTextValidator<TModel> validationWrapper) {
super(validationWrapper);
}
/**
* This method validates bounded view.
*
* @param focusOutObservable that emits items when bounded view get focus in or out.
* @param activatedObservable emits true when we need to show error on empty fields. Eg when user clicks on Done button but he missed some
* necessary fields to fill.
* @return observable without any concrete type. Simply subscribe to this method to make it works.
*/
@NonNull
public Observable<?> validation(@NonNull final Observable<Boolean> focusOutObservable, @NonNull final Observable<Boolean> activatedObservable) {
return Observable.combineLatest(activatedObservable,
getValidator().getWrapperModel().observe(),
focusOutObservable,
getValidator().getShowFullCheck().observe(),
this::getValidationPair)
.switchMap(validationPair -> {
if (validationPair == null) {
return Observable.empty();
}
return validationPair.getSecond()
.doOnNext(validationState -> {
if (!validationPair.getFirst()) {
getValidator().getShowFullCheck().set(validationState != ValidationState.VALID);
}
getValidator().getValidationState().set(validationState);
});
});
}
@Nullable
@SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"})
private NonNullPair<Boolean, Observable<ValidationState>> getValidationPair(final boolean activated,
@Nullable final String text,
@Nullable final Boolean focusIn,
final boolean showError) {
if (focusIn == null && TextUtils.isEmpty(text) && !activated && !showError) {
return null;
}
final boolean focus = focusIn == null ? false : focusIn;
if (TextUtils.isEmpty(text)) {
return new NonNullPair<>(focus, (activated || showError)
? getValidator().getValidationStateWhenEmpty().observe()
: Observable.just(ValidationState.INITIAL));
}
if (!showError && focus) {
return new NonNullPair<>(true, getValidator().primaryValidate(text));
}
return new NonNullPair<>(focus, getValidator().fullValidate(text));
}
}

View File

@ -0,0 +1,43 @@
package ru.touchin.templates.validation.validationcontrollers;
import android.support.annotation.NonNull;
import java.io.Serializable;
import ru.touchin.templates.validation.ValidationState;
import ru.touchin.templates.validation.validators.Validator;
import rx.Observable;
/**
* Created by Ilia Kurtov on 24/01/2017.
* {@link ValidationController} for models that have the same modal as wrapper model. You can use it when you simply need to be sure
* that user have selected some item and it is not null.
* @param <TModel> type of the model.
* @param <TValidator> corresponding {@link Validator}
*/
public class SimpleValidationController<TModel extends Serializable, TValidator extends Validator<TModel, TModel>>
extends ValidationController<TModel, TModel, TValidator> {
public SimpleValidationController(@NonNull final TValidator validator) {
super(validator);
}
/**
* This method validates bounded view.
* @param activatedObservable emits true when we need to show error on empty fields. Eg when user clicks on Done button but he missed some
* necessary fields to fill.
* @return observable without any concrete type. Simply subscribe to this method to make it works.
*/
@NonNull
public Observable<?> validation(@NonNull final Observable<Boolean> activatedObservable) {
return Observable.combineLatest(activatedObservable,
getValidator().getWrapperModel().observe(), (activated, model) -> {
if (model == null) {
return activated ? ValidationState.ERROR_NO_DESCRIPTION : ValidationState.INITIAL;
}
return ValidationState.VALID;
})
.doOnNext(validationState -> getValidator().getValidationState().set(validationState));
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright (c) 2016 Touch Instinct
*
* This file is part of RoboSwag library.
*
* 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 ru.touchin.templates.validation.validationcontrollers;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.Serializable;
import ru.touchin.templates.validation.ValidationState;
import ru.touchin.templates.validation.ViewWithError;
import ru.touchin.templates.validation.validators.Validator;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action1;
/**
* Created by Ilia Kurtov on 24/01/2017.
* This class holds information about related {@link Validator} class and how to connect model with view.
*/
public class ValidationController
<TWrapperModel extends Serializable, TModel extends Serializable, TValidator extends Validator<TWrapperModel, TModel>> {
@NonNull
private final TValidator validator;
@NonNull
public TValidator getValidator() {
return validator;
}
public ValidationController(@NonNull final TValidator validator) {
this.validator = validator;
}
/**
* Bind to this observable to connect view and model. If you provide first argument (viewStateObservable) - the connection would be two-way.
* If not - one-way. This method changes updates view with current {@link ValidationState}.
* @param viewStateObservable input view state {@link Observable}.
* Eg it can be observable with input text from the {@link android.widget.EditText}
* @param updateViewAction action that updates current state of the bounded view.
* @param viewWithError view that implements {@link ViewWithError} interface and could reacts to the validation errors.
* @return observable without any concrete type. Simply subscribe to this method to make it works.
*/
@NonNull
public Observable<?> modelAndViewUpdating(@Nullable final Observable<TWrapperModel> viewStateObservable,
@NonNull final Action1<TWrapperModel> updateViewAction,
@NonNull final ViewWithError viewWithError) {
final Observable<?> stateObservable = viewStateObservable != null
? viewStateObservable.doOnNext(flag -> getValidator().getWrapperModel().set(flag))
: Observable.empty();
return Observable
.merge(getValidator().getWrapperModel().observe()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(updateViewAction),
getValidator().getValidationState().observe()
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(validationState -> {
if (!showError(validationState)) {
viewWithError.hideError();
} else {
viewWithError.showError(validationState);
}
}),
stateObservable);
}
/**
* Helper function to check if validation state in error state ot not
* @param validationState the state you want to check for the errors.
* @return true if validation state is in error and false otherwise.
*/
protected boolean showError(@NonNull final ValidationState validationState) {
return !validationState.equals(ValidationState.VALID) && !validationState.equals(ValidationState.INITIAL);
}
}

View File

@ -0,0 +1,153 @@
/*
* Copyright (c) 2016 Touch Instinct
*
* This file is part of RoboSwag library.
*
* 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 ru.touchin.templates.validation.validators;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.Serializable;
import ru.touchin.roboswag.core.observables.Changeable;
import ru.touchin.roboswag.core.observables.NonNullChangeable;
import ru.touchin.roboswag.core.utils.pairs.HalfNullablePair;
import ru.touchin.templates.validation.ValidationFunc;
import ru.touchin.templates.validation.ValidationState;
import rx.Observable;
import rx.schedulers.Schedulers;
/**
* Created by Ilia Kurtov on 24/01/2017.
* Special class for {@link android.widget.EditText} validation. It holds information about how primary check on typing should be
* ({@link #getPrimaryCheck()} - get and set check here) and how final check should be processed too ({@link #getFinalCheck()} - get and set check
* here).
*/
public abstract class EditTextValidator<TModel extends Serializable> extends Validator<String, TModel> {
@NonNull
private final NonNullChangeable<Boolean> showFullCheck = new NonNullChangeable<>(false);
@NonNull
private final Changeable<ValidationFunc<TModel, HalfNullablePair<ValidationState, TModel>>> finalCheck = new Changeable<>(null);
@NonNull
private final Changeable<ValidationFunc<String, HalfNullablePair<ValidationState, TModel>>> primaryCheck = new Changeable<>(null);
/**
* This flag needed to force showing errors. You don't want to show final error when you start to enter data in some field at first time.
* But if user leaves this view and final check not passed - you need to force to show an error till user not enters correct data and leaves
* the view.
* @return {@link NonNullChangeable} with current state of the flag - do we need to show errors from final checks while user types.
*/
@NonNull
public NonNullChangeable<Boolean> getShowFullCheck() {
return showFullCheck;
}
/**
* Use this method to get or set final check.
* @return final check.
*/
@NonNull
protected Changeable<ValidationFunc<TModel, HalfNullablePair<ValidationState, TModel>>> getFinalCheck() {
return finalCheck;
}
/**
* Use this method to get or set primary check.
* @return primary check.
*/
@NonNull
protected Changeable<ValidationFunc<String, HalfNullablePair<ValidationState, TModel>>> getPrimaryCheck() {
return primaryCheck;
}
@NonNull
private HalfNullablePair<ValidationState, TModel> validateText(
@Nullable final ValidationFunc<TModel, HalfNullablePair<ValidationState, TModel>> finalCheck,
@Nullable final ValidationFunc<String, HalfNullablePair<ValidationState, TModel>> primaryCheck,
@NonNull final String text, final boolean fullCheck)
throws Throwable {
if (primaryCheck == null && finalCheck == null) {
return new HalfNullablePair<>(ValidationState.VALID, convertWrapperModelToModel(text));
}
if (primaryCheck != null) {
final HalfNullablePair<ValidationState, TModel> primaryPair = primaryCheck.call(text);
if (finalCheck == null || primaryPair.getFirst() != ValidationState.VALID || primaryPair.getSecond() == null || !fullCheck) {
return primaryPair;
}
return finalCheck.call(primaryPair.getSecond());
}
return finalCheck.call(convertWrapperModelToModel(text));
}
@NonNull
@SuppressWarnings("PMD.AvoidCatchingThrowable")
// It's intended
private Observable<HalfNullablePair<ValidationState, TModel>> createValidationObservable(@NonNull final String text, final boolean fullCheck) {
return Observable
.combineLatest(finalCheck.observe().observeOn(Schedulers.computation()),
primaryCheck.observe().observeOn(Schedulers.computation()),
(finalCheck, primaryCheck) -> {
try {
return validateText(finalCheck, primaryCheck, text, fullCheck);
} catch (final Throwable exception) {
return new HalfNullablePair<>(ValidationState.ERROR_CONVERSION, null);
}
});
}
@NonNull
private Observable<ValidationState> processChecks(@NonNull final String text, final boolean fullCheck) {
return createValidationObservable(text, fullCheck)
.map(HalfNullablePair::getFirst);
}
/**
* Validates text with primary check.
* @param text - input text.
* @return {@link Observable} with the result of the primary check.
*/
@NonNull
public Observable<ValidationState> primaryValidate(@NonNull final String text) {
return processChecks(text, false);
}
/**
* Validates text with final check.
* @param text - input text.
* @return {@link Observable} with the result of the final check.
*/
@NonNull
public Observable<ValidationState> fullValidate(@NonNull final String text) {
return processChecks(text, true);
}
/**
* Validates text with primary and final check consequentially and returns {@link Observable} with {@link HalfNullablePair} of final state
* and resulting model.
* @param text - input text.
* @return pair with final {@link ValidationState} that is always not null and a model that we get after converting the text.
* Model can be null if validation fails on primary or final checks.
*/
@NonNull
public Observable<HalfNullablePair<ValidationState, TModel>> fullValidateAndGetModel(@NonNull final String text) {
return createValidationObservable(text, true)
.first();
}
}

View File

@ -0,0 +1,28 @@
package ru.touchin.templates.validation.validators;
import android.support.annotation.NonNull;
import java.io.Serializable;
/**
* Created by Ilia Kurtov on 24/01/2017.
* Class that simplifies work with {@link Validator}'s that have the same wrapper model and model type.
* @param <TModel> model that should be bounded with a view.
*/
public class SameTypeValidator<TModel extends Serializable> extends Validator<TModel, TModel> {
/**
* Simply returns the same model without any converting.
* @param wrapperModel input model.
* @return the same model as input parameter.
* @throws Throwable - in this case no throwable would be thrown.
*/
@NonNull
@Override
protected TModel convertWrapperModelToModel(@NonNull final TModel wrapperModel)
throws Throwable {
return wrapperModel;
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright (c) 2016 Touch Instinct
*
* This file is part of RoboSwag library.
*
* 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 ru.touchin.templates.validation.validators;
import android.support.annotation.NonNull;
import java.io.Serializable;
import ru.touchin.roboswag.core.observables.Changeable;
import ru.touchin.roboswag.core.observables.NonNullChangeable;
import ru.touchin.templates.validation.ValidationState;
/**
* Created by Ilia Kurtov on 24/01/2017.
* This class holds information about current state of the object - {@link #getWrapperModel()}(that should be connected to some view),
* current error state {@link #getValidationState()}. Also you need to provide a {@link ValidationState} or class that extends it
* as an empty state. Eg, if you need to show some text in your view to show user that this view shouldn't be empty - pass needed state
* to the {@link #getValidationStateWhenEmpty()}
* {@link TWrapperModel} is type of class that should be connected to its bounded view. {@link TModel} is type of class
* that represent object that we need at the end. Eg, if we want to enter some digits to {@link android.widget.EditText}
* and get {@link java.util.Date} as a result - {@link CharSequence} or {@link String} should be the {@link TWrapperModel}
* and {@link java.util.Date} would be the {@link TModel} type.
*/
public abstract class Validator<TWrapperModel extends Serializable, TModel extends Serializable>
implements Serializable {
private static final long serialVersionUID = 1L;
@NonNull
private final NonNullChangeable<ValidationState> validationState = new NonNullChangeable<>(ValidationState.INITIAL);
@NonNull
private final Changeable<TWrapperModel> wrapperModel = new Changeable<>(null);
@NonNull
private final NonNullChangeable<ValidationState> validationStateWhenEmpty = new NonNullChangeable<>(ValidationState.ERROR_NO_DESCRIPTION);
/**
* This method converts {@link TWrapperModel} into a {@link TModel}.
* @param wrapperModel - not null value that should be converted into a {@link TModel} object.
* @return converted wrapperModel into a {@link TModel}.
* @throws Throwable for the cases when converting cannot be processed.
*/
@NonNull
protected abstract TModel convertWrapperModelToModel(@NonNull final TWrapperModel wrapperModel) throws Throwable;
/**
* Call this method to get {@link Changeable} with {@link TWrapperModel} inside it that should be connected to its bounded view.
* @return {@link Changeable} with {@link TWrapperModel}.
*/
@NonNull
public Changeable<TWrapperModel> getWrapperModel() {
return wrapperModel;
}
/**
* Returns current {@link ValidationState} or its successor. Needed to connect with bounded view and react to this state changes.
* @return current validation state.
*/
@NonNull
public NonNullChangeable<ValidationState> getValidationState() {
return validationState;
}
/**
* This method needed to get {@link ValidationState} that needed to be shown when bounded view is empty and you need to show to user reminder,
* that he or she needs to fill this view.
* @return {@link ValidationState} that should be shown for an empty field.
*/
@NonNull
public NonNullChangeable<ValidationState> getValidationStateWhenEmpty() {
return validationStateWhenEmpty;
}
}