Skip to content

Commit

Permalink
Add ReservedWords builder for simpler construction
Browse files Browse the repository at this point in the history
The ReservedWordsBuilder can be used to more easily construct a
ReservedWords implementation, including loading reserved words from a
newline delimited file that uses "#" as comments.
  • Loading branch information
mtdowling committed Sep 3, 2019
1 parent a81662e commit 322cf8c
Show file tree
Hide file tree
Showing 11 changed files with 501 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 software.amazon.smithy.codegen.core;

import java.util.Locale;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

final class CaseInsensitiveReservedWords implements ReservedWords {

private final Set<String> words;
private final Function<String, String> escaper;

CaseInsensitiveReservedWords(Set<String> words, Function<String, String> escaper) {
this.words = words.stream().map(word -> word.toLowerCase(Locale.ENGLISH)).collect(Collectors.toSet());
this.escaper = escaper;
}

@Override
public String escape(String word) {
return isReserved(word) ? escaper.apply(word) : word;
}

@Override
public boolean isReserved(String word) {
return words.contains(word.toLowerCase(Locale.ENGLISH));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ public static Builder builder() {
@Override
public String escape(String word) {
String result = mappings.get(word);
if (result == null) {

if (result == null && !caseInsensitiveMappings.isEmpty()) {
result = caseInsensitiveMappings.get(word.toLowerCase(Locale.US));
}

Expand All @@ -86,7 +87,11 @@ public String escape(String word) {

@Override
public boolean isReserved(String word) {
return mappings.containsKey(word) || caseInsensitiveMappings.containsKey(word.toLowerCase(Locale.US));
if (mappings.containsKey(word)) {
return true;
}

return !caseInsensitiveMappings.isEmpty() && caseInsensitiveMappings.containsKey(word.toLowerCase(Locale.US));
}

/**
Expand All @@ -111,7 +116,13 @@ public Builder put(String reservedWord, String conversion) {
}

/**
* Add a new case-insensitive reserved words.
* Add a new case-insensitive reserved word that converts the given
* reserved word to the given conversion string.
*
* <p>Note that the conversion string is used literally. The casing
* of the original word has no effect on the conversion. Use
* {@link ReservedWordsBuilder} for a case-insensitive reserved words
* implementation that can take casing into account.
*
* @param reservedWord Case-insensitive reserved word to convert.
* @param conversion Word to convert to.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

package software.amazon.smithy.codegen.core;

import java.util.function.BiPredicate;
import java.util.logging.Logger;
import software.amazon.smithy.model.shapes.Shape;
import software.amazon.smithy.utils.SmithyBuilder;
Expand Down Expand Up @@ -44,13 +45,15 @@ public final class ReservedWordSymbolProvider implements SymbolProvider {
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 static ReservedWords resolveReserved(ReservedWords specific) {
Expand All @@ -69,11 +72,30 @@ public static Builder 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(nameReservedWords.escape(upstream.getName()))
.namespace(namespaceReservedWords.escape(upstream.getNamespace()), upstream.getNamespaceDelimiter())
.declarationFile(filenameReservedWords.escape(upstream.getDeclarationFile()))
.definitionFile(filenameReservedWords.escape(upstream.getDefinitionFile()))
.name(newName)
.namespace(newNamespace, upstream.getNamespaceDelimiter())
.declarationFile(newDeclarationFile)
.definitionFile(newDefinitionFile)
.build();
}

Expand Down Expand Up @@ -103,6 +125,7 @@ public static final class Builder {
private ReservedWords namespaceReservedWords;
private ReservedWords nameReservedWords;
private ReservedWords memberReservedWords;
private BiPredicate<Shape, Symbol> escapePredicate = (shape, symbol) -> true;

/**
* Builds the provider.
Expand Down Expand Up @@ -180,5 +203,28 @@ public Builder memberReservedWords(ReservedWords memberReservedWords) {
this.memberReservedWords = memberReservedWords;
return this;
}

/**
* Sets a predicate that is used to control when a shape + symbol
* combination should be checked if it's a reserved word.
*
* <p>The predicate is invoked when {@code toSymbol} is called. It
* is used to disable/enable escaping reserved words based on the
* shape and symbol. The given predicate accepts the {@code Shape}
* and the {@code Symbol} that was created for the shape and returns
* true if reserved word checks should be made or false if reserved
* word checks should not be made. For example, some code generators
* only escape words that have namespaces to differentiate between
* language built-ins and user-defined types.
*
* <p>By default, all symbols are checked for reserved words.
*
* @param escapePredicate Predicate that returns true if escaping should be checked.
* @return Returns the builder.
*/
public Builder escapePredicate(BiPredicate<Shape, Symbol> escapePredicate) {
this.escapePredicate = escapePredicate;
return this;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/*
* Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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 software.amazon.smithy.codegen.core;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import software.amazon.smithy.utils.StringUtils;

/**
* Builds a {@link ReservedWords} implementation from explicit
* mappings and from line-delimited files that contain reserved words.
*/
public class ReservedWordsBuilder {

private final Map<String, String> mappings = new HashMap<>();
private final List<ReservedWords> delegates = new ArrayList<>();

/**
* Builds the reserved words.
*
* @return Returns the created reserved words implementation.
*/
public ReservedWords build() {
ReservedWords[] words = new ReservedWords[1 + delegates.size()];
words[0] = new MappedReservedWords(mappings, Collections.emptyMap());
for (int i = 0; i < delegates.size(); i++) {
words[i + 1] = delegates.get(i);
}

return ReservedWords.compose(words);
}

/**
* Add a new reserved words.
*
* @param reservedWord Reserved word to convert.
* @param conversion Word to convert to.
* @return Returns the builder.
*/
public ReservedWordsBuilder put(String reservedWord, String conversion) {
mappings.put(reservedWord, conversion);
return this;
}

/**
* Load a list of case-sensitive, line-delimited reserved words from a file.
*
* <p>This method will escape words by prefixing them with "_". Use
* {@link #loadWords(URL, Function)} to customize how words are escaped.
*
* <p>Blank lines and lines that start with # are ignored.
*
* @param location URL of the file to load.
* @return Returns the builder.
*/
public ReservedWordsBuilder loadWords(URL location) {
return loadWords(location, ReservedWordsBuilder::escapeWithUnderscore);
}

/**
* Load a list of case-sensitive, line-delimited reserved words from a file.
*
* <p>Blank lines and lines that start with # are ignored.
*
* @param location URL of the file to load.
* @param escaper Function used to escape reserved words.
* @return Returns the builder.
*/
public ReservedWordsBuilder loadWords(URL location, Function<String, String> escaper) {
for (String word : readNonBlankNonCommentLines(location)) {
put(word, escaper.apply(word));
}
return this;
}

/**
* Load a list of case-insensitive, line-delimited reserved words from a file.
*
* <p>This method will escape words by prefixing them with "_". Use
* {@link #loadCaseInsensitiveWords(URL, Function)} to customize how words
* are escaped.
*
* <p>Blank lines and lines that start with # are ignored.
*
* @param location URL of the file to load.
* @return Returns the builder.
*/
public ReservedWordsBuilder loadCaseInsensitiveWords(URL location) {
return loadCaseInsensitiveWords(location, ReservedWordsBuilder::escapeWithUnderscore);
}

/**
* Load a list of case-insensitive, line-delimited reserved words from a file.
*
* <p>Blank lines and lines that start with # are ignored.
*
* @param location URL of the file to load.
* @param escaper Function used to escape reserved words.
* @return Returns the builder.
*/
public ReservedWordsBuilder loadCaseInsensitiveWords(URL location, Function<String, String> escaper) {
delegates.add(new CaseInsensitiveReservedWords(readNonBlankNonCommentLines(location), escaper));
return this;
}

private static String escapeWithUnderscore(String word) {
return "_" + word;
}

private static Set<String> readNonBlankNonCommentLines(URL url) {
try (InputStream is = url.openConnection().getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
return reader.lines()
.filter(StringUtils::isNotBlank)
.filter(line -> !line.startsWith("#"))
.map(word -> StringUtils.stripEnd(word, null))
.collect(Collectors.toSet());
} catch (IOException e) {
throw new UncheckedIOException("Error loading reserved words from " + url + ": " + e.getMessage(), e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ public Builder references(List<SymbolReference> references) {
* Add a symbol reference to indicate that this symbol points to
* or contains references to other symbols.
*
* @param reference Symbol that is referenced with no specific reference name.
* @param reference Symbol that is referenced.
* @return Returns the builder.
*/
public Builder addReference(Symbol reference) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public void escapesReservedFilenames() {
Shape s1 = StringShape.builder().id("foo.bar#Baz").build();
Shape s2 = StringShape.builder().id("foo.bar#Bam").build();

ReservedWords reservedWords = MappedReservedWords.builder().put("/foo/bar/bam", "/rewritten").build();
ReservedWords reservedWords = new ReservedWordsBuilder().put("/foo/bar/bam", "/rewritten").build();
MockProvider delegate = new MockProvider();
SymbolProvider provider = ReservedWordSymbolProvider.builder()
.symbolProvider(delegate)
Expand All @@ -50,7 +50,7 @@ public void escapesReservedNamespaces() {
Shape s1 = StringShape.builder().id("foo.bar#Baz").build();
Shape s2 = StringShape.builder().id("foo.baz#Bam").build();

ReservedWords reservedWords = MappedReservedWords.builder().put("foo.baz", "foo._baz").build();
ReservedWords reservedWords = new ReservedWordsBuilder().put("foo.baz", "foo._baz").build();
MockProvider delegate = new MockProvider();
SymbolProvider provider = ReservedWordSymbolProvider.builder()
.symbolProvider(delegate)
Expand All @@ -69,7 +69,7 @@ public void escapesReservedNames() {
Shape s1 = StringShape.builder().id("foo.bar#Baz").build();
Shape s2 = StringShape.builder().id("foo.baz#Bam").build();

ReservedWords reservedWords = MappedReservedWords.builder().put("Bam", "_Bam").build();
ReservedWords reservedWords = new ReservedWordsBuilder().put("Bam", "_Bam").build();
MockProvider delegate = new MockProvider();
SymbolProvider provider = ReservedWordSymbolProvider.builder()
.symbolProvider(delegate)
Expand All @@ -88,7 +88,7 @@ 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();

ReservedWords reservedWords = MappedReservedWords.builder().put("baz", "_baz").build();
ReservedWords reservedWords = new ReservedWordsBuilder().put("baz", "_baz").build();
SymbolProvider delegate = new MockProvider();
SymbolProvider provider = ReservedWordSymbolProvider.builder()
.symbolProvider(delegate)
Expand All @@ -99,6 +99,22 @@ public void escapesReservedMemberNames() {
assertThat(provider.toMemberName(s2), equalTo("_baz"));
}

@Test
public void escapesOnlyWhenPredicateReturnsTrue() {
Shape stringShape = StringShape.builder().id("foo.baz#Bam").build();

ReservedWords reservedWords = new ReservedWordsBuilder().put("Bam", "_Bam").build();
MockProvider delegate = new MockProvider();
SymbolProvider provider = ReservedWordSymbolProvider.builder()
.symbolProvider(delegate)
.nameReservedWords(reservedWords)
.escapePredicate((shape, symbol) -> false)
.build();

delegate.mock = Symbol.builder().namespace("foo.baz", ".").name("Bam").build();
assertThat(provider.toSymbol(stringShape).getName(), equalTo("Bam"));
}

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

Expand Down
Loading

0 comments on commit 322cf8c

Please sign in to comment.