Skip to content

feat: Add FHIR R4-to-R4 conversion with template variable support (Phase 1)#614

Open
Chethan77 wants to merge 2 commits intomainfrom
feature/fhir-r4-variable-support
Open

feat: Add FHIR R4-to-R4 conversion with template variable support (Phase 1)#614
Chethan77 wants to merge 2 commits intomainfrom
feature/fhir-r4-variable-support

Conversation

@Chethan77
Copy link
Copy Markdown

Summary

Implements Phase 1 of the Convert Engine Variable Support specification by adding a new FHIR R4-to-R4 conversion pathway with named string variables available at template render time.

The primary use case for this phase is Dragon Copilot MRN identifier injection: selecting a Patient identifier by type.coding.code and writing a new Dragon Copilot identifier with a configurable system URI.

Scope Delivered

Requirement Status Implementation
P1-R1: Support FHIR R4 → FHIR R4 transformation Added FhirR4Processor using the existing JSON parsing and post-processing pipeline
P1-R2: Add named string variables to the conversion call Added variable-aware overloads on IFhirConverterWithVariables
P1-R3: Variables accessible in templates Injected variables into Liquid context under vars
P1-R4: Create Dragon MRN template Added DragonPatientMrn.liquid
P1-R5: Single match enforcement Template raises descriptive errors for zero or multiple matches
P1-R6: Automated test coverage Added focused unit and functional coverage for the new pathway

Changes Included

New files

  • FhirR4Processor.cs — R4-to-R4 processor with variable support
  • IFhirConverterWithVariables.cs — interface extending IFhirConverter with variable-aware overloads
  • ErrorFilters.csraise_error Liquid filter for template-level business rule enforcement
  • DragonPatientMrn.liquid — sample Dragon MRN transformation template
  • Passthrough.liquid — minimal template used by shared processor smoke tests
  • metadata.json — metadata for the FhirR4 template folder
  • FhirR4ProcessorTests.cs — unit coverage for processor behavior and edge cases
  • FhirR4ConvertDataFunctionalTests.cs — end-to-end functional tests
  • ErrorFiltersTests.cs — unit tests for raise_error
  • Sample data: PatientWithIdentifiers.json, PatientNoMR.json, PatientDuplicateMR.json, PatientMultiCodingMR.json
  • Expected output: DragonPatientMrn.json

Modified files

  • DataType.cs — added FhirR4
  • DefaultRootTemplateParentPath.cs — added FhirR4
  • FhirConverterErrorCode.cs — added InvalidVariableName = 1314
  • ConvertProcessorFactory.cs — added FhirR4 processor routing
  • Resources.resx — added variable validation error strings
  • ProcessorTests.cs — added FhirR4 to shared processor coverage
  • ConvertProcessorFactoryTests.cs — added FhirR4 factory and interface assertions
  • DefaultTemplateCollectionProviderTests.cs — scoped assertions to currently packaged template sets
  • TestConstants.cs — added FhirR4 template directory constant

Validation

Focused FhirR4 coverage

  • Added 35 unit tests for:

    • happy path conversion
    • field preservation
    • zero-match and multi-match failures
    • multi-coding single-identifier edge case
    • sparse identifier shapes
    • null/empty variable handling
    • variable name validation
    • missing/empty dragonSystem
    • factory and cancellation behavior
  • Added 5 functional tests for:

    • successful Dragon identifier generation
    • no-match failure
    • duplicate-match failure
    • factory-based invocation
    • missing dragonSystem failure

Full solution verification

Suite Passed Failed
Converter Unit Tests 468 0
TemplateManagement Unit Tests 324 0
Converter Functional Tests 2410 0

Build verification:

  • dotnet build --warnaserror → 0 warnings, 0 errors

Explicitly Out of Scope for Phase 1

CLI tool integration

CLI integration was not added in this PR.

