Skip to content
Merged
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Needs: impl, utest
### Code Style & Conventions

- **Clean Code**: Meaningful names, small functiKons, single responsibility.
- **Type References**: Use plain class names with imports instead of fully qualified names such as `java.util.List`.
- **Formatting**: Use the project's Eclipse formatter (`doc/itsallcode_formatter.xml`).
- **Logging**: Use `java.util.logging`. Test config: `core/src/test/resources/logging.properties`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,16 @@ public SpecificationItemId getId()
return this.id.getId();
}

/**
* Get the declared ID together with its source occurrence.
*
* @return declared located ID
*/
public LocatedSpecificationItemId getLocatedId()
{
return this.id;
}

/**
* Get the artifact type of the specification item
*
Expand Down Expand Up @@ -134,6 +144,16 @@ public List<SpecificationItemId> getCoveredIds()
return this.coveredIds.stream().map(LocatedSpecificationItemId::getId).toList();
}

/**
* Get covered IDs together with their source occurrences.
*
* @return located covered IDs
*/
public List<LocatedSpecificationItemId> getLocatedCoveredIds()
{
return Collections.unmodifiableList(this.coveredIds);
}

/**
* Add a covered {@link SpecificationItemId} to the list of covered IDs.
* <p>
Expand Down Expand Up @@ -167,6 +187,16 @@ public List<SpecificationItemId> getDependOnIds()
return this.dependOnIds.stream().map(LocatedSpecificationItemId::getId).toList();
}

/**
* Get dependency IDs together with their source occurrences.
*
* @return located dependency IDs
*/
public List<LocatedSpecificationItemId> getLocatedDependOnIds()
{
return this.dependOnIds;
}

/**
* Get the list of artifact types this specification item need to be covered
* in
Expand Down Expand Up @@ -274,6 +304,28 @@ public static Builder builder()
return new Builder();
}

/**
* Create a builder pre-populated with this item's values.
*
* @return builder initialized from this item
*/
public Builder toBuilder()
{
final Builder builder = builder().id(this.id)
.title(this.title)
.description(this.description)
.rationale(this.rationale)
.comment(this.comment)
.status(this.status)
.location(this.location)
.forwards(this.forwards);
this.coveredIds.forEach(builder::addCoveredId);
this.dependOnIds.forEach(builder::addDependOnId);
this.needsArtifactTypes.forEach(builder::addNeedsArtifactType);
this.tags.forEach(builder::addTag);
return builder;
}

/**
* Builder for objects of type {@link SpecificationItem}
*/
Expand Down Expand Up @@ -443,6 +495,20 @@ public Builder addCoveredId(final SpecificationItemId coveredId)
return this.addCoveredId(locatedId(coveredId));
}

/**
* Replace the IDs of specification items covered by the item to build.
*
* @param coveredIds
* the covered IDs
* @return this builder instance
*/
public Builder coveredIds(final Collection<SpecificationItemId> coveredIds)
{
this.coveredIds.clear();
coveredIds.forEach(this::addCoveredId);
return this;
}

/**
* Add the ID of a specification item covered by the item to build
*
Expand Down Expand Up @@ -486,6 +552,20 @@ public Builder addDependOnId(final LocatedSpecificationItemId dependOnId)
return this;
}

/**
* Replace the IDs of specification items the item to build depends on.
*
* @param dependOnIds
* the dependency IDs
* @return this builder instance
*/
public Builder dependOnIds(final Collection<SpecificationItemId> dependOnIds)
{
this.dependOnIds.clear();
dependOnIds.forEach(this::addDependOnId);
return this;
}

