##// END OF EJS Templates
WIP variant sources model
cin -
r40:924d9107c025 default
parent child
Show More
@@ -0,0 +1,29
1 package org.implab.gradle.common.core.lang;
2
3 import java.util.LinkedList;
4 import java.util.List;
5 import java.util.function.Consumer;
6
7 public final class Deferred<T> {
8 private final List<Consumer<T>> listeners = new LinkedList<>();
9 private T value;
10 private boolean resolved = false;
11
12 public void resolve(T value) {
13 if (resolved) {
14 throw new IllegalStateException("Already resolved");
15 }
16 this.value = value;
17 this.resolved = true;
18 listeners.forEach(listener -> listener.accept(value));
19 listeners.clear();
20 }
21
22 public void whenResolved(Consumer<T> listener) {
23 if (resolved) {
24 listener.accept(value);
25 } else {
26 listeners.add(listener);
27 }
28 }
29 }
@@ -0,0 +1,346
1 # Identity-First Model: Preserving Lazy Semantics Without Custom Events
2
3 In build and configuration systems, the same tension appears again and again:
4
5 * you want an **observable model** that consumers can subscribe to using something like `all(...)`
6 * you do **not** want that subscription to destroy laziness
7 * and you would prefer to avoid introducing a custom event bus, event ordering, and a separate lifecycle model
8
9 A practical way to solve this is to **separate identity from computed state**.
10
11 ## Core idea
12
13 The idea is simple:
14
15 * one object is responsible only for **identity** and minimal selection metadata
16 * the actual aggregate content is obtained through **separate API calls**
17 * those API calls may be lazy, expensive, cached, provider-based, or computed on demand
18
19 So instead of one “fat” object, we get two layers.
20
21 ### 1. Identity layer
22
23 Lightweight objects that:
24
25 * are cheap to create
26 * are effectively immutable
27 * are safe to observe eagerly
28 * work well as keys and subscription points
29
30 ### 2. State / aggregate access layer
31
32 Separate APIs that:
33
34 * resolve or compute content from identity
35 * do heavy work only when needed
36 * preserve lazy semantics where that actually matters
37
38 This is especially useful when a collection must be replayable, but should not drag expensive materialization along with it.
39
40 ---
41
42 ## The problem this solves
43
44 Consider a typical Gradle-like scenario.
45
46 An adapter wants to subscribe to a collection:
47
48 ```java
49 projections.getProjections().all(projection -> {
50 ...
51 });
52 ```
53
54 If `projection` is a heavy object that already contains:
55
56 * computed bindings
57 * providers of source sets
58 * derived state
59 * partially materialized objects
60
61 then `all(...)` starts forcing things that were supposed to stay lazy.
62
63 Typical symptoms:
64
65 * lazy semantics are weakened or broken
66 * coupling increases
67 * plugin application order starts to matter
68 * custom events begin to look tempting: `onCreated`, `onResolved`, `onMaterialized`
69
70 The problem is not that `all(...)` is bad.
71 The problem is that **too much meaning has been packed into the observable object**.
72
73 ---
74
75 ## The solution: observe identity, not state
76
77 If the observable collection contains only identity objects, the picture changes.
78
79 For example:
80
81 ```java
82 public interface SourceSetProjection extends Named {
83 }
84 ```
85
86 This object contains only identity:
87
88 * `name`
89 * perhaps a small amount of selection metadata
90 * but not heavy aggregate state
91
92 Now this subscription:
93
94 ```java
95 projections.getProjections().all(projection -> {
96 ...
97 });
98 ```
99
100 is no longer dangerous.
101 It eagerly materializes only cheap keys, not the full aggregate graph.
102
103 The actual content is requested separately:
104
105 ```java
106 Set<VariantLayerBinding> bindings = projections.getBindings(projection);
107 NamedDomainObjectProvider<GenericSourceSet> sourceSet =
108 materializer.getSourceSet(projection.getName());
109 ```
110
111 Those calls may remain:
112
113 * lazy
114 * computed
115 * cached
116 * tied to runtime lifecycle
117 * delegated to a dedicated materializer
118
119 ---
120
121 ## Example
122
123 ### A problematic design
124
125 Suppose we define projection like this:
126
127 ```java
128 public interface SourceSetProjection extends Named {
129 Set<VariantLayerBinding> getBindings();
130 NamedDomainObjectProvider<GenericSourceSet> getSourceSet();
131 }
132 ```
133
134 This is problematic because `SourceSetProjection` is no longer just identity. It is already close to an aggregate.
135
136 It mixes:
137
138 * symbolic identity
139 * relation data
140 * runtime references into a foreign domain
141
142 Subscribing via `all(...)` now risks pulling in much more than intended.
143
144 The type says “projection”, but internally it already carries half the system.
145
146 ---
147
148 ### A cleaner design
149
150 Split responsibilities instead:
151
152 ```java
153 public interface SourceSetProjection extends Named {
154 }
155 ```
156
157 ```java
158 public interface SourceSetProjections {
159 NamedDomainObjectCollection<SourceSetProjection> getProjections();
160 Set<VariantLayerBinding> getBindings(String sourceSetName);
161 }
162 ```
163
164 ```java
165 public interface SourceSetMaterializer {
166 NamedDomainObjectProvider<GenericSourceSet> getSourceSet(String sourceSetName);
167 }
168 ```
169
170 Now the adapter flow looks like this:
171
172 ```java
173 projections.getProjections().all(projection -> {
174 Set<VariantLayerBinding> bindings =
175 projections.getBindings(projection.getName());
176
177 NamedDomainObjectProvider<GenericSourceSet> sourceSet =
178 materializer.getSourceSet(projection.getName());
179
180 // apply adapter-specific policy
181 });
182 ```
183
184 What changed:
185
186 * replayable subscription is preserved
187 * eager observation is acceptable because `SourceSetProjection` is cheap
188 * expensive and computed state has moved to separate APIs
189 * materialization remains under the control of a single owner
190
191 ---
192
193 ## Why this is often better than events
194
195 When identity and state are mixed together, people quickly start inventing events:
196
197 * `projectionCreated`
198 * `projectionResolved`
199 * `sourceSetAvailable`
200 * `sourceSetMaterialized`
201
202 That usually happens because it becomes important to know **when exactly** an object is “ready enough”.
203
204 If the observable object contains only identity, and heavy state is obtained separately, then many of those events become unnecessary.
205
206 The architecture becomes calmer:
207
208 * an **identity registry**
209 * a **lookup API** for relations
210 * a **lazy materialization API** for heavy objects
211
212 Instead of saying:
213
214 > “When this object becomes sufficiently ready, I will react.”
215
216 you can say:
217
218 > “I can observe identity immediately, and ask for the expensive state only when I actually need it.”
219
220 This is easier to reason about, easier to test, and usually easier to evolve.
221
222 ---
223
224 ## What should live inside an identity object
225
226 An identity object does not have to be completely empty.
227 It may carry **selection metadata**, as long as that metadata is:
228
229 * cheap
230 * stable
231 * not expensive to initialize
232 * not turning the object into an aggregate
233
234 Typical examples:
235
236 * `id`
237 * `name`
238 * `kind`
239 * `type`
240 * domain key
241
242 What should usually stay out:
243
244 * computed aggregate content
245 * runtime references to foreign domains
246 * lazy providers of heavy objects
247 * derived state that can trigger premature materialization
248
249 A useful rule of thumb:
250
251 **An identity object contains selection metadata; aggregate content is obtained separately.**
252
253 ---
254
255 ## Why this works well with `all(...)`
256
257 `all(...)` weakens laziness only if the observed objects are themselves heavy or stateful.
258
259 If the observed objects are:
260
261 * cheap
262 * identity-only
263 * effectively immutable
264
265 then eager observation is usually acceptable.
266
267 So the real principle is:
268
269 **Eager observation of identity is often harmless.
270 Eager observation of computed state is not.**
271
272 That is why `all(...)` can be perfectly fine for collections of:
273
274 * `Variant`
275 * `Layer`
276 * `Role`
277 * `SourceSetProjection`
278
279 as long as those objects stay on the identity side of the boundary.
280
281 ---
282
283 ## Where this principle is especially useful
284
285 This approach is particularly effective when:
286
287 * there is replayable observation via `all(...)`
288 * identity objects are cheap and stable
289 * aggregate content may be expensive
290 * symbolic model and runtime model should remain separate
291 * you want to avoid building a custom event system
292
293 For Gradle-like models, this is often a very natural fit.
294
295 ---
296
297 ## When it is unnecessary
298
299 If an object is:
300
301 * small
302 * cheap
303 * and already fully represents its useful content
304
305 then splitting identity and state may be overengineering.
306
307 So this is not a universal rule. It is a tool to use when there is real tension between:
308
309 * key
310 * state
311 * computation
312 * runtime reference
313
314 ---
315
316 ## Practical conclusion
317
318 The principle can be summarized like this:
319
320 1. **Identity objects** hold only identity and cheap selection metadata.
321 2. **Aggregate content** is not stored inside them, but retrieved through separate API calls.
322 3. Those API calls may perform:
323
324 * lazy resolution
325 * caching
326 * heavy computation
327 * materialization on demand
328 4. This makes it possible to use replayable mechanisms such as `all(...)` without destroying laziness where laziness actually matters.
329
330 This is how you can combine:
331
332 * simple observation
333 * a clean model
334 * no custom event bus
335 * lazy materialization of heavy state
336
337 ---
338
339 # Short design note version
340
341 A concise version of the same principle:
342
343 > Use identity objects as cheap, observable keys.
344 > Keep expensive or computed aggregate content out of them.
345 > Resolve that content through separate APIs on demand.
346 > This allows replayable observation (`all(...)`) without forcing premature materialization, and often removes the need for a custom event model.
@@ -0,0 +1,306
1 # Identity-first model: как сохранить lazy semantics без своих событий
2
3 В системах конфигурации и сборки почти всегда возникает одно и то же напряжение:
4
5 * хочется иметь **наблюдаемую модель**, на которую можно подписаться через что-то вроде `all(...)`
6 * но не хочется, чтобы такая подписка **ломала ленивость**
7 * ещё меньше хочется тащить в архитектуру собственную шину событий, порядок подписки и отдельный lifecycle
8
9 Один из рабочих способов решить это — **разделить identity и вычисляемое состояние**.
10
11 ## Суть принципа
12
13 Идея простая:
14
15 * отдельный объект отвечает только за **identity** и минимальные метаданные выбора
16 * всё содержательное состояние агрегата получается **отдельными вызовами API**
17 * эти вызовы уже могут быть lazy, дорогими, вычисляемыми, кэшируемыми, провайдерными — какими угодно
18
19 То есть вместо одного “толстого” объекта мы получаем два слоя:
20
21 ### 1. Identity layer
22
23 Лёгкие объекты, которые:
24
25 * дёшево создаются
26 * не мутируют
27 * безопасно наблюдаются eagerly
28 * годятся как ключи и точки подписки
29
30 ### 2. State / aggregate access layer
31
32 Отдельные API, которые:
33
34 * по identity находят или вычисляют содержимое
35 * делают heavy work только по требованию
36 * могут сохранять ленивую семантику
37
38 Это особенно полезно там, где коллекция должна быть replayable, но при этом не должна тащить за собой дорогую материализацию.
39
40 ---
41
42 ## Проблема, которую это решает
43
44 Рассмотрим типичную ситуацию в Gradle-подобной модели.
45
46 Есть коллекция объектов, на которую адаптер хочет подписаться:
47
48 ```java
49 projections.getProjections().all(projection -> {
50 ...
51 });
52 ```
53
54 Если `projection` — это тяжёлый объект, внутри которого уже лежат:
55
56 * вычисленные bindings
57 * провайдеры на source sets
58 * derived state
59 * полу-материализованные сущности
60
61 то `all(...)` начинает рано раскрывать то, что хотелось оставить ленивым.
62
63 Появляются симптомы:
64
65 * ломается lazy semantics
66 * растёт связность
67 * становится важен порядок применения плагинов
68 * хочется заводить собственные события: `onCreated`, `onResolved`, `onMaterialized`
69
70 Архитектура начинает скрипеть не потому, что `all(...)` плох, а потому что **слишком много смысла засунуто в наблюдаемый объект**.
71
72 ---
73
74 ## Решение: наблюдать identity, а не состояние
75
76 Если сделать наблюдаемыми только identity objects, картина меняется.
77
78 Например:
79
80 ```java
81 public interface SourceSetProjection extends Named {
82 }
83 ```
84
85 Такой объект содержит только identity:
86
87 * `name`
88 * возможно, ещё пару метаданных выбора
89 * но не тяжёлое содержимое
90
91 Тогда подписка:
92
93 ```java
94 projections.getProjections().all(projection -> {
95 ...
96 });
97 ```
98
99 больше не страшна.
100 Она eagerly materializes только дешёвые ключи, а не весь агрегатный граф.
101
102 Содержимое запрашивается отдельно:
103
104 ```java
105 Set<VariantLayerBinding> bindings = projections.getBindings(projection);
106 NamedDomainObjectProvider<GenericSourceSet> sourceSet =
107 materializer.getSourceSet(projection.getName());
108 ```
109
110 И вот эти вызовы уже могут быть:
111
112 * lazy
113 * вычисляемыми
114 * кэшируемыми
115 * привязанными к runtime lifecycle
116
117 ---
118
119 ## Пример
120
121 ### Неудачный вариант
122
123 Представим такой интерфейс:
124
125 ```java
126 public interface SourceSetProjection extends Named {
127 Set<VariantLayerBinding> getBindings();
128 NamedDomainObjectProvider<GenericSourceSet> getSourceSet();
129 }
130 ```
131
132 Проблемы тут сразу видны:
133
134 * `SourceSetProjection` уже не identity object, а почти агрегат
135 * внутри смешаны:
136
137 * symbolic identity
138 * relation data
139 * runtime reference в чужой домен
140 * подписка через `all(...)` начинает тащить за собой больше, чем хотелось бы
141
142 На словах объект называется “projection”, а по факту внутри у него уже полсистемы.
143
144 ---
145
146 ### Более удачный вариант
147
148 Разделяем ответственность:
149
150 ```java
151 public interface SourceSetProjection extends Named {
152 }
153 ```
154
155 ```java
156 public interface SourceSetProjections {
157 NamedDomainObjectCollection<SourceSetProjection> getProjections();
158 Set<VariantLayerBinding> getBindings(String sourceSetName);
159 }
160 ```
161
162 ```java
163 public interface SourceSetMaterializer {
164 NamedDomainObjectProvider<GenericSourceSet> getSourceSet(String sourceSetName);
165 }
166 ```
167
168 Теперь сценарий адаптера выглядит так:
169
170 ```java
171 projections.getProjections().all(projection -> {
172 Set<VariantLayerBinding> bindings =
173 projections.getBindings(projection.getName());
174
175 NamedDomainObjectProvider<GenericSourceSet> sourceSet =
176 materializer.getSourceSet(projection.getName());
177
178 // apply adapter-specific policy
179 });
180 ```
181
182 Что изменилось:
183
184 * replayable подписка сохранилась
185 * eager materialization допустима, потому что `SourceSetProjection` дешёвый
186 * дорогое и вычисляемое состояние ушло в отдельные API
187 * materialization остаётся под контролем одного владельца
188
189 ---
190
191 ## Почему это лучше событий
192
193 Когда в модели смешаны identity и состояние, очень быстро хочется изобретать события:
194
195 * `projectionCreated`
196 * `projectionResolved`
197 * `sourceSetAvailable`
198 * `sourceSetMaterialized`
199
200 Потому что в какой-то момент становится важно, **когда именно** объект уже “достаточно готов”.
201
202 Если же наблюдаемый объект содержит только identity, а всё тяжёлое получается отдельными вызовами, событийная модель часто вообще не нужна.
203
204 Получается более спокойная схема:
205
206 * есть **identity registry**
207 * есть **lookup API** для связей
208 * есть **lazy materialization API** для тяжёлых сущностей
209
210 То есть вместо событий:
211
212 > “когда объект станет достаточно готов, я что-то сделаю”
213
214 получается обычный и понятный flow:
215
216 > “я вижу identity, а нужное состояние спрошу отдельно, когда оно мне действительно понадобится”
217
218 Это проще и для reasoning, и для тестирования, и для эволюции API.
219
220 ---
221
222 ## Что именно должно жить в identity object
223
224 Identity object не обязан быть “абсолютно пустым”.
225 Он может содержать **метаданные выбора**, если они:
226
227 * дешевы
228 * стабильны
229 * не требуют тяжёлой инициализации
230 * не превращают объект в агрегат
231
232 Например:
233
234 * `id`
235 * `name`
236 * `kind`
237 * `domain key`
238 * maybe `type`
239
240 Но он не должен содержать:
241
242 * вычисляемое содержимое агрегата
243 * ссылки времени выполнения на чужие домены
244 * lazy providers на heavy objects
245 * derived state, который может провоцировать раннюю материализацию
246
247 Хорошая практическая формула:
248
249 **identity object contains selection metadata; aggregate content is obtained separately.**
250
251 ---
252
253 ## Когда этот принцип особенно полезен
254
255 Он особенно хорош, если:
256
257 * есть replayable наблюдение через `all(...)`
258 * identity-объекты дешёвые и почти immutable
259 * содержимое агрегата может быть дорогим
260 * нужен clean split между symbolic model и runtime model
261 * хочется избежать собственной событийной шины
262
263 Для Gradle-подобных моделей это вообще очень естественный приём.
264
265 ---
266
267 ## Когда он не нужен
268
269 Если объект:
270
271 * маленький
272 * дешёвый
273 * уже сам по себе и есть всё его содержимое
274
275 то разделение на identity и отдельные lookup API может быть лишним.
276
277 То есть этот принцип полезен не как догма, а как инструмент.
278 Его стоит применять там, где реально есть риск смешения:
279
280 * ключа
281 * состояния
282 * вычисления
283 * runtime reference
284
285 ---
286
287 ## Практический итог
288
289 Если сформулировать коротко, то принцип такой:
290
291 1. **Identity objects** содержат только identity и дешёвые метаданные выбора.
292 2. **Агрегатное содержимое** не хранится внутри них, а получается отдельными API-вызовами.
293 3. Эти API уже могут выполнять:
294
295 * lazy resolution
296 * кэширование
297 * heavy computation
298 * materialization
299 4. Благодаря этому можно безопасно использовать replayable механизмы вроде `all(...)`, не разрушая ленивую семантику там, где она действительно важна.
300
301 Именно так удаётся совместить:
302
303 * простое наблюдение
304 * чистую модель
305 * отсутствие собственных событий
306 * ленивую материализацию тяжёлого состояния
@@ -0,0 +1,185
1 package org.implab.gradle.variants;
2
3 import java.util.ArrayList;
4 import java.util.HashMap;
5 import java.util.HashSet;
6 import java.util.List;
7 import java.util.Map;
8 import java.util.Set;
9 import java.util.function.Consumer;
10 import java.util.stream.Stream;
11
12 import org.eclipse.jdt.annotation.NonNullByDefault;
13 import org.gradle.api.Action;
14 import org.gradle.api.NamedDomainObjectCollection;
15 import org.gradle.api.NamedDomainObjectContainer;
16 import org.gradle.api.NamedDomainObjectProvider;
17 import org.gradle.api.Plugin;
18 import org.gradle.api.Project;
19 import org.implab.gradle.common.core.lang.Deferred;
20 import org.implab.gradle.common.core.lang.Strings;
21 import org.implab.gradle.common.sources.GenericSourceSet;
22 import org.implab.gradle.common.sources.SourcesPlugin;
23 import org.implab.gradle.variants.model.Layer;
24 import org.implab.gradle.variants.model.Variant;
25 import org.implab.gradle.variants.model.VariantsExtension;
26 import org.implab.gradle.variants.sources.LayerProjectionRule;
27 import org.implab.gradle.variants.sources.SourceSetMaterializer;
28 import org.implab.gradle.variants.sources.SourceSetProjection;
29 import org.implab.gradle.variants.sources.SourceSetProjections;
30 import org.implab.gradle.variants.sources.VariantLayerBinding;
31 import org.implab.gradle.variants.sources.VariantSourcesContext;
32 import org.implab.gradle.variants.sources.VariantSourcesExtension;
33
34 @NonNullByDefault
35 public abstract class VariantSourcesPlugin implements Plugin<Project> {
36 @Override
37 public void apply(Project target) {
38 // Apply the main VariantsPlugin to ensure the core variant model is available.
39 target.getPlugins().apply(VariantsPlugin.class);
40 target.getPlugins().apply(SourcesPlugin.class);
41 // Access the VariantsExtension to configure variant sources.
42 var variantsExtension = target.getExtensions().getByType(VariantsExtension.class);
43 var objectFactory = target.getObjects();
44
45 var sources = SourcesPlugin.getSourcesExtension(target);
46
47 var deferred = new Deferred<VariantSourcesContext>();
48 var layerProjectionRules = objectFactory.domainObjectContainer(LayerProjectionRule.class);
49
50 var variantSourcesExtension = new VariantSourcesExtension() {
51 @Override
52 public NamedDomainObjectContainer<LayerProjectionRule> getLayerRules() {
53 return layerProjectionRules;
54 }
55
56 @Override
57 public void whenFinalized(Action<? super VariantSourcesContext> action) {
58 deferred.whenResolved(action::execute);
59 }
60 };
61 target.getExtensions().add(VariantSourcesExtension.class, "variantSources", variantSourcesExtension);
62
63 // create convention to automatically create layer projection rules for each
64 // variant layer
65 variantsExtension.getLayers().all(layer -> {
66 // Automatically create a layer projection rule for each variant layer.
67 variantSourcesExtension.layer(layer.getName(), rule -> {
68 // Configure the source set name pattern based on the layer name.
69 rule.getSourceSetNamePattern()
70 .convention("{variant}{layerCapitalized}")
71 .finalizeValueOnRead();
72 });
73 });
74
75 var projections = objectFactory.domainObjectContainer(SourceSetProjection.class);
76
77 Map<String, Set<VariantLayerBinding>> projectionBindings = new HashMap<>();
78
79 var sourceSetProjections = new SourceSetProjections() {
80 @Override
81 public NamedDomainObjectCollection<SourceSetProjection> getProjections() {
82 return projections;
83 }
84
85 @Override
86 public Set<VariantLayerBinding> getBindings(String sourceSetName) {
87 return projectionBindings.getOrDefault(sourceSetName, Set.of());
88 }
89
90 @Override
91 public Set<VariantLayerBinding> getBindings(SourceSetProjection projection) {
92 return getBindings(projection.getName());
93 }
94 };
95
96 Set<String> materializedSourceSets = new HashSet<>();
97
98 var materializer = new SourceSetMaterializer() {
99 @Override
100 public NamedDomainObjectProvider<GenericSourceSet> getSourceSet(String sourceSetName) {
101 return materializedSourceSets.add(sourceSetName)
102 ? sources.register(sourceSetName)
103 : sources.named(sourceSetName);
104 }
105 };
106
107 var bindings = new VariantBindings();
108
109 target.afterEvaluate(t -> {
110 // Once the project is evaluated, resolve the deferred context and finalize the
111 // sources configuration.
112 variantsExtension.getLayers().all(bindings::addLayer);
113 variantsExtension.getVariants().all(bindings::addVariant);
114
115 variantsExtension.getLayers().all(layer -> {
116 // For each layer, apply the projection rules to generate source set projections.
117
118 var rule = layerProjectionRules.maybeCreate(layer.getName());
119 var pattern = rule.getSourceSetNamePattern().getOrElse("{variant}{layerCapitalized}");
120 // Generate source set names based on the pattern and variant/layer information.
121 // This is a simplified example; real implementation would need to consider
122 // all variants and layers.
123 var sourceSetName = pattern.replace("{layer}", layer.getName())
124 .replace("{variant}", "main") // Placeholder for actual variant name
125 .replace("{layerCapitalized}", Strings.capitalize(layer.getName()));
126
127 var projection = objectFactory.newInstance(SourceSetProjection.class, sourceSetName);
128 projections.add(projection);
129 // Bind the projection to the corresponding variant layer.
130 projectionBindings.computeIfAbsent(sourceSetName, k -> new HashSet<>())
131 .add(new VariantLayerBinding(layer.getName(), projection));
132 });
133
134 var context = new VariantSourcesContext() {
135
136 @Override
137 public SourceSetProjections getProjections() {
138 return sourceSetProjections;
139 }
140
141 @Override
142 public SourceSetMaterializer getMaterializer() {
143 return materializer;
144 }
145
146 // Implementation of the context that provides access to variant and layer
147 // information.
148 };
149 deferred.resolve(context);
150 });
151 }
152
153 class VariantBindings {
154 private final Set<Layer> layers = new HashSet<>();
155 private final Set<Variant> variants = new HashSet<>();
156
157 private final List<Consumer<? super VariantLayerBinding>> listeners = new ArrayList<>();
158
159 void addLayer(Layer layer) {
160 layers.add(layer);
161 variants.stream()
162 .map(variant -> VariantLayerBinding.of(variant, layer))
163 .forEach(this::notifyBindingAdded);
164 }
165
166 void addVariant(Variant variant) {
167 variants.add(variant);
168 layers.stream()
169 .map(layer -> VariantLayerBinding.of(variant, layer))
170 .forEach(this::notifyBindingAdded);
171 }
172
173 void whenBindingAdded(Consumer<? super VariantLayerBinding> listener) {
174 layers.stream()
175 .flatMap(layer -> variants.stream().map(variant -> VariantLayerBinding.of(variant, layer)))
176 .forEach(listener);
177 listeners.add(listener);
178 }
179
180 private void notifyBindingAdded(VariantLayerBinding binding) {
181 listeners.forEach(listener -> listener.accept(binding));
182 }
183 }
184
185 }
@@ -0,0 +1,70
1 package org.implab.gradle.variants.model;
2
3 import java.util.HashSet;
4 import java.util.Set;
5 import java.util.function.Consumer;
6 import java.util.stream.Stream;
7
8 import org.gradle.api.Action;
9 import org.gradle.api.Named;
10 import org.gradle.api.provider.SetProperty;
11 import org.implab.gradle.common.core.lang.Closures;
12 import org.implab.gradle.common.core.lang.Strings;
13
14 import groovy.lang.Closure;
15
16 public interface VariantDefinition extends Named {
17 /**
18 * Role bindings declared inside this variant.
19 *
20 * The binding pair of role and layer names.
21 */
22 SetProperty<RoleLayerBinding> getRoleBindings();
23
24 /**
25 * Creates or returns an existing role binding and configures it.
26 */
27 default void role(String name, Action<? super RoleSpec> action) {
28 var spec = new RoleSpec(name);
29 action.execute(spec);
30 spec.accept(getRoleBindings()::add);
31 }
32
33 default void role(String name, Closure<?> closure) {
34 role(name, Closures.action(closure));
35 }
36
37 default void finalizeVariant() {
38 getRoleBindings().finalizeValue();
39 }
40
41 public static class RoleSpec {
42 private final String name;
43 private final Set<String> layerNames;
44
45 public RoleSpec(String name) {
46 this.name = name;
47 this.layerNames = new HashSet<>();
48 }
49
50 public String getName() {
51 return name;
52 }
53
54 public Set<String> getLayerNames() {
55 return layerNames;
56 }
57
58 public void layers(String name, String... extraNames) {
59 Stream.concat(Stream.of(name), Stream.of(extraNames))
60 .map(Strings::requireNonBlank)
61 .forEach(this.layerNames::add);
62 }
63
64 void accept(Consumer<? super RoleLayerBinding> consumer) {
65 layerNames.stream()
66 .map(layerName -> new RoleLayerBinding(name, layerName))
67 .forEach(consumer);
68 }
69 }
70 }
@@ -0,0 +1,26
1 package org.implab.gradle.variants.sources;
2
3 import org.gradle.api.Action;
4 import org.gradle.api.Named;
5 import org.gradle.api.provider.Property;
6
7 /**
8 * Projection rule for a layer.
9 */
10 public interface LayerProjectionRule extends Named {
11
12 /**
13 * Pattern used to calculate the source set name.
14 * Examples:
15 * "{layer}"
16 * "{variant}{layerCapitalized}"
17 */
18 Property<String> getSourceSetNamePattern();
19
20 /**
21 * Optional hook for future extension.
22 */
23 default void configure(Action<? super LayerProjectionRule> action) {
24 action.execute(this);
25 }
26 } No newline at end of file
@@ -0,0 +1,15
1 package org.implab.gradle.variants.sources;
2
3 import org.gradle.api.NamedDomainObjectProvider;
4 import org.implab.gradle.common.sources.GenericSourceSet;
5
6 /**
7 * Materializes symbolic source set names into actual GenericSourceSet instances.
8 */
9 public interface SourceSetMaterializer {
10 NamedDomainObjectProvider<GenericSourceSet> getSourceSet(String sourceSetName);
11
12 default NamedDomainObjectProvider<GenericSourceSet> getSourceSet(SourceSetProjection projection) {
13 return getSourceSet(projection.getName());
14 }
15 } No newline at end of file
@@ -0,0 +1,10
1 package org.implab.gradle.variants.sources;
2
3 import org.gradle.api.Named;
4
5 /**
6 * Represents a projected source set. This is an identity object and doesn't contain any state.
7 * The name of the projection is used as the source set name by the {@link SourceSetMaterializer}.
8 */
9 public interface SourceSetProjection extends Named {
10 }
@@ -0,0 +1,28
1 package org.implab.gradle.variants.sources;
2
3 import java.util.Set;
4
5 import org.gradle.api.NamedDomainObjectCollection;
6
7 /**
8 * Registry of symbolic source set names produced by sources projection.
9 *
10 * Identity in this registry is the GenericSourceSet name.
11 */
12 public interface SourceSetProjections {
13
14 /**
15 * Returns all source set projections. This is a separate
16 */
17 NamedDomainObjectCollection<SourceSetProjection> getProjections();
18
19 /**
20 * Returns all logical bindings projected into the given source set name.
21 */
22 Set<VariantLayerBinding> getBindings(String sourceSetName);
23
24 /**
25 * Returns all logical bindings projected into the given source set name.
26 */
27 Set<VariantLayerBinding> getBindings(SourceSetProjection projection);
28 } No newline at end of file
@@ -0,0 +1,27
1 package org.implab.gradle.variants.sources;
2
3 import org.implab.gradle.variants.model.Layer;
4 import org.implab.gradle.variants.model.Variant;
5
6 /**
7 * Logical usage of a layer inside a variant.
8 * Identity: (variantName, layerName)
9 */
10 public interface VariantLayerBinding {
11 Variant getVariant();
12 Layer getLayer();
13
14 public static VariantLayerBinding of(Variant variant, Layer layer) {
15 return new VariantLayerBinding() {
16 @Override
17 public Variant getVariant() {
18 return variant;
19 }
20
21 @Override
22 public Layer getLayer() {
23 return layer;
24 }
25 };
26 }
27 } No newline at end of file
@@ -0,0 +1,7
1 package org.implab.gradle.variants.sources;
2
3 public interface VariantSourcesContext {
4 SourceSetProjections getProjections();
5
6 SourceSetMaterializer getMaterializer();
7 }
@@ -0,0 +1,37
1 package org.implab.gradle.variants.sources;
2
3 import org.gradle.api.Action;
4 import org.gradle.api.NamedDomainObjectContainer;
5 import org.implab.gradle.common.core.lang.Closures;
6
7 import groovy.lang.Closure;
8
9 public interface VariantSourcesExtension {
10
11 /**
12 * Projection rules keyed by layer name.
13 */
14 NamedDomainObjectContainer<LayerProjectionRule> getLayerRules();
15
16 /**
17 * Creates or returns an existing layer rule and configures it.
18 */
19 default LayerProjectionRule layerRule(String name) {
20 return getLayerRules().maybeCreate(name);
21 }
22
23 /**
24 * Creates or returns an existing layer rule and configures it.
25 */
26 default LayerProjectionRule layer(String name, Action<? super LayerProjectionRule> action) {
27 LayerProjectionRule rule = layerRule(name);
28 action.execute(rule);
29 return rule;
30 }
31
32 void whenFinalized(Action<? super VariantSourcesContext> action);
33
34 default void whenFinalized(Closure<?> closure) {
35 whenFinalized(Closures.action(closure));
36 }
37 } No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now