Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,22 @@ public final class SpecificationItemId implements Comparable<SpecificationItemId
public static final String ITEM_NAME_PATTERN = "(?U)(\\p{Alpha}[\\w-]*(?:\\.[\\w-]+)*+)";
private static final String LEGACY_ID_NAME = "(\\p{Alpha}+)(?:~\\p{Alpha}+)?:"
+ ITEM_NAME_PATTERN;
/** Separator between artifact type and name in an item ID. */
/** Separator between the artifact type and name in an item ID. */
public static final String ARTIFACT_TYPE_SEPARATOR = "~";
/** Separator between name and revision in an item ID. */
/**
* Separator between name and revision in an item ID.
* @deprecated with 4.12.0 we widened the accepted separators to include '#'.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a link to the new constant REVISION_SEPARATOR

*/
@Deprecated(forRemoval = true, since = "4.12.0")
public static final String REVISION_SEPARATOR = "~";
/** Regexp pattern for accepted separators between name and revision. */
public static final String REVISION_SEPARATOR_PATTERN = "[~#]";
static final int REVISION_WILDCARD = Integer.MIN_VALUE;
// [impl->dsn~md.specification-item-id-format~3]
// [impl->dsn~md.specification-item-id-format~4]
private static final String ID = "(\\p{Alpha}+)" //
+ ARTIFACT_TYPE_SEPARATOR //
+ ITEM_NAME_PATTERN //
+ REVISION_SEPARATOR //
+ REVISION_SEPARATOR_PATTERN //
+ ITEM_REVISION_PATTERN;
private static final String LEGACY_ID = LEGACY_ID_NAME + ", *v" //
+ ITEM_REVISION_PATTERN;
Expand Down Expand Up @@ -137,6 +143,37 @@ public static SpecificationItemId parseId(final String idText)
return new Builder(idText).build();
}

/**
* Create a specification item ID from an already-matched {@link #ID_PATTERN}
* matcher.
* <p>
* The matcher must use {@link #ID_PATTERN} and must already have a current
* match from {@link Matcher#matches()} or {@link Matcher#find()}. This does
* not accept {@link #LEGACY_ID_PATTERN}.
* </p>
*
* @param matcher
* an already-matched matcher whose pattern is {@link #ID_PATTERN}
* @return the specification item ID
* @throws IllegalArgumentException
* if the matcher's pattern is not {@link #ID_PATTERN}, or if the
* revision cannot be parsed
* @throws IllegalStateException
* if the matcher has no current match
*/
public static SpecificationItemId fromIdPatternMatcher(final Matcher matcher)
{
if (matcher.pattern() != ID_PATTERN)
{
throw new IllegalArgumentException(
"Matcher pattern must be SpecificationItemId.ID_PATTERN, but was '"
+ matcher.pattern() + "'");
}
Comment thread
kaklakariada marked this conversation as resolved.
Resolved
final Builder builder = new Builder();
builder.setIdPartsFromMatches(matcher);
return builder.build();
}

/**
* Create a specification item ID
*
Expand Down Expand Up @@ -174,7 +211,7 @@ public static SpecificationItemId createId(final String artifactType, final Stri
*/
public static class Builder
{
// [impl->dsn~md.specification-item-id-format~3]
// [impl->dsn~md.specification-item-id-format~4]
private final String id;
private String artifactType;
private String name;
Expand Down Expand Up @@ -331,7 +368,7 @@ private void parseId()
else
{
throw new IllegalArgumentException("Invalid specification item ID format: \"" + this.id
+ "\". Format must be <arifact-type>~<requirement-name-or-number>~<revision>.");
+ "\". Format must be <artifact-type>~<requirement-name-or-number>(~|#)<revision>.");
}
}
}
Expand All @@ -340,10 +377,10 @@ private void setIdPartsFromMatches(final Matcher matcher)
{
this.artifactType = matcher.group(ARTIFACT_TYPE_MATCHING_GROUP);
this.name = matcher.group(NAME_MATCHING_GROUP);
parseRevision(matcher.group(REVISION_MATCHING_GROUP));
parseRevision(matcher.group(REVISION_MATCHING_GROUP), matcher.group());
}

