Skip to content
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 @@ -6,8 +6,11 @@

import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import software.amazon.smithy.model.node.ObjectNode;
import software.amazon.smithy.utils.SetUtils;
import software.amazon.smithy.utils.SmithyUnstableApi;

/**
Expand All @@ -29,6 +32,15 @@
*/
@SmithyUnstableApi
public final class TraitCodegenSettings {
private static final String SMITHY_KEYWORD = "smithy";
private static final Set<String> RESERVED_NAMESPACES = SetUtils.of("smithy.api",
"smithy.framework",
"smithy.mqtt",
"smithy.openapi",
"smithy.protocols",
"smithy.rules",
"smithy.test",
"smithy.waiters");

private final String packageName;
private final String smithyNamespace;
Expand All @@ -53,6 +65,9 @@ public final class TraitCodegenSettings {
List<String> excludeTags
) {
this.packageName = Objects.requireNonNull(packageName);
if (isReservedNamespace(smithyNamespace)) {
throw new IllegalArgumentException("The `smithy` namespace and its sub-namespaces are reserved.");
}
this.smithyNamespace = Objects.requireNonNull(smithyNamespace);
this.headerLines = Objects.requireNonNull(headerLines);
this.excludeTags = Objects.requireNonNull(excludeTags);
Expand Down Expand Up @@ -110,4 +125,26 @@ public List<String> headerLines() {
public List<String> excludeTags() {
return excludeTags;
}

/**
* Checks if a namespace is reserved. A namespace is considered reserved if it is
* "smithy", one of the predefined reserved namespaces or if it starts with one of the reserved namespaces followed by a dot.
*
* @param namespace the namespace to check
* @return true if the namespace is reserved, false otherwise
*/
private static boolean isReservedNamespace(String namespace) {
String namespaceLowerCase = namespace.toLowerCase(Locale.ROOT);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's remove this conversion, namespaces are case sensitive.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed offline that this needs to be case sensitive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, while namespaces are case-sensitive, the rationale of this was anyone should be forbidden from creating a trait with SMITHY or SMITHY.API or other versions of it as it conflicts with our reserved namespaces.

I am leaning towards blocking those case-insensitively. @mtdowling and I discussed, the reasoning for case-insensitive checking is that shapes are case-insensitive.

FWIW, for future record putting this here from our docs :

While shape ID references within the semantic model are case-sensitive, no two shapes in the semantic model can have the same case-insensitive shape ID. This restriction makes it easier to use Smithy models for code generation in programming languages that do not support case-sensitive identifiers or that perform some kind of normalization on generated identifiers (for example, a Python code generator might convert all member names to lower snake case). To illustrate, com.Foo#baz and com.foo#BAZ are not allowed in the same semantic model. This restriction also extends to member names: com.foo#Baz$bar and com.foo#Baz$BAR are in conflict.

if (namespaceLowerCase.equals(SMITHY_KEYWORD) || RESERVED_NAMESPACES.contains(namespaceLowerCase)) {
return true;
}

for (String reserved : RESERVED_NAMESPACES) {
if (namespaceLowerCase.startsWith(reserved + ".")) {
return true;
}
}

return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/
package software.amazon.smithy.traitcodegen;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.Collections;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

class TraitCodegenSettingsTest {

@ParameterizedTest
@ValueSource(strings = {
"smithy",
"smithy.api",
"smithy.waiters",
"smithy.test",
"smithy.api.foo",
"smithy.waiters.bar",
"smithy.test.baz",
"Smithy.api",
"sMithy",
"smithy.API",
"smiThy.Api.Baz"
})
void constructorRejectsReservedNamespaces(String namespace) {
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
() -> new TraitCodegenSettings("com.example",
namespace,
Collections.emptyList(),
Collections.emptyList()));
assertEquals("The `smithy` namespace and its sub-namespaces are reserved.", exception.getMessage());
}

@ParameterizedTest
@ValueSource(strings = {
"smithy.foo",
"smithy.bar.baz",
"example.namespace",
"my.custom.namespace",
"smithy.custom",
"smithyapi",
"smithy.apiextension"
})
void constructorAcceptsValidNamespaces(String namespace) {
assertDoesNotThrow(() -> new TraitCodegenSettings(
"com.example",
namespace,
Collections.emptyList(),
Collections.emptyList()));
}

@ParameterizedTest
@MethodSource("nullParameterCombinations")
void constructorRequiresNonNullParameters(String packageName, String namespace) {
assertThrows(NullPointerException.class,
() -> new TraitCodegenSettings(packageName,
namespace,
Collections.emptyList(),
Collections.emptyList()));
}

static Stream<Arguments> nullParameterCombinations() {
return Stream.of(
Arguments.of(null, "smithy.foo"),
Arguments.of("com.example", null));
}

@Test
void gettersReturnCorrectValues() {
TraitCodegenSettings settings = new TraitCodegenSettings(
"com.example",
"smithy.foo",
Collections.emptyList(),
Collections.emptyList());

assertEquals("com.example", settings.packageName());
assertEquals("smithy.foo", settings.smithyNamespace());
assertEquals(Collections.emptyList(), settings.headerLines());
assertEquals(Collections.emptyList(), settings.excludeTags());
}
}
Loading