Skip to content

Make reachable public API members public - #2

Merged
tomasf merged 1 commit into
tomasf:devfrom
fponticelli:public-api-access
Sep 11, 2026
Merged

tomasf merged 1 commit into
tomasf:devfrom
fponticelli:public-api-access

Conversation

@fponticelli

Copy link
Copy Markdown
Contributor

What

Makes five members public that are already reachable from the public API but cannot be used from outside the module. No behaviour changes, no signature changes, no renames.

Symbol Problem
Multiproperties.Layer.property / .blendMethod handed out by layerSequences, unreadable
Object.ObjectType.default named by Object.type's own doc comment, unreferenceable
Mesh.TriangleSet readable from a parsed mesh, not constructible
Texture2DGroup.Coordinate readable from a parsed group, not constructible
Alternative readable from a parsed object, not constructible

Why

Two Swift rules combine here. Nesting a type inside a public extension makes the type public but does not extend that to members declared inside the type. And a synthesized memberwise initializer is internal even when the struct is public.

The result is a value a client can obtain and then do nothing with. Each of these is reachable from public API, so this is not a matter of taste about what should be exposed. The package already decided to expose them; the access levels just did not follow.

Multiproperties.Layer is the one that has practical consequences. layerSequences is documented as the way to read a multiproperties resource as resolved layers, and it is the only such accessor. Outside the module it returns [[Layer]] where neither property nor blendMethod can be read, and Layer has no public initializer either. So multiproperties cannot be resolved through the intended API at all. A consumer has to bypass it and pair up propertyGroupIDs with multis by hand, duplicating the logic layerSequences exists to provide, and losing the blend methods on the way. That is what I ended up doing while adding 3MF material support to Cadova (tomasf/Cadova#2), and it is the reason I went looking.

ObjectType.default is a smaller but plainer contradiction. Object.type is documented as "nil means the file doesn't say, which per the spec means ObjectType/model", which invites object.type ?? .default. That does not compile outside the module. The two sibling enums in Texture2D already get this right with public static let default.

The three unconstructible types are all accepted by public initializers: Mesh.init(vertices:triangles:triangleSets:) takes [TriangleSet], Texture2DGroup.init(id:texture2DID:displayPropertiesID:coordinates:) takes [Coordinate], and Object.init(...) takes [Alternative]. Today those parameters can only ever receive an empty array or values recovered from parsing an existing file, so writing a package with triangle sets, texture coordinates or alternatives is not possible from outside.

Fixes

Four are a public keyword. Mesh.TriangleSet needed an explicit memberwise initializer, since the synthesized one is internal regardless. Doc comments were added to the initializers to match the surrounding style.

Tests

Tests/PublicAPIAccessTests.swift, five tests, one per symbol. The file uses a plain import ThreeMF rather than @testable import ThreeMF on purpose, so it sees the module exactly as a client does. Every existing test file uses @testable, which is why none of this was caught.

I wrote the tests before the fix and confirmed each one fails to compile against main, with errors like:

error: 'property' is inaccessible due to 'internal' protection level
error: 'default' is inaccessible due to 'internal' protection level
error: extra arguments at positions #1, #2, #3 in call

Full suite green after the fix: 142 tests.

Compatibility

Source and binary compatible. Widening access breaks no existing caller. Worth noting that it does commit Layer's two field names and the three initializer signatures to the public API, so if you would rather shape any of them differently, now is the moment and I am happy to adjust.

Several types are public and reachable from the public API, but members
declared inside them default to internal, so a client can hold the value and
not use it. Nesting a type in a public extension makes the type public without
extending that to its own members, and a synthesized memberwise initializer is
internal even for a public struct.

Multiproperties.Layer is the clearest case: layerSequences hands out layers
whose property and blendMethod cannot be read, which leaves multiproperties
unresolvable outside the module. ObjectType.default is named by Object.type's
own documentation but cannot be referenced. Alternative, Mesh.TriangleSet and
Texture2DGroup.Coordinate can be read back from a parsed model but not
constructed, though the public initializers that take them accept arrays of
exactly those types.

The new tests import ThreeMF plainly rather than with @testable, so they see
the package the way a client does and stop compiling if any of this regresses.
@tomasf
tomasf changed the base branch from main to dev September 11, 2026 20:45
@tomasf
tomasf merged commit 5429e65 into tomasf:dev Sep 11, 2026
@tomasf

tomasf commented Sep 11, 2026

Copy link
Copy Markdown
Owner

Good catch! Merged, thanks.

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.

2 participants