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 createAssert(ValueProvider) to AssertFactory #3377

Merged
merged 23 commits into from May 5, 2024
Merged

Add createAssert(ValueProvider) to AssertFactory #3377

merged 23 commits into from May 5, 2024

Conversation

scordio
Copy link
Member

@scordio scordio commented Feb 25, 2024

Closes #3368.

Open points:

  • Proper test coverage for the new InstanceOfAssertFactory constructor
  • Proper test coverage for the new createAssert(ValueProvider) of InstanceOfAssertFactory
  • Proper test coverage for all the existing InstanceOfAssertFactories instances and related createAssert(ValueProvider) behavior.
  • Integration tests based on DefaultConversionService from spring-core

@scordio
Copy link
Member Author

scordio commented Feb 25, 2024

I believe the reported binary incompatibility issue is a false positive – raised siom79/japicmp#384.

@scordio scordio added this to the 3.26.0 milestone Feb 25, 2024
Copy link

github-actions bot commented Feb 25, 2024

Qodana Community for JVM

It seems all right 👌

No new problems were found according to the checks applied

💡 Qodana analysis was run in the pull request mode: only the changed files were checked
☁️ View the detailed Qodana report

Contact Qodana team

Contact us at qodana-support@jetbrains.com

@scordio scordio force-pushed the gh-3368 branch 2 times, most recently from 6351e68 to 684063f Compare February 25, 2024 17:35
@scordio
Copy link
Member Author

scordio commented Mar 2, 2024

@snicoll aside from enhancing test coverage, I don't expect major changes to the productive code.

Could you check if this is what you expect?

About:

  • Evaluate the addition of a new interface for getType() / getRawClass() contract
  • Evaluate the impact of using the new interface on the existing API (e.g., asInstanceOf, extracting, etc.)

I have the feeling such a change would be binary incompatible — not the interface per se but applying it consistently in the rest of the library. I'll have a better look in the upcoming days.
If it's proven to be binary incompatible, I'd rather address this topic in AssertJ 4.

About:

Evaluate the addition of a type similar to ParameterizedTypeReference and the corresponding method factories into InstanceOfAssertFactories

I haven't introduced it yet but I still have the feeling it would be valuable. I'll probably do it once the rest is covered.

@snicoll
Copy link

snicoll commented Mar 6, 2024

Could you check if this is what you expect?

I want to acknowledge I've seen this but I wasn't able to spend time on this yet. It's high on my list. Thanks a lot for your efforts!

@scordio
Copy link
Member Author

scordio commented Mar 6, 2024

I want to acknowledge I've seen this but I wasn't able to spend time on this yet. It's high on my list. Thanks a lot for your efforts!

No problem at all and good luck with your speech tomorrow! I'll be around if you'd like to chat about this topic 😉

@snicoll
Copy link

snicoll commented Mar 6, 2024

Looking at the Javadoc of InstanceOfAssertFactory and the reason that made this PR to exist, I am starting to wonder if things couldn't be restructured a little bit differently.

What we need is an AssertFactory that expresses the Type it's expecting for its createAssert method. It already does so using the generic type, but the actual generic information may be lost or not available. From that perspective, I wonder if a default Type getType() on AssertFactory could be a way out. If the type is not exposed, then it's fine and we can reject such instances upfront if we need to the information to validate/convert the actual value.

Doing this means that InstanceOfAssertFactory largely becomes an implementation detail still and additional AssertFactory can join the club as long as they expose their type.

The problem currently is that InstanceOfAssertFactory has to say something along the lines of "it's going to cast the actual to the target type" while this new methods makes it so that we can use such a type without doing a casting at all.

I also wonder if the rawClass has to be exposed. It could be left package private from convenience as you can rebuild the raw type from Type.

@scordio
Copy link
Member Author

scordio commented Mar 6, 2024

What we need is an AssertFactory that expresses the Type it's expecting for its createAssert method.

Locally, I introduced an interface that extends AssertFactory and defines the getType method. However, I've been holding that back because of the overall impact on the existing methods that today expect InstanceOfAssertFactory, like asInstanceOf and extracting variants. Such a change is probably binary-incompatible and I would rather deliver it in version 4.

I'll polish my changes and raise them as an additional PR on top of this one so that we can look at it.

However, reading the rest of your message, I have the impression that you don't want to rely on the existing available methods but only take advantage of the instances in InstanceOfAssertFactories, right?

If the type is not exposed, then it's fine and we can reject such instances upfront if we need to the information to validate/convert the actual value

What do you mean by "if the type is not exposed"? Or, more generally, what kind of implementation do you imagine for default Type getType() in AssertFactory?

In #3368 (comment), you mentioned an approach with default method but that was about having a conversionFactory available.

I also wonder if the rawClass has to be exposed. It could be left package private from convenience as you can rebuild the raw type from Type.

Indeed, that's not mandatory and can be kept package-private.

@scordio
Copy link
Member Author

scordio commented Mar 7, 2024

Locally, I introduced an interface that extends AssertFactory and defines the getType method. However, I've been holding that back because of the overall impact on the existing methods that today expect InstanceOfAssertFactory, like asInstanceOf and extracting variants. Such a change is probably binary-incompatible and I would rather deliver it in version 4.

As expected, the binary compatibility of #3389 is a disaster 🙃

We might introduce the interface but applying it consistently would be postponed until version 4.

@snicoll
Copy link

snicoll commented Mar 7, 2024

As expected, the binary compatibility of #3389 is a disaster 🙃

I don't think we need a separate interface. This is what I was suggesting:

@FunctionalInterface
public interface AssertFactory<T, ASSERT extends Assert<?, ?>> {

