Skip to content

Commit

Permalink
Support null/undefined values in extern params and return (record) va…
Browse files Browse the repository at this point in the history
…lues.

PiperOrigin-RevId: 679294308
  • Loading branch information
Jesse-Good authored and copybara-github committed Sep 26, 2024
1 parent e1dbc7b commit 1ce8b06
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 45 deletions.
38 changes: 19 additions & 19 deletions documentation/dev/externs.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,25 +192,25 @@ import {imageUrlFromOptions} from 'path/to/functions.soy';

## Supported type mappings between Soy and Java {#javatypes}

Soy type | Allowed Java types | Notes
---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | -----
`int` | `int`\*, `java.lang.Integer`, `long`\*, `java.lang.Long` | Integer overflow throws a runtime error.
`float` | `double`\*, `java.lang.Double`, `float`\*, `java.lang.Float` |
`number` | `double`\*, `java.lang.Double`, `java.lang.Number` | `number` is an alias for `int\|float`.
`string` | `java.lang.String` |
`bool` | `boolean`*, `java.lang.Boolean` |
`Message` | `com.google.protobuf.Message` |
protos | the proto message Java type |
proto enums | the proto enum Java type |
`uri` | `com.google.common.html.types.SafeUrl`, `com.google.common.html.types.SafeUrlProto` |
`trusted_resource_uri` | `com.google.common.html.types.TrustedResourceUrl`, `com.google.common.html.types.TrustedResourceUrlProto` |
`html` | `com.google.common.html.types.SafeHtml`, `com.google.common.html.types.SafeHtmlProto` |
`list<?>` | `java.util.List`, `com.google.common.collect.ImmutableList`, `java.util.Collection`, `java.lang.Iterable` | Supported element types are: `int`, `float`, `string`, `bool`, `Message`, proto, and proto enum.
`map<?,?>` | `java.util.Map`, `com.google.common.collect.ImmutableMap` | Same supported element types as list.
records | `java.util.Map`, `com.google.common.collect.ImmutableMap` | Supports all value types other than `list`, `map`, records, and unions.
unions | `java.lang.Object`, `com.google.template.soy.data.SoyValue` | All unions other than `int\|float`. Supported union members are: `int`, `float`, `string`, `bool`, proto, proto enum, `uri`, `trusted_resource_uri`, and `html`.
`any` | `java.lang.Object`, `com.google.template.soy.data.SoyValue` | For params, `NullData` is provided as Java `null` while `UndefinedData` is provided as-is.
template types | `com.google.template.soy.data.SoyTemplate`, `com.google.template.soy.data.PartialSoyTemplate`,`com.google.template.soy.data.TemplateValue` | the `SoyTemplate` type can only match fully bound template types. To accept a template type as a parameter you must use `TemplateValue`
Soy type | Allowed Java types | Notes
---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -----
`int` | `int`\*, `java.lang.Integer`, `long`\*, `java.lang.Long` | Integer overflow throws a runtime error.
`float` | `double`\*, `java.lang.Double`, `float`\*, `java.lang.Float` |
`number` | `double`\*, `java.lang.Double`, `java.lang.Number` | `number` is an alias for `int\|float`.
`string` | `java.lang.String` |
`bool` | `boolean`*, `java.lang.Boolean` |
`Message` | `com.google.protobuf.Message` |
protos | the proto message Java type |
proto enums | the proto enum Java type |
`uri` | `com.google.common.html.types.SafeUrl`, `com.google.common.html.types.SafeUrlProto` |
`trusted_resource_uri` | `com.google.common.html.types.TrustedResourceUrl`, `com.google.common.html.types.TrustedResourceUrlProto` |
`html` | `com.google.common.html.types.SafeHtml`, `com.google.common.html.types.SafeHtmlProto` |
`list<?>` | `java.util.List`, `com.google.common.collect.ImmutableList`, `java.util.Collection`, `java.lang.Iterable` | Supported element types are: `int`, `float`, `string`, `bool`, `Message`, proto, and proto enum.
`map<?,?>` | `java.util.Map`, `com.google.common.collect.ImmutableMap` | Same supported element types as list.
records | `java.util.Map`, `com.google.common.collect.ImmutableMap` | Supports all value types other than `list`, `map`, and records. `null` values provided as Java `null` if the Java map type allows null values, otherwise as `NullData`. `undefined` values provided as `UndefinedData`.
unions | `java.lang.Object`, `com.google.template.soy.data.SoyValue` | All unions other than `int\|float`. Supported union members are: `int`, `float`, `string`, `bool`, proto, proto enum, `uri`, `trusted_resource_uri`, and `html`.
`any` | `java.lang.Object`, `com.google.template.soy.data.SoyValue` | For params, `NullData` is provided as Java `null` while `UndefinedData` is provided as-is.
template types | `com.google.template.soy.data.SoyTemplate`, `com.google.template.soy.data.PartialSoyTemplate`, `com.google.template.soy.data.TemplateValue` | the `SoyTemplate` type can only match fully bound template types. To accept a template type as a parameter you must use `TemplateValue`

