Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement @Undoable #7

Merged
merged 14 commits into from
Nov 23, 2015
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package info.izumin.android.droidux.processor.element;

import android.databinding.Bindable;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.JavaFile;
Expand All @@ -11,7 +13,9 @@
import javax.lang.model.element.Modifier;

import info.izumin.android.droidux.Action;
import info.izumin.android.droidux.History;
import info.izumin.android.droidux.Store;
import info.izumin.android.droidux.action.HistoryAction;
import info.izumin.android.droidux.processor.model.DispatchableModel;
import info.izumin.android.droidux.processor.model.ReducerModel;
import info.izumin.android.droidux.processor.model.StoreModel;
Expand All @@ -26,6 +30,11 @@ public class StoreClassElement {
public static final String TAG = StoreClassElement.class.getSimpleName();

private static final String DISPATCH_TO_REDUCER_METHOD_NAME = "dispatchToReducer";
private static final String HISTORY_VARIABLE_NAME = "history";
private static final String HISTORY_SIZE_SETTER_METHOD_NAME = "setHistorySize";
private static final String STATE_GETTER_METHOD_NAME = "getState";
private static final String IS_UNDOABLE_METHOD_NAME = "isUndoable";
private static final String IS_REDOABLE_METHOD_NAME = "isRedoable";

private final ReducerModel reducerModel;
private final StoreModel storeModel;
Expand All @@ -41,27 +50,50 @@ public JavaFile createJavaFile() {
}

private TypeSpec createTypeSpec() {
return TypeSpec.classBuilder(storeModel.getClassName())
TypeSpec.Builder builder = TypeSpec.classBuilder(storeModel.getClassName())
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.superclass(ParameterizedTypeName.get(ClassName.get(Store.class), storeModel.getState()))
.addField(reducerModel.getReducer(), reducerModel.getVariableName(), Modifier.PRIVATE, Modifier.FINAL)
.addMethod(createConstructor())
.addMethod(createMethodSpec())
.addType(new StoreBuilderClassElement(storeModel).createBuilderTypeSpec())
.addField(reducerModel.getReducer(), reducerModel.getVariableName(), Modifier.PRIVATE, Modifier.FINAL);

if (storeModel.isUndoable()) {
ParameterizedTypeName historyFieldName = ParameterizedTypeName.get(ClassName.get(History.class), storeModel.getState());
builder = builder.addField(historyFieldName, HISTORY_VARIABLE_NAME, Modifier.PRIVATE, Modifier.FINAL);
}

builder = builder.addMethod(createConstructor())
.addMethod(createMethodSpec());

if (storeModel.isUndoable()) {
builder = builder
.addMethod(createUndoableStateGetterMethodSpec())
.addMethod(createIsUndoableMethodSpec())
.addMethod(createIsRedoableMethodSpec())
.addMethod(createHistorySizeSetterMethodSpec());
}

return builder.addType(new StoreBuilderClassElement(storeModel).createBuilderTypeSpec())
.build();
}

private MethodSpec createConstructor() {
return MethodSpec.constructorBuilder()
MethodSpec.Builder builder = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PROTECTED)
.addParameter(getParameterSpec(storeModel.getBuilder()))
.addStatement("super($N)", storeModel.getBuilderVariableName())
.addStatement("this.$N = $N.$N",
reducerModel.getVariableName(), storeModel.getBuilderVariableName(),
reducerModel.getVariableName())
.addStatement("setState($N.$N)",
storeModel.getBuilderVariableName(), storeModel.getStateVariableName())
.build();
reducerModel.getVariableName());

if (storeModel.isUndoable()) {
builder = builder.addStatement("this.$N = new $T<>($N.$N)",
HISTORY_VARIABLE_NAME, History.class,
storeModel.getBuilderVariableName(), storeModel.getStateVariableName());
} else {
builder = builder.addStatement("setState($N.$N)",
storeModel.getBuilderVariableName(), storeModel.getStateVariableName());
}

return builder.build();
}

