Skip to content

Commit

Permalink
Allow server side implementations of SoyLogger to output `LoggingAttr…
Browse files Browse the repository at this point in the history
…s` to set attributes on velog roots

This follows the js implementation and adds integration tests to cover all backends in addition to unit tests.

PiperOrigin-RevId: 708080550
  • Loading branch information
lukesandberg authored and copybara-github committed Jan 3, 2025
1 parent 3802a4d commit 3d8a2e4
Show file tree
Hide file tree
Showing 8 changed files with 375 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public abstract class LoggingFunctionInvocation {
new AutoValue_LoggingFunctionInvocation(
"$$flushPendingAttributes",
"",
ImmutableList.of(BooleanData.FALSE),
ImmutableList.of(BooleanData.TRUE),
/* isFlushPendingAttributes= */ true,
Optional.<Consumer<String>>empty());

Expand Down
44 changes: 39 additions & 5 deletions java/src/com/google/template/soy/jbcsrc/api/OutputAppendable.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import com.google.common.collect.ImmutableList;
import com.google.common.flogger.GoogleLogger;
import com.google.common.flogger.StackSize;
import com.google.common.html.types.SafeHtml;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.template.soy.data.LogStatement;
Expand All @@ -36,7 +37,6 @@
* <p>This object is for soy internal use only. Do not use.
*/
public final class OutputAppendable extends LoggingAdvisingAppendable {

public static OutputAppendable create(Appendable outputAppendable, @Nullable SoyLogger logger) {
return new OutputAppendable(outputAppendable, logger);
}
Expand All @@ -54,6 +54,7 @@ public static OutputAppendable create(StringBuilder sb, @Nullable SoyLogger logg
@Nullable private final SoyLogger logger;
private final Appendable outputAppendable;
private int logOnlyDepth;
@Nullable private SoyLogger.LoggingAttrs loggingAttrs;

private OutputAppendable(Appendable outputAppendable, @Nullable SoyLogger logger) {
this.outputAppendable = checkNotNull(outputAppendable);
Expand Down Expand Up @@ -108,9 +109,8 @@ public LoggingAdvisingAppendable appendLoggingFunctionInvocation(
value = funCall.placeholderValue();
} else {
if (funCall.isFlushPendingAttributes()) {
// For now, just no-op these calls.
// TODO-b/383661457: implement this.
value = "";
maybeFlushPendingAttributes(funCall);
return this;
} else {
value = logger.evalLoggingFunction(funCall);
}
Expand All @@ -133,6 +133,25 @@ public LoggingAdvisingAppendable appendLoggingFunctionInvocation(
return this;
}

private void maybeFlushPendingAttributes(LoggingFunctionInvocation funCall) throws IOException {
var loggingAttrs = this.loggingAttrs;
var consumer = funCall.resultConsumer();
if (loggingAttrs != null) {
this.loggingAttrs = null;
boolean isAnchorTag = funCall.args().get(0).booleanValue();
if (consumer.isPresent()) {
StringBuilder sb = new StringBuilder();
loggingAttrs.writeTo(isAnchorTag, sb);
consumer.get().accept(sb.toString());
} else {
loggingAttrs.writeTo(isAnchorTag, outputAppendable);
}
} else if (consumer.isPresent()) {
// We have to ensure the consumer is always filled.
consumer.get().accept("");
}
}

@CanIgnoreReturnValue
@Override
public LoggingAdvisingAppendable enterLoggableElement(LogStatement statement) {
Expand All @@ -151,7 +170,13 @@ public LoggingAdvisingAppendable enterLoggableElement(LogStatement statement) {
}
logOnlyDepth = depth;
}
appendDebugOutput(logger.enter(statement));
var enterData = logger.enter(statement);
appendDebugOutput(enterData.debugHtml());
var loggingAttrs = enterData.loggingAttrs().orElse(null);
if (loggingAttrs != null && depth > 0) {
loggingAttrs = null; // we cannot render them when logonly is set, so drop them now.
}
setLoggingAttrs(loggingAttrs);
return this;
}

Expand All @@ -166,11 +191,20 @@ public LoggingAdvisingAppendable exitLoggableElement() {
depth--;
logOnlyDepth = depth;
}
setLoggingAttrs(null);
// should debug output be guarded by logonly?
appendDebugOutput(logger.exit());
return this;
}

private void setLoggingAttrs(@Nullable SoyLogger.LoggingAttrs loggingAttrs) {
if (this.loggingAttrs != null) {
googleLogger.atWarning().withStackTrace(StackSize.SMALL).log(
"a logger configured logging attrs that were not rendered onto an element. Your {velog}"
+ " command must not be wrapping an element, this is undefined behavior.");
}
this.loggingAttrs = loggingAttrs;
}

private void appendDebugOutput(Optional<SafeHtml> veDebugOutput) {
if (veDebugOutput.isPresent()) {
Expand Down
2 changes: 2 additions & 0 deletions java/src/com/google/template/soy/logging/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ java_library(
deps = [
"//java/src/com/google/template/soy/data",
"//java/src/com/google/template/soy/plugin/restricted",
"@com_google_auto_value_auto_value",
"@maven//:com_google_common_html_types_types",
"@maven//:com_google_guava_guava",
],
)

Expand Down
181 changes: 176 additions & 5 deletions java/src/com/google/template/soy/logging/SoyLogger.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,18 @@
*/
package com.google.template.soy.logging;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableMap;
import com.google.common.html.HtmlEscapers;
import com.google.common.html.types.SafeHtml;
import com.google.common.html.types.SafeUrl;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.template.soy.data.LogStatement;
import com.google.template.soy.data.LoggingFunctionInvocation;
import java.io.IOException;
import java.util.Optional;

/**
Expand All @@ -26,15 +35,177 @@
* <p>This implements a callback protocol with the {@code velog} syntax.
*/
public interface SoyLogger {
/** Logging attributes for {@code velog} root elements. */
@AutoValue
public abstract class LoggingAttrs {
public static Builder builder() {
return new Builder();
}

LoggingAttrs() {}

/**
* The attributes to be added to the element.
*
* <p>The attributes are added in the order they are added to the builder. The values are
* escaped and suitable for encoding in a double-quoted attribute value.
*/
abstract ImmutableMap<String, String> attrs();

abstract boolean hasAnchorAttributes();

/** Writes the attributes to the output appendable. */
public void writeTo(boolean isAnchorTag, Appendable outputAppendable) throws IOException {
if (hasAnchorAttributes() && !isAnchorTag) {
throw new IllegalStateException(
"logger attempted to add anchor attributes to a non-anchor element.");
}
for (var entry : attrs().entrySet()) {
var name = entry.getKey();
outputAppendable
.append(' ')
.append(name)
.append("=\"")
.append(entry.getValue())
.append('"');
}
}

@Override
public final String toString() {
StringBuilder sb = new StringBuilder();
try {
writeTo(true, sb);
} catch (IOException e) {
throw new AssertionError(e);
}
return sb.substring(1); // skip the leading space
}

/** Builder for {@link LoggingAttrs}. */
public static final class Builder {
private boolean hasAnchorAttributes = false;
private final ImmutableMap.Builder<String, String> attrsBuilder = ImmutableMap.builder();

private Builder() {}

private void addAttribute(String key, String value) {
attrsBuilder.put(key, HtmlEscapers.htmlEscaper().escape(value));
}

/**
* Adds a data attribute to the logging attributes.
*
* <p>The key must start with "data-"
*/
@CanIgnoreReturnValue
public Builder addDataAttribute(String key, String value) {
checkArgument(key.startsWith("data-"), "data attribute key must start with 'data-'.");
checkNotNull(value);
addAttribute(key, value);
return this;
}

/**
* Adds an anchor href attribute to the logging attributes.
*
* <p>If the element this is attached to is an anchor tag, this will be used as the href, if
* it isn't an anchor an error will be thrown.
*/
@CanIgnoreReturnValue
public Builder addAnchorHref(SafeUrl value) {
addAttribute("href", value.getSafeUrlString());
this.hasAnchorAttributes = true;
return this;
}

/**
* Adds an anchor ping attribute to the logging attributes.
*
* <p>If the element this is attached to is an anchor tag, this will be used as the ping, if
* it isn't an anchor an error will be thrown.
*/
@CanIgnoreReturnValue
public Builder addAnchorPing(SafeUrl value) {
addAttribute("ping", value.getSafeUrlString());
this.hasAnchorAttributes = true;
return this;
}

/**
* Adds an anchor ping attribute to the logging attributes.
*
* <p>If the element this is attached to is an anchor tag, this will be used as the ping, if
* it isn't an anchor an error will be thrown.
*/
@CanIgnoreReturnValue
public Builder addAnchorPing(Iterable<? extends SafeUrl> value) {
StringBuilder pingBuilder = new StringBuilder();
boolean first = true;
for (SafeUrl url : value) {
if (!first) {
pingBuilder.append(' ');
}
pingBuilder.append(url.getSafeUrlString());
first = false;
}
addAttribute("ping", pingBuilder.toString());
return this;
}

/**
* Builds the {@link LoggingAttrs}.
*
* @throws IllegalStateException if no attributes were added.
*/
public LoggingAttrs build() {
var attrs = attrsBuilder.buildOrThrow();
if (attrs.isEmpty()) {
throw new IllegalStateException("LoggingAttrs must have at least one attribute");
}
return new AutoValue_SoyLogger_LoggingAttrs(attrs, hasAnchorAttributes);
}
}
}

/** Data to be used to output VE logging info to be outputted to the DOM while in debug mode. */
@AutoValue
public abstract static class EnterData {
public static final EnterData EMPTY = create(Optional.empty(), Optional.empty());

public static EnterData create(SafeHtml debugHtml) {
return create(Optional.of(debugHtml), Optional.empty());
}

public static EnterData create(LoggingAttrs loggingAttrs) {
return create(Optional.empty(), Optional.of(loggingAttrs));
}

public static EnterData create(SafeHtml debugHtml, LoggingAttrs loggingAttrs) {
return create(Optional.of(debugHtml), Optional.of(loggingAttrs));
}

private static EnterData create(
Optional<SafeHtml> debugHtml, Optional<LoggingAttrs> loggingAttrs) {
return new AutoValue_SoyLogger_EnterData(debugHtml, loggingAttrs);
}

public abstract Optional<SafeHtml> debugHtml();

public abstract Optional<LoggingAttrs> loggingAttrs();

EnterData() {}
}

/**
* Called when a {@code velog} statement is entered.
*
* @return Optional VE logging info to be outputted to the DOM while in debug mode. Method
* implementation must check and only return VE logging info if in debug mode. Most
* implementations will likely return Optional.empty(). TODO(b/148167210): This is currently
* under implementation.
* @return Data to be used to output VE logging info to be outputted to the DOM while in debug
* mode. Method implementation must check and only return VE logging info if in debug mode.
* Most implementations will likely return Optional.empty(). TODO(b/148167210): This is
* currently under implementation.
*/
Optional<SafeHtml> enter(LogStatement statement);
EnterData enter(LogStatement statement);

/**
* Called when a {@code velog} statement is exited.
Expand Down
Loading

0 comments on commit 3d8a2e4

Please sign in to comment.