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
Improve coercions esp with generics #982
Conversation
now have two methods for parsing a list as a string, depending whether it wants object or string
previously we would return e.g. a List not containing T if List<T> was requested. this was deprecated and we would warn, but now we are stricter.
previous commit used yaml parse even for things like List<String>; now do string-list parse in that case. means the json adapters have access to the type token, but that means running them earlier and ensuring we do a re-coercion if needed.
previously we converted to Class in places, losing generic info for resolution
previously config keys did not properly keep generic information for their contents; now this information is preserved and the results coerced as per the types. also enhances the ValueResolver to have clearer semantics when coercing generics in maps/iterables.
e8fa71e
to
8e48531
Compare
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.
Looks really good. Only minor comments, but some of them are worth addressing before merging.
I've also played around with a couple more unit tests, which pass. Easiest if I just submit a separate PR for those after this is merged.
|
||
/** returns reference to null without error if valid; otherwise returns reference to predicate and a good error message */ | ||
@SuppressWarnings("unchecked") | ||
<V> ReferenceWithError<Predicate<?>> checkValueValid(ConfigKey<V> configKey, V value) { |
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.
Naming convention in guava is that checkXyz
methods will throw an exception if the check fails (e.g. https://google.github.io/guava/releases/19.0/api/docs/com/google/common/base/Preconditions.html). I'd prefer the name validateValue(...)
.
* if the second argument is true, the type specified here is used against non-map/iterable items | ||
* inside maps and iterables encountered. if false, any generic signature on the supplied type | ||
* is traversed to match contained items. if null (default), it is inferred from the type, | ||
* those with generics mean true, and those without mean false. |
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.
Is it not the other way round: "those with generics mean false, and those without mean true"?
@@ -315,12 +320,121 @@ else if (c=='\"') { | |||
throw new IllegalArgumentException("String '"+s+"' is not a valid Java string (unterminated string)"); | |||
} | |||
|
|||
/** @deprecated since 1.0.0, use {@link #unwrapJsonishListStringIfPossible(String)} (old semantics) | |||
* or {@link #unwrapJsonishListStringIfPossible(String)} (improved) */ |
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.
It looks like the two methods being suggested (for old semantics and improved) are identical - what should they be? Should it be pointing folk at tryUnwrapJsonishList(String)
as well?
* <ll> 2) if not of form <code>[ X ]</code>, wrap in brackets and parse as YAML, | ||
* and if that succeeds and is a list, return the result. | ||
* <li> 3) otherwise, expect comma-separated tokens which after trimming are of the form "A" or B, | ||
* where "A" is a valid Java string or C is any string not starting with ' |
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.
Could do we rewording. This line introduces C
- should it be B
? There was no mention of C previously.
List<Object> result = (List<Object>)r; | ||
return Maybe.of(result); | ||
} | ||
} catch (Exception e) {} |
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.
Strong preference to never have empty catch blocks. I'd do:
catch(Exception e) {
Exceptions.propagateIfFatal(e);
// Logic below decides whether to return absent or to keep trying
}
Object coercedArgument = TypeCoercions.coerce(argument, TypeToken.of(paramType)); | ||
return Maybe.of(accessibleMethod.invoke(instance, coercedArgument)); | ||
Maybe<?> coercedArgumentM = TypeCoercions.tryCoerce(argument, TypeToken.of(paramType)); | ||
RuntimeException exception = Maybe.getException(coercedArgumentM); |
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.
Looks wrong - this will cast coercedArgumentM
to Maybe.absent
, but on the line below you do coercedArgumentM.isPresent
. So presumably the call to getException
will sometimes throw a ClassCastException
?
Ah, I see you've changed the semantics of getException
to return null if it wasn't an exception.
Why change it? I liked the way that asking for the exception when present
was a programming error - there will never be an exception when present. It looks like you can easily avoid that by removing this line, and changing the if statement below to just if (coercedArgumentM.isAbsent()) {
.
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.
The javadoc on Maybe.getException
claimed it would return null for present items; I relied on this and then discovered the error so updated the code to bring it in line with the javadoc.
Can change back if you like but personally I think it's useful to be able to get null
back as an exception, saves a little bit of work sometimes, as in this case: because we set exception
manually in the boxing case we'd need one more line than you suggest.
@@ -365,7 +365,7 @@ public RuntimeException getException() { | |||
}); | |||
} | |||
public static <T> Maybe<T> changeExceptionSupplier(Maybe<T> original, Function<AnyExceptionSupplier<?>,Supplier<? extends RuntimeException>> transform) { | |||
if (original.isPresent()) return original; | |||
if (original==null || original.isPresent()) return original; |
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 we should fail-fast if you pass in null. The caller has asked to change the exception supplier, and we've ignored their request without telling them of the problem.
This kind of null check just leads to more NullPointerException
s later in the code paths, or more null checks later (which coders/reviewers would not expect to have to do when using Maybe
or 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.
semantics of method were to change exception supplier if it's absent, and do nothing if it's present. logical extension of that is also to do nothing if it's null IMO. if we were to "fail fast" we are applying a particular dogma that says a Maybe
shouldn't be null, which might be helpful in some situations but it gets in the way in others. if the Maybe
being null is a problem the user will likely discover that, and it's not much helpful if we discover that slightly earlier. more helpful i think to allow callers who might have a null Maybe.
@@ -501,6 +501,6 @@ public boolean equals(Object obj) { | |||
|
|||
/** Finds the {@link Absent#getException()} if {@link #isAbsent()}, or null */ | |||
public static RuntimeException getException(Maybe<?> t) { | |||
return ((Maybe.Absent<?>)t).getException(); | |||
return t instanceof Maybe.Absent ? ((Maybe.Absent<?>)t).getException() : null; |
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.
As per earlier comment, I'd leave it as throwing an exception if you try to get the exception of a present
.
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.
(note the javadoc here, which is now accurate)
Assert.fail("Should have thrown"); | ||
} catch (Exception e) { /* expected */ } | ||
|
||
Assert.assertEquals(MutableList.of("hello", "world"), |
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.
These assert calls are the wrong way round - first argument is actual
, second argument is expected
for testng.
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.
yeah ... i just copy-pasted other examples; sometimes when i encounter this i change it if it's irritating, but in this case i didn't (yet).
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.
fixed, in light of comment below
} catch (Exception e) { /* expected */ } | ||
|
||
Assert.assertEquals(MutableList.of("hello", "world"), | ||
JavaStringEscapes.unwrapJsonishListStringIfPossible("\"hello\", \"world\"")); |
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.
Minor: I'd follow the java convention (and the Brooklyn convention) for indents: 8 spaces. The 4 spaces looks too much like a new code block, rather than a continuation of the previous line.
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.
yeah - in light of this i've updated this and the previous
thx, great review @aledsage . addressed all comments except those re permitting |
6618d72
to
bae72fc
Compare
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.
Thanks @ahgittin - looks great, merging now.
previously when we coerced or resolved values even with config keys, we did not reliably obey generics. now we do. preserves previous behaviour except now can be a bit stricter when convering, e.g.
ConfigKey<Set<Integer>>
now is only allowed to contain integers (but strings will be converted; previously it could contain strings and wouldn't even attempt to convert them)