From 61da2fc2a194c7de788d33626cf00100039f6aec Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Thu, 10 Sep 2026 16:31:06 +0100 Subject: [PATCH] Don't strip invalid characters from ImplementationGuide name (#1271) Leave sushi-config.yaml name as authored and warn when it violates the FHIR name constraint, instead of silently removing non-alphanumerics. --- src/ig/IGExporter.ts | 17 +++++++++-- test/ig/IGExporter.test.ts | 59 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/ig/IGExporter.ts b/src/ig/IGExporter.ts index 83462cf3c..ed66363b1 100644 --- a/src/ig/IGExporter.ts +++ b/src/ig/IGExporter.ts @@ -51,6 +51,8 @@ const IG_RESOURCE_FORMAT_EXTENSIONS = [ DEPRECATED_RESOURCE_FORMAT_EXTENSION, CURRENT_RESOURCE_FORMAT_EXTENSION ]; +// FHIR ImplementationGuide.name invariant: name.matches('[A-Z]([A-Za-z0-9_]){0,254}') +const IG_NAME_REGEX = /^[A-Z]([A-Za-z0-9_]){0,254}$/; function isR4(fhirVersion: string[]) { return fhirVersion.some(v => /^R4B?$/.test(getFHIRVersionInfo(v).name)); @@ -161,8 +163,7 @@ export class IGExporter { modifierExtension: this.config.modifierExtension, url: this.config.url ?? `${this.config.canonical}/ImplementationGuide/${this.config.id}`, version: this.config.version, - // name must be alphanumeric (allowing underscore as well) - name: this.config.name.replace(/[^A-Za-z0-9_]/g, ''), + name: this.config.name, title: this.config.title, status: this.config.status, experimental: this.config.experimental, @@ -196,6 +197,18 @@ export class IGExporter { template: this.config.templates } }; + if (this.config.name != null && !IG_NAME_REGEX.test(this.config.name)) { + logger.warn( + `The ImplementationGuide name "${this.config.name}" does not match the FHIR constraint ` + + "'[A-Z]([A-Za-z0-9_]){0,254}'. Valid names start with an upper-case ASCII letter ('A'..'Z') " + + "followed by any combination of upper- or lower-case ASCII letters ('A'..'Z', and 'a'..'z'), " + + "numerals ('0'-'9') and '_', with a length limit of 255 characters. " + + `Update the "name" property in ${this.configName}.`, + { + file: this.config.filePath + } + ); + } // Add the path-history, if applicable (only applies to HL7 IGs) if ( /^https?:\/\/hl7.org\//.test(this.config.canonical) && diff --git a/test/ig/IGExporter.test.ts b/test/ig/IGExporter.test.ts index ba6c69557..4fce3305d 100644 --- a/test/ig/IGExporter.test.ts +++ b/test/ig/IGExporter.test.ts @@ -138,4 +138,63 @@ describe('IGExporter', () => { expect(fs.existsSync(igIniPath)).toBeFalsy(); }); }); + + describe('#ig-name', () => { + let tempOut: string; + let defs: Awaited>; + let configPath: string; + let configYaml: string; + + beforeAll(async () => { + defs = await getTestFHIRDefinitions(true, testDefsPath('r4-definitions')); + configPath = path.join(__dirname, '..', 'import', 'fixtures', 'minimal-config.yaml'); + configYaml = fs.readFileSync(configPath, 'utf8'); + }); + + beforeEach(() => { + loggerSpy.reset(); + tempOut = temp.mkdirSync('sushi-test'); + }); + + afterEach(() => { + temp.cleanupSync(); + }); + + function exportIGWithName(name: string) { + const config = importConfiguration(configYaml, configPath); + config.name = name; + const pkg = new Package(config); + const exporter = new IGExporter(pkg, defs, __dirname); + exporter.export(tempOut); + const igPath = path.join( + tempOut, + 'fsh-generated', + 'resources', + 'ImplementationGuide-fhir.us.minimal.json' + ); + return fs.readJSONSync(igPath); + } + + function hasNameConstraintWarning() { + return loggerSpy.getAllMessages('warn').some(m => m.includes('[A-Z]([A-Za-z0-9_]){0,254}')); + } + + it('should keep an authored IG name that contains a space and warn about the FHIR name constraint', () => { + const igContent = exportIGWithName('Minimal IG'); + expect(igContent.name).toBe('Minimal IG'); + expect(hasNameConstraintWarning()).toBe(true); + }); + + it('should keep a valid IG name and not warn about the FHIR name constraint', () => { + const igContent = exportIGWithName('MinimalIG'); + expect(igContent.name).toBe('MinimalIG'); + expect(hasNameConstraintWarning()).toBe(false); + }); + + it('should keep an authored IG name with a hyphen or leading lowercase letter and warn about the FHIR name constraint', () => { + const igContent = exportIGWithName('example-ig'); + expect(igContent.name).toBe('example-ig'); + expect(hasNameConstraintWarning()).toBe(true); + }); + }); });