Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ ValidationResult result = webLinkProcessor.process(rawHeader);
SignPostingProcessor processor = new SignPostingProcessor.Builder().build();
SignPostingResult signPostResult = processor.process(result.weblinks());

if(signPostResult.containsIssues()){
if(signPostResult.hasIssues()){
// Retrieve the report
var report = result.report();
// Investigate the report
Expand Down
Binary file removed assets/compass-social-preview.png
Binary file not shown.
80 changes: 80 additions & 0 deletions src/main/java/life/qbic/compass/model/SignPostingResult.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package life.qbic.compass.model;

import java.util.Objects;
import life.qbic.linksmith.model.WebLink;
import life.qbic.linksmith.spi.WebLinkValidator.IssueReport;

Expand Down Expand Up @@ -51,6 +52,13 @@
* <li>compose validation results in higher-level workflows.</li>
* </ul>
*
* <h2>Null-handling and optional Level 2 view</h2>
* <p>
* {@link #level2LinksetView()} may be {@code null}. This is intentional: not every validation step
* constructs a Level 2 interpretation. Use {@link #hasLinkSetView()} to check for presence, or wrap it
* using {@code Optional.ofNullable(result.level2LinksetView())}.
* </p>
*
* @param signPostingView a read-only view on the validated weblinks
* @param issueReport an aggregated report of all recoded issues during validation
* @param level2LinksetView a Signposting Level 2 compliant view semantics in case the validator
Expand All @@ -64,6 +72,47 @@ public record SignPostingResult(
IssueReport issueReport,
Level2LinksetView level2LinksetView) {

public SignPostingResult {
Objects.requireNonNull(signPostingView);
Objects.requireNonNull(issueReport);
}

/**
* Creates a result without a Level 2 Link Set view.
*
* <p>
* Use this factory when validation did not (or must not) produce a {@link Level2LinksetView}.
* The returned result will have {@link #level2LinksetView()} set to {@code null}.
* </p>
*
* @param signPostingView a read-only view on the validated weblinks (never {@code null})
* @param issueReport an aggregated report of all recorded issues during validation (never {@code null})
* @return a {@code SignPostingResult} with no Level 2 view
* @since 1.0.0
*/
public static SignPostingResult withoutLinksetView(SignPostingView signPostingView, IssueReport issueReport) {
return new SignPostingResult(signPostingView, issueReport, null);
}

/**
* Creates a result with a non-null Level 2 Link Set view.
*
* @param signPostingView a read-only view on the validated weblinks (never {@code null})
* @param issueReport an aggregated report of all recorded issues during validation (never {@code null})
* @param level2LinksetView a Level 2 interpretation view (must not be {@code null})
* @return a {@code SignPostingResult} with a Level 2 view attached
* @throws NullPointerException if {@code level2LinksetView} is {@code null}
* @since 1.0.0
*/
public static SignPostingResult withLinksetView(
SignPostingView signPostingView,
IssueReport issueReport,
Level2LinksetView level2LinksetView
) {
Objects.requireNonNull(level2LinksetView, "level2LinksetView");
return new SignPostingResult(signPostingView, issueReport, level2LinksetView);
}

/**
* Convenience method for aggregators or filters to check, if the current SignPosting result
* contains a linkset view or not.
Expand All @@ -73,4 +122,35 @@ public record SignPostingResult(
public boolean hasLinkSetView() {
return level2LinksetView != null;
}

/**
* Convenience method to check if any issues (warnings or errors) were recorded.
*
* @return true if the {@link #issueReport()} contains any issues
* @since 1.0.0
*/
public boolean hasIssues() {
return !issueReport.issues().isEmpty();
}

/**
* Convenience method to check if any errors were recorded.
*
* @return true if the {@link #issueReport()} contains errors
* @since 1.0.0
*/
public boolean hasErrors() {
return issueReport.hasErrors();
}

/**
* Convenience method to check if any warnings were recorded.
*
* @return true if the {@link #issueReport()} contains warnings
* @since 1.0.0
*/
public boolean hasWarnings() {
return issueReport.hasWarnings();
}

}
180 changes: 180 additions & 0 deletions src/test/groovy/life/qbic/compass/model/SignPostingResultSpec.groovy
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package life.qbic.compass.model

import spock.lang.Specification
import spock.lang.Unroll

class SignPostingResultSpec extends Specification {

@Unroll
def "stable API: method '#methodName' is available with return type '#returnType.simpleName' and params #paramTypes*.simpleName"() {
when:
def method = SignPostingResult.getMethod(methodName, paramTypes as Class[])

then:
method != null
method.returnType == returnType

where:
methodName | returnType | paramTypes
"signPostingView" | SignPostingView | ([] as Class[])
"issueReport" | life.qbic.linksmith.spi.WebLinkValidator.IssueReport | ([] as Class[])
"level2LinksetView" | Level2LinksetView | ([] as Class[])
"hasLinkSetView" | boolean | ([] as Class[])
"hasIssues" | boolean | ([] as Class[])
"hasErrors" | boolean | ([] as Class[])
"hasWarnings" | boolean | ([] as Class[])
"withoutLinksetView" | SignPostingResult | ([SignPostingView, life.qbic.linksmith.spi.WebLinkValidator.IssueReport] as Class[])
"withLinksetView" | SignPostingResult | ([SignPostingView, life.qbic.linksmith.spi.WebLinkValidator.IssueReport, Level2LinksetView] as Class[])
}

def "stable API: canonical record constructor is available"() {
when:
def ctor = SignPostingResult.getDeclaredConstructor(
SignPostingView,
life.qbic.linksmith.spi.WebLinkValidator.IssueReport,
Level2LinksetView
)

then:
ctor != null
}

def "stable API: record fundamental methods exist"() {
expect:
SignPostingResult.getMethod("equals", Object) != null
SignPostingResult.getMethod("hashCode") != null
SignPostingResult.getMethod("toString") != null
}

def "behavior: hasLinkSetView returns false when level2LinksetView is null"() {
given:
def view = new SignPostingView([])
def report = new life.qbic.linksmith.spi.WebLinkValidator.IssueReport([])

and:
def result = new SignPostingResult(view, report, null)

expect:
!result.hasLinkSetView()
}

def "behavior: hasLinkSetView returns true when level2LinksetView is present"() {
given:
def view = new SignPostingView([])
def report = new life.qbic.linksmith.spi.WebLinkValidator.IssueReport([])

and:
def nonNullLevel2View = linkSetView()

and:
def result = new SignPostingResult(view, report, nonNullLevel2View)

expect:
result.hasLinkSetView()
}

@Unroll
def "behavior: hasLinkSetView is consistent with null-check (#caseName)"(String caseName, Level2LinksetView level2View) {
given:
def view = new SignPostingView([])
def report = new life.qbic.linksmith.spi.WebLinkValidator.IssueReport([])

and:
def result = new SignPostingResult(view, report, level2View)

expect:
result.hasLinkSetView() == (result.level2LinksetView() != null)

where:
caseName | level2View
"null view" | null
"non-null view" | linkSetView()
}

def "behavior: hasIssues/hasErrors/hasWarnings are all false when IssueReport is empty"() {
given:
def view = new SignPostingView([])
def report = new life.qbic.linksmith.spi.WebLinkValidator.IssueReport([])

and:
def result = new SignPostingResult(view, report, null)

expect:
!result.hasIssues()
!result.hasErrors()
!result.hasWarnings()
}

def "behavior: hasWarnings true implies hasIssues true (warnings are issues)"() {
given:
def view = new SignPostingView([])
def warning = life.qbic.linksmith.spi.WebLinkValidator.Issue.warning("w1")
def report = new life.qbic.linksmith.spi.WebLinkValidator.IssueReport([warning])

and:
def result = new SignPostingResult(view, report, null)

expect:
result.hasWarnings()
!result.hasErrors()
result.hasIssues()
}

def "behavior: hasErrors true implies hasIssues true"() {
given:
def view = new SignPostingView([])
def error = life.qbic.linksmith.spi.WebLinkValidator.Issue.error("e1")
def report = new life.qbic.linksmith.spi.WebLinkValidator.IssueReport([error])

and:
def result = new SignPostingResult(view, report, null)

expect:
!result.hasWarnings()
result.hasErrors()
result.hasIssues()
}

def "behavior: withoutLinksetView creates a result with null level2LinksetView"() {
given:
def view = new SignPostingView([])
def report = new life.qbic.linksmith.spi.WebLinkValidator.IssueReport([])

when:
def result = SignPostingResult.withoutLinksetView(view, report)

then:
result.level2LinksetView() == null
!result.hasLinkSetView()
}

def "behavior: withLinksetView rejects null level2LinksetView"() {
given:
def view = new SignPostingView([])
def report = new life.qbic.linksmith.spi.WebLinkValidator.IssueReport([])

when:
SignPostingResult.withLinksetView(view, report, null)

then:
thrown(NullPointerException)
}

def "behavior: withLinksetView creates a result with non-null level2LinksetView"() {
given:
def view = new SignPostingView([])
def report = new life.qbic.linksmith.spi.WebLinkValidator.IssueReport([])
def level2 = linkSetView()

when:
def result = SignPostingResult.withLinksetView(view, report, level2)

then:
result.level2LinksetView().is(level2)
result.hasLinkSetView()
}

private static Level2LinksetView linkSetView() {
return new Level2LinksetView([], [], [], [])
}
}
Loading