Skip to content

Commit ea72ff2

Browse files
committed
#44 block mixins
Adds support for mixing in the contents of an existing block. The block can be referenced by name, or provided in the form of an annotated helper type.
1 parent a7bc450 commit ea72ff2

12 files changed

Lines changed: 264 additions & 49 deletions

File tree

Documentation.java

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,10 +278,10 @@ public enum TestEnum {
278278
* descriptor. (This technique can also be used recursively to
279279
* return a new instance of the current block.)
280280
*
281-
* When a block is declared, a name can be provided which uniquely
281+
* (When a block is declared, a name can be provided which uniquely
282282
* identifies it within the scope of your descriptor. A block can
283283
* also be declared anonymously, in which case it cannot be
284-
* referenced.
284+
* referenced.)
285285
*/
286286

287287
// Add a method which starts a new block, but by referencing it
@@ -290,6 +290,30 @@ public enum TestEnum {
290290
.addBlockReference(String blockName, String methodSignature)
291291

292292

293+
/**
294+
* ### Block Mixins
295+
*
296+
* Similar to references, it is possible to combine the contents of
297+
* a previously declared block with the current one by using a
298+
* 'block mixin'. The mixin can be in the form of a string naming
299+
* another block in the descriptor, or a class containing Flapi
300+
* method annotations (see the [Annotations](#annotations) section
301+
* below). Mixins are applied late, so it is possible to use
302+
* forward references to blocks which have not yet been decalared.
303+
*
304+
* (When a block is declared, a name can be provided which uniquely
305+
* identifies it within the scope of your descriptor. A block can
306+
* also be declared anonymously, in which case it cannot be
307+
* referenced.)
308+
*/
309+
310+
// mixin the contents of another named block
311+
.addMixin(String blockName)
312+
313+
// mixin the contents of an annotated interface or class
314+
.addMixin(Class blockType)
315+
316+
293317
/**
294318
* ## Methods
295319
*

flapi-descriptor/src/test/java/unquietcode/tools/flapi/builder/MainDescriptor.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,15 @@ public Descriptor descriptor() {
222222
.end()
223223
.any()
224224

225+
// Mixins
226+
.addMethod("addMixin(String blockName)")
227+
.withDocumentation("mix in the contents of another block by name")
228+
.any()
229+
230+
.addMethod("addMixin(Class helper)")
231+
.withDocumentation("mix in the contents of an annotated helper")
232+
.any()
233+
225234
.addMethod("addBlockReference(String blockName, String methodSignature)")
226235
.withDocumentation("add a new method which proceeds to an existing block")
227236
.addBlockChain()
@@ -290,6 +299,16 @@ public Descriptor descriptor() {
290299
.addBlockReference("Method")
291300
.end()
292301
.any()
302+
303+
// Mixins for the top level block
304+
.addMethod("addMixin(String blockName)")
305+
.withDocumentation("mix in the contents of another block by name")
306+
.any()
307+
308+
.addMethod("addMixin(Class helper)")
309+
.withDocumentation("mix in the contents of an annotated block helper")
310+
.any()
311+
293312
.build();
294313

295314
return builder;

src/main/java/unquietcode/tools/flapi/Flapi.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ public static DescriptorBuilder.Start<Void> builder(ExecutionListener...listener
6262
public static DescriptorConfiguratorBuilder.Start<Void> create(Class<?> topBlock) {
6363
checkNotNull(topBlock, "a starting block is required");
6464

65-
AnnotationIntrospector introspector = new AnnotationIntrospector();
66-
DescriptorOutline outline = introspector.createDescriptor(topBlock);
65+
DescriptorOutline outline = AnnotationIntrospector.createDescriptor(topBlock);
6766
DescriptorConfiguratorHelperImpl helper = new DescriptorConfiguratorHelperImpl(outline);
6867

6968
return DescriptorConfiguratorGenerator.create(helper);
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*********************************************************************
2+
Copyright 2015 the Flapi authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
********************************************************************/
16+
17+
package unquietcode.tools.flapi;
18+
19+
import com.google.common.base.Function;
20+
import com.google.common.collect.Sets;
21+
22+
import java.util.*;
23+
24+
public final class Utilities {
25+
26+
private Utilities() { }
27+
28+
29+
// TODO use JDK8 classes
30+
31+
public static <T> void safeRecurse(T first, Function<T, Collection<T>> function) {
32+
safeRecurse(Arrays.asList(first), function);
33+
}
34+
35+
public static <T> void safeRecurse(Collection<T> first, Function<T, Collection<T>> function) {
36+
safeRecurse(Sets.<T>newIdentityHashSet(), first, function);
37+
}
38+
39+
private static <T> void safeRecurse(Set<T> seen, Collection<T> first, Function<T, Collection<T>> function) {
40+
List<Collection<T>> collections = new ArrayList<>();
41+
collections.add(first);
42+
43+
while (!collections.isEmpty()) {
44+
Collection<T> collection = collections.remove(0);
45+
if (collection == null || collection.isEmpty()) { continue; }
46+
47+
for (T e : collection) {
48+
if (seen.contains(e)) { continue; }
49+
else { seen.add(e); }
50+
51+
Collection<T> next = function.apply(e);
52+
if (next != null) { collections.add(next); }
53+
}
54+
}
55+
}
56+
}

src/main/java/unquietcode/tools/flapi/annotations/AnnotationIntrospector.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
public class AnnotationIntrospector extends IntrospectorSupport {
4646
private final Map<Class<?>, BlockOutline> blocks = new HashMap<Class<?>, BlockOutline>();
4747

48-
4948
public static boolean isAnnotated(Class<?> clazz) {
5049

5150
// for every method in the class
@@ -67,15 +66,16 @@ public static boolean isAnnotated(Class<?> clazz) {
6766

6867
private static Set<Method> getAllMethods(Class<?> clazz) {
6968
Method[] methods = SpringMethodUtils.getUniqueDeclaredMethods(clazz);
70-
return new HashSet<Method>(Arrays.asList(methods));
69+
return new HashSet<>(Arrays.asList(methods));
7170
}
7271

73-
public DescriptorOutline createDescriptor(Class<?> clazz) {
72+
public static DescriptorOutline createDescriptor(Class<?> clazz) {
7473
DescriptorOutline descriptor = new DescriptorOutline();
7574
descriptor.setPackageName(clazz.getPackage().getName() + ".builder");
7675

7776
// discover methods and set them on the blocks
78-
boolean found = handleClass(descriptor, clazz);
77+
AnnotationIntrospector introspector = new AnnotationIntrospector();
78+
boolean found = introspector.handleClass(descriptor, clazz);
7979

8080
if (found) {
8181
return descriptor;
@@ -87,6 +87,10 @@ public DescriptorOutline createDescriptor(Class<?> clazz) {
8787
return descriptor;
8888
}
8989

90+
public static BlockOutline createBlock(Class<?> clazz) {
91+
return createDescriptor(clazz);
92+
}
93+
9094
private BlockOutline handleClass(Class<?> blockClass) {
9195
if (blocks.containsKey(blockClass)) {
9296
return blocks.get(blockClass);

src/main/java/unquietcode/tools/flapi/graph/GraphBuilder.java

Lines changed: 7 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -35,47 +35,18 @@ public class GraphBuilder {
3535
private Map<BlockReference, BlockOutline> referenceMap = new IdentityHashMap<BlockReference, BlockOutline>();
3636

3737
public StateClass buildGraph(DescriptorOutline descriptor) {
38+
3839
// resolve block references
39-
Map<String, BlockOutline> blocks = new HashMap<String, BlockOutline>();
40-
findAllBlocks(blocks, descriptor);
40+
Map<String, BlockOutline> blocks = BlockOutline.findAllBlocks(descriptor);
4141
initializeReferenceMap(blocks, referenceMap, descriptor);
4242

4343
return convertBlock(descriptor);
4444
}
4545

46-
private static void findAllBlocks(Map<String, BlockOutline> blocks, BlockOutline block) {
47-
if (block instanceof BlockReference) {
48-
return;
49-
}
50-
51-
final String blockName = block.getName();
52-
53-
// Defensive, but really it is never ok to have reference cycles,
54-
// and usually it means the helpers were called incorrectly.
55-
if (blocks.containsKey(blockName)) {
56-
if (blocks.get(blockName) == block) {
57-
return;
58-
} else {
59-
throw new DescriptorBuilderException("Duplicate block name: "+blockName);
60-
}
61-
}
62-
63-
blocks.put(blockName, block);
64-
65-
for (MethodOutline method : block.getAllMethods()) {
66-
for (BlockOutline chain : method.getBlockChain()) {
67-
findAllBlocks(blocks, chain);
68-
}
69-
}
70-
71-
for (BlockOutline child : block.getBlocks()) {
72-
findAllBlocks(blocks, child);
73-
}
74-
}
75-
76-
private static void initializeReferenceMap(Map<String, BlockOutline> blocks,
77-
Map<BlockReference, BlockOutline> references,
78-
BlockOutline block
46+
private static void initializeReferenceMap(
47+
Map<String, BlockOutline> blocks,
48+
Map<BlockReference, BlockOutline> references,
49+
BlockOutline block
7950
){
8051
if (block instanceof BlockReference) {
8152
final BlockReference reference = (BlockReference) block;
@@ -131,7 +102,7 @@ private StateClass convertBlock(BlockOutline block) {
131102
// create the sibling states
132103
Set<StateClass> seen = Collections.newSetFromMap(new IdentityHashMap<StateClass, Boolean>());
133104
Set<Set<MethodOutline>> workingSet = new HashSet<Set<MethodOutline>>();
134-
workingSet.add(new TreeSet<MethodOutline>(allMethods));
105+
workingSet.add(new TreeSet<>(allMethods));
135106

136107
while (!workingSet.isEmpty()) {
137108
Set<Set<MethodOutline>> nextSet = new HashSet<Set<MethodOutline>>();

src/main/java/unquietcode/tools/flapi/helpers/BlockHelperImpl.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package unquietcode.tools.flapi.helpers;
1818

1919
import unquietcode.tools.flapi.DescriptorBuilderException;
20+
import unquietcode.tools.flapi.annotations.AnnotationIntrospector;
2021
import unquietcode.tools.flapi.builder.Block.BlockHelper;
2122
import unquietcode.tools.flapi.builder.Method.MethodHelper;
2223
import unquietcode.tools.flapi.outline.BlockOutline;
@@ -120,6 +121,27 @@ static void _addEnumSelector(BlockOutline block, Class clazz, String methodSigna
120121
}
121122
}
122123

124+
@Override
125+
public void addMixin(Class helper) {
126+
_addMixin(block, helper);
127+
}
128+
129+
static void _addMixin(BlockOutline block, Class mixin) {
130+
if (!AnnotationIntrospector.isAnnotated(mixin)) {
131+
System.err.println("the provided mixin type contains no Flapi method annotations");
132+
}
133+
block.addMixin(mixin);
134+
}
135+
136+
@Override
137+
public void addMixin(String blockName) {
138+
_addMixin(block, blockName);
139+
}
140+
141+
static void _addMixin(BlockOutline block, String mixin) {
142+
block.addMixin(mixin);
143+
}
144+
123145
@Override
124146
public void endBlock() {
125147
// nothing

src/main/java/unquietcode/tools/flapi/helpers/DescriptorHelperImpl.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,14 @@ public void startBlock(String methodSignature, AtomicReference<MethodHelper> _he
9292
public void addEnumSelector(Class clazz, String methodSignature, AtomicReference<MethodHelper> _helper1) {
9393
BlockHelperImpl._addEnumSelector(outline, clazz, methodSignature, _helper1);
9494
}
95+
96+
@Override
97+
public void addMixin(Class helper) {
98+
BlockHelperImpl._addMixin(outline, helper);
99+
}
100+
101+
@Override
102+
public void addMixin(String blockName) {
103+
BlockHelperImpl._addMixin(outline, blockName);
104+
}
95105
}

src/main/java/unquietcode/tools/flapi/outline/BlockOutline.java

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ public class BlockOutline implements Outline {
4242
// block methods
4343
private final Set<MethodOutline> methods = new TreeSet<MethodOutline>();
4444

45-
// ------------------------------ //
45+
// mixins
46+
private final Set<String> mixinBlocks = new HashSet<>();
47+
private final Set<Class<?>> mixinTypes = new HashSet<>();
48+
49+
// ---------------------------------------------------- //
4650

4751
public Class<?> getHelperClass() {
4852
return helperClass;
@@ -88,6 +92,22 @@ public void setName(String name) {
8892
this.name = name.trim();
8993
}
9094

95+
public void addMixin(String block) {
96+
mixinBlocks.add(Objects.requireNonNull(block));
97+
}
98+
99+
public void addMixin(Class<?> block) {
100+
mixinTypes.add(Objects.requireNonNull(block));
101+
}
102+
103+
public Set<String> getBlockMixins() {
104+
return mixinBlocks;
105+
}
106+
107+
public Set<Class<?>> getClassMixins() {
108+
return mixinTypes;
109+
}
110+
91111
public BlockOutline addBlock(String blockName) {
92112
BlockOutline block = new BlockOutline();
93113
block.name = blockName;
@@ -127,4 +147,48 @@ public Set<MethodOutline> getTriggeredMethods() {
127147
public Set<MethodOutline> getAllMethods() {
128148
return methods;
129149
}
130-
}
150+
151+
//---o---o---o---o---o---o---o---o---o---o---o---o---o---o---o---o---o---o---o---o---o---o---//
152+
153+
public static Map<String, BlockOutline> findAllBlocks(BlockOutline block) {
154+
Map<String, BlockOutline> blocks = new HashMap<>();
155+
findAllBlocks(blocks, block);
156+
return blocks;
157+
}
158+
159+
private static void findAllBlocks(Map<String, BlockOutline> blocks, BlockOutline block) {
160+
if (block instanceof BlockReference) {
161+
return;
162+
}
163+
164+
final String blockName = block.getName();
165+
166+
// Defensive, but really it is never ok to have reference cycles,
167+
// and usually it means the helpers were called incorrectly.
168+
if (blocks.containsKey(blockName)) {
169+
if (blocks.get(blockName) == block) {
170+
return;
171+
} else {
172+
throw new DescriptorBuilderException("Duplicate block name: "+blockName);
173+
}
174+
}
175+
176+
blocks.put(blockName, block);
177+
178+
for (MethodOutline method : block.getAllMethods()) {
179+
for (BlockOutline chain : method.getBlockChain()) {
180+
findAllBlocks(blocks, chain);
181+
}
182+
}
183+
184+
for (BlockOutline child : block.getBlocks()) {
185+
findAllBlocks(blocks, child);
186+
}
187+
}
188+
189+
protected final void applyMixin(BlockOutline other) {
190+
for (MethodOutline method : other.getAllMethods()) {
191+
this.methods.add(method.copy());
192+
}
193+
}
194+
}

0 commit comments

Comments
 (0)