/**
* Add the ID of a specification item the item to be build depends on
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,28 +92,31 @@ public String getArtifactType()
}

@Override
public boolean equals(final Object o) {
if (!(o instanceof final SpecificationItemId that)) {
public boolean equals(final Object o)
{
if (!(o instanceof final SpecificationItemId that))
{
return false;
}
return revision == that.revision && Objects.equals(name, that.name)
&& Objects.equals(artifactType, that.artifactType);
}

@Override
public int hashCode() {
public int hashCode()
{
return Objects.hash(name, revision, artifactType);
}

@Override
public String toString()
{
return this.artifactType + ARTIFACT_TYPE_SEPARATOR + this.name + REVISION_SEPARATOR + this.revision;
return this.artifactType + ARTIFACT_TYPE_SEPARATOR + this.name + REVISION_SEPARATOR + this.revision;
}

/**
* Get this item with a wildcard as revision.
*
*
* @return a copy of this ID with a wildcard as revision.
*/
public SpecificationItemId toRevisionWildcard()
Expand Down Expand Up @@ -145,8 +148,7 @@ public static SpecificationItemId parseId(final String idText)
* the revision
* @return the specification item ID
*/
public static SpecificationItemId createId(final String artifactType, final String name,
final int revision)
public static SpecificationItemId createId(final String artifactType, final String name, final int revision)
{
return new SpecificationItemId.Builder().artifactType(artifactType).name(name)
.revision(revision).build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public interface ImportEventListener
/**
* The importer found a new specification item. The
* {@link SpecificationItemId} must be defined using
* {@link #setId(SpecificationItemId)}.
* {@link #setId(LocatedSpecificationItemId)}.
*/
void beginSpecificationItem();

Expand All @@ -22,7 +22,9 @@ public interface ImportEventListener
*
* @param id
* the ID of the new item
* @deprecated Use {@link #setId(LocatedSpecificationItemId)} instead.
*/
@Deprecated(since = "4.9.0", forRemoval = true)
void setId(final SpecificationItemId id);

/**
Expand Down Expand Up @@ -82,7 +84,9 @@ default void setId(final LocatedSpecificationItemId id)
*
* @param id
* the ID of the item that is covered
* @deprecated Use {@link #addCoveredId(LocatedSpecificationItemId)} instead.
*/
@Deprecated(since = "4.9.0", forRemoval = true)
void addCoveredId(final SpecificationItemId id);

/**
Expand All @@ -101,7 +105,9 @@ default void addCoveredId(final LocatedSpecificationItemId id)
*
* @param id
* the ID of the item depends on
* @deprecated Use {@link #addDependsOnId(LocatedSpecificationItemId)} instead.
*/
@Deprecated(since = "4.9.0", forRemoval = true)
void addDependsOnId(final SpecificationItemId id);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public final class SpecificationListBuilder implements ImportEventListener
private final FilterSettings filterSettings;
private final List<SpecificationItem> items = new LinkedList<>();
private SpecificationItem.Builder itemBuilder;
private SpecificationItemId id;
private LocatedSpecificationItemId id;
private StringBuilder description = new StringBuilder();
private StringBuilder rationale = new StringBuilder();
private StringBuilder comment = new StringBuilder();
Expand Down Expand Up @@ -66,8 +66,16 @@ private void resetState()
}

@Override
@SuppressWarnings("removal") // Need to implement method from interface for backward compatibility
public void setId(final SpecificationItemId id)
{
this.setId(LocatedSpecificationItemId.builder().id(id).build());
}

@Override
public void setId(final LocatedSpecificationItemId id)
{
// [impl->dsn~located-specification-item-id-storage~1]
this.id = id;
}

Expand All @@ -78,10 +86,18 @@ public void setStatus(final ItemStatus status)
}

@Override
@SuppressWarnings("removal") // Need to implement method from interface for backward compatibility
public void addCoveredId(final SpecificationItemId id)
{
this.addCoveredId(LocatedSpecificationItemId.builder().id(id).build());
}

@Override
public void addCoveredId(final LocatedSpecificationItemId id)
{
// [impl->dsn~filtering-by-artifact-types-during-import~1]
if (isAcceptedArtifactType(id.getArtifactType()))
// [impl->dsn~located-specification-item-id-storage~1]
if (isAcceptedArtifactType(id.getId().getArtifactType()))
{
this.itemBuilder.addCoveredId(id);
}
Expand All @@ -106,10 +122,18 @@ public void appendComment(final String fragment)
}

