-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow for moving truncation into the implementing API #68
Changes from all commits
46ff97c
034e114
32263c4
5fa067d
a320167
e92aa81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
package io.jenkins.plugins.checks.api; | ||
|
||
import edu.umd.cs.findbugs.annotations.NonNull; | ||
|
||
import java.util.*; | ||
import java.util.function.BiConsumer; | ||
import java.util.function.BinaryOperator; | ||
import java.util.function.Function; | ||
import java.util.function.Supplier; | ||
import java.util.stream.Collector; | ||
|
||
/** | ||
* Utility wrapper that silently truncates output with a message at a certain size. | ||
* <p> | ||
* The GitHub Checks API has a size limit on text fields. Because it also accepts markdown, it is not trivial to | ||
* truncate to the required length as this could lead to unterminated syntax. The use of this class allows for adding | ||
* chunks of complete markdown until an overflow is detected, at which point a message will be added and all future | ||
* additions will be silently discarded. | ||
*/ | ||
public class TruncatedString { | ||
|
||
@NonNull | ||
private final List<String> chunks; | ||
@NonNull | ||
private final String truncationText; | ||
private final boolean truncateStart; | ||
private final boolean chunkOnNewlines; | ||
|
||
|
||
private TruncatedString(@NonNull final List<String> chunks, @NonNull final String truncationText, final boolean truncateStart, final boolean chunkOnNewlines) { | ||
this.chunks = Collections.unmodifiableList(Objects.requireNonNull(chunks)); | ||
this.truncationText = Objects.requireNonNull(truncationText); | ||
this.truncateStart = truncateStart; | ||
this.chunkOnNewlines = chunkOnNewlines; | ||
} | ||
|
||
/** | ||
* Wrap the provided string as a {@link TruncatedString}. | ||
* | ||
* @param string String to wrap as a {@link TruncatedString} | ||
* @return a {@link TruncatedString} wrapping the provided input | ||
*/ | ||
static TruncatedString fromString(final String string) { | ||
return new Builder().setChunkOnNewlines().addText(string).build(); | ||
} | ||
|
||
/** | ||
* Builds the string without truncation. | ||
* | ||
* @return A string comprising the joined chunks. | ||
*/ | ||
@Override | ||
public String toString() { | ||
return String.join("", chunks); | ||
} | ||
|
||
private List<String> getChunks() { | ||
if (chunkOnNewlines) { | ||
return Arrays.asList(String.join("", chunks).split("(?<=\r?\n)")); | ||
} | ||
return new ArrayList<>(chunks); | ||
} | ||
|
||
/** | ||
* Builds the string such that it does not exceed maxSize, including the truncation string. | ||
* | ||
* @param maxSize the maximum size of the resultant string. | ||
* @return A string comprising as many of the joined chunks that will fit in the given size, plus the truncation | ||
* string if truncation was necessary. | ||
*/ | ||
public String build(final int maxSize) { | ||
List<String> parts = getChunks(); | ||
if (truncateStart) { | ||
Collections.reverse(parts); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic here is a little bit hard to understand at a start (I even left a comment first asking why we want a reversed output), since it requires you to understand the implementation of the joiner first and the join method in reverse acknowledges this special case. So, although the joiner is just an embedded class, adding another field (e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I agree that it's a bit manky. I think part of the problem is that we reverse it in two different places. I think I should be able to refactor it so that the
Truncated to three lines would come out in reverse come out as
or
I'm thinking possibly the latter. |
||
} | ||
List<String> truncatedParts = parts.stream().collect(new Joiner(truncationText, maxSize)); | ||
if (truncateStart) { | ||
Collections.reverse(truncatedParts); | ||
} | ||
return String.join("", truncatedParts); | ||
} | ||
|
||
|
||
/** | ||
* Builder for {@link TruncatedString}. | ||
*/ | ||
public static class Builder { | ||
private String truncationText = "Output truncated."; | ||
private boolean truncateStart = false; | ||
private boolean chunkOnNewlines = false; | ||
private final List<String> chunks = new ArrayList<>(); | ||
|
||
/** | ||
* Builds the {@link TruncatedString}. | ||
* | ||
* @return the build {@link TruncatedString}. | ||
*/ | ||
public TruncatedString build() { | ||
return new TruncatedString(chunks, truncationText, truncateStart, chunkOnNewlines); | ||
} | ||
|
||
/** | ||
* Adds a chunk of text to the builder. | ||
* | ||
* @param text the chunk of text to append to this builder | ||
* @return this builder | ||
*/ | ||
public Builder addText(@NonNull final String text) { | ||
this.chunks.add(Objects.requireNonNull(text)); | ||
return this; | ||
} | ||
|
||
/** | ||
* Sets the truncation text. | ||
* | ||
* @param truncationText the text to append on overflow | ||
* @return this builder | ||
*/ | ||
@SuppressWarnings("HiddenField") | ||
public Builder withTruncationText(@NonNull final String truncationText) { | ||
this.truncationText = Objects.requireNonNull(truncationText); | ||
return this; | ||
} | ||
|
||
/** | ||
* Sets truncator to remove excess text from the start, rather than the end. | ||
* | ||
* @return this builder | ||
*/ | ||
public Builder setTruncateStart() { | ||
this.truncateStart = true; | ||
return this; | ||
} | ||
|
||
/** | ||
* Sets truncator to chunk on newlines rather than the chunks. | ||
* | ||
* @return this builder | ||
*/ | ||
public Builder setChunkOnNewlines() { | ||
this.chunkOnNewlines = true; | ||
return this; | ||
} | ||
|
||
} | ||
|
||
private static class Joiner implements Collector<String, Joiner.Accumulator, List<String>> { | ||
|
||
private final int maxLength; | ||
private final String truncationText; | ||
|
||
Joiner(final String truncationText, final int maxLength) { | ||
if (maxLength < truncationText.length()) { | ||
throw new IllegalArgumentException("Maximum length is less than truncation text."); | ||
} | ||
this.truncationText = truncationText; | ||
this.maxLength = maxLength; | ||
} | ||
|
||
@Override | ||
public Supplier<Joiner.Accumulator> supplier() { | ||
return Accumulator::new; | ||
} | ||
|
||
@Override | ||
public BiConsumer<Joiner.Accumulator, String> accumulator() { | ||
return Accumulator::add; | ||
} | ||
|
||
@Override | ||
public BinaryOperator<Accumulator> combiner() { | ||
return Accumulator::combine; | ||
} | ||
|
||
@Override | ||
public Function<Accumulator, List<String>> finisher() { | ||
return Accumulator::truncate; | ||
} | ||
|
||
@Override | ||
public Set<Characteristics> characteristics() { | ||
return Collections.emptySet(); | ||
} | ||
|
||
private class Accumulator { | ||
private final List<String> chunks = new ArrayList<>(); | ||
private int length = 0; | ||
private boolean truncated = false; | ||
|
||
Accumulator combine(final Accumulator other) { | ||
other.chunks.forEach(this::add); | ||
return this; | ||
} | ||
|
||
void add(final String chunk) { | ||
if (truncated) { | ||
return; | ||
} | ||
if (length + chunk.length() > maxLength) { | ||
truncated = true; | ||
return; | ||
} | ||
chunks.add(chunk); | ||
length += chunk.length(); | ||
} | ||
|
||
List<String> truncate() { | ||
if (truncated) { | ||
if (length + truncationText.length() > maxLength) { | ||
chunks.remove(chunks.size() - 1); | ||
} | ||
chunks.add(truncationText); | ||
} | ||
return chunks; | ||
} | ||
} | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
adding the truncated string builder in line 66 make sense since it makes things convenient for implementations, but since we already have details URL, I'm not sure the see more link would be necessary for consumers, if not, the truncated string build is unnecessary as well IMO.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#68 (comment)
the see more link was just a helpful extra added in junit it could probably be skipped but truncating is needed afaict