Skip to content

Commit

Permalink
Merge pull request #7 from izumin5210/undoable
Browse files Browse the repository at this point in the history
Implement @undoable
  • Loading branch information
Masayuki IZUMI committed Nov 23, 2015
2 parents 98d8cdd + f680557 commit 423bc29
Show file tree
Hide file tree
Showing 62 changed files with 1,436 additions and 50 deletions.
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

0 comments on commit 423bc29

Please sign in to comment.