Entity resolution
Contact, Venue, and Body share a three-tier resolution pattern. Wherever one of these entities appears (a meeting’s chair, a component’s venue, a meeting’s committee), the field holds either the data itself or a reference to where the data lives:
| Tier | Discriminator | Resolves against |
|---|---|---|
| 1. Inline | neither ref nor local_ref set |
the entity itself — it IS the data |
| 2. Document-scoped | local_ref set |
the containing document’s scoped collection (Meeting#contacts[], Meeting#venues[], Meeting#bodies[]) |
| 3. Global register | ref set |
the top-level register document (ContactRegister, VenueRegister, BodyRegister) |
The reference? predicate on each entity is true when ref or
local_ref is set. When either is set, the other fields are ignored.
# Tier 1 — inline
contact:
kind: person
name:
- spelling: eng
value: { formatted: Dr. Anaya Müller }
# Tier 2 — document-scoped: local_ref matches the urn of an entry in
# this same Meeting's contacts[] collection
contact: { local_ref: urn:edoxen:contact:isotc154:anaya-muller }
contacts:
- urn: urn:edoxen:contact:isotc154:anaya-muller
kind: person
name:
- spelling: eng
value: { formatted: Dr. Anaya Müller }
# Tier 3 — global register: ref resolves against a ContactRegister
# document (a separate file, shared across meetings)
contact: { ref: urn:edoxen:contact:isotc154:anaya-muller }
Tier 2 keeps repeated people/places/bodies DRY within one file; tier 3 shares them across files via a published register.
Matching keys
- Contact / Venue — scoped-collection members are matched on their
urn; register members are matched onurn(find_by_urn). - Body — has no
urnattribute. Scoped members are matched oncode;BodyRegister#find_by_urnmatches a member’scodeorref. See Body Register.
Note:
local_refhere is an entity-resolution pointer on Contact/Venue/Body. The EntityRef type also has alocal_reffield — a within-file pointer between structural entities (e.g. to an agenda item). Same name, different mechanism.
Edoxen::EntityResolver
The gem ships a service that walks the tier hierarchy for you. It is
pure — no mutation — and returns the resolved entity or nil when
a reference matches nothing.
resolver = Edoxen::EntityResolver.new(
scoped: { Edoxen::Contact => meeting.contacts },
registers: { Edoxen::Contact => contact_register },
)
# One entity — inline entities pass through unchanged
resolver.resolve(meeting.contact)
# => #<Edoxen::Contact ...> (or nil if the reference doesn't resolve)
# A mixed list of inline and referenced entities
resolver.resolve_all(meeting.attendance.map(&:person))
Resolution order inside resolve:
reference?false → return the entity as-is (tier 1).local_refset → look up in the scoped collection registered for the entity’s class, matching memberurnagainstlocal_ref(tier 2).refset → look up in the global register registered for the entity’s class viafind_by_urn(tier 3).- No match →
nil.
Both the scoped: and registers: hashes are keyed by entity class
(Edoxen::Contact, Edoxen::Venue, Edoxen::Body; a subclass such as
Edoxen::Person falls back to its superclass’s entry), so one resolver
instance can serve all three entity types:
resolver = Edoxen::EntityResolver.new(
scoped: {
Edoxen::Contact => meeting.contacts,
Edoxen::Venue => meeting.venues,
Edoxen::Body => meeting.bodies,
},
registers: {
Edoxen::Contact => contact_register,
Edoxen::Venue => venue_register,
Edoxen::Body => body_register,
},
)
See also
- Contact / Venue / Body — the resolvable entities
- Contact Register / Venue Register / Body Register — the tier-3 registers
- EntityRef — typed cross-references between structural entities (Motion → Decision, etc.)