Reason:

  • The Phase 1 spec defines library-level requirements only.
  • The CLI tool is not called out in Phase 1 requirements.
  • The current CLI has no variable-passing mechanism such as a --var argument model.
  • Routing FhirR4 through the CLI without a variable input contract would not provide a usable end-to-end experience.

This should be handled as a separate follow-up if CLI support is required.

Default template packaging / embedded template flows

Default template packaging was not added in this PR.

Reason:

  • The spec first introduces default template collection usage in Phase 3 via templateCollectionReference.
  • FhirR4 templates in this phase depend on runtime variables.
  • The current default/embedded template flow has no mechanism to provide those variables.
  • Packaging FhirR4 templates into the default template bundle in Phase 1 would add templates that cannot be meaningfully used through that path yet.

This should be addressed alongside API/default-template flow support in a later phase.

Adding variable overloads to IFhirConverter

The base IFhirConverter interface was intentionally not changed.

Reason:

  • Adding variable overloads there would force existing non-variable processors to implement methods they do not use.
  • IFhirConverterWithVariables : IFhirConverter keeps the new capability isolated to processors that support it.
  • Callers can discover support via interface cast/check, which is the intended design for this phase.

Notes

This PR is intended to complete the Phase 1 library slice:

  • new FHIR R4-to-R4 processor
  • variable injection into Liquid templates
  • Dragon MRN sample template
  • rendering error support
  • focused automated validation

It does not attempt to complete later-phase integration surfaces.

@Chethan77 Chethan77 self-assigned this Apr 10, 2026
@microsoft-github-policy-service
Copy link
Copy Markdown

@Chethan77 please read the following Contributor License Agreement(CLA). If you agree with the CLA, please reply with the following information.

@microsoft-github-policy-service agree [company="{your company}"]

Options:

  • (default - no company specified) I have sole ownership of intellectual property rights to my Submissions and I am not making Submissions in the course of work for my employer.
@microsoft-github-policy-service agree
  • (when company given) I am making Submissions in the course of work for my employer (or my employer has intellectual property rights in my Submissions by contract or applicable law). I have permission from my employer to make Submissions and enter into this Agreement on behalf of my employer. By signing below, the defined term “You” includes me and my employer.
@microsoft-github-policy-service agree company="Microsoft"
Contributor License Agreement

Contribution License Agreement