  /**
   * Creates the custom {@link Assert} instance for the given element value.
   * <p>
   * Typically, this will just invoke <code>assertThat(actual)</code>
   * @param actual the input value for the {@code Assert} instance
   * @return returns the custom {@code Assert} instance for the given element value
   */
  ASSERT createAssert(T actual);

  /**
   * Return the target {@link Type} this factory expects, or {@code null} if
   * the information is not available.
   * @return the element type.
   */
  default Type getType() {
    return null;
  }

}

With that, you need to rename getType in InstanceOfAssertFactory to getRawType or something but it being package private does not matter.

As far as I can see this is fully backward compatible. I would not introduce a complete new interface to expose the type as it's required for a very specific case (the one discussed here).

If you don't like the getter there in such a high level interface, which I understand, here's an alternative that's also 100% backward compatible:

@FunctionalInterface
public interface AssertFactory<T, ASSERT extends Assert<?, ?>> {

  /**
   * Creates the custom {@link Assert} instance for the given element value.
   * <p>
   * Typically, this will just invoke <code>assertThat(actual)</code>
   * @param actual the input value for the {@code Assert} instance
   * @return returns the custom {@code Assert} instance for the given element value
   */
  ASSERT createAssert(T actual);

  /**
   * Creates the custom {@link Assert} instance for the value provided by the
   * given {@code valueProvider}.
   * @param valueProvider the value provider for the {@code Assert} instance
   * @return returns the custom {@code Assert} instance for the value produced by
   * the given value provider
   */
  default ASSERT createAssert(Function<Type, T> valueProvider) {
    throw new UnsupportedOperationException();
  }

}

Then, any AssertFactory that has access to a Type can invoke the factory and then invoke the regular createAssert method with the produced T.

@scordio
Copy link
Member Author

scordio commented Mar 7, 2024

I'm fine with getType on AssertFactory. I would maybe favor an Optional as a return type.

I must admit I didn't understand how the other alternative would impact InstanceOfAssertFactory. Maybe it doesn't matter much and we just go ahead with the first option?

One remaining topic because I'm still not sure about it: do you expect your users to rely directly on all the InstanceOfAssertFactories from AssertJ, or do you plan to introduce a layer on top of each of them?

Lastly, would you see an advantage if AssertJ offered native support for parameterized type references, like in the snippet at #3368 (comment)?

@scordio
Copy link
Member Author

scordio commented Mar 24, 2024

Evaluate the addition of a type similar to ParameterizedTypeReference and the corresponding method factories into InstanceOfAssertFactories

We discussed that we'll wait for a concrete demand.

# Conflicts:
#	assertj-core/src/test/java/org/assertj/core/api/InstanceOfAssertFactoriesTest.java
# Conflicts:
#	assertj-core/src/main/java/org/assertj/core/api/InstanceOfAssertFactories.java
#	assertj-core/src/test/java/org/assertj/core/api/InstanceOfAssertFactoriesTest.java
@scordio scordio changed the title Expose expected InstanceOfAssertFactory type and raw type Add createAssert(ValueProvider) to AssertFactory Mar 24, 2024
@scordio
Copy link
Member Author

scordio commented Mar 24, 2024

@snicoll could you try the latest changes? It should be in line with what we discussed, although I still need to spend some time on it for proper test coverage.

Any feedback is welcome!

@snicoll
Copy link

snicoll commented Mar 25, 2024

Looks good. I like the interface that keeps a link between Type and the expected generic!

@scordio
Copy link
Member Author

scordio commented Mar 25, 2024

BTW, we already have spring-core in the test classpath so I can try to put together an integration test close to the usage pattern you expect.

@snicoll
Copy link

snicoll commented Mar 25, 2024

Cool, I hijacked this in InstanceOfAssertFactoriesTest:

private static final DefaultConversionService conversionService = new DefaultConversionService();

@Test
void springConversionTest() {
	List<String> values = List.of("123", "456", "789");
	// WHEN
	ListAssert<Integer> result = list(Integer.class).createAssert(type -> convert(values, type));
	// THEN
	result.containsExactly(123, 456, 789);

}

@SuppressWarnings("unchecked")
private <T> T convert(Object instance, Type type) {
	return (T) conversionService.convert(instance,
			new TypeDescriptor(ResolvableType.forType(type), null, null));
}

This is one example of many. The json path that was the inspiration for this ask is relying on Jackson to do the conversion based on a Type but it's a very similar arrangement. Thanks again!

Copy link

github-actions bot commented Mar 30, 2024

Qodana Community for JVM

It seems all right 👌

No new problems were found according to the checks applied

💡 Qodana analysis was run in the pull request mode: only the changed files were checked
☁️ View the detailed Qodana report

Contact Qodana team

Contact us at qodana-support@jetbrains.com

@scordio scordio force-pushed the gh-3368 branch 4 times, most recently from ff2d61d to b63aea7 Compare April 7, 2024 10:40
@scordio scordio marked this pull request as ready for review April 12, 2024 13:27
@scordio
Copy link
Member Author

scordio commented May 5, 2024

I'm still unsure how much the Javadoc should elaborate on the concrete usage of the new createAssert.

@snicoll I will merge the changes and release them as part of 3.26.0 in the close future. I would be very interested in seeing how this will be used so that I can derive a meaningful example for our Javadoc or docs 🙂

@scordio scordio merged commit 64888d5 into 3.x May 5, 2024
21 checks passed
@scordio scordio deleted the gh-3368 branch May 5, 2024 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

AssertFactory with the ability to convert the value upfront
2 participants