diff --git a/common/src/main/java/org/implab/gradle/common/core/lang/Deferred.java b/common/src/main/java/org/implab/gradle/common/core/lang/Deferred.java new file mode 100644 --- /dev/null +++ b/common/src/main/java/org/implab/gradle/common/core/lang/Deferred.java @@ -0,0 +1,29 @@ +package org.implab.gradle.common.core.lang; + +import java.util.LinkedList; +import java.util.List; +import java.util.function.Consumer; + +public final class Deferred { + private final List> listeners = new LinkedList<>(); + private T value; + private boolean resolved = false; + + public void resolve(T value) { + if (resolved) { + throw new IllegalStateException("Already resolved"); + } + this.value = value; + this.resolved = true; + listeners.forEach(listener -> listener.accept(value)); + listeners.clear(); + } + + public void whenResolved(Consumer listener) { + if (resolved) { + listener.accept(value); + } else { + listeners.add(listener); + } + } +} diff --git a/identity-first-model.md b/identity-first-model.md new file mode 100644 --- /dev/null +++ b/identity-first-model.md @@ -0,0 +1,346 @@ +# Identity-First Model: Preserving Lazy Semantics Without Custom Events + +In build and configuration systems, the same tension appears again and again: + +* you want an **observable model** that consumers can subscribe to using something like `all(...)` +* you do **not** want that subscription to destroy laziness +* and you would prefer to avoid introducing a custom event bus, event ordering, and a separate lifecycle model + +A practical way to solve this is to **separate identity from computed state**. + +## Core idea + +The idea is simple: + +* one object is responsible only for **identity** and minimal selection metadata +* the actual aggregate content is obtained through **separate API calls** +* those API calls may be lazy, expensive, cached, provider-based, or computed on demand + +So instead of one “fat” object, we get two layers. + +### 1. Identity layer + +Lightweight objects that: + +* are cheap to create +* are effectively immutable +* are safe to observe eagerly +* work well as keys and subscription points + +### 2. State / aggregate access layer + +Separate APIs that: + +* resolve or compute content from identity +* do heavy work only when needed +* preserve lazy semantics where that actually matters + +This is especially useful when a collection must be replayable, but should not drag expensive materialization along with it. + +--- + +## The problem this solves + +Consider a typical Gradle-like scenario. + +An adapter wants to subscribe to a collection: + +```java +projections.getProjections().all(projection -> { + ... +}); +``` + +If `projection` is a heavy object that already contains: + +* computed bindings +* providers of source sets +* derived state +* partially materialized objects + +then `all(...)` starts forcing things that were supposed to stay lazy. + +Typical symptoms: + +* lazy semantics are weakened or broken +* coupling increases +* plugin application order starts to matter +* custom events begin to look tempting: `onCreated`, `onResolved`, `onMaterialized` + +The problem is not that `all(...)` is bad. +The problem is that **too much meaning has been packed into the observable object**. + +--- + +## The solution: observe identity, not state + +If the observable collection contains only identity objects, the picture changes. + +For example: + +```java +public interface SourceSetProjection extends Named { +} +``` + +This object contains only identity: + +* `name` +* perhaps a small amount of selection metadata +* but not heavy aggregate state + +Now this subscription: + +```java +projections.getProjections().all(projection -> { + ... +}); +``` + +is no longer dangerous. +It eagerly materializes only cheap keys, not the full aggregate graph. + +The actual content is requested separately: + +```java +Set bindings = projections.getBindings(projection); +NamedDomainObjectProvider sourceSet = + materializer.getSourceSet(projection.getName()); +``` + +Those calls may remain: + +* lazy +* computed +* cached +* tied to runtime lifecycle +* delegated to a dedicated materializer + +--- + +## Example + +### A problematic design + +Suppose we define projection like this: + +```java +public interface SourceSetProjection extends Named { + Set getBindings(); + NamedDomainObjectProvider getSourceSet(); +} +``` + +This is problematic because `SourceSetProjection` is no longer just identity. It is already close to an aggregate. + +It mixes: + +* symbolic identity +* relation data +* runtime references into a foreign domain + +Subscribing via `all(...)` now risks pulling in much more than intended. + +The type says “projection”, but internally it already carries half the system. + +--- + +### A cleaner design + +Split responsibilities instead: + +```java +public interface SourceSetProjection extends Named { +} +``` + +```java +public interface SourceSetProjections { + NamedDomainObjectCollection getProjections(); + Set getBindings(String sourceSetName); +} +``` + +```java +public interface SourceSetMaterializer { + NamedDomainObjectProvider getSourceSet(String sourceSetName); +} +``` + +Now the adapter flow looks like this: + +```java +projections.getProjections().all(projection -> { + Set bindings = + projections.getBindings(projection.getName()); + + NamedDomainObjectProvider sourceSet = + materializer.getSourceSet(projection.getName()); + + // apply adapter-specific policy +}); +``` + +What changed: + +* replayable subscription is preserved +* eager observation is acceptable because `SourceSetProjection` is cheap +* expensive and computed state has moved to separate APIs +* materialization remains under the control of a single owner + +--- + +## Why this is often better than events + +When identity and state are mixed together, people quickly start inventing events: + +* `projectionCreated` +* `projectionResolved` +* `sourceSetAvailable` +* `sourceSetMaterialized` + +That usually happens because it becomes important to know **when exactly** an object is “ready enough”. + +If the observable object contains only identity, and heavy state is obtained separately, then many of those events become unnecessary. + +The architecture becomes calmer: + +* an **identity registry** +* a **lookup API** for relations +* a **lazy materialization API** for heavy objects + +Instead of saying: + +> “When this object becomes sufficiently ready, I will react.” + +you can say: + +> “I can observe identity immediately, and ask for the expensive state only when I actually need it.” + +This is easier to reason about, easier to test, and usually easier to evolve. + +--- + +## What should live inside an identity object + +An identity object does not have to be completely empty. +It may carry **selection metadata**, as long as that metadata is: + +* cheap +* stable +* not expensive to initialize +* not turning the object into an aggregate + +Typical examples: + +* `id` +* `name` +* `kind` +* `type` +* domain key + +What should usually stay out: + +* computed aggregate content +* runtime references to foreign domains +* lazy providers of heavy objects +* derived state that can trigger premature materialization + +A useful rule of thumb: + +**An identity object contains selection metadata; aggregate content is obtained separately.** + +--- + +## Why this works well with `all(...)` + +`all(...)` weakens laziness only if the observed objects are themselves heavy or stateful. + +If the observed objects are: + +* cheap +* identity-only +* effectively immutable + +then eager observation is usually acceptable. + +So the real principle is: + +**Eager observation of identity is often harmless. +Eager observation of computed state is not.** + +That is why `all(...)` can be perfectly fine for collections of: + +* `Variant` +* `Layer` +* `Role` +* `SourceSetProjection` + +as long as those objects stay on the identity side of the boundary. + +--- + +## Where this principle is especially useful + +This approach is particularly effective when: + +* there is replayable observation via `all(...)` +* identity objects are cheap and stable +* aggregate content may be expensive +* symbolic model and runtime model should remain separate +* you want to avoid building a custom event system + +For Gradle-like models, this is often a very natural fit. + +--- + +## When it is unnecessary + +If an object is: + +* small +* cheap +* and already fully represents its useful content + +then splitting identity and state may be overengineering. + +So this is not a universal rule. It is a tool to use when there is real tension between: + +* key +* state +* computation +* runtime reference + +--- + +## Practical conclusion + +The principle can be summarized like this: + +1. **Identity objects** hold only identity and cheap selection metadata. +2. **Aggregate content** is not stored inside them, but retrieved through separate API calls. +3. Those API calls may perform: + + * lazy resolution + * caching + * heavy computation + * materialization on demand +4. This makes it possible to use replayable mechanisms such as `all(...)` without destroying laziness where laziness actually matters. + +This is how you can combine: + +* simple observation +* a clean model +* no custom event bus +* lazy materialization of heavy state + +--- + +# Short design note version + +A concise version of the same principle: + +> Use identity objects as cheap, observable keys. +> Keep expensive or computed aggregate content out of them. +> Resolve that content through separate APIs on demand. +> This allows replayable observation (`all(...)`) without forcing premature materialization, and often removes the need for a custom event model. diff --git a/identity-first-model.ru.md b/identity-first-model.ru.md new file mode 100644 --- /dev/null +++ b/identity-first-model.ru.md @@ -0,0 +1,306 @@ +# Identity-first model: как сохранить lazy semantics без своих событий + +В системах конфигурации и сборки почти всегда возникает одно и то же напряжение: + +* хочется иметь **наблюдаемую модель**, на которую можно подписаться через что-то вроде `all(...)` +* но не хочется, чтобы такая подписка **ломала ленивость** +* ещё меньше хочется тащить в архитектуру собственную шину событий, порядок подписки и отдельный lifecycle + +Один из рабочих способов решить это — **разделить identity и вычисляемое состояние**. + +## Суть принципа + +Идея простая: + +* отдельный объект отвечает только за **identity** и минимальные метаданные выбора +* всё содержательное состояние агрегата получается **отдельными вызовами API** +* эти вызовы уже могут быть lazy, дорогими, вычисляемыми, кэшируемыми, провайдерными — какими угодно + +То есть вместо одного “толстого” объекта мы получаем два слоя: + +### 1. Identity layer + +Лёгкие объекты, которые: + +* дёшево создаются +* не мутируют +* безопасно наблюдаются eagerly +* годятся как ключи и точки подписки + +### 2. State / aggregate access layer + +Отдельные API, которые: + +* по identity находят или вычисляют содержимое +* делают heavy work только по требованию +* могут сохранять ленивую семантику + +Это особенно полезно там, где коллекция должна быть replayable, но при этом не должна тащить за собой дорогую материализацию. + +--- + +## Проблема, которую это решает + +Рассмотрим типичную ситуацию в Gradle-подобной модели. + +Есть коллекция объектов, на которую адаптер хочет подписаться: + +```java +projections.getProjections().all(projection -> { + ... +}); +``` + +Если `projection` — это тяжёлый объект, внутри которого уже лежат: + +* вычисленные bindings +* провайдеры на source sets +* derived state +* полу-материализованные сущности + +то `all(...)` начинает рано раскрывать то, что хотелось оставить ленивым. + +Появляются симптомы: + +* ломается lazy semantics +* растёт связность +* становится важен порядок применения плагинов +* хочется заводить собственные события: `onCreated`, `onResolved`, `onMaterialized` + +Архитектура начинает скрипеть не потому, что `all(...)` плох, а потому что **слишком много смысла засунуто в наблюдаемый объект**. + +--- + +## Решение: наблюдать identity, а не состояние + +Если сделать наблюдаемыми только identity objects, картина меняется. + +Например: + +```java +public interface SourceSetProjection extends Named { +} +``` + +Такой объект содержит только identity: + +* `name` +* возможно, ещё пару метаданных выбора +* но не тяжёлое содержимое + +Тогда подписка: + +```java +projections.getProjections().all(projection -> { + ... +}); +``` + +больше не страшна. +Она eagerly materializes только дешёвые ключи, а не весь агрегатный граф. + +Содержимое запрашивается отдельно: + +```java +Set bindings = projections.getBindings(projection); +NamedDomainObjectProvider sourceSet = + materializer.getSourceSet(projection.getName()); +``` + +И вот эти вызовы уже могут быть: + +* lazy +* вычисляемыми +* кэшируемыми +* привязанными к runtime lifecycle + +--- + +## Пример + +### Неудачный вариант + +Представим такой интерфейс: + +```java +public interface SourceSetProjection extends Named { + Set getBindings(); + NamedDomainObjectProvider getSourceSet(); +} +``` + +Проблемы тут сразу видны: + +* `SourceSetProjection` уже не identity object, а почти агрегат +* внутри смешаны: + + * symbolic identity + * relation data + * runtime reference в чужой домен +* подписка через `all(...)` начинает тащить за собой больше, чем хотелось бы + +На словах объект называется “projection”, а по факту внутри у него уже полсистемы. + +--- + +### Более удачный вариант + +Разделяем ответственность: + +```java +public interface SourceSetProjection extends Named { +} +``` + +```java +public interface SourceSetProjections { + NamedDomainObjectCollection getProjections(); + Set getBindings(String sourceSetName); +} +``` + +```java +public interface SourceSetMaterializer { + NamedDomainObjectProvider getSourceSet(String sourceSetName); +} +``` + +Теперь сценарий адаптера выглядит так: + +```java +projections.getProjections().all(projection -> { + Set bindings = + projections.getBindings(projection.getName()); + + NamedDomainObjectProvider sourceSet = + materializer.getSourceSet(projection.getName()); + + // apply adapter-specific policy +}); +``` + +Что изменилось: + +* replayable подписка сохранилась +* eager materialization допустима, потому что `SourceSetProjection` дешёвый +* дорогое и вычисляемое состояние ушло в отдельные API +* materialization остаётся под контролем одного владельца + +--- + +## Почему это лучше событий + +Когда в модели смешаны identity и состояние, очень быстро хочется изобретать события: + +* `projectionCreated` +* `projectionResolved` +* `sourceSetAvailable` +* `sourceSetMaterialized` + +Потому что в какой-то момент становится важно, **когда именно** объект уже “достаточно готов”. + +Если же наблюдаемый объект содержит только identity, а всё тяжёлое получается отдельными вызовами, событийная модель часто вообще не нужна. + +Получается более спокойная схема: + +* есть **identity registry** +* есть **lookup API** для связей +* есть **lazy materialization API** для тяжёлых сущностей + +То есть вместо событий: + +> “когда объект станет достаточно готов, я что-то сделаю” + +получается обычный и понятный flow: + +> “я вижу identity, а нужное состояние спрошу отдельно, когда оно мне действительно понадобится” + +Это проще и для reasoning, и для тестирования, и для эволюции API. + +--- + +## Что именно должно жить в identity object + +Identity object не обязан быть “абсолютно пустым”. +Он может содержать **метаданные выбора**, если они: + +* дешевы +* стабильны +* не требуют тяжёлой инициализации +* не превращают объект в агрегат + +Например: + +* `id` +* `name` +* `kind` +* `domain key` +* maybe `type` + +Но он не должен содержать: + +* вычисляемое содержимое агрегата +* ссылки времени выполнения на чужие домены +* lazy providers на heavy objects +* derived state, который может провоцировать раннюю материализацию + +Хорошая практическая формула: + +**identity object contains selection metadata; aggregate content is obtained separately.** + +--- + +## Когда этот принцип особенно полезен + +Он особенно хорош, если: + +* есть replayable наблюдение через `all(...)` +* identity-объекты дешёвые и почти immutable +* содержимое агрегата может быть дорогим +* нужен clean split между symbolic model и runtime model +* хочется избежать собственной событийной шины + +Для Gradle-подобных моделей это вообще очень естественный приём. + +--- + +## Когда он не нужен + +Если объект: + +* маленький +* дешёвый +* уже сам по себе и есть всё его содержимое + +то разделение на identity и отдельные lookup API может быть лишним. + +То есть этот принцип полезен не как догма, а как инструмент. +Его стоит применять там, где реально есть риск смешения: + +* ключа +* состояния +* вычисления +* runtime reference + +--- + +## Практический итог + +Если сформулировать коротко, то принцип такой: + +1. **Identity objects** содержат только identity и дешёвые метаданные выбора. +2. **Агрегатное содержимое** не хранится внутри них, а получается отдельными API-вызовами. +3. Эти API уже могут выполнять: + + * lazy resolution + * кэширование + * heavy computation + * materialization +4. Благодаря этому можно безопасно использовать replayable механизмы вроде `all(...)`, не разрушая ленивую семантику там, где она действительно важна. + +Именно так удаётся совместить: + +* простое наблюдение +* чистую модель +* отсутствие собственных событий +* ленивую материализацию тяжёлого состояния diff --git a/variants/src/main/java/org/implab/gradle/variants/VariantSourcesPlugin.java b/variants/src/main/java/org/implab/gradle/variants/VariantSourcesPlugin.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/VariantSourcesPlugin.java @@ -0,0 +1,185 @@ +package org.implab.gradle.variants; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.gradle.api.Action; +import org.gradle.api.NamedDomainObjectCollection; +import org.gradle.api.NamedDomainObjectContainer; +import org.gradle.api.NamedDomainObjectProvider; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.implab.gradle.common.core.lang.Deferred; +import org.implab.gradle.common.core.lang.Strings; +import org.implab.gradle.common.sources.GenericSourceSet; +import org.implab.gradle.common.sources.SourcesPlugin; +import org.implab.gradle.variants.model.Layer; +import org.implab.gradle.variants.model.Variant; +import org.implab.gradle.variants.model.VariantsExtension; +import org.implab.gradle.variants.sources.LayerProjectionRule; +import org.implab.gradle.variants.sources.SourceSetMaterializer; +import org.implab.gradle.variants.sources.SourceSetProjection; +import org.implab.gradle.variants.sources.SourceSetProjections; +import org.implab.gradle.variants.sources.VariantLayerBinding; +import org.implab.gradle.variants.sources.VariantSourcesContext; +import org.implab.gradle.variants.sources.VariantSourcesExtension; + +@NonNullByDefault +public abstract class VariantSourcesPlugin implements Plugin { + @Override + public void apply(Project target) { + // Apply the main VariantsPlugin to ensure the core variant model is available. + target.getPlugins().apply(VariantsPlugin.class); + target.getPlugins().apply(SourcesPlugin.class); + // Access the VariantsExtension to configure variant sources. + var variantsExtension = target.getExtensions().getByType(VariantsExtension.class); + var objectFactory = target.getObjects(); + + var sources = SourcesPlugin.getSourcesExtension(target); + + var deferred = new Deferred(); + var layerProjectionRules = objectFactory.domainObjectContainer(LayerProjectionRule.class); + + var variantSourcesExtension = new VariantSourcesExtension() { + @Override + public NamedDomainObjectContainer getLayerRules() { + return layerProjectionRules; + } + + @Override + public void whenFinalized(Action action) { + deferred.whenResolved(action::execute); + } + }; + target.getExtensions().add(VariantSourcesExtension.class, "variantSources", variantSourcesExtension); + + // create convention to automatically create layer projection rules for each + // variant layer + variantsExtension.getLayers().all(layer -> { + // Automatically create a layer projection rule for each variant layer. + variantSourcesExtension.layer(layer.getName(), rule -> { + // Configure the source set name pattern based on the layer name. + rule.getSourceSetNamePattern() + .convention("{variant}{layerCapitalized}") + .finalizeValueOnRead(); + }); + }); + + var projections = objectFactory.domainObjectContainer(SourceSetProjection.class); + + Map> projectionBindings = new HashMap<>(); + + var sourceSetProjections = new SourceSetProjections() { + @Override + public NamedDomainObjectCollection getProjections() { + return projections; + } + + @Override + public Set getBindings(String sourceSetName) { + return projectionBindings.getOrDefault(sourceSetName, Set.of()); + } + + @Override + public Set getBindings(SourceSetProjection projection) { + return getBindings(projection.getName()); + } + }; + + Set materializedSourceSets = new HashSet<>(); + + var materializer = new SourceSetMaterializer() { + @Override + public NamedDomainObjectProvider getSourceSet(String sourceSetName) { + return materializedSourceSets.add(sourceSetName) + ? sources.register(sourceSetName) + : sources.named(sourceSetName); + } + }; + + var bindings = new VariantBindings(); + + target.afterEvaluate(t -> { + // Once the project is evaluated, resolve the deferred context and finalize the + // sources configuration. + variantsExtension.getLayers().all(bindings::addLayer); + variantsExtension.getVariants().all(bindings::addVariant); + + variantsExtension.getLayers().all(layer -> { + // For each layer, apply the projection rules to generate source set projections. + + var rule = layerProjectionRules.maybeCreate(layer.getName()); + var pattern = rule.getSourceSetNamePattern().getOrElse("{variant}{layerCapitalized}"); + // Generate source set names based on the pattern and variant/layer information. + // This is a simplified example; real implementation would need to consider + // all variants and layers. + var sourceSetName = pattern.replace("{layer}", layer.getName()) + .replace("{variant}", "main") // Placeholder for actual variant name + .replace("{layerCapitalized}", Strings.capitalize(layer.getName())); + + var projection = objectFactory.newInstance(SourceSetProjection.class, sourceSetName); + projections.add(projection); + // Bind the projection to the corresponding variant layer. + projectionBindings.computeIfAbsent(sourceSetName, k -> new HashSet<>()) + .add(new VariantLayerBinding(layer.getName(), projection)); + }); + + var context = new VariantSourcesContext() { + + @Override + public SourceSetProjections getProjections() { + return sourceSetProjections; + } + + @Override + public SourceSetMaterializer getMaterializer() { + return materializer; + } + + // Implementation of the context that provides access to variant and layer + // information. + }; + deferred.resolve(context); + }); + } + + class VariantBindings { + private final Set layers = new HashSet<>(); + private final Set variants = new HashSet<>(); + + private final List> listeners = new ArrayList<>(); + + void addLayer(Layer layer) { + layers.add(layer); + variants.stream() + .map(variant -> VariantLayerBinding.of(variant, layer)) + .forEach(this::notifyBindingAdded); + } + + void addVariant(Variant variant) { + variants.add(variant); + layers.stream() + .map(layer -> VariantLayerBinding.of(variant, layer)) + .forEach(this::notifyBindingAdded); + } + + void whenBindingAdded(Consumer listener) { + layers.stream() + .flatMap(layer -> variants.stream().map(variant -> VariantLayerBinding.of(variant, layer))) + .forEach(listener); + listeners.add(listener); + } + + private void notifyBindingAdded(VariantLayerBinding binding) { + listeners.forEach(listener -> listener.accept(binding)); + } + } + +} diff --git a/variants/src/main/java/org/implab/gradle/variants/model/VariantDefinition.java b/variants/src/main/java/org/implab/gradle/variants/model/VariantDefinition.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/model/VariantDefinition.java @@ -0,0 +1,70 @@ +package org.implab.gradle.variants.model; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.gradle.api.Action; +import org.gradle.api.Named; +import org.gradle.api.provider.SetProperty; +import org.implab.gradle.common.core.lang.Closures; +import org.implab.gradle.common.core.lang.Strings; + +import groovy.lang.Closure; + +public interface VariantDefinition extends Named { + /** + * Role bindings declared inside this variant. + * + * The binding pair of role and layer names. + */ + SetProperty getRoleBindings(); + + /** + * Creates or returns an existing role binding and configures it. + */ + default void role(String name, Action action) { + var spec = new RoleSpec(name); + action.execute(spec); + spec.accept(getRoleBindings()::add); + } + + default void role(String name, Closure closure) { + role(name, Closures.action(closure)); + } + + default void finalizeVariant() { + getRoleBindings().finalizeValue(); + } + + public static class RoleSpec { + private final String name; + private final Set layerNames; + + public RoleSpec(String name) { + this.name = name; + this.layerNames = new HashSet<>(); + } + + public String getName() { + return name; + } + + public Set getLayerNames() { + return layerNames; + } + + public void layers(String name, String... extraNames) { + Stream.concat(Stream.of(name), Stream.of(extraNames)) + .map(Strings::requireNonBlank) + .forEach(this.layerNames::add); + } + + void accept(Consumer consumer) { + layerNames.stream() + .map(layerName -> new RoleLayerBinding(name, layerName)) + .forEach(consumer); + } + } +} diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/LayerProjectionRule.java b/variants/src/main/java/org/implab/gradle/variants/sources/LayerProjectionRule.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/LayerProjectionRule.java @@ -0,0 +1,26 @@ +package org.implab.gradle.variants.sources; + +import org.gradle.api.Action; +import org.gradle.api.Named; +import org.gradle.api.provider.Property; + +/** + * Projection rule for a layer. + */ +public interface LayerProjectionRule extends Named { + + /** + * Pattern used to calculate the source set name. + * Examples: + * "{layer}" + * "{variant}{layerCapitalized}" + */ + Property getSourceSetNamePattern(); + + /** + * Optional hook for future extension. + */ + default void configure(Action action) { + action.execute(this); + } +} \ No newline at end of file diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetMaterializer.java b/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetMaterializer.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetMaterializer.java @@ -0,0 +1,15 @@ +package org.implab.gradle.variants.sources; + +import org.gradle.api.NamedDomainObjectProvider; +import org.implab.gradle.common.sources.GenericSourceSet; + +/** + * Materializes symbolic source set names into actual GenericSourceSet instances. + */ +public interface SourceSetMaterializer { + NamedDomainObjectProvider getSourceSet(String sourceSetName); + + default NamedDomainObjectProvider getSourceSet(SourceSetProjection projection) { + return getSourceSet(projection.getName()); + } +} \ No newline at end of file diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetProjection.java b/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetProjection.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetProjection.java @@ -0,0 +1,10 @@ +package org.implab.gradle.variants.sources; + +import org.gradle.api.Named; + +/** + * Represents a projected source set. This is an identity object and doesn't contain any state. + * The name of the projection is used as the source set name by the {@link SourceSetMaterializer}. + */ +public interface SourceSetProjection extends Named { +} diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetProjections.java b/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetProjections.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/SourceSetProjections.java @@ -0,0 +1,28 @@ +package org.implab.gradle.variants.sources; + +import java.util.Set; + +import org.gradle.api.NamedDomainObjectCollection; + +/** + * Registry of symbolic source set names produced by sources projection. + * + * Identity in this registry is the GenericSourceSet name. + */ +public interface SourceSetProjections { + + /** + * Returns all source set projections. This is a separate + */ + NamedDomainObjectCollection getProjections(); + + /** + * Returns all logical bindings projected into the given source set name. + */ + Set getBindings(String sourceSetName); + + /** + * Returns all logical bindings projected into the given source set name. + */ + Set getBindings(SourceSetProjection projection); +} \ No newline at end of file diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/VariantLayerBinding.java b/variants/src/main/java/org/implab/gradle/variants/sources/VariantLayerBinding.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/VariantLayerBinding.java @@ -0,0 +1,27 @@ +package org.implab.gradle.variants.sources; + +import org.implab.gradle.variants.model.Layer; +import org.implab.gradle.variants.model.Variant; + +/** + * Logical usage of a layer inside a variant. + * Identity: (variantName, layerName) + */ +public interface VariantLayerBinding { + Variant getVariant(); + Layer getLayer(); + + public static VariantLayerBinding of(Variant variant, Layer layer) { + return new VariantLayerBinding() { + @Override + public Variant getVariant() { + return variant; + } + + @Override + public Layer getLayer() { + return layer; + } + }; + } +} \ No newline at end of file diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesContext.java b/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesContext.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesContext.java @@ -0,0 +1,7 @@ +package org.implab.gradle.variants.sources; + +public interface VariantSourcesContext { + SourceSetProjections getProjections(); + + SourceSetMaterializer getMaterializer(); +} diff --git a/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesExtension.java b/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesExtension.java new file mode 100644 --- /dev/null +++ b/variants/src/main/java/org/implab/gradle/variants/sources/VariantSourcesExtension.java @@ -0,0 +1,37 @@ +package org.implab.gradle.variants.sources; + +import org.gradle.api.Action; +import org.gradle.api.NamedDomainObjectContainer; +import org.implab.gradle.common.core.lang.Closures; + +import groovy.lang.Closure; + +public interface VariantSourcesExtension { + + /** + * Projection rules keyed by layer name. + */ + NamedDomainObjectContainer getLayerRules(); + + /** + * Creates or returns an existing layer rule and configures it. + */ + default LayerProjectionRule layerRule(String name) { + return getLayerRules().maybeCreate(name); + } + + /** + * Creates or returns an existing layer rule and configures it. + */ + default LayerProjectionRule layer(String name, Action action) { + LayerProjectionRule rule = layerRule(name); + action.execute(rule); + return rule; + } + + void whenFinalized(Action action); + + default void whenFinalized(Closure closure) { + whenFinalized(Closures.action(closure)); + } +} \ No newline at end of file