private void parseRevision(final String text)
private void parseRevision(final String text, final String idText)
{
try
{
Expand All @@ -352,7 +389,7 @@ private void parseRevision(final String text)
catch (final NumberFormatException exception)
{
throw new IllegalArgumentException(
"Error parsing version number from specification item ID: \"" + this.id
"Error parsing version number from specification item ID: \"" + idText
+ "\"",
exception);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
import static org.hamcrest.Matchers.equalTo;
import static org.itsallcode.openfasttrace.api.core.SpecificationItemId.createId;
import static org.itsallcode.openfasttrace.api.core.SpecificationItemId.parseId;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.*;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.itsallcode.openfasttrace.api.core.SpecificationItemId.Builder;
import org.junit.jupiter.api.Test;
Expand All @@ -15,7 +17,7 @@
import nl.jqno.equalsverifier.EqualsVerifier;

/**
* [utest->dsn~md.specification-item-id-format~3]
* [utest->dsn~md.specification-item-id-format~4]
*/
// [utest->dsn~specification-item-id~1]
class TestSpecificationItemId
Expand All @@ -24,6 +26,7 @@ class TestSpecificationItemId
@CsvSource(value =
{
"type~name~42, type, name, 42",
"type~name#42, type, name, 42",
"type~name-with-dash~42, type, name-with-dash, 42",
"type~name.with.dot~42, type, name.with.dot, 42",
"type~name-trailing-~42, type, name-trailing-, 42",
Expand Down Expand Up @@ -91,7 +94,7 @@ void testParseId_mustFailForIllegalIds(final String illegalId)
final Throwable exception = assertThrows(IllegalArgumentException.class, () -> parseId(illegalId));
assertThat(exception.getMessage(),
equalTo("Invalid specification item ID format: \"" + illegalId + "\". "
+ "Format must be <arifact-type>~<requirement-name-or-number>~<revision>."));
+ "Format must be <artifact-type>~<requirement-name-or-number>(~|#)<revision>."));
}

@Test
Expand All @@ -110,6 +113,14 @@ void testToString()
assertThat(builder.build().toString(), equalTo("dsn~dummy~3"));
}

@Test
void testHashRevisionSeparatorCreatesCanonicalId()
{
final SpecificationItemId id = parseId("dsn~dummy#3");
assertAll(() -> assertThat(id, equalTo(parseId("dsn~dummy~3"))),
() -> assertThat(id.toString(), equalTo("dsn~dummy~3")));
}

@Test
void testToStringRevisionWildcard()
{
Expand All @@ -123,4 +134,49 @@ void testEqualsAndHashContract()
{
EqualsVerifier.forClass(SpecificationItemId.class).verify();
}

@Test
void testFromIdPatternMatcherAfterMatches()
{
final Matcher matcher = SpecificationItemId.ID_PATTERN.matcher("dsn~dummy~3");
assertTrue(matcher.matches());
final SpecificationItemId id = SpecificationItemId.fromIdPatternMatcher(matcher);
assertAll(
() -> assertThat(id.getArtifactType(), equalTo("dsn")),
() -> assertThat(id.getName(), equalTo("dummy")),
() -> assertThat(id.getRevision(), equalTo(3)));
}

@Test
void testFromIdPatternMatcherAfterFind()
{
final Matcher matcher = SpecificationItemId.ID_PATTERN.matcher("prefix dsn~dummy~3 suffix");
assertTrue(matcher.find());
final SpecificationItemId id = SpecificationItemId.fromIdPatternMatcher(matcher);
assertAll(
() -> assertThat(id.getArtifactType(), equalTo("dsn")),
() -> assertThat(id.getName(), equalTo("dummy")),
() -> assertThat(id.getRevision(), equalTo(3)));
}

@Test
void testFromIdPatternMatcherRejectsWrongPattern()
{
final Matcher matcher = Pattern.compile("(\\p{Alpha}+)~(\\p{Alpha}+)~(\\d+)").matcher("dsn~dummy~3");
assertTrue(matcher.matches());
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> SpecificationItemId.fromIdPatternMatcher(matcher));
assertThat(exception.getMessage(),
equalTo("Matcher pattern must be SpecificationItemId.ID_PATTERN, but was '"
+ matcher.pattern() + "'"));
}

@Test
void testFromIdPatternMatcherUnmatchedThrowsIllegalStateException()
{
final Matcher matcher = SpecificationItemId.ID_PATTERN.matcher("dsn~dummy~3");
final IllegalStateException exception = assertThrows(IllegalStateException.class,
() -> SpecificationItemId.fromIdPatternMatcher(matcher));
assertThat(exception.getMessage(), equalTo("No match found"));
Comment thread
kaklakariada marked this conversation as resolved.
Resolved
}
}
3 changes: 2 additions & 1 deletion doc/changes/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ nav_order: 9

# Changes

* [4.11.0](changes_4.11.0.md) unreleased
* [4.11.0](changes_4.12.0.md) unreleased

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* [4.11.0](changes_4.12.0.md) unreleased
* [4.12.0](changes_4.12.0.md) unreleased

* [4.11.0](changes_4.11.0.md)
* [4.10.0](changes_4.10.0.md)
* [4.9.0](changes_4.9.0.md)
* [4.8.0](changes_4.8.0.md)
Expand Down
1 change: 1 addition & 0 deletions doc/changes/changes_4.11.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ The user guide documentation now has automatic lateral navigation.
* #401: Added regression test for RST parser corner case (issue was fixed already)
* #423: Fixed tag parsing inconsistency in Markdown and reStructuredText importers
* #574: Fixed invalid HTML when inline code contains underscores
* #630: Accept `#` as an alternative separator between specification item names and revisions

## Documentation

Expand Down
11 changes: 11 additions & 0 deletions doc/changes/changes_4.12.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# OpenFastTrace 4.12.0, released 2026-10-??

Code name: Syntax usability improvements

## Summary

This release includes several usability improvements to the requirement ID and other syntax used in OpenFastTrace. For example, the hash mark (#) can now be used in addition to the tilde (~) as a revision separator. This prevents situations where the name part of the ID is displayed as cross-out text in Markdown. Of course, the previous double-tilde syntax remains valid.

## Features

* #638: Allow hash mark in addition to tilde as revision separator
Loading
Loading