Skip to content
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

Add ability to manually escape reserved words #174

Merged
merged 1 commit into from
Sep 26, 2019
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;

Expand All @@ -39,7 +40,7 @@ public Symbol toSymbol(Shape shape) {
}

@Override
public String toMemberName(Shape shape) {
public String toMemberName(MemberShape shape) {
return memberCache.computeIfAbsent(shape.toShapeId(), id -> delegate.toMemberName(shape));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.util.function.BiPredicate;
import java.util.logging.Logger;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.utils.SmithyBuilder;

Expand All @@ -35,29 +36,25 @@
*
* <p>A warning is logged each time a symbol is renamed by a reserved words
* implementation.
*
* <p>SymbolProvider implementations that need to recursively call themselves
* in a way that requires recursive symbols to be escaped will need to
* manually make calls into {@link ReservedWordSymbolProvider.Escaper} and
* cannot be decorated by an instance of {@code ReservedWordSymbolProvider}.
* For example, this is the case if a list of strings needs to be turned
* into something like "Array[%s]" where "%s" is the symbol name of the
* targeted member.
*/
public final class ReservedWordSymbolProvider implements SymbolProvider {
private static final Logger LOGGER = Logger.getLogger(ReservedWordSymbolProvider.class.getName());
private static final ReservedWords IDENTITY = ReservedWords.identity();

private final SymbolProvider delegate;
private final ReservedWords filenameReservedWords;
private final ReservedWords namespaceReservedWords;
private final ReservedWords nameReservedWords;
private final ReservedWords memberReservedWords;
private final BiPredicate<Shape, Symbol> escapePredicate;

private ReservedWordSymbolProvider(Builder builder) {
this.delegate = SmithyBuilder.requiredState("symbolProvider", builder.delegate);
this.filenameReservedWords = resolveReserved(builder.filenameReservedWords);
this.namespaceReservedWords = resolveReserved(builder.namespaceReservedWords);
this.nameReservedWords = resolveReserved(builder.nameReservedWords);
this.memberReservedWords = resolveReserved(builder.memberReservedWords);
this.escapePredicate = builder.escapePredicate;
}
private final Escaper escaper;

private static ReservedWords resolveReserved(ReservedWords specific) {
return specific != null ? specific : IDENTITY;
private ReservedWordSymbolProvider(SymbolProvider delegate, Escaper escaper) {
this.delegate = delegate;
this.escaper = escaper;
}

/**
Expand All @@ -69,51 +66,18 @@ public static Builder builder() {
return new Builder();
}

@Override
public Symbol toSymbol(Shape shape) {
Symbol upstream = delegate.toSymbol(shape);

// Only escape symbols when the predicate returns true.
if (!escapePredicate.test(shape, upstream)) {
return upstream;
}

String newName = convertWord("name", upstream.getName(), nameReservedWords);
String newNamespace = convertWord("namespace", upstream.getNamespace(), namespaceReservedWords);
String newDeclarationFile = convertWord("filename", upstream.getDeclarationFile(), filenameReservedWords);
String newDefinitionFile = convertWord("filename", upstream.getDefinitionFile(), filenameReservedWords);

// Only create a new symbol when needed.
if (newName.equals(upstream.getName())
&& newNamespace.equals(upstream.getNamespace())
&& newDeclarationFile.equals(upstream.getDeclarationFile())
&& newDefinitionFile.equals(upstream.getDeclarationFile())) {
return upstream;
}

return upstream.toBuilder()
.name(newName)
.namespace(newNamespace, upstream.getNamespaceDelimiter())
.declarationFile(newDeclarationFile)
.definitionFile(newDefinitionFile)
.build();
private static ReservedWords resolveReserved(ReservedWords specific) {
return specific != null ? specific : IDENTITY;
}

@Override
public String toMemberName(Shape shape) {
return convertWord("member", delegate.toMemberName(shape), memberReservedWords);
public Symbol toSymbol(Shape shape) {
return escaper.escapeSymbol(shape, delegate.toSymbol(shape));
}

private static String convertWord(String name, String result, ReservedWords reservedWords) {
if (!reservedWords.isReserved(result)) {
return result;
}

String newResult = reservedWords.escape(result);
LOGGER.warning(() -> String.format(
"Reserved word: %s is a reserved word for a %s. Converting to %s",
result, name, newResult));
return newResult;
@Override
public String toMemberName(MemberShape shape) {
return escaper.escapeMemberName(delegate.toMemberName(shape));
}

/**
Expand All @@ -128,16 +92,40 @@ public static final class Builder {
private BiPredicate<Shape, Symbol> escapePredicate = (shape, symbol) -> true;

/**
* Builds the provider.
* Builds a {@code SymbolProvider} implementation that wraps another
* symbol provider and escapes its results.
*
* <p>This might not always be the right solution. For example,
* symbol providers often need to recursively resolve symbols to
* create shapes like arrays and maps. In these cases, delegating
* would be awkward or impossible since the symbol provider being
* wrapped would also need access to the wrapper. In cases like this,
* use {@link #buildEscaper()} and pass that into the SymbolProvider
* directly.
*
* @return Returns the built provider.
* @return Returns the built SymbolProvider that delegates to another.
*/
public SymbolProvider build() {
return new ReservedWordSymbolProvider(this);
return new ReservedWordSymbolProvider(
SmithyBuilder.requiredState("delegate", delegate),
buildEscaper());
}

/**
* Builds a {@code SymbolProvider.Escaper} that is used to manually
* escape {@code Symbol}s and member names.
*
* @return Returns the built escaper.
*/
public Escaper buildEscaper() {
return new Escaper(this);
}

/**
* Sets the <strong>required</strong> delegate symbol provider.
* Sets the delegate symbol provider.
*
* <p>This is only required when calling {@link #build} to build
* a {@code SymbolProvider} that delegates to another provider.
*
* @param delegate Symbol provider to delegate to.
* @return Returns the builder
Expand Down Expand Up @@ -227,4 +215,80 @@ public Builder escapePredicate(BiPredicate<Shape, Symbol> escapePredicate) {
return this;
}
}

/**
* Uses to manually escape {@code Symbol}s and member names.
*/
public static final class Escaper {
private final ReservedWords filenameReservedWords;
private final ReservedWords namespaceReservedWords;
private final ReservedWords nameReservedWords;
private final ReservedWords memberReservedWords;
private final BiPredicate<Shape, Symbol> escapePredicate;

private Escaper(Builder builder) {
this.filenameReservedWords = resolveReserved(builder.filenameReservedWords);
this.namespaceReservedWords = resolveReserved(builder.namespaceReservedWords);
this.nameReservedWords = resolveReserved(builder.nameReservedWords);
this.memberReservedWords = resolveReserved(builder.memberReservedWords);
this.escapePredicate = builder.escapePredicate;
}

/**
* Escapes the given symbol using the reserved words implementations
* registered for each component.
*
* @param shape Shape being turned into a {@code Symbol}.
* @param symbol {@code Symbol} to escape.
* @return Returns the escaped {@code Symbol}.
*/
public Symbol escapeSymbol(Shape shape, Symbol symbol) {
// Only escape symbols when the predicate returns true.
if (!escapePredicate.test(shape, symbol)) {
return symbol;
}

String newName = convertWord("name", symbol.getName(), nameReservedWords);
String newNamespace = convertWord("namespace", symbol.getNamespace(), namespaceReservedWords);
String newDeclarationFile = convertWord("filename", symbol.getDeclarationFile(), filenameReservedWords);
String newDefinitionFile = convertWord("filename", symbol.getDefinitionFile(), filenameReservedWords);

// Only create a new symbol when needed.
if (newName.equals(symbol.getName())
&& newNamespace.equals(symbol.getNamespace())
&& newDeclarationFile.equals(symbol.getDeclarationFile())
&& newDefinitionFile.equals(symbol.getDeclarationFile())) {
return symbol;
}

return symbol.toBuilder()
.name(newName)
.namespace(newNamespace, symbol.getNamespaceDelimiter())
.declarationFile(newDeclarationFile)
.definitionFile(newDefinitionFile)
.build();
}

/**
* Escapes the given member name if needed.
*
* @param memberName Member name to escape.
* @return Returns the possibly escaped member name.
*/
public String escapeMemberName(String memberName) {
return convertWord("member", memberName, memberReservedWords);
}

private static String convertWord(String name, String result, ReservedWords reservedWords) {
if (!reservedWords.isReserved(result)) {
return result;
}

String newResult = reservedWords.escape(result);
LOGGER.warning(() -> String.format(
"Reserved word: %s is a reserved word for a %s. Converting to %s",
result, name, newResult));
return newResult;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@

package software.amazon.smithy.codegen.core;

import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.utils.StringUtils;

/**
* Provides {@link Symbol} objects for shapes.
Expand Down Expand Up @@ -58,20 +58,17 @@ public interface SymbolProvider {
Symbol toSymbol(Shape shape);

/**
* Converts a shape to a member/property name of a containing
* Converts a member shape to a member/property name of a containing
* data structure.
*
* <p>The default implementation will return the member name of
* the provided shape ID if the shape ID contains a member. If no
* member is present, the name of the shape with the first letter
* converted to lowercase is returned. The default implementation may
* not work for all use cases and should be overridden as needed.
* the provided shape ID and should be overridden if necessary.
*
* @param shape Shape to convert.
* @return Returns the converted member name.
*/
default String toMemberName(Shape shape) {
return shape.getId().getMember().orElseGet(() -> StringUtils.uncapitalize(shape.getId().getName()));
default String toMemberName(MemberShape shape) {
return shape.getMemberName();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import java.util.List;
import org.junit.jupiter.api.Test;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.shapes.StringShape;

Expand All @@ -24,9 +23,9 @@ public void cachesResults() {

SymbolProvider cache = SymbolProvider.cache(delegate);

Shape a = StringShape.builder().id("foo.baz#A").build();
Shape b = StringShape.builder().id("foo.baz#B").build();
Shape c = MemberShape.builder().id("foo.baz#C$c").target(a).build();
StringShape a = StringShape.builder().id("foo.baz#A").build();
StringShape b = StringShape.builder().id("foo.baz#B").build();
MemberShape c = MemberShape.builder().id("foo.baz#C$c").target(a).build();

assertThat(cache.toSymbol(a).getName(), equalTo("A"));
assertThat(cache.toSymbol(b).getName(), equalTo("B"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,8 @@ public void escapesReservedNames() {

@Test
public void escapesReservedMemberNames() {
Shape s1 = MemberShape.builder().id("foo.bar#Baz$foo").target("foo.baz#T").build();
Shape s2 = MemberShape.builder().id("foo.baz#Baz$baz").target("foo.baz#T").build();
MemberShape s1 = MemberShape.builder().id("foo.bar#Baz$foo").target("foo.baz#T").build();
MemberShape s2 = MemberShape.builder().id("foo.baz#Baz$baz").target("foo.baz#T").build();

ReservedWords reservedWords = new ReservedWordsBuilder().put("baz", "_baz").build();
SymbolProvider delegate = new MockProvider();
Expand Down Expand Up @@ -115,6 +115,19 @@ public void escapesOnlyWhenPredicateReturnsTrue() {
assertThat(provider.toSymbol(stringShape).getName(), equalTo("Bam"));
}

@Test
public void canCreateEscaper() {
MemberShape s1 = MemberShape.builder().id("foo.bar#Baz$baz").target("foo.baz#T").build();

ReservedWords reservedWords = new ReservedWordsBuilder().put("baz", "_baz").build();
SymbolProvider delegate = new MockProvider();
ReservedWordSymbolProvider.Escaper escaper = ReservedWordSymbolProvider.builder()
.memberReservedWords(reservedWords)
.buildEscaper();

assertThat(escaper.escapeMemberName(delegate.toMemberName(s1)), equalTo("_baz"));
}

private static final class MockProvider implements SymbolProvider {
public Symbol mock;

Expand Down