implements PR-10a bank composition details dto projection

This commit is contained in:
bQUARKz 2026-03-19 00:42:04 +00:00
parent 1c17562cc9
commit 49d83e3ff8
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
13 changed files with 302 additions and 0 deletions

View File

@ -15,6 +15,7 @@ public record PackerAssetDetailsDTO(
List<OutputCodecCatalog> availableOutputCodecs,
Map<OutputCodecCatalog, List<PackerCodecConfigurationFieldDTO>> codecConfigurationFieldsByCodec,
List<PackerCodecConfigurationFieldDTO> metadataFields,
PackerBankCompositionDetailsDTO bankComposition,
Map<String, List<Path>> inputsByRole,
List<PackerDiagnosticDTO> diagnostics) {
@ -25,6 +26,7 @@ public record PackerAssetDetailsDTO(
availableOutputCodecs = List.copyOf(Objects.requireNonNull(availableOutputCodecs, "availableOutputCodecs"));
codecConfigurationFieldsByCodec = Map.copyOf(Objects.requireNonNull(codecConfigurationFieldsByCodec, "codecConfigurationFieldsByCodec"));
metadataFields = List.copyOf(Objects.requireNonNull(metadataFields, "metadataFields"));
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
inputsByRole = Map.copyOf(Objects.requireNonNull(inputsByRole, "inputsByRole"));
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
}

View File

@ -0,0 +1,18 @@
package p.packer.dtos;
import java.util.List;
import java.util.Objects;
public record PackerBankCompositionDetailsDTO(
List<PackerBankCompositionFileDTO> availableFiles,
List<PackerBankCompositionFileDTO> selectedFiles,
long measuredBankSizeBytes) {
public PackerBankCompositionDetailsDTO {
availableFiles = List.copyOf(Objects.requireNonNull(availableFiles, "availableFiles"));
selectedFiles = List.copyOf(Objects.requireNonNull(selectedFiles, "selectedFiles"));
if (measuredBankSizeBytes < 0L) {
throw new IllegalArgumentException("measuredBankSizeBytes must be non-negative");
}
}
}

View File

@ -0,0 +1,33 @@
package p.packer.dtos;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
public record PackerBankCompositionFileDTO(
String path,
String displayName,
long size,
long lastModified,
String fingerprint,
Map<String, Object> metadata) {
public PackerBankCompositionFileDTO {
path = Objects.requireNonNull(path, "path").trim();
displayName = Objects.requireNonNull(displayName, "displayName").trim();
if (path.isBlank()) {
throw new IllegalArgumentException("path must not be blank");
}
if (displayName.isBlank()) {
throw new IllegalArgumentException("displayName must not be blank");
}
if (size < 0L) {
throw new IllegalArgumentException("size must be non-negative");
}
if (lastModified < 0L) {
throw new IllegalArgumentException("lastModified must be non-negative");
}
fingerprint = fingerprint == null || fingerprint.isBlank() ? null : fingerprint;
metadata = Map.copyOf(new LinkedHashMap<>(Objects.requireNonNull(metadata, "metadata")));
}
}

View File

@ -15,6 +15,7 @@ public record PackerAssetDetails(
List<OutputCodecCatalog> availableOutputCodecs,
Map<OutputCodecCatalog, List<PackerCodecConfigurationField>> codecConfigurationFieldsByCodec,
List<PackerCodecConfigurationField> metadataFields,
PackerBankCompositionDetails bankComposition,
Map<String, List<Path>> inputsByRole,
List<PackerDiagnostic> diagnostics) {
@ -25,6 +26,7 @@ public record PackerAssetDetails(
availableOutputCodecs = List.copyOf(Objects.requireNonNull(availableOutputCodecs, "availableOutputCodecs"));
codecConfigurationFieldsByCodec = Map.copyOf(Objects.requireNonNull(codecConfigurationFieldsByCodec, "codecConfigurationFieldsByCodec"));
metadataFields = List.copyOf(Objects.requireNonNull(metadataFields, "metadataFields"));
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
inputsByRole = Map.copyOf(Objects.requireNonNull(inputsByRole, "inputsByRole"));
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
}

View File

@ -0,0 +1,20 @@
package p.packer.models;
import java.util.List;
import java.util.Objects;
public record PackerBankCompositionDetails(
List<PackerBankCompositionFile> availableFiles,
List<PackerBankCompositionFile> selectedFiles,
long measuredBankSizeBytes) {
public static final PackerBankCompositionDetails EMPTY = new PackerBankCompositionDetails(List.of(), List.of(), 0L);
public PackerBankCompositionDetails {
availableFiles = List.copyOf(Objects.requireNonNull(availableFiles, "availableFiles"));
selectedFiles = List.copyOf(Objects.requireNonNull(selectedFiles, "selectedFiles"));
if (measuredBankSizeBytes < 0L) {
throw new IllegalArgumentException("measuredBankSizeBytes must be non-negative");
}
}
}

View File

@ -0,0 +1,33 @@
package p.packer.models;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
public record PackerBankCompositionFile(
String path,
String displayName,
long size,
long lastModified,
String fingerprint,
Map<String, Object> metadata) {
public PackerBankCompositionFile {
path = Objects.requireNonNull(path, "path").trim();
displayName = Objects.requireNonNull(displayName, "displayName").trim();
if (path.isBlank()) {
throw new IllegalArgumentException("path must not be blank");
}
if (displayName.isBlank()) {
throw new IllegalArgumentException("displayName must not be blank");
}
if (size < 0L) {
throw new IllegalArgumentException("size must be non-negative");
}
if (lastModified < 0L) {
throw new IllegalArgumentException("lastModified must be non-negative");
}
fingerprint = fingerprint == null || fingerprint.isBlank() ? null : fingerprint;
metadata = Map.copyOf(new LinkedHashMap<>(Objects.requireNonNull(metadata, "metadata")));
}
}

View File

@ -8,6 +8,7 @@ import p.packer.messages.diagnostics.PackerDiagnosticSeverity;
import p.packer.models.*;
import p.packer.repositories.PackerRuntimeRegistry;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
@ -77,6 +78,7 @@ public final class PackerAssetDetailsService {
outputContract.availableCodecs(),
outputContract.codecConfigurationFieldsByCodec(),
metadataFields(outputContract.metadataFields(), declaration.outputMetadata()),
resolveBankCompositionDetails(runtimeAsset, declaration),
resolveInputs(resolved.assetRoot(), declaration.inputsByRole()),
diagnostics);
return new GetAssetDetailsResult(
@ -116,6 +118,7 @@ public final class PackerAssetDetailsService {
List.of(OutputCodecCatalog.NONE),
Map.of(OutputCodecCatalog.NONE, List.of()),
List.of(),
PackerBankCompositionDetails.EMPTY,
Map.of(),
diagnostics);
return new GetAssetDetailsResult(
@ -135,6 +138,72 @@ public final class PackerAssetDetailsService {
return Map.copyOf(resolved);
}
private PackerBankCompositionDetails resolveBankCompositionDetails(
final PackerRuntimeAsset runtimeAsset,
final PackerAssetDeclaration declaration) {
final var walkProjection = runtimeAsset.walkProjection();
final Map<String, PackerRuntimeWalkFile> walkFilesByPath = new LinkedHashMap<>();
walkProjection.buildCandidateFiles().forEach(file -> walkFilesByPath.put(file.relativePath(), file));
final List<PackerBankCompositionFile> availableFiles = walkProjection.buildCandidateFiles().stream()
.filter(file -> file.diagnostics().stream().noneMatch(PackerDiagnostic::blocking))
.map(this::toBankCompositionFile)
.toList();
final List<PackerBankCompositionFile> selectedFiles = flattenSelectedInputPaths(declaration.inputsByRole()).stream()
.map(path -> resolveSelectedBankFile(runtimeAsset.assetRoot(), path, walkFilesByPath))
.flatMap(Optional::stream)
.toList();
return new PackerBankCompositionDetails(
availableFiles,
selectedFiles,
walkProjection.measuredBankSizeBytes());
}
private List<String> flattenSelectedInputPaths(Map<String, List<String>> inputsByRole) {
final List<String> selected = new ArrayList<>();
inputsByRole.values().forEach(selected::addAll);
return List.copyOf(selected);
}
private Optional<PackerBankCompositionFile> resolveSelectedBankFile(
Path assetRoot,
String relativePath,
Map<String, PackerRuntimeWalkFile> walkFilesByPath) {
final PackerRuntimeWalkFile runtimeWalkFile = walkFilesByPath.get(relativePath);
if (runtimeWalkFile != null) {
return Optional.of(toBankCompositionFile(runtimeWalkFile));
}
final Path filePath = assetRoot.resolve(relativePath).toAbsolutePath().normalize();
if (!Files.isRegularFile(filePath)) {
return Optional.empty();
}
try {
return Optional.of(new PackerBankCompositionFile(
relativePath,
filePath.getFileName().toString(),
Files.size(filePath),
Files.getLastModifiedTime(filePath).toMillis(),
null,
Map.of()));
} catch (Exception exception) {
return Optional.empty();
}
}
private PackerBankCompositionFile toBankCompositionFile(PackerRuntimeWalkFile file) {
return new PackerBankCompositionFile(
file.relativePath(),
Path.of(file.relativePath()).getFileName().toString(),
file.size(),
file.lastModified(),
file.fingerprint(),
file.metadata());
}
private List<PackerCodecConfigurationField> metadataFields(
List<PackerCodecConfigurationField> definitions,
Map<String, String> outputMetadata) {

View File

@ -31,10 +31,28 @@ public final class PackerReadMessageMapper {
details.availableOutputCodecs(),
toCodecConfigurationFieldsByCodecDTO(details.codecConfigurationFieldsByCodec()),
toCodecConfigurationFieldDTOs(details.metadataFields()),
toBankCompositionDetailsDTO(details.bankComposition()),
details.inputsByRole(),
toDiagnosticDTOs(details.diagnostics()));
}
private static PackerBankCompositionDetailsDTO toBankCompositionDetailsDTO(PackerBankCompositionDetails details) {
return new PackerBankCompositionDetailsDTO(
details.availableFiles().stream().map(PackerReadMessageMapper::toBankCompositionFileDTO).toList(),
details.selectedFiles().stream().map(PackerReadMessageMapper::toBankCompositionFileDTO).toList(),
details.measuredBankSizeBytes());
}
private static PackerBankCompositionFileDTO toBankCompositionFileDTO(PackerBankCompositionFile file) {
return new PackerBankCompositionFileDTO(
file.path(),
file.displayName(),
file.size(),
file.lastModified(),
file.fingerprint(),
file.metadata());
}
public static List<PackerAssetSummaryDTO> toAssetSummaryDTOs(List<PackerAssetSummary> summaries) {
return summaries.stream().map(PackerReadMessageMapper::toAssetSummaryDTO).toList();
}

View File

@ -18,6 +18,8 @@ import p.packer.repositories.PackerRuntimeLoader;
import p.packer.repositories.PackerRuntimeRegistry;
import p.packer.testing.PackerFixtureLocator;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
@ -45,10 +47,39 @@ final class PackerAssetDetailsServiceTest {
assertEquals("TILES/indexed_v1", result.details().outputFormat().displayName());
assertEquals(List.of(OutputCodecCatalog.NONE), result.details().availableOutputCodecs());
assertEquals(List.of(), result.details().codecConfigurationFieldsByCodec().get(OutputCodecCatalog.NONE));
assertNotNull(result.details().bankComposition());
assertTrue(result.details().bankComposition().selectedFiles().isEmpty());
assertTrue(result.diagnostics().stream().noneMatch(diagnostic -> diagnostic.blocking()));
assertTrue(result.diagnostics().stream().anyMatch(diagnostic -> diagnostic.message().contains("Output metadata for tile bank cannot be empty")));
}
@Test
void projectsBankCompositionAvailableAndSelectedFiles() throws Exception {
final Path projectRoot = copyFixture("workspaces/managed-basic", tempDir.resolve("managed-bank-composition"));
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
final BufferedImage tile = new BufferedImage(16, 16, BufferedImage.TYPE_INT_ARGB);
ImageIO.write(tile, "png", assetRoot.resolve("confirm.png").toFile());
ImageIO.write(tile, "png", assetRoot.resolve("cancel.png").toFile());
final Path manifestPath = assetRoot.resolve("asset.json");
final ObjectMapper mapper = new ObjectMapper();
final ObjectNode manifest = (ObjectNode) mapper.readTree(manifestPath.toFile());
final ObjectNode inputs = manifest.putObject("inputs");
inputs.putArray("sprites").add("confirm.png");
mapper.writerWithDefaultPrettyPrinter().writeValue(manifestPath.toFile(), manifest);
final PackerAssetDetailsService service = service();
final var result = service.getAssetDetails(new GetAssetDetailsRequest(project(projectRoot), AssetReference.forAssetId(1)));
assertEquals(PackerOperationStatus.SUCCESS, result.status());
assertTrue(result.details().bankComposition().availableFiles().stream()
.anyMatch(file -> file.path().equals("confirm.png")));
assertTrue(result.details().bankComposition().selectedFiles().stream()
.anyMatch(file -> file.path().equals("confirm.png")));
assertTrue(result.details().bankComposition().availableFiles().stream()
.allMatch(file -> !file.displayName().isBlank()));
}
@Test
void returnsUnregisteredDetailsForValidUnregisteredRootReference() throws Exception {
final Path projectRoot = copyFixture("workspaces/orphan-valid", tempDir.resolve("orphan"));

View File

@ -18,6 +18,8 @@ import p.studio.utilities.i18n.I18n;
import p.studio.workspaces.assets.details.contract.AssetDetailsContractControl;
import p.studio.workspaces.assets.details.summary.AssetDetailsSummaryControl;
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetAction;
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionDetails;
import p.studio.workspaces.assets.messages.AssetWorkspaceBankCompositionFile;
import p.studio.workspaces.assets.messages.AssetWorkspaceAssetDetails;
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsStatus;
import p.studio.workspaces.assets.messages.AssetWorkspaceDetailsViewState;
@ -417,6 +419,24 @@ public final class AssetDetailsControl extends VBox implements StudioEventAware
details.availableOutputCodecs(),
details.codecConfigurationFieldsByCodec(),
details.metadataFields(),
mapBankComposition(details.bankComposition()),
Map.copyOf(details.inputsByRole()));
}
private AssetWorkspaceBankCompositionDetails mapBankComposition(p.packer.dtos.PackerBankCompositionDetailsDTO details) {
return new AssetWorkspaceBankCompositionDetails(
details.availableFiles().stream().map(this::mapBankCompositionFile).toList(),
details.selectedFiles().stream().map(this::mapBankCompositionFile).toList(),
details.measuredBankSizeBytes());
}
private AssetWorkspaceBankCompositionFile mapBankCompositionFile(p.packer.dtos.PackerBankCompositionFileDTO file) {
return new AssetWorkspaceBankCompositionFile(
file.path(),
file.displayName(),
file.size(),
file.lastModified(),
file.fingerprint(),
file.metadata());
}
}

View File

@ -17,6 +17,7 @@ public record AssetWorkspaceAssetDetails(
List<OutputCodecCatalog> availableOutputCodecs,
Map<OutputCodecCatalog, List<PackerCodecConfigurationFieldDTO>> codecConfigurationFieldsByCodec,
List<PackerCodecConfigurationFieldDTO> metadataFields,
AssetWorkspaceBankCompositionDetails bankComposition,
Map<String, List<Path>> inputsByRole) {
public AssetWorkspaceAssetDetails {
@ -27,6 +28,7 @@ public record AssetWorkspaceAssetDetails(
availableOutputCodecs = List.copyOf(Objects.requireNonNull(availableOutputCodecs, "availableOutputCodecs"));
codecConfigurationFieldsByCodec = Map.copyOf(Objects.requireNonNull(codecConfigurationFieldsByCodec, "codecConfigurationFieldsByCodec"));
metadataFields = List.copyOf(Objects.requireNonNull(metadataFields, "metadataFields"));
bankComposition = Objects.requireNonNull(bankComposition, "bankComposition");
inputsByRole = Map.copyOf(Objects.requireNonNull(inputsByRole, "inputsByRole"));
}
}

View File

@ -0,0 +1,21 @@
package p.studio.workspaces.assets.messages;
import java.util.List;
import java.util.Objects;
public record AssetWorkspaceBankCompositionDetails(
List<AssetWorkspaceBankCompositionFile> availableFiles,
List<AssetWorkspaceBankCompositionFile> selectedFiles,
long measuredBankSizeBytes) {
public static final AssetWorkspaceBankCompositionDetails EMPTY =
new AssetWorkspaceBankCompositionDetails(List.of(), List.of(), 0L);
public AssetWorkspaceBankCompositionDetails {
availableFiles = List.copyOf(Objects.requireNonNull(availableFiles, "availableFiles"));
selectedFiles = List.copyOf(Objects.requireNonNull(selectedFiles, "selectedFiles"));
if (measuredBankSizeBytes < 0L) {
throw new IllegalArgumentException("measuredBankSizeBytes must be non-negative");
}
}
}

View File

@ -0,0 +1,33 @@
package p.studio.workspaces.assets.messages;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
public record AssetWorkspaceBankCompositionFile(
String path,
String displayName,
long size,
long lastModified,
String fingerprint,
Map<String, Object> metadata) {
public AssetWorkspaceBankCompositionFile {
path = Objects.requireNonNull(path, "path").trim();
displayName = Objects.requireNonNull(displayName, "displayName").trim();
if (path.isBlank()) {
throw new IllegalArgumentException("path must not be blank");
}
if (displayName.isBlank()) {
throw new IllegalArgumentException("displayName must not be blank");
}
if (size < 0L) {
throw new IllegalArgumentException("size must be non-negative");
}
if (lastModified < 0L) {
throw new IllegalArgumentException("lastModified must be non-negative");
}
fingerprint = fingerprint == null || fingerprint.isBlank() ? null : fingerprint;
metadata = Map.copyOf(new LinkedHashMap<>(Objects.requireNonNull(metadata, "metadata")));
}
}