diff --git a/rune-integration-tests/src/test/java/com/regnosys/rosetta/config/StandaloneSetupConfigWiringTest.java b/rune-integration-tests/src/test/java/com/regnosys/rosetta/config/StandaloneSetupConfigWiringTest.java index 768fdcab3c..6f33b5a554 100644 --- a/rune-integration-tests/src/test/java/com/regnosys/rosetta/config/StandaloneSetupConfigWiringTest.java +++ b/rune-integration-tests/src/test/java/com/regnosys/rosetta/config/StandaloneSetupConfigWiringTest.java @@ -1,6 +1,8 @@ package com.regnosys.rosetta.config; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import java.net.URL; import java.net.URLClassLoader; @@ -82,4 +84,47 @@ void configFileAndClasspathClassLoaderSurviveACustomCreateInjector(@TempDir Path config.findSchemaConfig("parentSchema").orElseThrow().getConfigPath()); } } + + @Test + void missingProjectConfigDoesNotUseDependencyGeneratorSettings(@TempDir Path tempDir) throws Exception { + Path dependencyRoot = Files.createDirectory(tempDir.resolve("dependency-classes")); + Files.writeString(dependencyRoot.resolve("rune-config.yml"), + "model:\n name: Parent Model\ngenerators:\n namespaces:\n - parent.only\n" + + "namespaceConfig:\n- id: parentSchema\n namespace: parent.ns\n" + + " schemaConfig:\n schema: parentSchema\n configPath: xml-config/parent-config.json\n"); + + try (URLClassLoader dependencyClassLoader = + new URLClassLoader(new URL[] { dependencyRoot.toUri().toURL() }, null)) { + Injector injector = new CustomModuleSetup() + .setConfigFile(null) + .setClasspathClassLoader(dependencyClassLoader) + .createInjectorAndDoEMFRegistration(); + + RuneConfiguration config = injector.getInstance(RuneConfigurationHolder.class).get(); + + assertEquals("Just another Rosetta model", config.getModel().getName()); + assertTrue(config.getGenerators().getNamespaces().isEmpty()); + assertTrue(config.getGenerators().getNamespaceFilter().test("child.ns")); + assertEquals("xml-config/parent-config.json", + config.findSchemaConfig("parentSchema").orElseThrow().getConfigPath()); + } + } + + @Test + void unsetProjectConfigStillUsesClasspathDiscovery(@TempDir Path tempDir) throws Exception { + Files.writeString(tempDir.resolve("rune-config.yml"), + "model:\n name: Classpath Model\ngenerators:\n namespaces:\n - classpath.only\n"); + + try (URLClassLoader classLoader = new URLClassLoader(new URL[] { tempDir.toUri().toURL() }, null)) { + Injector injector = new CustomModuleSetup() + .setClasspathClassLoader(classLoader) + .createInjectorAndDoEMFRegistration(); + + RuneConfiguration config = injector.getInstance(RuneConfigurationHolder.class).get(); + + assertEquals("Classpath Model", config.getModel().getName()); + assertTrue(config.getGenerators().getNamespaceFilter().test("classpath.only")); + assertFalse(config.getGenerators().getNamespaceFilter().test("child.ns")); + } + } } diff --git a/rune-lang/src/main/java/com/regnosys/rosetta/RosettaStandaloneSetup.java b/rune-lang/src/main/java/com/regnosys/rosetta/RosettaStandaloneSetup.java index af5755945d..f21c9b3ac8 100644 --- a/rune-lang/src/main/java/com/regnosys/rosetta/RosettaStandaloneSetup.java +++ b/rune-lang/src/main/java/com/regnosys/rosetta/RosettaStandaloneSetup.java @@ -19,6 +19,7 @@ public class RosettaStandaloneSetup extends RosettaStandaloneSetupGenerated { private String configFile; + private boolean configFileSet; private ClassLoader classpathClassLoader; public static void doSetup() { @@ -27,10 +28,13 @@ public static void doSetup() { /** * Points the setup at an explicit configuration file, instead of discovering the project's own - * config on the classpath. Applied by {@link #createInjectorAndDoEMFRegistration()}. + * config on the classpath. Passing {@code null} explicitly declares that the project has no + * config, while not calling this method leaves classpath discovery enabled. Applied by + * {@link #createInjectorAndDoEMFRegistration()}. */ public RosettaStandaloneSetup setConfigFile(String configFile) { this.configFile = configFile; + this.configFileSet = true; return this; } @@ -85,7 +89,7 @@ public Injector createInjectorAndDoEMFRegistration() { * use, so this is still in time. */ private void configureRuneConfigurationFileProvider(Injector injector) { - if (configFile == null && classpathClassLoader == null) { + if (!configFileSet && classpathClassLoader == null) { return; } RuneConfigurationFileProvider fileProvider = injector.getInstance(RuneConfigurationFileProvider.class); @@ -96,7 +100,7 @@ private void configureRuneConfigurationFileProvider(Injector injector) { + "configuration. Remove the non-singleton binding of " + RuneConfigurationFileProvider.class.getName() + " from the runtime module."); } - if (configFile != null) { + if (configFileSet) { fileProvider.setConfigFile(configFile); } if (classpathClassLoader != null) { diff --git a/rune-maven-plugin/src/main/java/com/regnosys/rosetta/maven/RuneLanguageAccessFactory.java b/rune-maven-plugin/src/main/java/com/regnosys/rosetta/maven/RuneLanguageAccessFactory.java index 4d87958485..17f03d86e0 100644 --- a/rune-maven-plugin/src/main/java/com/regnosys/rosetta/maven/RuneLanguageAccessFactory.java +++ b/rune-maven-plugin/src/main/java/com/regnosys/rosetta/maven/RuneLanguageAccessFactory.java @@ -42,9 +42,9 @@ public Map createLanguageAccess(ILanguageConfiguration l + " must implement " + RosettaStandaloneSetup.class.getName()); } RosettaStandaloneSetup setup = (RosettaStandaloneSetup) loadClass.getDeclaredConstructor().newInstance(); - if (rosettaConfig != null) { - setup.setConfigFile(rosettaConfig); - } + // A null value explicitly means this project has no config; dependency configs must not + // become the primary config and restrict this project's generators. + setup.setConfigFile(rosettaConfig); // The thread context classloader during the build is the plugin realm, which cannot see the // project's compile dependencies. Hand the config provider a classloader over the project // classpath so dependency serializationConfig entries are discovered and unioned. diff --git a/rune-maven-plugin/src/test/java/com/regnosys/rosetta/maven/RuneLanguageAccessFactoryTest.java b/rune-maven-plugin/src/test/java/com/regnosys/rosetta/maven/RuneLanguageAccessFactoryTest.java new file mode 100644 index 0000000000..effb249557 --- /dev/null +++ b/rune-maven-plugin/src/test/java/com/regnosys/rosetta/maven/RuneLanguageAccessFactoryTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2026 REGnosys + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.regnosys.rosetta.maven; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Set; + +import org.eclipse.xtext.builder.standalone.ILanguageConfiguration; +import org.eclipse.xtext.generator.OutputConfiguration; +import org.junit.jupiter.api.Test; + +import com.regnosys.rosetta.RosettaStandaloneSetup; + +class RuneLanguageAccessFactoryTest { + + @Test + void nullConfigIsExplicitlyForwardedToTheLanguageSetup() { + assertConfigIsForwarded(null); + } + + @Test + void explicitConfigIsForwardedToTheLanguageSetup() { + assertConfigIsForwarded("/project/rune-config.yml"); + } + + private void assertConfigIsForwarded(String configFile) { + RecordingSetup.configFileSet = false; + RecordingSetup.configFile = "not-set"; + + ILanguageConfiguration languageConfiguration = new ILanguageConfiguration() { + @Override + public String getSetup() { + return RecordingSetup.class.getName(); + } + + @Override + public Set getOutputConfigurations() { + return Set.of(); + } + + @Override + public boolean isJavaSupport() { + return false; + } + }; + + new RuneLanguageAccessFactory().createLanguageAccess( + languageConfiguration, configFile, getClass().getClassLoader(), null); + + assertTrue(RecordingSetup.configFileSet); + assertEquals(configFile, RecordingSetup.configFile); + } + + public static final class RecordingSetup extends RosettaStandaloneSetup { + private static boolean configFileSet; + private static String configFile; + + @Override + public RosettaStandaloneSetup setConfigFile(String configFile) { + RecordingSetup.configFileSet = true; + RecordingSetup.configFile = configFile; + return super.setConfigFile(configFile); + } + } +} diff --git a/rune-runtime/src/main/java/com/regnosys/rosetta/config/file/FileBasedRuneConfigurationProvider.java b/rune-runtime/src/main/java/com/regnosys/rosetta/config/file/FileBasedRuneConfigurationProvider.java index 32c1cad578..b5fb5dfd22 100644 --- a/rune-runtime/src/main/java/com/regnosys/rosetta/config/file/FileBasedRuneConfigurationProvider.java +++ b/rune-runtime/src/main/java/com/regnosys/rosetta/config/file/FileBasedRuneConfigurationProvider.java @@ -44,11 +44,13 @@ public RuneConfiguration get() { protected RuneConfiguration readConfigFromFile() { try { URL primaryFile = fileProvider.get(); + RuneConfiguration primary; if (primaryFile == null) { - LOGGER.warn("No configuration file was found. Falling back to the default configuration."); - return null; + LOGGER.warn("No project configuration file was found. Falling back to the default configuration."); + primary = fallback.get(); + } else { + primary = configurationService.read(primaryFile); } - RuneConfiguration primary = configurationService.read(primaryFile); // The model and generators come from the current project's config only. // The namespace config is the union of all configs on the classpath (the current @@ -57,7 +59,7 @@ protected RuneConfiguration readConfigFromFile() { Set seenIds = new HashSet<>(); collectNamespaceConfig(primary, mergedNamespaceConfig, seenIds); for (URL file : fileProvider.getResources()) { - if (file.equals(primaryFile)) { + if (primaryFile != null && file.equals(primaryFile)) { continue; } collectNamespaceConfig(configurationService.read(file), mergedNamespaceConfig, seenIds); diff --git a/rune-runtime/src/main/java/com/regnosys/rosetta/config/file/RuneConfigurationFileProvider.java b/rune-runtime/src/main/java/com/regnosys/rosetta/config/file/RuneConfigurationFileProvider.java index e72f6fb210..c2b40f6738 100644 --- a/rune-runtime/src/main/java/com/regnosys/rosetta/config/file/RuneConfigurationFileProvider.java +++ b/rune-runtime/src/main/java/com/regnosys/rosetta/config/file/RuneConfigurationFileProvider.java @@ -52,8 +52,9 @@ public static RuneConfigurationFileProvider createFromClasspath(String fileName) /** * Points this provider at an explicit configuration file on disk, instead of discovering the - * primary config on the classpath. Dependency configs are still discovered from the classpath - * (see {@link #getResources()}). + * primary config on the classpath. A {@code null} value explicitly means that the current + * project has no config. Dependency configs are still discovered from the classpath (see + * {@link #getResources()}). */ public void setConfigFile(String configFile) { this.loadFromClasspath = false; @@ -81,6 +82,9 @@ private RuneConfigurationFileProvider(boolean loadFromClasspath, String fileName @Override public URL get() { + if (!loadFromClasspath && fileName == null) { + return null; + } if (fileName != null) { // An explicit file name was requested: resolve it as-is, without any fallback. return loadFromClasspath ? fromClasspath(fileName) : requireFile(fileName);