Skip to content

fix(sdk)!: enforce sane visibility#371

Open
mkleene wants to merge 12 commits into
mainfrom
enforce-sane-visibility
Open

fix(sdk)!: enforce sane visibility#371
mkleene wants to merge 12 commits into
mainfrom
enforce-sane-visibility

Conversation

@mkleene
Copy link
Copy Markdown
Contributor

@mkleene mkleene commented May 20, 2026

We expose types only if they are reachable from things we want people to use

If a type is not transitively reachable from one of our root classes then we
do not expose it. We make an exception for exceptions.

All types that are exposed by things we want people to use are public

This allows clients to do explicit typing for names.

Summary by CodeRabbit

  • Chores

    • Updated test dependencies including JUnit Platform and ArchUnit
    • Refined internal API visibility to clarify the public API surface
  • Bug Fixes

    • Corrected InvalidZipException to extend SDKException instead of RuntimeException
  • Tests

    • Added automated validation for the public API surface

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

📝 Walkthrough

Walkthrough

This PR establishes a controlled public API surface for the Java SDK by introducing an ArchUnit-based enforcement test, refactoring the exception hierarchy, systematically reducing visibility of internal implementation classes to package-private, and elevating TDF as the primary public entry point while making Autoconfigure.AttributeValueFQN public for attribute configuration.

Changes

API Surface Refactoring

Layer / File(s) Summary
Test Infrastructure and Enforcement
sdk/pom.xml, sdk/src/test/java/io/opentdf/platform/sdk/PublicApiSurfaceTest.java
Maven now pins JUnit Platform and ArchUnit test artifacts. PublicApiSurfaceTest adds ArchUnit-based verification that enforces reachability: all non-anonymous/non-local types reachable from API_ROOTS must be public/protected, and all public/protected types must be reachable from roots. Reachability is computed via queue-based traversal through raw superclasses, interfaces, enclosing types, and member signatures.
Exception Hierarchy Realignment
sdk/src/main/java/io/opentdf/platform/sdk/InvalidZipException.java, sdk/src/test/java/io/opentdf/platform/sdk/Fuzzing.java
InvalidZipException now extends SDKException instead of RuntimeException, aligning ZIP parsing exceptions with the SDK exception hierarchy. Fuzzing test updated to catch SDKException instead of the now-internal InvalidZipException.
Cryptographic and Encryption Implementation (Package-Private)
sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java, AsymDecryption.java, AsymEncryption.java, CryptoUtils.java, ECCurve.java, ECKeyPair.java
Cryptographic primitives (AesGcm, AsymDecryption, AsymEncryption, CryptoUtils, ECCurve, ECKeyPair) and their nested types (Encrypted, ECAlgorithm) are reduced from public to package-private, restricting external access to encryption/key pair utilities.
ZIP I/O Implementation (Package-Private)
sdk/src/main/java/io/opentdf/platform/sdk/ZipReader.java, ZipWriter.java
ZIP format handling (ZipReader class, nested Entry type, and ZipWriter class) is reduced from public to package-private, making ZIP I/O internal to the package.
Data Structures and Configuration Types (Package-Private)
sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java, AssertionConfig.java, PolicyObject.java, Resources.java
Manifest nested classes (Segment, RootSignature, IntegrityInformation, Binding, Assertion, HashValues) have modifier ordering standardized; PolicyBinding and AssertionValueAdapter are made package-private; AssertionConfig.BindingMethod is made package-private; Resources becomes package-private; PolicyObject.AttributeObject modifier ordering corrected.
Configuration and Strategic API Surface Definition
sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java
Autoconfigure.AttributeValueFQN is made public to serve as a public component of the configuration API surface.
Core SDK Entry Points
sdk/src/main/java/io/opentdf/platform/sdk/TDF.java, TDFReader.java, TDFWriter.java, Planner.java
TDF is elevated from package-private to public, becoming the primary SDK entry point and API root. TDFReader, TDFWriter, and Planner are reduced to package-private, becoming internal implementation details accessed only through the public TDF interface.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Our surface now polished, our boundaries defined,
With tests to enforce what's public and kind,
Old cryptic entrances sealed tight away,
While TDF shines bright as the SDK's gateway! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'enforce sane visibility' accurately describes the primary objective of the PR: systematically adjusting class/type visibility modifiers to enforce a coherent API surface based on reachability from designated root types.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch enforce-sane-visibility

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request refactors the SDK's public API surface by adjusting the visibility of numerous classes and members to ensure only intended components are exposed. Key changes include transitioning several classes to package-private visibility, updating InvalidZipException to extend SDKException, and adding a new ArchUnit test, PublicApiSurfaceTest, to enforce reachability-based visibility rules. Review feedback focuses on optimizing the new test by removing an unused import, using more efficient data structures, and eliminating redundant logic in the API reachability algorithm.

