feat: support polymorphic_embed's polymorphic_embeds_one/many (#40) - #64
Merged
Merged
Conversation
Intercept polymorphic_embeds_one/2 and polymorphic_embeds_many/2 calls in the syntax sugar, matching purely on the call name so polymorphic_embed never becomes a dependency of this library. The typespec is inferred as the union of the modules listed in the :types option (supporting both the `name: Module` and `name: [module: Module, ...]` forms), falling back to any() when the types are not statically resolvable. The original call is re-emitted untouched (minus our :null/:enforce/:: options) so the real macro still runs. The ::/:null/:enforce options work the same way they do for field/3. Also removes a field_is_nullable?/3 clause that was already unreachable (fully shadowed by the @schema_many_function_name clause) and now triggers a redundant-clause warning on recent Elixir versions. polymorphic_embed is added as a test-only dependency for the integration tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Covers the new polymorphic-embed edge paths (atom module in :types,
unresolvable module/entry fallbacks to any(), non-literal opts left
untouched) plus previously uncovered branches in the files touched by
the previous commit: field/3 with opts and a :: override, composite
{:array, _}/{:map, _} types, evaluated-atom and unknown Ecto types, and
the non-atom field name error in TypeBuilder.add_field/5.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The integration matches polymorphic_embeds_one/2 and
polymorphic_embeds_many/2 purely by name, so another library defining
same-named macros with different behavior would break: we would strip
its :null/:enforce options and register a possibly-wrong field type,
and the :: override can only fix the type, not the emitted call.
Make the integration opt-in via compile-time config, off by default:
config :typed_ecto_schema, polymorphic_embed: true
The flag is read with Application.compile_env/4 so schemas recompile
when it changes (requires Elixir 1.14+, the CI floor). With the flag
disabled the calls go through the pre-existing Macro.expand fallback,
behaving exactly as before the integration existed — locked in by tests
against both a same-named stub macro from another library and the real
polymorphic_embed macros. The integration is documented as experimental.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bamorim
force-pushed
the
feat-40-polymorphic-embed
branch
from
August 24, 2026 09:55
2de5e59 to
daf87a7
Compare
Closed
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 25, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First-party support for
polymorphic_embed'spolymorphic_embeds_one/2andpolymorphic_embeds_many/2insidetyped_schema/typed_embedded_schemablocks, following the approach discussed in the issue thread (intercept the calls in the syntax sugar).:null/:enforceoptions and register a possibly-wrong field type — and the::override can only fix the type, not the emitted call). With the flag off, these calls go through the pre-existingMacro.expandfallback and behave exactly as before this PR. The flag is read withApplication.compile_env/4so schemas recompile when it changes (requires Elixir 1.14+, the CI floor).SyntaxSugaralready matchesfield/embeds_one/etc.polymorphic_embedis only added as aonly: :testdep (latest~> 5.0) for the integration tests. The lib still compiles (and dialyzes) cleanly in dev/prod without it.types:option AST as a union of the listed modules —types: [sms: SMS, email: Email]infers(SMS.t() | Email.t()) | nilfor_oneandlist(SMS.t() | Email.t())for_many. Both thename: Moduleandname: [module: Module, ...]forms are supported. Modules are never resolved (no compile-time deps added); whentypes:isn't statically known (e.g. a module attribute), it falls back toany().::override supported on both macros, and:null/:enforcework like they do forfield/3.:null/:enforce/override options, whichPolymorphicEmbed.OptionsValidatorwould reject), so the real macro still runs with its ownarray?/defaulthandling — no more manual desugaring tofield(name, PolymorphicEmbed, ...).Implementation notes
EctoTypeMapper.type_for/4gets a dedicated head for the polymorphic function names that skipsbase_type_for(the base type is already built by the syntax sugar) and reuses the existing list-wrapping and nullability rules (_onenullable by default,_manyalways a list).add_fieldactually reads (:null,:enforce,:default, override) are forwarded to it, so thetypes:aliases are never evaluated by our generated code — keeping the compile-time-dependency avoidance thatpolymorphic_embeditself implements viaexpand_alias.field_is_nullable?/3clause that was already unreachable (fully shadowed by the@schema_many_function_nameclause —has_many/many_to_manyappear in both lists) and now triggers a redundant-clause compiler warning on recent Elixir versions. Behavior is unchanged (existing tests cover it).Tests (coverage: 100%)
polymorphic_embeds_onewith literal types → union type, nullable by defaultpolymorphic_embeds_manywith literal types →list(union), never nil::override on both → override winsmodule:keyword form oftypes:(and an atom-literal module form):enforce/:nulloptions (also proves the option-dropping works, sincepolymorphic_embedrejects unknown options)_manydefaults to[])types:from a module attribute → falls back toany(); unresolvabletypes:entries →any()polymorphic_embedmacros fall back to the oldPolymorphicEmbed.t() | nilbehaviorPolymorphicEmbedbut not using it is unaffected; the rest of the suite covers schemas withoutpolymorphic_embedentirelymix test(40 passing),mix credo --strictandmix dialyzerare clean in both dev and test envs.Documented (as experimental) in the README and the
TypedEctoSchemamoduledoc.Closes #40
🤖 Generated with Claude Code