\* If the Soy type is nullable then the primitive Java type is not allowed.

Expand Down
8 changes: 6 additions & 2 deletions java/src/com/google/template/soy/jbcsrc/ExternCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -368,14 +368,18 @@ private static Expression adaptParameter(
if (SoyTypes.NUMBER_TYPE.equals(elmType)) {
return JbcSrcExternRuntime.LIST_UNBOX_NUMBERS.invoke(unboxedList);
}
// fall through
// fall through
default:
throw new AssertionError("ValidateExternsPass should prevent this.");
}
} else if (javaType.equals(BytecodeUtils.MAP_TYPE)
|| javaType.equals(BytecodeUtils.IMMUTABLE_MAP_TYPE)) {
if (nonNullableSoyType.getKind() == Kind.RECORD) {
return JbcSrcExternRuntime.UNBOX_RECORD.invoke(actualParam);
if (javaType.equals(BytecodeUtils.MAP_TYPE)) {
return JbcSrcExternRuntime.RECORD_TO_MAP.invoke(actualParam);
} else {
return JbcSrcExternRuntime.RECORD_TO_IMMUTABLE_MAP.invoke(actualParam);
}
}
SoyType keyType = SoyTypes.getMapKeysType(nonNullableSoyType);
SoyType valueType = SoyTypes.getMapValuesType(nonNullableSoyType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ java_library(
"//java/src/com/google/template/soy/data",
"//java/src/com/google/template/soy/data:record_property",
"//java/src/com/google/template/soy/jbcsrc/restricted",
"//java/src/com/google/template/soy/plugin/java:extern_helpers",
"@com_google_protobuf//:protobuf_java",
"@maven//:com_google_code_findbugs_jsr305",
"@maven//:com_google_common_html_types_types",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,18 +40,17 @@
import com.google.template.soy.data.SanitizedContents;
import com.google.template.soy.data.SoyIterable;
import com.google.template.soy.data.SoyMap;
import com.google.template.soy.data.SoyRecord;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.data.SoyValueConverter;
import com.google.template.soy.data.SoyValueProvider;
import com.google.template.soy.data.SoyValueUnconverter;
import com.google.template.soy.data.internal.IterableImpl;
import com.google.template.soy.data.restricted.NumberData;
import com.google.template.soy.jbcsrc.restricted.MethodRef;
import com.google.template.soy.plugin.java.SharedExternRuntime;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

Expand Down Expand Up @@ -312,18 +311,10 @@ public static <T> T toEnum(SoyValue value, Class<T> clazz) {
public static final MethodRef UNBOX_OBJECT =
MethodRef.createPure(SoyValueUnconverter.class, "unconvert", SoyValueProvider.class);

public static final MethodRef UNBOX_RECORD = create("unboxRecord", SoyValue.class);

@Keep
@Nullable
public static ImmutableMap<?, ?> unboxRecord(SoyValue value) {
if (value.isNullish()) {
return null;
}
SoyRecord map = (SoyRecord) value;
return map.recordAsMap().entrySet().stream()
.collect(toImmutableMap(Entry::getKey, e -> SoyValueUnconverter.unconvert(e.getValue())));
}
public static final MethodRef RECORD_TO_MAP =
MethodRef.createPure(SharedExternRuntime.class, "recordToMap", SoyValue.class);
public static final MethodRef RECORD_TO_IMMUTABLE_MAP =
MethodRef.createPure(SharedExternRuntime.class, "recordToImmutableMap", SoyValue.class);

public static final MethodRef UNBOX_SAFE_HTML = create("unboxSafeHtml", SoyValue.class);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ private static Class<?> getType(String typeName) {
.add(SoyType.Kind.CSS)
.add(SoyType.Kind.ANY)
.add(SoyType.Kind.UNKNOWN)
.add(SoyType.Kind.NULL)
.add(SoyType.Kind.UNDEFINED)
.build();

private static boolean typesAreCompatible(
Expand Down Expand Up @@ -434,10 +436,8 @@ private static boolean typesAreCompatible(
case RECORD:
RecordType recordType = (RecordType) soyType;
if (!recordType.getMembers().stream()
.map(
m ->
(m.optional() ? SoyTypes.tryRemoveNull(m.declaredType()) : m.declaredType())
.getKind())
.flatMap(m -> SoyTypes.expandUnions(m.checkedType()).stream())
.map(SoyType::getKind)
.allMatch(ALLOWED_RECORD_MEMBERS::contains)) {
return false;
}
Expand Down
8 changes: 7 additions & 1 deletion java/src/com/google/template/soy/plugin/java/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ load("@rules_java//java:defs.bzl", "java_library")

package(default_visibility = ["//:soy_internal"])

EXTERN_HELPERS_SRCS = ["RenderCssHelper.java"]
EXTERN_HELPERS_SRCS = [
"RenderCssHelper.java",
"SharedExternRuntime.java",
]

REFLECTIVE_SRCS = ["ReflectiveMethodChecker.java"]

Expand Down Expand Up @@ -46,6 +49,9 @@ java_library(
name = "extern_helpers",
srcs = EXTERN_HELPERS_SRCS,
deps = [
"//java/src/com/google/template/soy/data",
"@maven//:com_google_code_findbugs_jsr305",
"@maven//:com_google_errorprone_error_prone_annotations",
"@maven//:com_google_guava_guava",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2024 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.template.soy.plugin.java;

import static com.google.common.collect.ImmutableMap.toImmutableMap;

import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.Keep;
import com.google.template.soy.data.SoyRecord;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.data.SoyValueProvider;
import com.google.template.soy.data.SoyValueUnconverter;
import com.google.template.soy.data.restricted.NullData;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Nullable;

/** Support for the renderCss extern function. */
public class SharedExternRuntime {

private SharedExternRuntime() {}

@Keep
@Nullable
public static Map<String, ?> recordToMap(SoyValue value) {
if (value.isNullish()) {
return null;
}
Map<String, Object> map = new HashMap<>();
for (Entry<String, SoyValueProvider> e : ((SoyRecord) value).recordAsMap().entrySet()) {
map.put(e.getKey(), unconvert(e.getValue(), false));
}
return map;
}

@Keep
@Nullable
public static ImmutableMap<String, ?> recordToImmutableMap(SoyValue value) {
if (value.isNullish()) {
return null;
}
SoyRecord map = (SoyRecord) value;
return map.recordAsMap().entrySet().stream()
.collect(toImmutableMap(Entry::getKey, e -> unconvert(e.getValue(), true)));
}

private static Object unconvert(SoyValueProvider svp, boolean marshalNull) {
Object value = SoyValueUnconverter.unconvert(svp.resolve());
if (marshalNull && value == null) {
return NullData.INSTANCE;
}
return value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
import com.google.template.soy.data.SoyDataException;
import com.google.template.soy.data.SoyMap;
import com.google.template.soy.data.SoyProtoValue;
import com.google.template.soy.data.SoyRecord;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.data.SoyValueConverter;
import com.google.template.soy.data.SoyValueProvider;
Expand All @@ -55,6 +54,7 @@
import com.google.template.soy.internal.proto.JavaQualifiedNames;
import com.google.template.soy.plugin.internal.JavaPluginExecContext;
import com.google.template.soy.plugin.java.PluginInstances;
import com.google.template.soy.plugin.java.SharedExternRuntime;
import com.google.template.soy.plugin.java.restricted.JavaValue;
import com.google.template.soy.plugin.java.restricted.JavaValueFactory;
import com.google.template.soy.plugin.java.restricted.MethodSignature;
Expand Down Expand Up @@ -349,10 +349,11 @@ private Object adaptParam(TofuJavaValue tofuVal, Class<?> type, Method method, i
} else if (Map.class.isAssignableFrom(type) && isExternApi) {
SoyType paramType = externSig.getParameters().get(i).getType();
if (paramType.getKind() == Kind.RECORD) {
ImmutableMap.Builder<String, Object> builder = ImmutableMap.builder();
((SoyRecord) value)
.forEach((s, v) -> builder.put(s.getName(), SoyValueUnconverter.unconvert(v)));
return builder.buildOrThrow();
if (ImmutableMap.class.isAssignableFrom(type)) {
return SharedExternRuntime.recordToImmutableMap(value);
} else {
return SharedExternRuntime.recordToMap(value);
}
}
MapType mapType = (MapType) paramType;
return ((SoyMap) value)
Expand Down

0 comments on commit 1ce8b06

Please sign in to comment.