This Contribution License Agreement (“Agreement”) is agreed to by the party signing below (“You”),
and conveys certain license rights to Microsoft Corporation and its affiliates (“Microsoft”) for Your
contributions to Microsoft open source projects. This Agreement is effective as of the latest signature
date below.

  1. Definitions.
    “Code” means the computer software code, whether in human-readable or machine-executable form,
    that is delivered by You to Microsoft under this Agreement.
    “Project” means any of the projects owned or managed by Microsoft and offered under a license
    approved by the Open Source Initiative (www.opensource.org).
    “Submit” is the act of uploading, submitting, transmitting, or distributing code or other content to any
    Project, including but not limited to communication on electronic mailing lists, source code control
    systems, and issue tracking systems that are managed by, or on behalf of, the Project for the purpose of
    discussing and improving that Project, but excluding communication that is conspicuously marked or
    otherwise designated in writing by You as “Not a Submission.”
    “Submission” means the Code and any other copyrightable material Submitted by You, including any
    associated comments and documentation.
  2. Your Submission. You must agree to the terms of this Agreement before making a Submission to any
    Project. This Agreement covers any and all Submissions that You, now or in the future (except as
    described in Section 4 below), Submit to any Project.
  3. Originality of Work. You represent that each of Your Submissions is entirely Your original work.
    Should You wish to Submit materials that are not Your original work, You may Submit them separately
    to the Project if You (a) retain all copyright and license information that was in the materials as You
    received them, (b) in the description accompanying Your Submission, include the phrase “Submission
    containing materials of a third party:” followed by the names of the third party and any licenses or other
    restrictions of which You are aware, and (c) follow any other instructions in the Project’s written
    guidelines concerning Submissions.
  4. Your Employer. References to “employer” in this Agreement include Your employer or anyone else
    for whom You are acting in making Your Submission, e.g. as a contractor, vendor, or agent. If Your
    Submission is made in the course of Your work for an employer or Your employer has intellectual
    property rights in Your Submission by contract or applicable law, You must secure permission from Your
    employer to make the Submission before signing this Agreement. In that case, the term “You” in this
    Agreement will refer to You and the employer collectively. If You change employers in the future and
    desire to Submit additional Submissions for the new employer, then You agree to sign a new Agreement
    and secure permission from the new employer before Submitting those Submissions.
  5. Licenses.
  • Copyright License. You grant Microsoft, and those who receive the Submission directly or
    indirectly from Microsoft, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license in the
    Submission to reproduce, prepare derivative works of, publicly display, publicly perform, and distribute
    the Submission and such derivative works, and to sublicense any or all of the foregoing rights to third
    parties.
  • Patent License. You grant Microsoft, and those who receive the Submission directly or
    indirectly from Microsoft, a perpetual, worldwide, non-exclusive, royalty-free, irrevocable license under
    Your patent claims that are necessarily infringed by the Submission or the combination of the
    Submission with the Project to which it was Submitted to make, have made, use, offer to sell, sell and
    import or otherwise dispose of the Submission alone or with the Project.
  • Other Rights Reserved. Each party reserves all rights not expressly granted in this Agreement.
    No additional licenses or rights whatsoever (including, without limitation, any implied licenses) are
    granted by implication, exhaustion, estoppel or otherwise.
  1. Representations and Warranties. You represent that You are legally entitled to grant the above
    licenses. You represent that each of Your Submissions is entirely Your original work (except as You may
    have disclosed under Section 3). You represent that You have secured permission from Your employer to
    make the Submission in cases where Your Submission is made in the course of Your work for Your
    employer or Your employer has intellectual property rights in Your Submission by contract or applicable
    law. If You are signing this Agreement on behalf of Your employer, You represent and warrant that You
    have the necessary authority to bind the listed employer to the obligations contained in this Agreement.
    You are not expected to provide support for Your Submission, unless You choose to do so. UNLESS
    REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING, AND EXCEPT FOR THE WARRANTIES
    EXPRESSLY STATED IN SECTIONS 3, 4, AND 6, THE SUBMISSION PROVIDED UNDER THIS AGREEMENT IS
    PROVIDED WITHOUT WARRANTY OF ANY KIND, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY OF
    NONINFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
  2. Notice to Microsoft. You agree to notify Microsoft in writing of any facts or circumstances of which
    You later become aware that would make Your representations in this Agreement inaccurate in any
    respect.
  3. Information about Submissions. You agree that contributions to Projects and information about
    contributions may be maintained indefinitely and disclosed publicly, including Your name and other
    information that You submit with Your Submission.
  4. Governing Law/Jurisdiction. This Agreement is governed by the laws of the State of Washington, and
    the parties consent to exclusive jurisdiction and venue in the federal courts sitting in King County,
    Washington, unless no federal subject matter jurisdiction exists, in which case the parties consent to
    exclusive jurisdiction and venue in the Superior Court of King County, Washington. The parties waive all
    defenses of lack of personal jurisdiction and forum non-conveniens.
  5. Entire Agreement/Assignment. This Agreement is the entire agreement between the parties, and
    supersedes any and all prior agreements, understandings or communications, written or oral, between
    the parties relating to the subject matter hereof. This Agreement may be assigned by Microsoft.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new FHIR R4 → FHIR R4 conversion path that supports passing named string variables into the Liquid render context (under vars), enabling templates like the provided Dragon MRN injection example and enforcing business rules via a new raise_error Liquid filter.

