-
Notifications
You must be signed in to change notification settings - Fork 43
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
Generator Should Read ExternalImportType Safety #1944
base: develop
Are you sure you want to change the base?
Generator Should Read ExternalImportType Safety #1944
Conversation
#1945 is a better solution |
conjure-java-core/src/integrationInput/java/com/palantir/product/SafeExternalLongExample.java
Show resolved
Hide resolved
} | ||
|
||
@JsonSetter(value = "optionalSafeExternalLong", nulls = Nulls.SKIP) | ||
public Builder optionalSafeExternalLong(@Nonnull Optional<? extends Long> optionalSafeExternalLong) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again, not sure if we want the parameter to have a safety annotation, or if we don't support wrapping external references in optional
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i think i am actually going to handle optionals wrapping externals, pushing that up soon
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally I think we'd end up with Optional<@Safe ? extends Long>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would we want @Safe Optional<@Safe ? extends Long>
like here? Or would we omit the external annotation for external imports?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the linked example should ideally only have a single @Safe
:
public Builder item(@Nonnull Optional<@Safe String> item) {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For posterity:
Poet has a bug that will make handling containers of external imports hard (like optional). We use WildcardTypeName to represent the wildcard type extending an external import. But when emitting, WildcardTypeName essentially ignores its annotations, which I verified in this (extremely rough) test. So in order to generate code that looks like @Safe ? extends Long>
we would have to do something extremely hacky, like eschew javapoet and just stick in a string which doesn't feel......great.
So we decided to settle for the annotation on the wrapper itself, as in @Safe Optional<...>
conjure-java-core/src/main/java/com/palantir/conjure/java/util/SafetyUtils.java
Show resolved
Hide resolved
2c16819
to
e29e2d8
Compare
FieldSpec field = enriched.poetSpec(); | ||
return BeanBuilderAuxiliarySettersUtils.createItemSetterBuilder(enriched, itemType, typeMapper, builderClass) | ||
return BeanBuilderAuxiliarySettersUtils.createItemSetterBuilder( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
previously, item setters for collection types didn't have safety annotations, even for primitive collections. For example, a field of type List<boolean>
would generate the following setter:
public Builder listBoolean(String listBoolean) {
checkNotBuilt();
this.listBoolean.add(listBoolean);
return this;
}
Now, both primitive and external import collections generate item setters that look like:
public Builder listBoolean(@Safe String listBoolean) {
checkNotBuilt();
this.listBoolean.add(listBoolean);
return this;
}
|
||
Class<?> builderSubclass = getMatchingSubclass(SafeExternalLongExample.class, "$Builder"); | ||
assertFieldTypeParamHasAnnotation(builderSubclass, "safeExternalLongSet", "Long", Safe.class); | ||
assertMethodParamHasAnnotation(builderSubclass, "safeExternalLongSet", "safeExternalLongSet", Safe.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this includes the item setter and the collection setter because the internal method uses allMatch
be32776
to
3c5b062
Compare
FieldSpec field = enriched.poetSpec(); | ||
return publicSetter(enriched, returnClass).addParameter(typeMapper.getClassName(itemType), field.name); | ||
return publicSetter(enriched, returnClass) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is to support this https://github.com/palantir/conjure-java/pull/1944/files#r1103276431
Generate changelog in
|
92673fe
to
da8b782
Compare
@@ -113,7 +115,7 @@ public Optional<LogSafety> visitUnion(UnionDefinition value) { | |||
return with(value.getTypeName(), () -> { | |||
Optional<LogSafety> safety = UNKNOWN_UNION_VARINT_SAFETY; | |||
for (FieldDefinition variant : value.getUnion()) { | |||
safety = combine(safety, getSafety(variant.getType(), variant.getSafety())); | |||
safety = combine(safety, getSafety(variant.getType(), SafetyUtils.getMaybeExternalSafety(variant))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand this correctly, I don't think any of the SafetyUtils
utility-method invocations used in this class are necessary. This class will walk the tree until it finds something that declares safety, so the updated visitExternal(ExternalReference value)
should take care of it.
public static Optional<LogSafety> getMaybeExternalSafety(AliasDefinition alias) { | ||
if (alias.getAlias().accept(WrapsExternalType.INSTANCE)) { | ||
return alias.getAlias().accept(GetMaybeExternalSafety.INSTANCE); | ||
} | ||
return alias.getSafety(); | ||
} | ||
|
||
public static Optional<LogSafety> getMaybeExternalSafety(FieldDefinition field) { | ||
if (field.getType().accept(WrapsExternalType.INSTANCE)) { | ||
return field.getType().accept(GetMaybeExternalSafety.INSTANCE); | ||
} | ||
return field.getSafety(); | ||
} | ||
|
||
public static Optional<LogSafety> getMaybeExternalSafety(ArgumentDefinition argument) { | ||
if (argument.getType().accept(WrapsExternalType.INSTANCE)) { | ||
return argument.getType().accept(GetMaybeExternalSafety.INSTANCE); | ||
} | ||
return argument.getSafety(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I might reframe and refactor these a bit to decouple behavior from external types. There's a class of values (applies to fields, arguments, and aliases) which require their safety to be explicitly listed. So, given a Type
, we need to determine whether the generated class declares safety itself, or if we need to add an annotation where the type is used.
Primitives (with the exception of bearer-token) and external-type-imports require safety at each place they're used. Determining the safety value can be handled using the existing SafetyEvaluator
class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, so I could instead think of this class as getting the usage time safety, which, for primitives, is declared in the wrapper type (field, argument, alias) itself and for external imports, is declared in the external-import-type itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep!
externalLong: | ||
type: DangerousLong |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add a few more args:
- declared-safe external imported long
- external imported long without any safety information declared
- Alias of each of the existing examples
@Test | ||
public void testAliasAnnotations() { | ||
assertThat(SafeExternalLongAlias.class).hasAnnotation(Safe.class); | ||
|
||
assertMethodHasAnnotation(SafeExternalLongAlias.class, "toString", Safe.class); | ||
assertMethodHasAnnotation(SafeExternalLongAlias.class, "get", Safe.class); | ||
assertMethodParamHasAnnotation(SafeExternalLongAlias.class, "valueOf", "value", Safe.class); | ||
assertMethodParamHasAnnotation(SafeExternalLongAlias.class, "of", "value", Safe.class); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could go either way on reflection-based testing. We have tests which generate integrationInput
, so future changes will have clear diffs which can be reviewed more clearly than reading reflection code, but that does rely on the reviewer to understand and account for all changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think there's any harm in having both? I see your point about the integrationInput being more readable, but I feel like external import safety is kind of brittle, because if someone forgets to use the SafetyUtils wrappers then we lose safety markings. So I thought a test at least provides some in-your-face protection against future breaks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally fine to have both :-)
If an Alias, Field, or Argument is an external import type, use the underlying import's safety instead of the declared safety (which will be empty). Converts all usages of AliasDefinition.getSafety(), FieldDefinition.getSafety(), and ArgumentDefinition.getSafety() to the corresponding wrapper methods in SafetyUtils.java.