Skip to content

Commit

Permalink
refactor: better names and java vs JSON capturing
Browse files Browse the repository at this point in the history
  • Loading branch information
jbee committed Aug 6, 2024
1 parent b6e54f3 commit 2e4fd41
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.hisp.dhis.jsontree.internal.Surly;

import java.lang.reflect.Method;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;

import static org.hisp.dhis.jsontree.Validation.NodeType.ARRAY;
Expand Down Expand Up @@ -180,7 +181,7 @@ public final <V extends JsonValue> V as( Class<V> as ) {
}

@Override
public <V extends JsonValue> V as( Class<V> as, BiPredicate<Method, Object[]> onCall ) {
public <V extends JsonValue> V as( Class<V> as, BiConsumer<Method, Object[]> onCall ) {
return viewed.as( as, onCall );
}

Expand Down
20 changes: 12 additions & 8 deletions src/main/java/org/hisp/dhis/jsontree/JsonObject.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,22 @@ public interface JsonObject extends JsonAbstractObject<JsonValue> {
/**
* An object property based on a default method declared in a type extending {@link JsonObject}.
*
* @param in the {@link JsonObject} type that declared the property
* @param name of the property
* @param type the type the property is resolved to internally when calling {@link #get(String, Class)}
* @param source the underlying method that declared the property
* @param sourceType the return type of the underlying method that declares the property
*
* @param in the {@link JsonObject} type that declared the property
* @param jsonName of the property
* @param jsonType the type the property is resolved to internally when calling {@link #get(String, Class)}
* @param javaName the name of the java property accessed that caused the JSON property to be resolved
* @param javaType the return type of the underlying method that declares the property
* @param source the underlying method that declared the property
* @since 1.4
*/
record Property(Class<? extends JsonObject> in, String name, Class<? extends JsonValue> type,
AnnotatedElement source, AnnotatedType sourceType) {}
record Property(Class<? extends JsonObject> in, String jsonName, Class<? extends JsonValue> jsonType,
String javaName, AnnotatedType javaType, AnnotatedElement source) {}

/**
* Note that there can be more than one property with the same {@link Property#javaName()} in case the method it
* reflects accesses more than one member from the JSON object. In such a case each access is a property of the
* accessed {@link Property#jsonName()} with the same {@link Property#javaName()}.
*
* @return a model of this object in form its properties in no particular order
* @since 1.4
*/
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/org/hisp/dhis/jsontree/JsonValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;
Expand Down Expand Up @@ -247,15 +248,17 @@ default boolean isBoolean() {
/**
* Same as {@link #as(Class)} but with an additional parameter to pass a callback function. This allows to observe
* the API calls for meta-programming. This should not be used in "normal" API usage.
* <p>
* Not all methods can be observed as some are handled internally without ever going via the proxy. However, in
* contrast to {@link #as(Class)} when using this method any call of a default method is handled via proxy.
*
* @param as assumed value type for this value
* @param onCall a function that is called before the proxy handles an API call that allows to observe and bypass
* calls (predicate returns false) in which case the result is always {@code null}
* @param onCall a function that is called before the proxy handles an API call that allows to observe calls
* @param <T> value type returned
* @return this object as the provided type, this might mean this object is wrapped as the provided type or
* @since 1.4
*/
<T extends JsonValue> T as( Class<T> as, BiPredicate<Method, Object[]> onCall );
<T extends JsonValue> T as( Class<T> as, BiConsumer<Method, Object[]> onCall );

/**
* @return This value as {@link JsonObject} (same as {@code as(JsonObject.class)})
Expand Down
16 changes: 7 additions & 9 deletions src/main/java/org/hisp/dhis/jsontree/JsonVirtualTree.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.BiConsumer;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.stream.Stream;
Expand Down Expand Up @@ -176,11 +176,11 @@ public <T extends JsonValue> T as( Class<T> as ) {

@Override
@SuppressWarnings( "unchecked" )
public <T extends JsonValue> T as( Class<T> as, BiPredicate<Method, Object[]> onCall ) {
public <T extends JsonValue> T as( Class<T> as, BiConsumer<Method, Object[]> onCall ) {
return (T) Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(), new Class[] { as },
( proxy, method, args ) -> {
if (!onCall.test( method, args )) return null;
onCall.accept( method, args );
return onInvoke( proxy, as, this, method, args, true );
} );
}
Expand Down Expand Up @@ -439,7 +439,7 @@ private void toSignature( Type type, StringBuilder str ) {
}

private static List<Property> captureProperties(Class<? extends JsonObject> of) {
Map<String, Property> res = new TreeMap<>();
List<Property> res = new ArrayList<>();
propertyMethods(of).forEach( m -> {
@SuppressWarnings( "unchecked" )
Class<? extends JsonObject> in = (Class<? extends JsonObject>) m.getDeclaringClass();
Expand All @@ -448,14 +448,12 @@ private static List<Property> captureProperties(Class<? extends JsonObject> of)
String name = (String) args[0];
@SuppressWarnings( "unchecked" )
Class<? extends JsonValue> type = (Class<? extends JsonValue>) args[1];
res.computeIfAbsent( name, n -> new Property( in, n, type, m, m.getAnnotatedReturnType() ) );
return false;
res.add( new Property( in, name, type, m.getName(), m.getAnnotatedReturnType(), m ) );
}
return true;
});
invokePropertyMethod( obj, m );
invokePropertyMethod( obj, m ); // may add zero, one or more properties via the callback
} );
return List.copyOf( res.values() );
return List.copyOf( res );
}

private static boolean isJsonObjectGetAs( Method method ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ private static ObjectValidation createInstance( Class<? extends JsonObject> sche
Map<String, PropertyValidation> properties = new HashMap<>();
Map<String, Type> types = new HashMap<>();
JsonObject.properties( schema ).stream().filter( ObjectValidation::isNotIgnored ).forEach( p -> {
properties.put(p.name(), fromProperty( p ));
types.put( p.name(), p.sourceType().getType() );
properties.put(p.jsonName(), fromProperty( p ));
types.put( p.jsonName(), p.javaType().getType() );
} );
return new ObjectValidation( schema, Map.copyOf( types ), Map.copyOf( properties ) );
}
Expand All @@ -96,7 +96,7 @@ private static boolean isNotIgnored( JsonObject.Property p ) {
@Maybe
private static PropertyValidation fromProperty( JsonObject.Property p ) {
PropertyValidation onMethod = fromAnnotations( p.source() );
PropertyValidation onReturnType = fromValueTypeUse( p.sourceType() );
PropertyValidation onReturnType = fromValueTypeUse( p.javaType() );
if ( onMethod == null ) return onReturnType;
if ( onReturnType == null ) return onMethod;
return onMethod.overlay( onReturnType );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,6 @@ record PropertyValidation(
@Maybe ArrayValidation arrays,
@Maybe ObjectValidation objects,
@Maybe PropertyValidation items
//TODO maybe add a Map<Rule, Set<Class<?>>> origin,
// which remembers where (annotation or validators) a validation originates from
// but this is difficult to keep accurate with the overlay
) {

/**
Expand Down
76 changes: 64 additions & 12 deletions src/test/java/org/hisp/dhis/jsontree/JsonObjectPropertiesTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,87 @@
import org.hisp.dhis.jsontree.JsonObject.Property;
import org.junit.jupiter.api.Test;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedType;
import java.util.List;
import java.util.Set;

import static java.util.stream.Collectors.toSet;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;

/**
* Tests the extraction of properties provided by {@link JsonObject#properties(Class)}
* Tests the extraction of properties provided by {@link JsonObject#properties(Class)}.
* <p>
* The coverage here is very shallow because the feature is used as part of the validation which has plenty of coverage
* for different types, annotations and so on.
*
* @author Jan Bernitt
*/
class JsonObjectPropertiesTest {

private static final ClassType STRING = new ClassType( String.class );

public interface User extends JsonObject {

@Required
default String username() {
return getString( "username" ).string();
}

default String name() {
return getString( "name" ).string();
return getString( "firstName" ).string() + " " + getString( "lastName" ).string();
}

}

@Test
void test() {
void testString() {
List<Property> properties = JsonObject.properties( User.class );
assertEquals( 1, properties.size());
Property name = properties.get( 0 );
assertProperty( "name", JsonString.class, name );
assertTrue( name.source().isAnnotationPresent( Required.class ) );
Property expected = new Property( User.class, "username", JsonString.class, "username",
STRING, null );
assertPropertyExists( "username", expected, properties );
}

@Test
void testString_Multiple() {
List<Property> properties = JsonObject.properties( User.class );
assertEquals( 3, properties.size() );
assertEquals( Set.of( "username", "firstName", "lastName" ),
properties.stream().map( Property::jsonName ).collect( toSet() ) );
assertEquals( Set.of( "username", "name" ), properties.stream().map( Property::javaName ).collect( toSet() ) );

assertPropertyExists( "firstName",
new Property( User.class, "firstName", JsonString.class, "name", STRING, null ),
properties );
assertPropertyExists( "lastName",
new Property( User.class, "lastName", JsonString.class, "name", STRING, null ),
properties );
}

private static void assertProperty(String name, Class<? extends JsonValue> type, Property actual) {
assertEquals( name, actual.name() );
assertEquals( type, actual.type() );
private void assertPropertyExists( String jsonName, Property expected, List<Property> actual ) {
Property prop = actual.stream().filter( p -> p.jsonName().equals( jsonName ) ).findFirst()
.orElse( null );
assertNotNull( prop );
assertSame( expected.in(), prop.in() );
assertEquals( expected.jsonName(), prop.jsonName() );
assertSame( expected.jsonType(), prop.jsonType() );
assertEquals( expected.javaName(), prop.javaName() );
assertSame( expected.javaType().getType(), prop.javaType().getType() );
}

record ClassType(Class<?> getType) implements AnnotatedType {

@Override public <T extends Annotation> T getAnnotation( Class<T> aClass ) {
return null;
}

@Override public Annotation[] getAnnotations() {
return new Annotation[0];
}

@Override public Annotation[] getDeclaredAnnotations() {
return new Annotation[0];
}
}
}

0 comments on commit 2e4fd41

Please sign in to comment.