From 4e036c04a8528dd13c890086e059bab46b6a2b7e Mon Sep 17 00:00:00 2001 From: Den Drobiazko Date: Mon, 5 Mar 2018 18:44:47 +0200 Subject: [PATCH 1/6] Add project code style --- .idea/codeStyleSettings.xml | 250 ++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 .idea/codeStyleSettings.xml diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000..4d9a639 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,250 @@ + + + + + + \ No newline at end of file From 2331a54031ca2e2501f9939f768f574d9149472e Mon Sep 17 00:00:00 2001 From: Den Drobiazko Date: Mon, 5 Mar 2018 18:46:37 +0200 Subject: [PATCH 2/6] Implement Attribute field type check Primitives, String, Boolean and boxed numbers are applicable --- .../validator/AttributesValidator.java | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/service-compiler/src/main/java/io/techery/analytics/compiler/validator/AttributesValidator.java b/service-compiler/src/main/java/io/techery/analytics/compiler/validator/AttributesValidator.java index 1f2707e..109f951 100644 --- a/service-compiler/src/main/java/io/techery/analytics/compiler/validator/AttributesValidator.java +++ b/service-compiler/src/main/java/io/techery/analytics/compiler/validator/AttributesValidator.java @@ -38,10 +38,17 @@ public Set validate(AnalyticActionClass value) { for (Element element : annotatedElements) { if (element.getKind() != ElementKind.FIELD) { - errors.add(new ValidationError(element, "Only fields can be annotated with %s! Please, see class %s", + errors.add(new ValidationError(element, "Only fields can be annotated with %s! Please see class %s", annotationName, value.typeName.toString())); } + if (!isBoolean(element) && !isNumber(element) + && !isString(element) && !isPrimitive(element)) { + errors.add(new ValidationError(element, + "Only primitives, Boolean, String and Number fields can have %s annotation! Please see class %s", + annotationName, value.typeName.toString())); + } + // TODO does not work correctly with kotlin classes - field is always private, need to check getters // if (element.getModifiers().contains(Modifier.PRIVATE)) { // errors.add(new ValidationError(element, "Attribute fields cannot have private access! Please, see class %s", @@ -51,4 +58,24 @@ public Set validate(AnalyticActionClass value) { return errors; } + + private boolean isPrimitive(Element element) { + return element.asType().getKind().isPrimitive(); + } + + private boolean isBoolean(Element element) { + return element.asType().toString().equals(Boolean.class.getName()); + } + + private boolean isString(Element element) { + return element.asType().toString().equals(String.class.getName()); + } + + private boolean isNumber(Element element) { + final String elementName = element.asType().toString(); + return elementName.equals(Integer.class.getName()) + || elementName.equals(Float.class.getName()) + || elementName.equals(Double.class.getName()) + || elementName.equals(Long.class.getName()); + } } From 4a8ffcc626f1492d45e3517e943c2b5e8fffcc64 Mon Sep 17 00:00:00 2001 From: Den Drobiazko Date: Mon, 5 Mar 2018 18:53:54 +0200 Subject: [PATCH 3/6] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 719e2c1..0d15981 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ The `@AnalyticsEvent` annotation flags a class as the one to be processed by `An Annotations for class fields: * `@KeyPath` – use this annotation if you want to format your `actionKey` at runtime - * `@Attribute` - the annotation `value` and field value will form a key-value pair in the `data` map that the tracker recieves + * `@Attribute` - the annotation `value` and field value will form a key-value pair in the `data` map that the tracker receives. Accepts primitives, String, Boolean and boxed numbers * `@AttributeMap` – sometimes, it might be easier to form a map than to create a [big] number of annotated fields. These map contents will be merged with attributes. For more info please see the `sample` project code. From e12ed81580d29ba6afbd8de23c6357ad923a848d Mon Sep 17 00:00:00 2001 From: Den Drobiazko Date: Tue, 6 Mar 2018 18:13:47 +0200 Subject: [PATCH 4/6] Add 'Short' type check, optimize check usage --- .../validator/AttributesValidator.java | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/service-compiler/src/main/java/io/techery/analytics/compiler/validator/AttributesValidator.java b/service-compiler/src/main/java/io/techery/analytics/compiler/validator/AttributesValidator.java index 109f951..ab9dcc5 100644 --- a/service-compiler/src/main/java/io/techery/analytics/compiler/validator/AttributesValidator.java +++ b/service-compiler/src/main/java/io/techery/analytics/compiler/validator/AttributesValidator.java @@ -42,8 +42,10 @@ public Set validate(AnalyticActionClass value) { annotationName, value.typeName.toString())); } - if (!isBoolean(element) && !isNumber(element) - && !isString(element) && !isPrimitive(element)) { + final String elementTypeName = element.asType().toString(); + + if (!isPrimitive(element) && !isString(elementTypeName) + && !isBoolean(elementTypeName) && !isNumber(elementTypeName)) { errors.add(new ValidationError(element, "Only primitives, Boolean, String and Number fields can have %s annotation! Please see class %s", annotationName, value.typeName.toString())); @@ -63,19 +65,21 @@ private boolean isPrimitive(Element element) { return element.asType().getKind().isPrimitive(); } - private boolean isBoolean(Element element) { - return element.asType().toString().equals(Boolean.class.getName()); + private boolean isBoolean(String elementTypeName) { + return elementTypeName.equals(Boolean.class.getName()) + || elementTypeName.equals("kotlin.Boolean"); } - private boolean isString(Element element) { - return element.asType().toString().equals(String.class.getName()); + private boolean isString(String elementTypeName) { + return elementTypeName.equals(String.class.getName()) + || elementTypeName.equals("kotlin.String"); } - private boolean isNumber(Element element) { - final String elementName = element.asType().toString(); - return elementName.equals(Integer.class.getName()) - || elementName.equals(Float.class.getName()) - || elementName.equals(Double.class.getName()) - || elementName.equals(Long.class.getName()); + private boolean isNumber(String elementTypeName) { + return elementTypeName.equals(Integer.class.getName()) || elementTypeName.equals("kotlin.Int") + || elementTypeName.equals(Float.class.getName()) || elementTypeName.equals("kotlin.Float") + || elementTypeName.equals(Double.class.getName()) || elementTypeName.equals("kotlin.Double") + || elementTypeName.equals(Long.class.getName()) || elementTypeName.equals("kotlin.Long") + || elementTypeName.equals(Short.class.getName()) || elementTypeName.equals("kotlin.Short"); } } From 9de23f898c7f050d525e04d28bb286827934aee6 Mon Sep 17 00:00:00 2001 From: Den Drobiazko Date: Tue, 6 Mar 2018 21:17:38 +0200 Subject: [PATCH 5/6] Fix bug with only one attribute processed --- .../java/io/techery/analytics/compiler/ActionClassUtils.java | 1 - 1 file changed, 1 deletion(-) diff --git a/service-compiler/src/main/java/io/techery/analytics/compiler/ActionClassUtils.java b/service-compiler/src/main/java/io/techery/analytics/compiler/ActionClassUtils.java index 90a25b7..07c63aa 100644 --- a/service-compiler/src/main/java/io/techery/analytics/compiler/ActionClassUtils.java +++ b/service-compiler/src/main/java/io/techery/analytics/compiler/ActionClassUtils.java @@ -85,7 +85,6 @@ public static Set getAttributes(Elements elementUtils, TypeElem element.getAnnotation(Attribute.class).value(), resolveAccessibleFieldName(elementUtils, typeElement, element.getSimpleName().toString()) )); - break; } } } From 10f3920536a0471b20e1f3e82243b4c4b24cf2e7 Mon Sep 17 00:00:00 2001 From: Den Drobiazko Date: Tue, 6 Mar 2018 23:10:37 +0200 Subject: [PATCH 6/6] Prettify testing --- .../analytics/sample/event/BuyPetEvent.java | 3 + .../analytics/sample/event/BuyPetEventKt.kt | 3 + .../sample/event/BuyPetEventKtTest.java | 34 ++++++++ .../sample/event/BuyPetEventTest.java | 31 ++----- .../sample/event/SuccessorEventTest.java | 11 ++- .../analytics/sample/utils/MapAssert.java | 31 +++++++ .../analytics/sample/utils/MapMatcher.java | 85 +++++++++++++++++++ 7 files changed, 169 insertions(+), 29 deletions(-) create mode 100644 sample/src/test/java/io/techery/analytics/sample/event/BuyPetEventKtTest.java create mode 100644 sample/src/test/java/io/techery/analytics/sample/utils/MapAssert.java create mode 100644 sample/src/test/java/io/techery/analytics/sample/utils/MapMatcher.java diff --git a/sample/src/main/java/io/techery/analytics/sample/event/BuyPetEvent.java b/sample/src/main/java/io/techery/analytics/sample/event/BuyPetEvent.java index c54aa6c..6885083 100644 --- a/sample/src/main/java/io/techery/analytics/sample/event/BuyPetEvent.java +++ b/sample/src/main/java/io/techery/analytics/sample/event/BuyPetEvent.java @@ -25,6 +25,9 @@ public class BuyPetEvent implements BaseAnalyticsAction { @Attribute("pet_birth_date") String petBirthDate; + @Attribute("pet_gender") + String petGender = "female"; + @AttributeMap Map data = new HashMap<>(); diff --git a/sample/src/main/java/io/techery/analytics/sample/event/BuyPetEventKt.kt b/sample/src/main/java/io/techery/analytics/sample/event/BuyPetEventKt.kt index 41efd12..d074209 100644 --- a/sample/src/main/java/io/techery/analytics/sample/event/BuyPetEventKt.kt +++ b/sample/src/main/java/io/techery/analytics/sample/event/BuyPetEventKt.kt @@ -24,6 +24,9 @@ class BuyPetEventKt(petEntity: PetEntity) : BaseAnalyticsAction { // kotlin clas @Attribute("pet_birth_date") val petBirthDate: String = DateFormat.getDateInstance().format(petEntity.birthDate.time) + @Attribute("pet_gender") + val gender: String = "female" + @AttributeMap val data: HashMap = HashMap() diff --git a/sample/src/test/java/io/techery/analytics/sample/event/BuyPetEventKtTest.java b/sample/src/test/java/io/techery/analytics/sample/event/BuyPetEventKtTest.java new file mode 100644 index 0000000..428b516 --- /dev/null +++ b/sample/src/test/java/io/techery/analytics/sample/event/BuyPetEventKtTest.java @@ -0,0 +1,34 @@ +package io.techery.analytics.sample.event; + +import org.junit.Test; + +import java.util.Calendar; + +import io.techery.analytics.sample.BaseTest; +import io.techery.analytics.sample.utils.MapMatcher; +import io.techery.analytics.sample_common.entity.PetEntity; +import io.techery.analytics.sample_common.entity.PetType; + +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +public class BuyPetEventKtTest extends BaseTest { + + @Test + public void eventKotlinSentWithCorrectData() { + Calendar petBirthDate = Calendar.getInstance(); + petBirthDate.set(2015, 4, 13); // formatted date will be "May 13, 2015" + PetEntity pet = new PetEntity(PetType.DOG, "Moohtar", petBirthDate); + BuyPetEventKt event = new BuyPetEventKt(pet); + + analyticsPipe.send(event); + verify(tracker).trackEvent(eq("user_bought_pet:dog:mall"), + argThat(MapMatcher.builder() + .with("pet_birth_date", "May 13, 2015") + .with("pet_name", "Moohtar") + .with("pet_gender", "female") + .build() + )); + } +} diff --git a/sample/src/test/java/io/techery/analytics/sample/event/BuyPetEventTest.java b/sample/src/test/java/io/techery/analytics/sample/event/BuyPetEventTest.java index c8d8f27..90dbf80 100644 --- a/sample/src/test/java/io/techery/analytics/sample/event/BuyPetEventTest.java +++ b/sample/src/test/java/io/techery/analytics/sample/event/BuyPetEventTest.java @@ -1,6 +1,8 @@ package io.techery.analytics.sample.event; import io.techery.analytics.sample.BaseTest; +import io.techery.analytics.sample.utils.MapAssert; +import io.techery.analytics.sample.utils.MapMatcher; import io.techery.analytics.sample_common.entity.PetEntity; import io.techery.analytics.sample_common.entity.PetType; import org.junit.Test; @@ -16,34 +18,17 @@ public class BuyPetEventTest extends BaseTest { @Test public void eventSentWithCorrectData() { Calendar petBirthDate = Calendar.getInstance(); - petBirthDate.set(2015, 4, 13); // formatted date will be "May 13, 2015 + petBirthDate.set(2015, 4, 13); // formatted date will be "May 13, 2015" PetEntity pet = new PetEntity(PetType.DOG, "Moohtar", petBirthDate); BuyPetEvent event = new BuyPetEvent(pet); analyticsPipe.send(event); verify(tracker).trackEvent(eq("user_bought_pet:dog:mall"), - argThat(argument -> - argument.containsKey("pet_birth_date") && - argument.get("pet_birth_date").equals("May 13, 2015") && - argument.containsKey("pet_name") && - argument.get("pet_name").equals("Moohtar") - )); - } - - @Test - public void eventKotlinSentWithCorrectData() { - Calendar petBirthDate = Calendar.getInstance(); - petBirthDate.set(2015, 4, 13); // formatted date will be "May 13, 2015 - PetEntity pet = new PetEntity(PetType.DOG, "Moohtar", petBirthDate); - BuyPetEventKt event = new BuyPetEventKt(pet); - - analyticsPipe.send(event); - verify(tracker).trackEvent(eq("user_bought_pet:dog:mall"), - argThat(argument -> - argument.containsKey("pet_birth_date") && - argument.get("pet_birth_date").equals("May 13, 2015") && - argument.containsKey("pet_name") && - argument.get("pet_name").equals("Moohtar") + argThat(MapMatcher.builder() + .with("pet_birth_date", "May 13, 2015") + .with("pet_name", "Moohtar") + .with("pet_gender", "female") + .build() )); } } diff --git a/sample/src/test/java/io/techery/analytics/sample/event/SuccessorEventTest.java b/sample/src/test/java/io/techery/analytics/sample/event/SuccessorEventTest.java index 4de92ae..354f1ff 100644 --- a/sample/src/test/java/io/techery/analytics/sample/event/SuccessorEventTest.java +++ b/sample/src/test/java/io/techery/analytics/sample/event/SuccessorEventTest.java @@ -3,6 +3,7 @@ import org.junit.Test; import io.techery.analytics.sample.BaseTest; +import io.techery.analytics.sample.utils.MapMatcher; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; @@ -18,11 +19,9 @@ public void successorEventSentWithCorrectData() { analyticsPipe.send(event); verify(tracker).trackEvent(eq("event_with_superclass:SuccessorEvent"), - argThat(argument -> - argument.containsKey("successor_attribute") && - argument.get("successor_attribute").equals("SUCCESSOR") && - argument.containsKey("attribute_from_parent") && - argument.get("attribute_from_parent").equals("PARENT") - )); + argThat(MapMatcher.builder() + .with("successor_attribute", successorAttribute) + .with("attribute_from_parent", parentAttribute) + .build())); } } diff --git a/sample/src/test/java/io/techery/analytics/sample/utils/MapAssert.java b/sample/src/test/java/io/techery/analytics/sample/utils/MapAssert.java new file mode 100644 index 0000000..ece9b13 --- /dev/null +++ b/sample/src/test/java/io/techery/analytics/sample/utils/MapAssert.java @@ -0,0 +1,31 @@ +package io.techery.analytics.sample.utils; + +public class MapAssert { + + public final String key; + public final Object value; + public final boolean negativeCheck; + + private MapAssert(String key, Object value, boolean negativeCheck) { + this.key = key; + this.value = value; + this.negativeCheck = negativeCheck; + } + + public static MapAssert hasNo(String key) { + return new MapAssert(key, null, true); + } + + public static MapAssert contains(String key, Object value) { + return new MapAssert(key, value, false); + } + + @Override + public String toString() { + if (negativeCheck) { + return ""; + } else { + return "\"" + key + "\" = \"" + value + "\""; + } + } +} diff --git a/sample/src/test/java/io/techery/analytics/sample/utils/MapMatcher.java b/sample/src/test/java/io/techery/analytics/sample/utils/MapMatcher.java new file mode 100644 index 0000000..7fe714e --- /dev/null +++ b/sample/src/test/java/io/techery/analytics/sample/utils/MapMatcher.java @@ -0,0 +1,85 @@ +package io.techery.analytics.sample.utils; + +import org.mockito.ArgumentMatcher; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class MapMatcher implements ArgumentMatcher> { + + private final List args = new ArrayList<>(); + + public MapMatcher(List args) { + this.args.addAll(args); + } + + public MapMatcher(MapAssert arg) { + this(Collections.singletonList(arg)); + } + + public MapMatcher(MapAssert... args) { + this(Arrays.asList(args)); + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean matches(Map argument) { + for (MapAssert item : args) { + boolean matches; + if (item.negativeCheck) { + matches = !argument.containsKey(item.key); + } else { + matches = argument.containsKey(item.key) && argument.get(item.key).equals(item.value); + } + if (!matches) return false; + } + + return true; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("{"); + Iterator iterator = args.iterator(); + + while (iterator.hasNext()) { + sb.append(iterator.next().toString()); + if (iterator.hasNext()) { + sb.append(", "); + } + } + + sb.append("}"); + + return sb.toString(); + } + + public static class Builder { + + private final List args = new ArrayList<>(); + + private Builder() { + } + + public Builder with(String key, Object value) { + args.add(MapAssert.contains(key, value)); + return this; + } + + public Builder without(String key) { + args.add(MapAssert.hasNo(key)); + return this; + } + + public MapMatcher build() { + return new MapMatcher(args); + } + } +}