From 0a01ab22ef38aed4d1519f74e8370f5e4ea247b2 Mon Sep 17 00:00:00 2001 From: ehennestad Date: Mon, 31 Aug 2026 11:41:18 +0200 Subject: [PATCH] feat: allow replacing a registered link resolver Registering a resolver whose IRI prefix was already registered was silently ignored, with no way to swap the registered resolver out. A library that reconfigures, for example connecting to a different server or with new credentials, could not make its new resolver take effect short of resetting the whole registry through an internal class. This is why the KG resolver kept its state in a persistent variable rather than on the instance. openminds.registerLinkResolver(resolver, Replace=true) now replaces the resolver registered for that prefix, keeping its position in the registry. The default remains idempotent registration, so startup code that registers unconditionally is unaffected. Co-Authored-By: Claude Opus 5 --- .../+resolver/LinkResolverRegistry.m | 31 ++++++++++++++----- .../+openminds/registerLinkResolver.m | 15 +++++++-- tools/tests/unitTests/ResolverTest.m | 27 ++++++++++++++++ 3 files changed, 63 insertions(+), 10 deletions(-) diff --git a/code/internal/+openminds/+internal/+resolver/LinkResolverRegistry.m b/code/internal/+openminds/+internal/+resolver/LinkResolverRegistry.m index 1234145b..166514ce 100644 --- a/code/internal/+openminds/+internal/+resolver/LinkResolverRegistry.m +++ b/code/internal/+openminds/+internal/+resolver/LinkResolverRegistry.m @@ -15,23 +15,33 @@ end methods - function addLinkResolver(obj, resolver) - % Add a resolver instance to the registry (no duplicates by handle). + function addLinkResolver(obj, resolver, options) + % Add a resolver to the registry, at most one per IRI prefix. + % + % By default a resolver whose prefix is already registered is + % ignored, so registering at startup is idempotent. Pass + % Replace=true to swap the registered resolver for this one, + % keeping its position: a library that reconfigures, for example + % with a new server or credentials, re-registers rather than + % mutating the resolver it registered earlier. arguments obj (1,1) openminds.internal.resolver.LinkResolverRegistry resolver (1,1) {mustBeA(resolver, "openminds.interface.LinkResolver")} + options.Replace (1,1) logical = false end - if obj.hasResolverForPrefix(resolver.IRIPrefix) - % Already registered - return - end + existingIndex = obj.indexOfPrefix(resolver.IRIPrefix); - obj.LinkResolvers{end+1} = resolver; + if isempty(existingIndex) + obj.LinkResolvers{end+1} = resolver; + elseif options.Replace + obj.LinkResolvers{existingIndex} = resolver; + end + % Otherwise already registered; keep the existing resolver. end function tf = hasResolverForPrefix(obj, iriPrefix) - tf = any( cellfun(@(r) r.IRIPrefix == iriPrefix, obj.LinkResolvers) ); + tf = ~isempty( obj.indexOfPrefix(iriPrefix) ); end function resolver = getLinkResolver(obj, IRI) @@ -70,6 +80,11 @@ function reset(obj) end methods (Access = private) + function index = indexOfPrefix(obj, iriPrefix) + % Position of the resolver registered for a prefix, or empty. + index = find( cellfun(@(r) r.IRIPrefix == iriPrefix, obj.LinkResolvers), 1 ); + end + function promoteResolver(obj, index) % promoteResolver - Reorder registry so the resolver at index is first arguments diff --git a/code/internal/+openminds/registerLinkResolver.m b/code/internal/+openminds/registerLinkResolver.m index 98c90fea..041024b0 100644 --- a/code/internal/+openminds/registerLinkResolver.m +++ b/code/internal/+openminds/registerLinkResolver.m @@ -1,14 +1,25 @@ -function registerLinkResolver(linkResolver) +function registerLinkResolver(linkResolver, options) % registerLinkResolver - Register a link resolver in the link resolver registry % +% openminds.registerLinkResolver(linkResolver) registers a resolver for +% the identifier prefix it declares. Registering a prefix that is +% already registered does nothing, so calling this from library startup +% code is idempotent. +% +% openminds.registerLinkResolver(linkResolver, Replace=true) replaces +% the resolver registered for that prefix with this one. Use this when +% a library reconfigures, for example connecting to a different server, +% and needs its new resolver to take effect. +% % See also: % openminds.interface.LinkResolver % openminds.internal.resolver.InstanceResolver arguments linkResolver (1,1) {mustBeA(linkResolver, "openminds.interface.LinkResolver")} + options.Replace (1,1) logical = false end resolverRegistry = openminds.internal.resolver.LinkResolverRegistry.instance(); - resolverRegistry.addLinkResolver(linkResolver); + resolverRegistry.addLinkResolver(linkResolver, "Replace", options.Replace); end diff --git a/tools/tests/unitTests/ResolverTest.m b/tools/tests/unitTests/ResolverTest.m index 6f06f838..154b7cc8 100644 --- a/tools/tests/unitTests/ResolverTest.m +++ b/tools/tests/unitTests/ResolverTest.m @@ -140,6 +140,33 @@ function testResolverRegistryPromotesUsedResolver(testCase) testCase.verifyEqual(registry.LinkResolvers{1}, mockResolver); end + function testReplaceRegisteredResolver(testCase) + % A library that reconfigures needs its new resolver to take + % effect. Without Replace the first registration wins; with it, + % the resolver for that prefix is swapped and keeps its position. + + registry = openminds.internal.resolver.LinkResolverRegistry.instance(); + + firstResolver = ommtest.helper.mock.MockLinkResolver(); + secondResolver = ommtest.helper.mock.MockLinkResolver(); + openminds.registerLinkResolver(firstResolver); + + openminds.registerLinkResolver(secondResolver); + registered = registry.getLinkResolver("https://mock.io/x"); + testCase.verifyTrue(registered == firstResolver, ... + 'Without Replace, the first registration should win.') + + openminds.registerLinkResolver(secondResolver, "Replace", true); + registered = registry.getLinkResolver("https://mock.io/x"); + testCase.verifyTrue(registered == secondResolver, ... + 'With Replace, the new resolver should take over the prefix.') + + testCase.verifyEqual( ... + sum(cellfun(@(r) isa(r, 'ommtest.helper.mock.MockLinkResolver'), ... + registry.LinkResolvers)), 1, ... + 'Replacing should not add a second resolver for the prefix.') + end + function testNoDuplicateResolvers(testCase) % Test that duplicate resolvers are not added resolver = ommtest.helper.mock.MockLinkResolver();