private MethodSpec createMethodSpec() {
Expand All @@ -81,20 +113,71 @@ private CodeBlock createCodeBlock() {

for (DispatchableModel dispatchableModel : reducerModel.getDispatchableModels()) {
builder = builder.beginControlFlow("if (actionClass.isAssignableFrom($T.class))", dispatchableModel.getAction());

String stateGetter = storeModel.isUndoable() ? "getState().clone()" : "getState()";

if (dispatchableModel.argumentCount() == 2) {
builder = builder.addStatement("result = $N.$N(getState(), ($T) action)",
builder = builder.addStatement("result = $N.$N(" + stateGetter + ", ($T) action)",
reducerModel.getVariableName(), dispatchableModel.getMethodName(), dispatchableModel.getAction());
} else {
builder = builder.addStatement("result = $N.$N(getState())",
builder = builder.addStatement("result = $N.$N(" + stateGetter + ")",
reducerModel.getVariableName(), dispatchableModel.getMethodName());
}
if (storeModel.isUndoable()) {
builder = builder.addStatement("$N.insert(result)", HISTORY_VARIABLE_NAME);
}
builder = builder.endControlFlow();
}

if (storeModel.isUndoable()) {
builder = builder.beginControlFlow("if ($T.class.isAssignableFrom(actionClass))", HistoryAction.class)
.addStatement("$T historyAction = ($T) action", HistoryAction.class, HistoryAction.class)
.beginControlFlow("if (historyAction.isAssignableTo(this))")
.addStatement("result = historyAction.handle(history)")
.endControlFlow()
.endControlFlow();
}

return builder
.beginControlFlow("if (result != null)")
.addStatement("setState(result)")
.endControlFlow()
.build();
}

private MethodSpec createUndoableStateGetterMethodSpec() {
return MethodSpec.methodBuilder(STATE_GETTER_METHOD_NAME)
.addAnnotation(getOverrideAnnotation())
.addModifiers(Modifier.PUBLIC)
.returns(storeModel.getState())
.addStatement("return $N.getPresent()", HISTORY_VARIABLE_NAME)
.build();
}

private MethodSpec createIsUndoableMethodSpec() {
return MethodSpec.methodBuilder(IS_UNDOABLE_METHOD_NAME)
.addAnnotation(Bindable.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.BOOLEAN)
.addStatement("return $N.isUndoable()", HISTORY_VARIABLE_NAME)
.build();
}

private MethodSpec createIsRedoableMethodSpec() {
return MethodSpec.methodBuilder(IS_REDOABLE_METHOD_NAME)
.addAnnotation(Bindable.class)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.BOOLEAN)
.addStatement("return $N.isRedoable()", HISTORY_VARIABLE_NAME)
.build();
}

private MethodSpec createHistorySizeSetterMethodSpec() {
return MethodSpec.methodBuilder(HISTORY_SIZE_SETTER_METHOD_NAME)
.addModifiers(Modifier.PUBLIC)
.returns(TypeName.VOID)
.addParameter(TypeName.INT, "size")
.addStatement("$N.setLimit(size)", HISTORY_VARIABLE_NAME)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package info.izumin.android.droidux.processor.exception;

/**
* Created by izumin on 11/24/15.
*/
public class InvalidStateClassException extends RuntimeException {
public InvalidStateClassException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;

import info.izumin.android.droidux.UndoableState;
import info.izumin.android.droidux.annotation.Dispatchable;
import info.izumin.android.droidux.annotation.Reducer;
import info.izumin.android.droidux.annotation.Undoable;
import info.izumin.android.droidux.processor.exception.InvalidClassNameException;
import info.izumin.android.droidux.processor.exception.InvalidStateClassException;
import info.izumin.android.droidux.processor.util.StringUtils;

import static info.izumin.android.droidux.processor.util.AnnotationUtils.findMethodsByAnnotation;
Expand All @@ -35,6 +38,8 @@ public class ReducerModel {
private String stateName;
private String stateVariableName;

private final boolean isUndoable;

private StoreModel storeModel;
private List<DispatchableModel> dispatchableModels;

Expand All @@ -46,6 +51,18 @@ public ReducerModel(TypeElement element) {
this.stateVariableName = StringUtils.getLowerCamelFromUpperCamel(stateName);
}

isUndoable = element.getAnnotation(Undoable.class) != null;

if (isUndoable) {
try {
if (!UndoableState.class.isAssignableFrom(Class.forName(state.packageName() + "." + state.simpleName()))) {
throw new InvalidStateClassException("State class for undoable reducer must implement \"UndoableState<T>\".");
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

this.qualifiedName = element.getQualifiedName().toString();
this.packageName = StringUtils.getPackageName(qualifiedName);
this.className = StringUtils.getClassName(qualifiedName);
Expand Down Expand Up @@ -106,4 +123,8 @@ public StoreModel getStoreModel() {
public List<DispatchableModel> getDispatchableModels() {
return dispatchableModels;
}

public boolean isUndoable() {
return isUndoable;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public class StoreModel {
private final String builderName;
private final String builderVariableName;

private final boolean isUndoable;

private final ReducerModel reducerModel;

public StoreModel(ReducerModel reducerModel) {
Expand All @@ -45,6 +47,7 @@ public StoreModel(ReducerModel reducerModel) {
this.builderName = BUILDER_CLASS_NAME;
this.builder = store.nestedClass(builderName);
this.builderVariableName = getLowerCamelFromUpperCamel(builderName);
this.isUndoable = reducerModel.isUndoable();
}

public ClassName getState() {
Expand Down Expand Up @@ -94,4 +97,8 @@ public String getBuilderVariableName() {
public ReducerModel getReducerModel() {
return reducerModel;
}

public boolean isUndoable() {
return isUndoable;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ public void combinedTwoReducers() {
);
}

@Test
public void undoableReducer() {
assertJavaSource(
forSourceLines("UndoableTodoListReducer", Source.UndoableTodoList.TARGET),
forSourceLines("DroiduxUndoableTodoListStore", Source.UndoableTodoList.GENERATED)
);
}

@Test
public void dispatchableMethodTakesWrongStateType() {
expectedException.expect(RuntimeException.class);
Expand Down Expand Up @@ -111,4 +119,14 @@ public void reducerWithoutSuffix() {
forSourceLines("DroiduxTodoListReduce", Source.EMPTY)
);
}

@Test
public void undoableReducerWithoutUndoableState() {
expectedException.expect(RuntimeException.class);
expectedException.expectMessage("State class for undoable reducer must implement \"UndoableState<T>\".");
assertJavaSource(
forSourceLines("CounterReduce", Source.UndoableReducerWithoutUndoableState.TARGET),
forSourceLines("DroiduxTodoListReduce", Source.EMPTY)
);
}
}
Loading