@Override
@SuppressWarnings("removal") // Need to implement method from interface for backward compatibility
public void addDependsOnId(final SpecificationItemId id)
{
this.addDependsOnId(LocatedSpecificationItemId.builder().id(id).build());
}

@Override
public void addDependsOnId(final LocatedSpecificationItemId id)
{
// [impl->dsn~filtering-by-artifact-types-during-import~1]
if (isAcceptedArtifactType(id.getArtifactType()))
// [impl->dsn~located-specification-item-id-storage~1]
if (isAcceptedArtifactType(id.getId().getArtifactType()))
{
this.itemBuilder.addDependOnId(id);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package org.itsallcode.openfasttrace.api.core;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.*;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.List;

import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -41,6 +43,77 @@ void testPreservesCompatibilityForUnlocatedIds()
() -> assertThat(item.getDependOnIds(), contains(DEPEND_ON_ID)));
}

// [utest->dsn~specification-item~3]
@Test
void testToBuilderPreservesAllValues()
{
final SpecificationItem item = SpecificationItem.builder().id(locatedId(ID)).title("Title")
.description("Description").rationale("Rationale").comment("Comment").status(ItemStatus.DRAFT)
.location(Location.create("file.md", 7)).addCoveredId(locatedId(COVERED_ID))
.addDependOnId(locatedId(DEPEND_ON_ID)).addNeedsArtifactType("impl").addTag("important")
.forwards(true).build();

assertThat(item.toBuilder().build(), equalTo(item));
}

// [utest->dsn~specification-item~3]
@Test
void testToBuilderDoesNotModifyOriginalItem()
{
final SpecificationItem item = SpecificationItem.builder().id(ID).addCoveredId(COVERED_ID).build();

final SpecificationItem copy = item.toBuilder().addCoveredId(DEPEND_ON_ID).build();

assertAll(
() -> assertThat(item.getCoveredIds(), contains(COVERED_ID)),
() -> assertThat(copy, not(equalTo(item))),
() -> assertThat(copy.getCoveredIds(), contains(COVERED_ID, DEPEND_ON_ID)));
}

// [utest->dsn~specification-item~3]
@Test
void testBuilderReplacesCoveredIds()
{
final SpecificationItem item = SpecificationItem.builder().id(ID).addCoveredId(DEPEND_ON_ID)
.coveredIds(List.of(COVERED_ID)).build();

assertThat(item.getCoveredIds(), contains(COVERED_ID));
}

// [utest->dsn~specification-item~3]
@Test
void testBuilderReplacesDependencyIds()
{
final SpecificationItem item = SpecificationItem.builder().id(ID).addDependOnId(COVERED_ID)
.dependOnIds(List.of(DEPEND_ON_ID)).build();

assertThat(item.getDependOnIds(), contains(DEPEND_ON_ID));
}

// [utest->dsn~located-specification-item-id-storage~1]
@Test
void testLocatedCoveredIdsAreImmutable()
{
final SpecificationItem item = SpecificationItem.builder().id(locatedId(ID))
.addCoveredId(locatedId(COVERED_ID)).addDependOnId(locatedId(DEPEND_ON_ID)).build();

final List<LocatedSpecificationItemId> immutableList = item.getLocatedCoveredIds();
final LocatedSpecificationItemId locatedId = locatedId(ID);
assertThrows(UnsupportedOperationException.class, () -> immutableList.add(locatedId));
}

// [utest->dsn~located-specification-item-id-storage~1]
@Test
void testLocatedDependOnIdsAreImmutable()
{
final SpecificationItem item = SpecificationItem.builder().id(locatedId(ID))
.addCoveredId(locatedId(COVERED_ID)).addDependOnId(locatedId(DEPEND_ON_ID)).build();

final List<LocatedSpecificationItemId> immutableList = item.getLocatedDependOnIds();
final LocatedSpecificationItemId locatedId = locatedId(ID);
assertThrows(UnsupportedOperationException.class, () -> immutableList.add(locatedId));
}

private static LocatedSpecificationItemId locatedId(final SpecificationItemId id)
{
return LocatedSpecificationItemId.builder().id(id).range(
Expand Down
Loading
Loading