Changes:

  • Introduces FhirR4Processor and IFhirConverterWithVariables to support variable-aware conversions.
  • Adds raise_error filter and a sample DragonPatientMrn.liquid template demonstrating MRN selection + identifier injection.
  • Expands enums/factory routing and adds unit + functional coverage for the new pathway.

Reviewed changes

Copilot reviewed 24 out of 25 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/Microsoft.Health.Fhir.TemplateManagement.UnitTests/Providers/DefaultTemplateCollectionProviderTests.cs Adjusts assertions to match currently packaged default template sets.
src/Microsoft.Health.Fhir.Liquid.Converter/Resources.resx Adds variable-validation resource strings.
src/Microsoft.Health.Fhir.Liquid.Converter/Resources.Designer.cs Adds strongly-typed accessors for new resource strings.
src/Microsoft.Health.Fhir.Liquid.Converter/Processors/IFhirConverterWithVariables.cs New interface adding variable-aware Convert overloads.
src/Microsoft.Health.Fhir.Liquid.Converter/Processors/FhirR4Processor.cs New R4→R4 processor with variable injection + validation.
src/Microsoft.Health.Fhir.Liquid.Converter/Processors/ConvertProcessorFactory.cs Routes DataType.FhirR4 + Fhir output to FhirR4Processor.
src/Microsoft.Health.Fhir.Liquid.Converter/Models/FhirConverterErrorCode.cs Adds InvalidVariableName error code.
src/Microsoft.Health.Fhir.Liquid.Converter/Models/DefaultRootTemplateParentPath.cs Adds FhirR4 root template parent path.
src/Microsoft.Health.Fhir.Liquid.Converter/Models/DataType.cs Adds FhirR4 data type.
src/Microsoft.Health.Fhir.Liquid.Converter/Filters/ErrorFilters.cs Adds raise_error template filter.
src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/TestData/Expected/FhirR4/DragonPatientMrn.json Adds expected output for the Dragon MRN template.
src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/TestConstants.cs Adds test template directory constant for FhirR4.
src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Processors/ProcessorTests.cs Extends shared processor tests to include FhirR4Processor.
src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Processors/FhirR4ProcessorTests.cs Adds focused unit tests for variable validation + MRN template behavior.
src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Processors/ConvertProcessorFactoryTests.cs Adds factory coverage for FhirR4 and variable-interface assertion.
src/Microsoft.Health.Fhir.Liquid.Converter.UnitTests/Filters/ErrorFiltersTests.cs Adds unit tests for raise_error.
src/Microsoft.Health.Fhir.Liquid.Converter.FunctionalTests/TestData/Expected/FhirR4/DragonPatientMrn.json Adds functional expected output for R4 conversion.
src/Microsoft.Health.Fhir.Liquid.Converter.FunctionalTests/FhirR4ConvertDataFunctionalTests.cs Adds end-to-end functional tests for R4 conversion with variables.
data/Templates/FhirR4/Passthrough.liquid Adds minimal passthrough template for R4 smoke/shared tests.
data/Templates/FhirR4/metadata.json Adds metadata for the new FhirR4 template set.
data/Templates/FhirR4/DragonPatientMrn.liquid Adds sample Dragon MRN injection template using vars + raise_error.
data/SampleData/FhirR4/PatientWithIdentifiers.json Adds sample R4 patient input with identifiers.
data/SampleData/FhirR4/PatientNoMR.json Adds sample R4 patient input with no MR identifier.
data/SampleData/FhirR4/PatientMultiCodingMR.json Adds sample R4 patient input for multi-coding edge case.
data/SampleData/FhirR4/PatientDuplicateMR.json Adds sample R4 patient input with duplicate MR identifiers.
Files not reviewed (1)
  • src/Microsoft.Health.Fhir.Liquid.Converter/Resources.Designer.cs: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +93 to +105
