├── .gitignore ├── pom.xml └── src ├── main └── java │ └── typeref │ ├── DefaultValue.java │ ├── MethodAwareConsumer.java │ ├── MethodAwareFunction.java │ ├── MethodAwarePredicate.java │ ├── MethodFinder.java │ ├── NameOf.java │ ├── NamedValue.java │ ├── Newable.java │ ├── NewableConsumer.java │ ├── NewableSupplier.java │ ├── Parameters.java │ └── TypeReference.java └── test └── java └── com └── benjiweber └── recordmixins ├── DecomposeRecordsTest.java ├── OptionalPatternMatchTest.java ├── RecordMixinsTest.java └── RecordTuplesTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | *.iml 4 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.benjiweber 8 | recordmixins 9 | 1.0-SNAPSHOT 10 | 11 | 16 12 | 16 13 | 14 | 15 | 16 | 17 | org.apache.maven.plugins 18 | maven-compiler-plugin 19 | 3.8.1 20 | 21 | 17 22 | --enable-preview 23 | 17 24 | 17 25 | 26 | 27 | 28 | org.apache.maven.plugins 29 | maven-surefire-plugin 30 | 3.0.0-M3 31 | 32 | --enable-preview 33 | 34 | 35 | 36 | 37 | 38 | 39 | junit 40 | junit 41 | 4.12 42 | test 43 | 44 | 45 | org.hamcrest 46 | hamcrest 47 | 2.2 48 | test 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/main/java/typeref/DefaultValue.java: -------------------------------------------------------------------------------- 1 | package typeref; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | 6 | public class DefaultValue { 7 | private static Map, Object> defaultValues = new HashMap<>(); 8 | static { 9 | defaultValues.put(int.class, 0); 10 | defaultValues.put(Integer.class, 0); 11 | defaultValues.put(boolean.class, false); 12 | defaultValues.put(Boolean.class, false); 13 | defaultValues.put(byte.class, (byte)0); 14 | defaultValues.put(Byte.class, 0); 15 | defaultValues.put(char.class, ' '); 16 | defaultValues.put(Character.class, ' '); 17 | defaultValues.put(short.class, (short)0.0); 18 | defaultValues.put(Short.class, (short)0.0); 19 | defaultValues.put(long.class, 0l); 20 | defaultValues.put(Long.class, 0L); 21 | defaultValues.put(float.class, 0.0f); 22 | defaultValues.put(Float.class, 0.0f); 23 | defaultValues.put(double.class, 0.0d); 24 | defaultValues.put(Double.class, 0.0d); 25 | } 26 | 27 | public static T ofType(Class type) { 28 | return (T) defaultValues.getOrDefault(type, null); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/typeref/MethodAwareConsumer.java: -------------------------------------------------------------------------------- 1 | package typeref; 2 | 3 | import java.util.function.Consumer; 4 | 5 | public interface MethodAwareConsumer extends Consumer, MethodFinder { } 6 | -------------------------------------------------------------------------------- /src/main/java/typeref/MethodAwareFunction.java: -------------------------------------------------------------------------------- 1 | package typeref; 2 | 3 | import java.util.function.Function; 4 | 5 | public interface MethodAwareFunction extends Function, MethodFinder { } -------------------------------------------------------------------------------- /src/main/java/typeref/MethodAwarePredicate.java: -------------------------------------------------------------------------------- 1 | package typeref; 2 | 3 | import java.util.function.Predicate; 4 | 5 | public interface MethodAwarePredicate extends Predicate, MethodFinder { } -------------------------------------------------------------------------------- /src/main/java/typeref/MethodFinder.java: -------------------------------------------------------------------------------- 1 | package typeref; 2 | 3 | import java.io.Serializable; 4 | import java.lang.invoke.SerializedLambda; 5 | import java.lang.reflect.Method; 6 | import java.lang.reflect.Parameter; 7 | import java.util.Objects; 8 | 9 | import static java.util.Arrays.asList; 10 | 11 | public interface MethodFinder extends Serializable { 12 | default SerializedLambda serialized() { 13 | try { 14 | Method replaceMethod = getClass().getDeclaredMethod("writeReplace"); 15 | replaceMethod.setAccessible(true); 16 | return (SerializedLambda) replaceMethod.invoke(this); 17 | } catch (Exception e) { 18 | throw new RuntimeException(e); 19 | } 20 | } 21 | 22 | default Class getContainingClass() { 23 | try { 24 | String className = serialized().getImplClass().replaceAll("/", "."); 25 | return Class.forName(className); 26 | } catch (Exception e) { 27 | throw new RuntimeException(e); 28 | } 29 | } 30 | 31 | default Method method() { 32 | SerializedLambda lambda = serialized(); 33 | Class containingClass = getContainingClass(); 34 | return asList(containingClass.getDeclaredMethods()) 35 | .stream() 36 | .filter(method -> Objects.equals(method.getName(), lambda.getImplMethodName())) 37 | .findFirst() 38 | .orElseThrow(UnableToGuessMethodException::new); 39 | } 40 | 41 | default Parameter parameter(int n) { 42 | return method().getParameters()[n]; 43 | } 44 | 45 | default Object defaultValueForParameter(int n) { 46 | return DefaultValue.ofType(parameter(n).getType()); 47 | } 48 | 49 | class UnableToGuessMethodException extends RuntimeException {} 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/typeref/NameOf.java: -------------------------------------------------------------------------------- 1 | package typeref; 2 | 3 | public class NameOf { 4 | public static void main(String... args) { 5 | new NameOf().aMethod(null); 6 | } 7 | public void aMethod(String aParam) { 8 | throw new NullPointerException(nameof(this::aMethod, 0)); 9 | } 10 | 11 | public static String nameof(NewableConsumer method, int arg) { 12 | try { 13 | StackTraceElement caller = new Throwable().fillInStackTrace().getStackTrace()[1]; 14 | Class cls = Class.forName(caller.getClassName()); 15 | return cls.getDeclaredMethod(caller.getMethodName(), method.type()).getParameters()[arg].getName(); 16 | } catch (Exception e) { 17 | throw new RuntimeException(e); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/typeref/NamedValue.java: -------------------------------------------------------------------------------- 1 | package typeref; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Function; 5 | 6 | public interface NamedValue extends MethodFinder, Function { 7 | default String name() { 8 | checkParametersEnabled(); 9 | return parameter(0).getName(); 10 | } 11 | default void checkParametersEnabled() { 12 | if (Objects.equals("arg0", parameter(0).getName())) { 13 | throw new IllegalStateException("You need to compile with javac -parameters for parameter reflection to work; You also need java 8u60 or newer to use it with lambdas"); 14 | } 15 | } 16 | 17 | default T value() { 18 | return apply(name()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/typeref/Newable.java: -------------------------------------------------------------------------------- 1 | package typeref; 2 | 3 | public interface Newable extends MethodFinder { 4 | default Class type() { 5 | return (Class)parameter(0).getType(); 6 | } 7 | default T newInstance() { 8 | try { 9 | return type().newInstance(); 10 | } catch (Exception e) { 11 | throw new RuntimeException(e); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/typeref/NewableConsumer.java: -------------------------------------------------------------------------------- 1 | package typeref; 2 | 3 | import java.util.function.Consumer; 4 | 5 | public interface NewableConsumer extends Consumer, Newable { 6 | 7 | default boolean canCast(Object o) { 8 | T t = (T)o; 9 | 10 | return true; 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/typeref/NewableSupplier.java: -------------------------------------------------------------------------------- 1 | package typeref; 2 | 3 | import java.util.function.Supplier; 4 | 5 | public interface NewableSupplier extends Supplier, Newable {} 6 | -------------------------------------------------------------------------------- /src/main/java/typeref/Parameters.java: -------------------------------------------------------------------------------- 1 | package typeref; 2 | 3 | import java.util.function.Consumer; 4 | 5 | public interface Parameters extends NewableConsumer { 6 | default T get() { 7 | T t = newInstance(); 8 | accept(t); 9 | return t; 10 | } 11 | 12 | default void with(Consumer action) { 13 | action.accept(newInstance()); 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/java/typeref/TypeReference.java: -------------------------------------------------------------------------------- 1 | package typeref; 2 | 3 | import java.util.function.Consumer; 4 | 5 | public interface TypeReference extends Newable { 6 | T typeIs(T t); 7 | default Consumer consumer() { 8 | return this::typeIs; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/com/benjiweber/recordmixins/DecomposeRecordsTest.java: -------------------------------------------------------------------------------- 1 | package com.benjiweber.recordmixins; 2 | 3 | import org.junit.Test; 4 | import typeref.MethodFinder; 5 | 6 | import java.util.Optional; 7 | import java.util.concurrent.atomic.AtomicReference; 8 | import java.util.function.BiConsumer; 9 | import java.util.function.BiFunction; 10 | 11 | import static com.benjiweber.recordmixins.DecomposeRecordsTest.If.withFallback; 12 | import static org.junit.Assert.assertEquals; 13 | 14 | public class DecomposeRecordsTest { 15 | 16 | record Nums(Integer first, Integer last) {} 17 | record Colour(Integer r, Integer g, Integer b) {} 18 | record Wrapper(String value) {} 19 | record Name(String first, String last) {} 20 | record NameWithMiddle(String first, String middle, String last) {} 21 | 22 | @Test 23 | public void decompose_matching_types() { 24 | AtomicReference result = new AtomicReference<>("Fail"); 25 | Name name = new Name("Benji", "Weber"); 26 | 27 | If.instance(name, (String first, String last) -> { 28 | result.set(first.toLowerCase() + last.toLowerCase()); 29 | }); 30 | 31 | assertEquals("benjiweber", result.get()); 32 | } 33 | 34 | @Test 35 | public void decompose_mismatching_types() { 36 | AtomicReference result = new AtomicReference<>("Nothing Happened"); 37 | Nums nums = new Nums(5,6); 38 | 39 | If.instance(nums, (String first, String last) -> { 40 | result.set(first.toLowerCase() + last.toLowerCase()); 41 | }); 42 | 43 | assertEquals("Nothing Happened", result.get()); 44 | } 45 | 46 | @Test 47 | public void decompose_matching_types_to_value() { 48 | Name name = new Name("Benji", "Weber"); 49 | 50 | String result = withFallback("Fail").If.instance(name, (String first, String last) -> 51 | first.toLowerCase() + last.toLowerCase() 52 | ); 53 | 54 | assertEquals("benjiweber", result); 55 | } 56 | 57 | @Test 58 | public void decompose_mismatching_types_to_value() { 59 | Nums nums = new Nums(5,6); 60 | 61 | String result = withFallback("Nothing").If.instance(nums, (String first, String last) -> 62 | first.toLowerCase() + last.toLowerCase() 63 | ); 64 | 65 | assertEquals("Nothing", result); 66 | } 67 | 68 | @Test 69 | public void decompose_insufficient_arity_to_value() { 70 | Wrapper wrapper = new Wrapper("Benji"); 71 | 72 | String result = withFallback("Nothing").If.instance(wrapper, (String first, String last) -> 73 | first.toLowerCase() + last.toLowerCase() 74 | ); 75 | 76 | assertEquals("Nothing", result); 77 | } 78 | 79 | @Test 80 | public void decompose_surplus_arity_to_value() { 81 | NameWithMiddle name = new NameWithMiddle("Benji", "???", "Weber"); 82 | 83 | String result = withFallback("Fail").If.instance(name, (String first, String middle) -> 84 | first.toLowerCase() + middle.toLowerCase() 85 | ); 86 | 87 | assertEquals("benji???", result); 88 | } 89 | 90 | @Test 91 | public void decompose_matching_types_triple() { 92 | AtomicReference result = new AtomicReference<>(-1); 93 | Colour c = new Colour(5,6,7); 94 | 95 | If.instance(c, (Integer r, Integer g, Integer b) -> { 96 | result.set(r + g + b); 97 | }); 98 | 99 | assertEquals(Integer.valueOf(18), result.get()); 100 | } 101 | 102 | @Test 103 | public void decompose_matching_types_to_value_triple() { 104 | Colour c = new Colour(5,6,7); 105 | 106 | int result = withFallback(-1).If.instance(c, (Integer r, Integer g, Integer b) -> 107 | r + g + b 108 | ); 109 | 110 | assertEquals(18, result); 111 | } 112 | 113 | @Test 114 | public void decompose_mismatching_types_triple() { 115 | AtomicReference result = new AtomicReference<>("Nothing Happened"); 116 | Colour c = new Colour(5,6,7); 117 | 118 | If.instance(c, (String r, String g, String b) -> { 119 | result.set(r.toLowerCase() + r.toLowerCase()); 120 | }); 121 | 122 | assertEquals("Nothing Happened", result.get()); 123 | } 124 | 125 | @Test 126 | public void decompose_mismatching_types_to_value_triple() { 127 | Colour c = new Colour(5,6,7); 128 | 129 | String result = withFallback("Expected").If.instance(c, (Integer r, String g, Integer b) -> 130 | r + g + b 131 | ); 132 | 133 | assertEquals("Expected", result); 134 | } 135 | 136 | @Test 137 | public void decompose_supertypes() { 138 | interface Animal { String noise(); } 139 | record Duck(String noise) implements Animal {} 140 | record Dog(String noise) implements Animal {} 141 | 142 | record Zoo(Animal one, Animal two) {} 143 | 144 | Zoo zoo = new Zoo(new Duck("Quack"), new Dog("Woof")); 145 | 146 | String result = withFallback("Fail").If.instance(zoo, (Animal duck, Animal dog) -> 147 | duck.noise() + dog.noise() 148 | ); 149 | 150 | assertEquals("QuackWoof", result); 151 | } 152 | 153 | @Test 154 | public void decompose_subtypes() { 155 | interface Animal { String noise(); } 156 | record Duck(String noise) implements Animal {} 157 | record Dog(String noise) implements Animal {} 158 | 159 | record Zoo(Animal one, Animal two) {} 160 | 161 | Zoo zoo = new Zoo(new Duck("Quack"), new Dog("Woof")); 162 | 163 | String result = withFallback("Fail").If.instance(zoo, (Duck duck, Dog dog) -> 164 | duck.noise() + dog.noise() 165 | ); 166 | 167 | assertEquals("QuackWoof", result); 168 | } 169 | 170 | @Test 171 | public void decompose_subtypes_infer_lhs() { 172 | interface Animal { String noise(); } 173 | record Duck(String noise) implements Animal {} 174 | record Dog(String noise) implements Animal {} 175 | 176 | record Zoo(Animal one, Animal two) {} 177 | 178 | Zoo zoo = new Zoo(new Duck("Quack"), new Dog("Woof")); 179 | 180 | String result = withFallback("Fail").If.instance(zoo, (Duck duck, Dog dog) -> 181 | duck.noise() + dog.noise() 182 | ); 183 | 184 | assertEquals("QuackWoof", result); 185 | } 186 | 187 | @Test 188 | public void decompose_mismatching_subtypes() { 189 | interface Animal { String noise(); } 190 | record Duck(String noise) implements Animal {} 191 | record Dog(String noise) implements Animal {} 192 | 193 | record Zoo(Animal one, Animal two) {} 194 | 195 | Zoo zoo = new Zoo(new Duck("Quack"), new Dog("Woof")); 196 | 197 | String result = withFallback("Fail").If.instance(zoo, (Dog duck, Duck dog) -> 198 | duck.noise() + dog.noise() 199 | ); 200 | 201 | assertEquals("Fail", result); 202 | } 203 | 204 | 205 | interface ParamTypeAware extends MethodFinder { 206 | default Class paramType(int n) { 207 | return method().getParameters()[(actualParamCount() - expectedParamCount()) + n].getType(); 208 | } 209 | int expectedParamCount(); 210 | private int actualParamCount() { 211 | return method().getParameters().length; 212 | } 213 | 214 | } 215 | interface MethodAwareBiFunction extends BiFunction, ParamTypeAware { 216 | default Optional tryApply(L left, R right) { 217 | return acceptsTypes(left, right) 218 | ? Optional.ofNullable(apply(left, right)) 219 | : Optional.empty(); 220 | } 221 | 222 | default boolean acceptsTypes(Object left, Object right) { 223 | return paramType(0).isAssignableFrom(left.getClass()) 224 | && paramType(1).isAssignableFrom(right.getClass()); 225 | } 226 | default int expectedParamCount() { return 2; } 227 | } 228 | 229 | interface MethodAwareBiConsumer extends BiConsumer, ParamTypeAware { 230 | default void tryAccept(L left, R right) { 231 | if (acceptsTypes(left,right)) { 232 | accept(left, right); 233 | } 234 | } 235 | 236 | default boolean acceptsTypes(Object left, Object right) { 237 | return paramType(0).isAssignableFrom(left.getClass()) 238 | && paramType(1).isAssignableFrom(right.getClass()); 239 | } 240 | default int expectedParamCount() { return 2; } 241 | } 242 | 243 | interface TriFunction { 244 | R apply(T t, U u, V v); 245 | } 246 | interface MethodAwareTriFunction extends TriFunction, ParamTypeAware { 247 | default Optional tryApply(T one, U two, V three) { 248 | return acceptsTypes(one, two, three) 249 | ? Optional.ofNullable(apply(one, two, three)) 250 | : Optional.empty(); 251 | } 252 | 253 | default boolean acceptsTypes(Object one, Object two, Object three) { 254 | return paramType(0).isAssignableFrom(one.getClass()) 255 | && paramType(1).isAssignableFrom(two.getClass()) 256 | && paramType(2).isAssignableFrom(three.getClass()); 257 | } 258 | default int expectedParamCount() { return 3; } 259 | } 260 | 261 | interface TriConsumer { 262 | void accept(T t, U u, V v); 263 | } 264 | interface MethodAwareTriConsumer extends TriConsumer, ParamTypeAware { 265 | default void tryAccept(T one, U two, V three) { 266 | if (acceptsTypes(one, two, three)) { 267 | accept(one, two, three); 268 | } 269 | } 270 | 271 | default boolean acceptsTypes(Object one, Object two, Object three) { 272 | return paramType(0).isAssignableFrom(one.getClass()) 273 | && paramType(1).isAssignableFrom(two.getClass()) 274 | && paramType(2).isAssignableFrom(three.getClass()); 275 | } 276 | default int expectedParamCount() { return 3; } 277 | } 278 | abstract static class Match { 279 | public final Match If = this; 280 | public abstract TResult instance(Object toMatch, MethodAwareBiFunction action); 281 | public abstract TResult instance(Object toMatch, MethodAwareTriFunction action); 282 | } 283 | interface If { 284 | static Match withFallback(TResult defaultResult) { 285 | return new Match<>() { 286 | public TResult instance(Object toMatch, MethodAwareBiFunction action) { 287 | return DecomposeRecordsTest.If.instance(toMatch, action).orElse(defaultResult); 288 | } 289 | 290 | public TResult instance(Object toMatch, MethodAwareTriFunction action) { 291 | return DecomposeRecordsTest.If.instance(toMatch, action).orElse(defaultResult); 292 | } 293 | }; 294 | } 295 | static void instance(Object o, MethodAwareBiConsumer action) { 296 | if (o instanceof Record r) { 297 | if (r.getClass().getRecordComponents().length < 2) { 298 | return; 299 | } 300 | action.tryAccept((L) nthComponent(0, r), (R) nthComponent(1, r)); 301 | } 302 | } 303 | static void instance(Object o, MethodAwareTriConsumer action) { 304 | if (o instanceof Record r) { 305 | if (r.getClass().getRecordComponents().length < 3) { 306 | return; 307 | } 308 | action.tryAccept((T) nthComponent(0, r), (U) nthComponent(1, r), (V) nthComponent(2, r)); 309 | } 310 | } 311 | static Optional instance(Object o, MethodAwareBiFunction action) { 312 | if (o instanceof Record r) { 313 | if (r.getClass().getRecordComponents().length < 2) { 314 | return Optional.empty(); 315 | } 316 | return action.tryApply((L) nthComponent(0, r), (R) nthComponent(1, r)); 317 | } 318 | return Optional.empty(); 319 | } 320 | static Optional instance(Object o, MethodAwareTriFunction action) { 321 | if (o instanceof Record r) { 322 | if (r.getClass().getRecordComponents().length < 3) { 323 | return Optional.empty(); 324 | } 325 | return action.tryApply((T) nthComponent(0, r), (U) nthComponent(1, r), (V) nthComponent(2, r)); 326 | } 327 | return Optional.empty(); 328 | } 329 | private static Object nthComponent(int n, Record r) { 330 | try { 331 | return r.getClass().getRecordComponents()[n].getAccessor().invoke(r); 332 | } catch (Exception e) { 333 | throw new RuntimeException(e); 334 | } 335 | } 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /src/test/java/com/benjiweber/recordmixins/OptionalPatternMatchTest.java: -------------------------------------------------------------------------------- 1 | package com.benjiweber.recordmixins; 2 | 3 | import org.junit.Test; 4 | import typeref.MethodFinder; 5 | 6 | import java.util.Optional; 7 | import java.util.function.BiConsumer; 8 | import java.util.function.BiFunction; 9 | 10 | import static org.junit.Assert.assertEquals; 11 | import static com.benjiweber.recordmixins.OptionalPatternMatchTest.None.*; 12 | 13 | public class OptionalPatternMatchTest { 14 | 15 | @Test 16 | public void traditional_unwrap() { 17 | Optional unknown = Optional.of("Hello World"); 18 | assertEquals( 19 | "hello world", 20 | unknown 21 | .map(String::toLowerCase) 22 | .orElse("absent") 23 | ); 24 | } 25 | 26 | @Test 27 | public void unwrap_optional() { 28 | Optional unknown = Optional.of("Hello World"); 29 | assertEquals( 30 | "hello world", 31 | unwrap(unknown) instanceof String s 32 | ? s.toLowerCase() 33 | : "absent" 34 | ); 35 | } 36 | 37 | @Test 38 | public void unwrap_empty_optional() { 39 | Optional unknown = Optional.empty(); 40 | assertEquals( 41 | "absent", 42 | unwrap(unknown) instanceof String s 43 | ? s.toLowerCase() 44 | : "absent" 45 | ); 46 | } 47 | 48 | @Test 49 | public void unwrap_wrong_type_optional() { 50 | Optional unknown = Optional.of(5); 51 | assertEquals( 52 | "absent", 53 | unwrap(unknown) instanceof String s 54 | ? s.toLowerCase() 55 | : "absent" 56 | ); 57 | } 58 | 59 | @Test 60 | public void unwrap_wrong_type_raw_optional() { 61 | Optional unknown = Optional.of(5); 62 | assertEquals( 63 | "absent", 64 | unwrap(unknown) instanceof String s 65 | ? s.toLowerCase() 66 | : "absent" 67 | ); 68 | } 69 | 70 | @Test 71 | public void unwrap_null() { 72 | Optional unknown = null; 73 | assertEquals( 74 | "absent", 75 | unwrap(unknown) instanceof String s 76 | ? s.toLowerCase() 77 | : "absent" 78 | ); 79 | } 80 | 81 | @Test 82 | public void unwrap_unboxed_null() { 83 | String unknown = null; 84 | assertEquals( 85 | "absent", 86 | unwrap(unknown) instanceof String s 87 | ? s.toLowerCase() 88 | : "absent" 89 | ); 90 | } 91 | 92 | @Test 93 | public void unwrap_unboxed_value() { 94 | String unknown = "Hello World"; 95 | assertEquals( 96 | "hello world", 97 | unwrap(unknown) instanceof String s 98 | ? s.toLowerCase() 99 | : "absent" 100 | ); 101 | } 102 | 103 | @Test 104 | public void unwrap_unboxed_value_unknown_type() { 105 | Object unknown = "Hello World"; 106 | assertEquals( 107 | "hello world", 108 | unwrap(unknown) instanceof String s 109 | ? s.toLowerCase() 110 | : "absent" 111 | ); 112 | } 113 | 114 | @Test 115 | public void unwrap_unboxed_value_wrong_type() { 116 | Integer unknown = 5; 117 | assertEquals( 118 | "absent", 119 | unwrap(unknown) instanceof String s 120 | ? s.toLowerCase() 121 | : "absent" 122 | ); 123 | } 124 | 125 | @Test 126 | public void unwrap_unboxed_absent_unknown_type() { 127 | Object unknown = null; 128 | assertEquals( 129 | "absent", 130 | unwrap(unknown) instanceof String s 131 | ? s.toLowerCase() 132 | : "absent" 133 | ); 134 | } 135 | 136 | @Test 137 | public void unwrap_object_optional_empty() { 138 | Object unknown = Optional.empty(); 139 | assertEquals( 140 | "absent", 141 | unwrap(unknown) instanceof String s 142 | ? s.toLowerCase() 143 | : "absent" 144 | ); 145 | } 146 | 147 | @Test 148 | public void unwrap_object_optional() { 149 | Object unknown = Optional.of("Hello World"); 150 | assertEquals( 151 | "hello world", 152 | unwrap(unknown) instanceof String s 153 | ? s.toLowerCase() 154 | : "absent" 155 | ); 156 | } 157 | 158 | 159 | @Test 160 | public void unwrap_raw_optional() { 161 | Optional unknown = Optional.of("Hello World"); 162 | assertEquals( 163 | "hello world", 164 | unwrap(unknown) instanceof String s 165 | ? s.toLowerCase() 166 | : "absent" 167 | ); 168 | } 169 | 170 | @Test 171 | public void unwrap_empty_raw_optional() { 172 | Optional unknown = Optional.empty(); 173 | assertEquals( 174 | "absent", 175 | unwrap(unknown) instanceof String s 176 | ? s.toLowerCase() 177 | : "absent" 178 | ); 179 | } 180 | 181 | @Test 182 | public void unwrap_optionals_all_the_way_down() { 183 | var unknown = Optional.of(Optional.of(Optional.of(Optional.of("Hello World")))); 184 | assertEquals( 185 | "hello world", 186 | unwrap(unknown) instanceof String s 187 | ? s.toLowerCase() 188 | : "absent" 189 | ); 190 | } 191 | 192 | static Object unwrap(Object o) { 193 | if (o instanceof Optional opt) { 194 | return opt.isPresent() ? unwrap(opt.get()) : None; 195 | } else if (o != null) { 196 | return o; 197 | } else { 198 | return None; 199 | } 200 | } 201 | static class None { 202 | private None() {} 203 | public static final None None = new None(); 204 | } 205 | 206 | 207 | } 208 | -------------------------------------------------------------------------------- /src/test/java/com/benjiweber/recordmixins/RecordMixinsTest.java: -------------------------------------------------------------------------------- 1 | package com.benjiweber.recordmixins; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.Serializable; 6 | import java.lang.invoke.SerializedLambda; 7 | import java.lang.reflect.*; 8 | import java.util.*; 9 | import java.util.function.*; 10 | import java.util.stream.Collectors; 11 | import java.util.stream.Stream; 12 | 13 | import static java.util.stream.Collectors.toList; 14 | import static org.junit.Assert.*; 15 | 16 | public class RecordMixinsTest { 17 | 18 | private static final List example = List.of("one", "two", "three", "four", "five"); 19 | 20 | @Test 21 | public void map() { 22 | var mappable = new EnhancedList<>(example); 23 | 24 | assertEquals( 25 | List.of("oneone", "twotwo", "threethree", "fourfour", "fivefive"), 26 | mappable.map(s -> s + s) 27 | ); 28 | } 29 | 30 | 31 | @Test 32 | public void filter() { 33 | var filterable = new EnhancedList<>(example); 34 | 35 | assertEquals( 36 | List.of("one", "two"), 37 | filterable.where(s -> s.length() < 4) 38 | ); 39 | } 40 | 41 | @Test 42 | public void group() { 43 | var groupable = new EnhancedList<>(example); 44 | 45 | assertEquals( 46 | Map.of( 47 | 3, List.of("one", "two"), 48 | 4, List.of("four", "five"), 49 | 5, List.of("three") 50 | ), 51 | groupable.groupBy(String::length) 52 | ); 53 | } 54 | 55 | @Test 56 | public void chain_filter() { 57 | var filterable = new EnhancedList<>(example); 58 | 59 | assertEquals( 60 | List.of("one"), 61 | filterable 62 | .where(s -> s.length() < 4) 63 | .where(s -> s.endsWith("e")) 64 | ); 65 | } 66 | 67 | 68 | public record EnhancedList(List inner) implements 69 | ForwardingList, 70 | Mappable, 71 | Filterable>, 72 | Groupable {} 73 | 74 | public interface Mappable extends Forwarding> { 75 | default List map(Function f) { 76 | return inner().stream().map(f).collect(toList()); 77 | } 78 | } 79 | 80 | public interface Filterable> extends ForwardingAllTheWayDown, R> { 81 | default R where(Predicate p) { 82 | return forwarding(inner().stream().filter(p).collect(toList())); 83 | } 84 | } 85 | 86 | public interface Groupable extends Forwarding> { 87 | default Map> groupBy(Function keyExtractor) { 88 | return inner().stream().collect(Collectors.groupingBy(keyExtractor)); 89 | } 90 | } 91 | 92 | 93 | interface Forwarding { 94 | T inner(); 95 | } 96 | 97 | interface ForwardingAllTheWayDown extends Forwarding { 98 | default R forwarding(T t) { 99 | try { 100 | return (R) compatibleConstructor(getClass().getConstructors(), t) 101 | .newInstance(t); 102 | } catch (Exception e) { 103 | throw new IllegalStateException(e); 104 | } 105 | } 106 | 107 | default Constructor compatibleConstructor(Constructor[] constructors, T t) { 108 | return Stream.of(constructors) 109 | .filter(ctor -> ctor.getParameterCount() == 1) 110 | .filter(ctor -> ctor.getParameters()[0].getType().isAssignableFrom(t.getClass())) 111 | .findAny().orElseThrow(IllegalStateException::new); 112 | } 113 | } 114 | 115 | interface ForwardingList extends List, Forwarding> { 116 | List inner(); 117 | 118 | default int size() { 119 | return inner().size(); 120 | } 121 | 122 | default boolean isEmpty() { 123 | return inner().isEmpty(); 124 | } 125 | 126 | default boolean contains(Object o) { 127 | return inner().contains(o); 128 | } 129 | 130 | default Iterator iterator() { 131 | return inner().iterator(); 132 | } 133 | 134 | default Object[] toArray() { 135 | return inner().toArray(); 136 | } 137 | 138 | default T1[] toArray(T1[] a) { 139 | return inner().toArray(a); 140 | } 141 | 142 | default boolean add(T t) { 143 | return inner().add(t); 144 | } 145 | 146 | default boolean remove(Object o) { 147 | return inner().remove(o); 148 | } 149 | 150 | default boolean containsAll(Collection c) { 151 | return inner().containsAll(c); 152 | } 153 | 154 | default boolean addAll(Collection c) { 155 | return inner().addAll(c); 156 | } 157 | 158 | default boolean addAll(int index, Collection c) { 159 | return inner().addAll(index, c); 160 | } 161 | 162 | default boolean removeAll(Collection c) { 163 | return inner().removeAll(c); 164 | } 165 | 166 | default boolean retainAll(Collection c) { 167 | return inner().retainAll(c); 168 | } 169 | 170 | default void replaceAll(UnaryOperator operator) { 171 | inner().replaceAll(operator); 172 | } 173 | 174 | default void sort(Comparator c) { 175 | inner().sort(c); 176 | } 177 | 178 | default void clear() { 179 | inner().clear(); 180 | } 181 | 182 | default T get(int index) { 183 | return inner().get(index); 184 | } 185 | 186 | default T set(int index, T element) { 187 | return inner().set(index, element); 188 | } 189 | 190 | default void add(int index, T element) { 191 | inner().add(index, element); 192 | } 193 | 194 | default T remove(int index) { 195 | return inner().remove(index); 196 | } 197 | 198 | default int indexOf(Object o) { 199 | return inner().indexOf(o); 200 | } 201 | 202 | default int lastIndexOf(Object o) { 203 | return inner().lastIndexOf(o); 204 | } 205 | 206 | default ListIterator listIterator() { 207 | return inner().listIterator(); 208 | } 209 | 210 | default ListIterator listIterator(int index) { 211 | return inner().listIterator(index); 212 | } 213 | 214 | default List subList(int fromIndex, int toIndex) { 215 | return inner().subList(fromIndex, toIndex); 216 | } 217 | 218 | default Spliterator spliterator() { 219 | return inner().spliterator(); 220 | } 221 | 222 | 223 | default T1[] toArray(IntFunction generator) { 224 | return inner().toArray(generator); 225 | } 226 | 227 | default boolean removeIf(Predicate filter) { 228 | return inner().removeIf(filter); 229 | } 230 | 231 | default Stream stream() { 232 | return inner().stream(); 233 | } 234 | 235 | default Stream parallelStream() { 236 | return inner().parallelStream(); 237 | } 238 | 239 | default void forEach(Consumer action) { 240 | inner().forEach(action); 241 | } 242 | } 243 | 244 | 245 | } -------------------------------------------------------------------------------- /src/test/java/com/benjiweber/recordmixins/RecordTuplesTest.java: -------------------------------------------------------------------------------- 1 | package com.benjiweber.recordmixins; 2 | 3 | import org.junit.Test; 4 | 5 | import java.io.Serializable; 6 | import java.lang.invoke.SerializedLambda; 7 | import java.lang.reflect.*; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Objects; 11 | import java.util.function.Function; 12 | import java.util.stream.Collectors; 13 | import java.util.stream.Stream; 14 | 15 | import static com.benjiweber.recordmixins.RecordTuplesTest.TriTuple.builder; 16 | import static com.benjiweber.recordmixins.RecordTuplesTest.TriTuple.safebuilder; 17 | import static java.util.stream.Collectors.toList; 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.fail; 20 | 21 | public class RecordTuplesTest { 22 | 23 | public record Colour(int red, int green, int blue) implements TriTuple {} 24 | public record Person(String name, int age, double height) implements TriTuple {} 25 | public record Town(int population, int altitude, int established) implements TriTuple { } 26 | 27 | @Test 28 | public void decomposable_record() { 29 | Colour colour = new Colour(1,2,3); 30 | 31 | colour.decompose((r,g,b) -> { 32 | assertEquals(1, r.intValue()); 33 | assertEquals(2, g.intValue()); 34 | assertEquals(3, b.intValue()); 35 | }); 36 | 37 | var sum = colour.decomposeTo((r,g,b) -> r+g+b); 38 | assertEquals(6, sum.intValue()); 39 | } 40 | 41 | @Test 42 | public void structural_convert_reflection() { 43 | Colour colour = new Colour(1,2,3); 44 | Town town = colour.to(Town.class); 45 | assertEquals(1, town.population()); 46 | assertEquals(2, town.altitude()); 47 | assertEquals(3, town.established()); 48 | } 49 | @Test 50 | public void structural_convert_method_reference() { 51 | Colour colour = new Colour(1, 2, 3); 52 | Town town = colour.to(Town::new); 53 | assertEquals(1, town.population()); 54 | assertEquals(2, town.altitude()); 55 | assertEquals(3, town.established()); 56 | } 57 | 58 | @Test 59 | public void replace_property() { 60 | Colour colour = new Colour(1,2,3); 61 | Colour changed = colour.with(Colour::red, 5); 62 | assertEquals(new Colour(5,2,3), changed); 63 | 64 | Person p1 = new Person("Leslie", 12, 48.3); 65 | Person p2 = p1.with(Person::name, "Beverly"); 66 | assertEquals(new Person("Beverly", 12, 48.3), p2); 67 | } 68 | 69 | @Test 70 | public void auto_builders() { 71 | Person sam = builder(Person::new) 72 | .with(Person::name, "Sam") 73 | .with(Person::age, 34) 74 | .with(Person::height, 83.2); 75 | 76 | assertEquals(new Person("Sam", 34, 83.2), sam); 77 | } 78 | 79 | @Test 80 | public void mandatory_builders() { 81 | Person sam = safebuilder(Person::new) 82 | .with(Person::name, "Sam") 83 | .with(Person::age, 34) 84 | .with(Person::height, 83.2); 85 | 86 | assertEquals(new Person("Sam", 34, 83.2), sam); 87 | } 88 | 89 | @Test 90 | public void auto_builders_reflection() { 91 | Person sam = builder(Person.class) 92 | .with(Person::name, "Sam") 93 | .with(Person::age, 34) 94 | .with(Person::height, 83.2); 95 | 96 | assertEquals(new Person("Sam", 34, 83.2), sam); 97 | } 98 | 99 | interface TriTuple,T,U,V> extends DecomposableRecord, PrimitiveMappings { 100 | default T one() { 101 | return getComponentValue(0); 102 | } 103 | 104 | default U two() { 105 | return getComponentValue(1); 106 | } 107 | 108 | default V three() { 109 | return getComponentValue(2); 110 | } 111 | 112 | 113 | default void decompose(TriConsumer withComponents) { 114 | withComponents.apply(one(), two(), three()); 115 | } 116 | 117 | default R decomposeTo(TriFunction withComponents) { 118 | return withComponents.apply(one(), two(), three()); 119 | } 120 | 121 | default > R to(Class cls) { 122 | try { 123 | Constructor constructor = findCompatibleConstructorFor(cls); 124 | 125 | return (R)constructor.newInstance(one(), two(), three()); 126 | } catch (Exception e) { 127 | throw new RuntimeException(e); 128 | } 129 | } 130 | 131 | default > R to(TriFunction ctor) { 132 | return decomposeTo(ctor); 133 | } 134 | 135 | default TRecord with(MethodAwareFunction prop, R newValue) { 136 | try { 137 | Class typeParameter = (Class) findTypeParameters()[0]; 138 | Constructor constructor = findCompatibleConstructorFor(typeParameter); 139 | String propName = prop.method().getName(); 140 | Object[] ctorArgs = Stream.of(0, 1, 2) 141 | .map(i -> getComponent(i).replaceIfNamed(propName, newValue)) 142 | .toArray(); 143 | return (TRecord) constructor.newInstance(ctorArgs); 144 | } catch (Exception e) { 145 | throw new RuntimeException(e); 146 | } 147 | } 148 | 149 | static > ThreeMissing safebuilder(MethodAwareTriFunction ctor) { 150 | var reflectedConstructor = ctor.getContainingClass().getConstructors()[0]; 151 | var defaultConstructorValues = Stream.of(reflectedConstructor.getParameterTypes()) 152 | .map(defaultValues::get) 153 | .collect(toList()); 154 | return (__, t) -> (___, u) -> (____, v)-> ctor.apply( 155 | t, 156 | u, 157 | v 158 | ); 159 | } 160 | 161 | interface ThreeMissing { 162 | TwoMissing with(MethodAwareFunction prop, T newValue); 163 | } 164 | interface TwoMissing { 165 | OneMissing with(MethodAwareFunction prop, U newValue); 166 | } 167 | interface OneMissing { 168 | TRecord with(MethodAwareFunction prop, V newValue); 169 | } 170 | 171 | static > TBuild builder(MethodAwareTriFunction ctor) { 172 | var reflectedConstructor = ctor.getContainingClass().getConstructors()[0]; 173 | var defaultConstructorValues = Stream.of(reflectedConstructor.getParameterTypes()) 174 | .map(defaultValues::get) 175 | .collect(toList()); 176 | return ctor.apply( 177 | (T)defaultConstructorValues.get(0), 178 | (U)defaultConstructorValues.get(1), 179 | (V)defaultConstructorValues.get(2) 180 | ); 181 | } 182 | 183 | static > TBuild builder(Class cls) { 184 | Constructor constructor = Stream.of(cls.getConstructors()) 185 | .filter(ctor -> ctor.getParameterCount() == 3) 186 | .findFirst() 187 | .orElseThrow(IllegalStateException::new); 188 | 189 | try { 190 | return (TBuild) constructor.newInstance( 191 | defaultValues.get(constructor.getParameters()[0].getType()), 192 | defaultValues.get(constructor.getParameters()[1].getType()), 193 | defaultValues.get(constructor.getParameters()[2].getType()) 194 | ); 195 | } catch (Exception e) { 196 | throw new RuntimeException(e); 197 | } 198 | } 199 | 200 | 201 | private > Constructor findCompatibleConstructorFor(Class cls) { 202 | var paramTypes = getParameterTypes(); 203 | return Stream.of(cls.getConstructors()) 204 | .filter(ctor -> match(ctor.getParameterTypes(), paramTypes)) 205 | .findFirst().orElseThrow(IllegalStateException::new); 206 | } 207 | 208 | private boolean match(Class[] constructorParamTypes, List> ourParamTypes) { 209 | if (constructorParamTypes.length != 3) return false; 210 | return Stream.of(0,1,2) 211 | .allMatch(i -> match(constructorParamTypes[i], ourParamTypes.get(i))); 212 | } 213 | 214 | private boolean match(Class a, Class b) { 215 | return a.isAssignableFrom(b) || boxingMappings.getOrDefault(a, a).isAssignableFrom(b); 216 | } 217 | 218 | private List> getParameterTypes() { 219 | Type[] types = findTypeParameters(); 220 | return List.of((Class)types[1], (Class)types[2], (Class)types[3]); 221 | } 222 | 223 | private Type[] findTypeParameters() { 224 | ParameterizedType triTupleType = Stream.of(getClass().getGenericInterfaces()).map(iface -> (ParameterizedType) iface).filter(iface -> iface.getRawType() == TriTuple.class).findFirst().orElseThrow(IllegalStateException::new); 225 | return triTupleType.getActualTypeArguments(); 226 | } 227 | 228 | } 229 | 230 | interface MethodFinder extends Serializable { 231 | default SerializedLambda serialized() { 232 | try { 233 | Method replaceMethod = getClass().getDeclaredMethod("writeReplace"); 234 | replaceMethod.setAccessible(true); 235 | return (SerializedLambda) replaceMethod.invoke(this); 236 | } catch (Exception e) { 237 | throw new RuntimeException(e); 238 | } 239 | } 240 | 241 | default Method method() { 242 | SerializedLambda lambda = serialized(); 243 | Class containingClass = getContainingClass(); 244 | return Stream.of(containingClass.getDeclaredMethods()) 245 | .filter(method -> Objects.equals(method.getName(), lambda.getImplMethodName())) 246 | .findFirst() 247 | .orElseThrow(IllegalStateException::new); 248 | } 249 | 250 | default Class getContainingClass() { 251 | try { 252 | String className = serialized().getImplClass().replaceAll("/", "."); 253 | return Class.forName(className); 254 | } catch (Exception e) { 255 | throw new RuntimeException(e); 256 | } 257 | } 258 | } 259 | 260 | interface DecomposableRecord { 261 | default T getComponentValue(int index) { 262 | try { 263 | return this.getComponent(index).value(); 264 | } catch (Exception e) { 265 | throw new RuntimeException(e); 266 | } 267 | } 268 | 269 | default NamedProperty getComponent(int index) { 270 | return new NamedProperty((Record)this, this.getClass().getRecordComponents()[index]); 271 | } 272 | 273 | record NamedProperty(Record record, RecordComponent component) { 274 | public T value() { 275 | try { 276 | return (T) component.getAccessor().invoke(record); 277 | } catch (Exception e) { 278 | throw new RuntimeException(e); 279 | } 280 | } 281 | 282 | public String name() { 283 | return component.getName(); 284 | } 285 | 286 | public T replaceIfNamed(String propName, T newValue) { 287 | return Objects.equals(name(), propName) 288 | ? newValue 289 | : value(); 290 | } 291 | } 292 | 293 | 294 | } 295 | 296 | 297 | 298 | public interface MethodAwareFunction extends Function, MethodFinder { } 299 | public interface MethodAwareTriFunction extends TriFunction, MethodFinder { } 300 | 301 | interface TriFunction { 302 | R apply(T t, U u, V v); 303 | } 304 | 305 | interface TriConsumer { 306 | void apply(T t, U u, V v); 307 | } 308 | 309 | interface PrimitiveMappings { 310 | Map, Object> defaultValues = Map.of( 311 | int.class, 0, 312 | double.class, 0.0 313 | ); 314 | 315 | Map, Class> boxingMappings = Map.of( 316 | Integer.class, int.class, 317 | int.class, Integer.class, 318 | Double.class, double.class, 319 | double.class, Double.class 320 | ); 321 | 322 | } 323 | 324 | } 325 | --------------------------------------------------------------------------------