import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The import org.junit.jupiter.api.BeforeEach is unused and should be removed to keep the code clean.

Config.class.getName(),
RequestHelper.class.getName()
);
static List<JavaClass> reachableClasses;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Consider using a Set<JavaClass> for reachableClasses instead of a List. This would improve the performance of the contains checks in the test methods from O(N) to O(1).

Suggested change
static List<JavaClass> reachableClasses;
static Set<JavaClass> reachableClasses;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

this isn't java.lang.reflect

Comment thread sdk/src/test/java/io/opentdf/platform/sdk/PublicApiSurfaceTest.java
Comment on lines +124 to +127
for (JavaType r: m.getParameterTypes()) {
addAllRawTypes(r, work, queued);
}
m.getParameterTypes().forEach(t -> addAllRawTypes(t, work, queued));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The loop and the forEach call are performing the exact same operation on m.getParameterTypes(). One of them should be removed to avoid redundancy.

                m.getParameterTypes().forEach(t -> addAllRawTypes(t, work, queued));

}
for (JavaConstructor ctor : c.getConstructors()) {
if (!isExposed(ctor.getModifiers())) continue;
addAllRawTypes(ctor.getRawReturnType(), work, queued);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Calling addAllRawTypes on the constructor's return type is redundant because JavaConstructor.getRawReturnType() returns the declaring class c, which is already being processed in the current iteration.

ctor.getParameterTypes().forEach(t -> addAllRawTypes(t, work, queued));
ctor.getExceptionTypes().forEach(t -> addAllRawTypes(t, work, queued));
}
for (JavaField f : c.getFields()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Similar to methods, using c.getDeclaredFields() is more efficient than c.getFields() as it avoids re-processing inherited fields that are already covered by the hierarchy traversal.

Suggested change
for (JavaField f : c.getFields()) {
for (JavaField f : c.getDeclaredFields()) {

@github-actions
Copy link
Copy Markdown
Contributor

X-Test Failure Report

@mkleene mkleene marked this pull request as ready for review May 20, 2026 22:33
@mkleene mkleene requested review from a team as code owners May 20, 2026 22:33
@github-actions
Copy link
Copy Markdown
Contributor

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sdk/src/main/java/io/opentdf/platform/sdk/InvalidZipException.java (1)

4-6: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update stale Javadoc to match the new exception hierarchy.

The class comment still states RuntimeException, but Line 8 now extends SDKException.

Proposed patch
 /**
  * InvalidZipException is thrown to indicate that a ZIP file being read
- * is invalid or corrupted in some way. This exception extends RuntimeException,
- * allowing it to be thrown during the normal operation of the Java Virtual Machine.
+ * is invalid or corrupted in some way. This exception extends SDKException.
  */
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/src/main/java/io/opentdf/platform/sdk/InvalidZipException.java` around
lines 4 - 6, The class Javadoc for InvalidZipException is outdated (mentions
RuntimeException) but the class now extends SDKException; update the Javadoc for
InvalidZipException to state it extends SDKException and accurately describe its
role in the new exception hierarchy, referencing InvalidZipException and
SDKException so readers know this is a specialized SDKException thrown when a
ZIP file is invalid or corrupted.
🧹 Nitpick comments (1)
sdk/src/test/java/io/opentdf/platform/sdk/PublicApiSurfaceTest.java (1)

63-102: 💤 Low value

Minor: inconsistent access modifier on test methods.

Line 64 declares void onlyReachableTypesAreExposed() without an access modifier, while line 85 declares public void allReachableTypesAreExposed(). For consistency, both should omit the public modifier (JUnit 5 doesn't require it).

Suggested fix
 `@Test`
-public void allReachableTypesAreExposed() {
+void allReachableTypesAreExposed() {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/src/test/java/io/opentdf/platform/sdk/PublicApiSurfaceTest.java` around
lines 63 - 102, In PublicApiSurfaceTest, make the access modifiers consistent by
removing the unnecessary public modifier from the allReachableTypesAreExposed()
test method so it matches onlyReachableTypesAreExposed() (both package-private),
i.e., change the signature of allReachableTypesAreExposed() to have no access
modifier; leave the method body and annotations unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java`:
- Line 57: The public nested type AttributeValueFQN is inaccessible because its
enclosing class Autoconfigure is package-private; update the visibility so
external callers of Config.TDFConfig.attributes, Config.withDataAttributes,
Config.withDataAttributeValues, and SDK.attributeValueExists can reference it:
either make Autoconfigure public or extract AttributeValueFQN into its own
public top-level class (public class AttributeValueFQN) and update any
references accordingly to preserve API compatibility.

---

Outside diff comments:
In `@sdk/src/main/java/io/opentdf/platform/sdk/InvalidZipException.java`:
- Around line 4-6: The class Javadoc for InvalidZipException is outdated
(mentions RuntimeException) but the class now extends SDKException; update the
Javadoc for InvalidZipException to state it extends SDKException and accurately
describe its role in the new exception hierarchy, referencing
InvalidZipException and SDKException so readers know this is a specialized
SDKException thrown when a ZIP file is invalid or corrupted.

---

Nitpick comments:
In `@sdk/src/test/java/io/opentdf/platform/sdk/PublicApiSurfaceTest.java`:
- Around line 63-102: In PublicApiSurfaceTest, make the access modifiers
consistent by removing the unnecessary public modifier from the
allReachableTypesAreExposed() test method so it matches
onlyReachableTypesAreExposed() (both package-private), i.e., change the
signature of allReachableTypesAreExposed() to have no access modifier; leave the
method body and annotations unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 78021bcb-1ccf-441c-af28-c172effae882

📥 Commits

Reviewing files that changed from the base of the PR and between 9991b07 and 41c4951.

📒 Files selected for processing (21)
  • sdk/pom.xml
  • sdk/src/main/java/io/opentdf/platform/sdk/AesGcm.java
  • sdk/src/main/java/io/opentdf/platform/sdk/AssertionConfig.java
  • sdk/src/main/java/io/opentdf/platform/sdk/AsymDecryption.java
  • sdk/src/main/java/io/opentdf/platform/sdk/AsymEncryption.java
  • sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java
  • sdk/src/main/java/io/opentdf/platform/sdk/CryptoUtils.java
  • sdk/src/main/java/io/opentdf/platform/sdk/ECCurve.java
  • sdk/src/main/java/io/opentdf/platform/sdk/ECKeyPair.java
  • sdk/src/main/java/io/opentdf/platform/sdk/EntityIdentifiers.java
  • sdk/src/main/java/io/opentdf/platform/sdk/InvalidZipException.java
  • sdk/src/main/java/io/opentdf/platform/sdk/Manifest.java
  • sdk/src/main/java/io/opentdf/platform/sdk/Planner.java
  • sdk/src/main/java/io/opentdf/platform/sdk/PolicyObject.java
  • sdk/src/main/java/io/opentdf/platform/sdk/Resources.java
  • sdk/src/main/java/io/opentdf/platform/sdk/TDFReader.java
  • sdk/src/main/java/io/opentdf/platform/sdk/TDFWriter.java
  • sdk/src/main/java/io/opentdf/platform/sdk/ZipReader.java
  • sdk/src/main/java/io/opentdf/platform/sdk/ZipWriter.java
  • sdk/src/test/java/io/opentdf/platform/sdk/Fuzzing.java
  • sdk/src/test/java/io/opentdf/platform/sdk/PublicApiSurfaceTest.java

Comment thread sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java Outdated
@github-actions
Copy link
Copy Markdown
Contributor

X-Test Failure Report

@github-actions
Copy link
Copy Markdown
Contributor

X-Test Failure Report

@github-actions
Copy link
Copy Markdown
Contributor

@sonarqubecloud
Copy link
Copy Markdown

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sdk/src/test/java/io/opentdf/platform/sdk/PublicApiSurfaceTest.java (1)

43-50: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add TDF to the designated roots if this PR intends it to be a supported entry point.

TDF becomes public in this change set, but this test still seeds reachability only from SDK, SDKBuilder, AssertionConfig, Config, RequestHelper, PolicyEnums, and EntityIdentifiers. That means the ArchUnit rule validates TDF only if some other root happens to reference it, instead of protecting it as an intentional public root.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/src/test/java/io/opentdf/platform/sdk/PublicApiSurfaceTest.java` around
lines 43 - 50, The API roots set in PublicApiSurfaceTest omits TDF even though
TDF was made public; update the API_ROOTS constant used by the ArchUnit test to
include TDF.class.getName() so TDF is treated as an explicit public entry point
(modify the Set.of(...) in the API_ROOTS declaration that currently lists SDK,
SDKBuilder, AssertionConfig, Config, RequestHelper, PolicyEnums,
EntityIdentifiers to also include TDF.class.getName()).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@sdk/src/main/java/io/opentdf/platform/sdk/TDF.java`:
- Line 34: TDF is declared public but its constructors and entry-point factory
methods are package-private, making the type unusable to external callers; fix
by making the TDF constructors and all factory/entry methods (notably createTDF
and loadTDF and any overloads) public so external consumers can instantiate/use
the class, and ensure any other package-private methods in the TDF class that
are intended as part of the public SDK surface are likewise promoted to public
(adjust visibility for constructors and the methods referenced in the TDF class
accordingly).

In `@sdk/src/test/java/io/opentdf/platform/sdk/PublicApiSurfaceTest.java`:
- Around line 119-121: The superclass lambda currently ignores the discovered
superclass value `ec` and re-enqueues `c`, so change the lambda to operate on
`ec`: call addAllRawTypes(ec, work, queued) (instead of addAllRawTypes(c,...))
and also use the superclass's interfaces by replacing the external
work.addAll(c.getRawInterfaces()) with work.addAll(ec.getRawInterfaces()) inside
the ifPresent block (or otherwise ensure you call getRawInterfaces() on `ec` not
`c`) so base classes become reachable through subclass exposure.

---

Outside diff comments:
In `@sdk/src/test/java/io/opentdf/platform/sdk/PublicApiSurfaceTest.java`:
- Around line 43-50: The API roots set in PublicApiSurfaceTest omits TDF even
though TDF was made public; update the API_ROOTS constant used by the ArchUnit
test to include TDF.class.getName() so TDF is treated as an explicit public
entry point (modify the Set.of(...) in the API_ROOTS declaration that currently
lists SDK, SDKBuilder, AssertionConfig, Config, RequestHelper, PolicyEnums,
EntityIdentifiers to also include TDF.class.getName()).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bae3c51a-a659-463b-a63d-4d5305550333

📥 Commits

Reviewing files that changed from the base of the PR and between 41c4951 and f1fc93c.

📒 Files selected for processing (3)
  • sdk/src/main/java/io/opentdf/platform/sdk/Autoconfigure.java
  • sdk/src/main/java/io/opentdf/platform/sdk/TDF.java
  • sdk/src/test/java/io/opentdf/platform/sdk/PublicApiSurfaceTest.java

* operations and configurations.
*/
class TDF {
public class TDF {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

TDF is public but still unusable outside the package.

External callers can now see TDF, but both constructors and all of the actual entry points (createTDF / loadTDF) remain package-private. As shipped, consumers can reference the type but cannot create or operate on one, so this public promotion is incomplete.

As per coding guidelines, keep public SDK APIs stable and additive where possible.

Also applies to: 71-80, 369-542

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/src/main/java/io/opentdf/platform/sdk/TDF.java` at line 34, TDF is
declared public but its constructors and entry-point factory methods are
package-private, making the type unusable to external callers; fix by making the
TDF constructors and all factory/entry methods (notably createTDF and loadTDF
and any overloads) public so external consumers can instantiate/use the class,
and ensure any other package-private methods in the TDF class that are intended
as part of the public SDK surface are likewise promoted to public (adjust
visibility for constructors and the methods referenced in the TDF class
accordingly).

Comment on lines +119 to +121
// Superclass and interfaces are part of the API surface of c.
c.getRawSuperclass().ifPresent(ec -> addAllRawTypes(c, work, queued));
work.addAll(c.getRawInterfaces());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the discovered superclass type here, not c.

Line 120 ignores ec and re-enqueues the current class, so exposed base classes never become reachable through subclass exposure. That can make this enforcement test misclassify public supertypes on both assertions.

Suggested fix
-            c.getRawSuperclass().ifPresent(ec -> addAllRawTypes(c, work, queued));
+            c.getRawSuperclass().ifPresent(ec -> addAllRawTypes(ec, work, queued));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@sdk/src/test/java/io/opentdf/platform/sdk/PublicApiSurfaceTest.java` around
lines 119 - 121, The superclass lambda currently ignores the discovered
superclass value `ec` and re-enqueues `c`, so change the lambda to operate on
`ec`: call addAllRawTypes(ec, work, queued) (instead of addAllRawTypes(c,...))
and also use the superclass's interfaces by replacing the external
work.addAll(c.getRawInterfaces()) with work.addAll(ec.getRawInterfaces()) inside
the ifPresent block (or otherwise ensure you call getRawInterfaces() on `ec` not
`c`) so base classes become reachable through subclass exposure.

@github-actions
Copy link
Copy Markdown
Contributor

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.

1 participant