if (variables.Count > MaxVariableCount)
{
throw new RenderException(FhirConverterErrorCode.InvalidVariableName, string.Format(Resources.TooManyVariables, MaxVariableCount, variables.Count));
}

foreach (var kvp in variables)
{
ValidateVariableName(kvp.Key);

if (kvp.Value != null && kvp.Value.Length > MaxVariableValueLength)
{
throw new RenderException(FhirConverterErrorCode.InvalidVariableName, string.Format(Resources.VariableValueTooLong, kvp.Key, MaxVariableValueLength));
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Variable validation failures for too many variables and oversized values are being reported with FhirConverterErrorCode.InvalidVariableName, which is misleading for callers that want to distinguish name vs value/count problems. Consider introducing dedicated error codes (e.g., TooManyVariables / InvalidVariableValue) or renaming/repurposing the code to cover all variable validation errors consistently.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with AI comment, more specific error codes are warrented.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another alternative is to make the error code more generic i.e. Invalid Variable and let the specific message determine the error .

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in a26187d. Went with your suggestion — renamed InvalidVariableName → InvalidVariable as a single generic error code. The specific error message (too many variables, value too long, invalid name format, reserved name, duplicate name) carries the detail. This avoids proliferating error codes for every validation scenario while keeping the messages actionable.

Comment on lines +78 to +87
private string InternalConvertWithVariables(string data, string rootTemplate, ITemplateProvider templateProvider, IDictionary<string, string> variables, TraceInfo traceInfo = null)
{
if (string.IsNullOrEmpty(rootTemplate))
{
throw new RenderException(FhirConverterErrorCode.NullOrEmptyRootTemplate, Resources.NullOrEmptyRootTemplate);
}

if (templateProvider == null)
{
throw new RenderException(FhirConverterErrorCode.NullTemplateProvider, Resources.NullTemplateProvider);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

InternalConvertWithVariables duplicates most of the standard conversion pipeline (template lookup, context creation, render, post-process) that already exists in BaseProcessor.InternalConvertFromObject / JsonProcessor.InternalConvert. This duplication increases the risk of behavior drift (e.g., future changes to error handling/timeouts/telemetry not being applied here). Consider refactoring to reuse the shared pipeline (e.g., by extending the base pipeline to accept additional context data like vars).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If too complex can be skipped for this iteration but adding to the base converter would allow context to work across conversion types.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — skipping for this iteration as suggested.

Comment on lines +162 to +183
public static void ValidateVariableName(string name)
{
if (string.IsNullOrEmpty(name))
{
throw new RenderException(FhirConverterErrorCode.InvalidVariableName, Resources.InvalidVariableName);
}

if (name.Length > MaxVariableNameLength)
{
throw new RenderException(FhirConverterErrorCode.InvalidVariableName, string.Format(Resources.VariableNameTooLong, MaxVariableNameLength));
}

if (!ValidVariableNameRegex.IsMatch(name))
{
throw new RenderException(FhirConverterErrorCode.InvalidVariableName, string.Format(Resources.InvalidVariableNameFormat, name));
}

if (ReservedVariableNames.Contains(name))
{
throw new RenderException(FhirConverterErrorCode.InvalidVariableName, string.Format(Resources.ReservedVariableName, name));
}
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ValidateVariableName is public but appears to be an internal helper used only to support Convert validation (and unit tests). Exposing it publicly expands the library surface area and creates a compatibility contract. Consider making it private/internal (and validating only via Convert), or moving it to an internal utility with InternalsVisibleTo for tests.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in a26187d. ValidateVariableName is now internal on a new VariableValidator utility class under Utilities/. Test access is via InternalsVisibleTo in the .csproj. The method is no longer on FhirR4Processor at all.

{
public static string RaiseError(string message)
{
throw new RenderException(FhirConverterErrorCode.TemplateRenderingError, message);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RaiseError can be invoked with a null/empty message, which results in a RenderException that may surface with the default .NET exception text (or an empty message), making template failures hard to diagnose. Consider normalizing null/empty to a non-empty default message (or throwing with a dedicated resource string) so consumers always get a useful error.

Suggested change
throw new RenderException(FhirConverterErrorCode.TemplateRenderingError, message);
string normalizedMessage = string.IsNullOrWhiteSpace(message) ? "Template raised an error." : message;
throw new RenderException(FhirConverterErrorCode.TemplateRenderingError, normalizedMessage);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in a26187d. Null/empty/whitespace messages are now normalized to a resource string (Resources.DefaultTemplateError → "Template raised an error.") rather than a hardcoded literal, consistent with the project's localization pattern. Tests assert the exact normalized message.

Comment on lines +78 to +87
private string InternalConvertWithVariables(string data, string rootTemplate, ITemplateProvider templateProvider, IDictionary<string, string> variables, TraceInfo traceInfo = null)
{
if (string.IsNullOrEmpty(rootTemplate))
{
throw new RenderException(FhirConverterErrorCode.NullOrEmptyRootTemplate, Resources.NullOrEmptyRootTemplate);
}

if (templateProvider == null)
{
throw new RenderException(FhirConverterErrorCode.NullTemplateProvider, Resources.NullTemplateProvider);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If too complex can be skipped for this iteration but adding to the base converter would allow context to work across conversion types.

Comment on lines +93 to +105
if (variables.Count > MaxVariableCount)
{
throw new RenderException(FhirConverterErrorCode.InvalidVariableName, string.Format(Resources.TooManyVariables, MaxVariableCount, variables.Count));
}

foreach (var kvp in variables)
{
ValidateVariableName(kvp.Key);

if (kvp.Value != null && kvp.Value.Length > MaxVariableValueLength)
{
throw new RenderException(FhirConverterErrorCode.InvalidVariableName, string.Format(Resources.VariableValueTooLong, kvp.Key, MaxVariableValueLength));
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with AI comment, more specific error codes are warrented.

Comment on lines +93 to +105
if (variables.Count > MaxVariableCount)
{
throw new RenderException(FhirConverterErrorCode.InvalidVariableName, string.Format(Resources.TooManyVariables, MaxVariableCount, variables.Count));
}

foreach (var kvp in variables)
{
ValidateVariableName(kvp.Key);

if (kvp.Value != null && kvp.Value.Length > MaxVariableValueLength)
{
throw new RenderException(FhirConverterErrorCode.InvalidVariableName, string.Format(Resources.VariableValueTooLong, kvp.Key, MaxVariableValueLength));
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another alternative is to make the error code more generic i.e. Invalid Variable and let the specific message determine the error .


namespace Microsoft.Health.Fhir.Liquid.Converter.Processors
{
public interface IFhirConverterWithVariables : IFhirConverter
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we will eventually add this to all IFhirConverters I would suggest adding this to the base interface. For existing classes they can throw not implemented exceptions.


// Verify expected number of templates per data type as per templates packaged from the data/Templates directory.
foreach (var defaultRootTemplateParentPath in Enum.GetValues<DefaultRootTemplateParentPath>())
foreach (var defaultRootTemplateParentPath in _defaultTemplatesFolderInfo.Keys)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More context on the necessity of this change is needed. This change looks to be unrelated to the PR.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context: Adding DataType.FhirR4 to the enum caused the previous test to break because it iterated all DefaultRootTemplateParentPath enum values and expected each to have a packaged default template collection. FhirR4 is intentionally not packaged as a default template set in Phase 1 — its templates require runtime variables, and the default/embedded template flow has no variable injection mechanism yet.

The fix switches from enum iteration to an explicit dictionary of the 5 currently-packaged template families. This is more correct — it tests what is actually shipped rather than assuming every enum value maps to a packaged template. When FhirR4 templates are added to the default package in a later phase, that work would add the entry here alongside the packaging change.

public class FhirR4Processor : JsonProcessor, IFhirConverterWithVariables
{
private static readonly Regex ValidVariableNameRegex = new Regex(@"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled);
private static readonly HashSet<string> ReservedVariableNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "msg", "vars" };
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the AI comment but it would be good to separate/decouple the vars parsing and validation logic so it isn't embedded only in this processor.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in a26187d. Validation logic has been extracted to Utilities/VariableValidator.cs — a shared internal static class with:

ValidateVariables(IDictionary<string, string>) — count limit, per-key name validation, per-value size limit, case-insensitive duplicate detection
ValidateVariableName(string) — null/empty, length, regex format, reserved name check
FhirR4Processor now calls VariableValidator.ValidateVariables(variables) — one line. Any future processor that supports variables reuses the same validator.

{
public string Convert(string data, string rootTemplate, ITemplateProvider templateProvider, IDictionary<string, string> variables, TraceInfo traceInfo = null);

public string Convert(string data, string rootTemplate, ITemplateProvider templateProvider, IDictionary<string, string> variables, CancellationToken cancellationToken, TraceInfo traceInfo = null);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine for a first iteration but one downside of just taking the variables as a dictionary is you can't control if they are case sensitive or in sensitive. Ideally that isn't defined by the caller. Internally in the processor the reserved variables are case insensitive and you would want to be consistent with that decision.

var dictionary = new Dictionary<string, object> { { DataKey, jsonData } };
if (variables != null && variables.Count > 0)
{
dictionary["vars"] = Hash.FromDictionary(variables.ToDictionary(kv => kv.Key, kv => (object)kv.Value));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for this to go from a dictionary of string/string to string/object and then be wrapped in a Hashset before being assigned to the context.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added an inline comment in a26187d explaining the chain. The three conversions are each forced by a different layer:

  1. Dictionary<string, string> (public API) — Phase 1 spec restricts variables to string values
  2. .ToDictionary(... (object)kv.Value)Hash.FromDictionary requires IDictionary<string, object>
  3. Hash.FromDictionary()Hash — DotLiquid's rendering context requires Hash for nested property resolution ({{ vars.dragonSystem }})

Keeping the public API as string,string is intentional to enforce the Phase 1 constraint. Phase 2 may widen to string,object for typed variables.

public class FhirR4Processor : JsonProcessor, IFhirConverterWithVariables
{
private static readonly Regex ValidVariableNameRegex = new Regex(@"^[a-zA-Z_][a-zA-Z0-9_]*$", RegexOptions.Compiled);
private static readonly HashSet<string> ReservedVariableNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "msg", "vars" };
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming is confusing. Originally thought you were using as reserved values in the provided vars and would error if they were used. That isn't the case, these are the names used for the context dictionary that is in scope for the template. Suggest adding TemplateContext like ReservedTemplateContextVariableNames to be distinguish.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in a26187d. Renamed to ReservedTemplateContextVariableNames in the new VariableValidator class. The name now clearly communicates that these are the template rendering context's reserved keys ("msg" for input data, "vars" for the variable namespace), not restrictions on user-provided variable names in general.

…idator, consolidate interface

- Rename InvalidVariableName -> InvalidVariable error code (generic for all variable errors)
- Extract VariableValidator utility class to decouple validation from processor
- Rename ReservedVariableNames -> ReservedTemplateContextVariableNames
- Make validation methods internal with InternalsVisibleTo for test access
- Move variable-aware Convert overloads to IFhirConverter base interface
- Add virtual NotImplementedException stubs in BaseProcessor
- Override in FhirR4Processor with actual implementation
- Delete IFhirConverterWithVariables (consolidated into IFhirConverter)
- Normalize RaiseError null/empty message to resource string
- Add case-insensitive duplicate variable name rejection
- Add inline comment explaining Hash type conversion chain
- Update all unit and functional tests (469+2410+324 = 3203 passing)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants