├── .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 extends T> c) {
155 | return inner().addAll(c);
156 | }
157 |
158 | default boolean addAll(int index, Collection extends T> 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 super T> 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 super T> 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 super T> 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 |
--------------------------------------------------------------------------------