Skip to content

Conversation

@AshokThangavel
Copy link
Contributor

Support Recursive Placeholder Resolution in Module Parameter

Description

This PR introduces deeply nested variables or chained configuration settings where one ${variable} depends on another.
with a safety handle complex dependencies and prevent infinite loops in the case of circular references.

fixes: #1009

Changes

  • Recursive Logic: Updated ResolvePlaceholders to use a while loop that continues as long as substitutions are being made.
  • Efficiency: The loop exits immediately if a pass results in no changes, ensuring no performance penalty for simple configurations.

Testing Performed

  1. Unit Tests: Verified with complex nested strings:
  • ${A} -> ${B} -> ${C} -> FinalValue.
  1. Integration Test: Added a new integration test class Test.PM.Integration.Module that:
  • Generates a module.xml from XData.
  • Writes it to the filesystem.
  • Executes an IPM load command to verify the full lifecycle of the variable resolution.

Test Execution Results: Test.PM.Integration.Module

Counter Action Status Description
1 LogMessage passed start Loading the demo-module1 module
2 AssertStatusOK passed Directory created /home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/varresolver/
3 AssertStatusOK passed module.xml File created on /home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/varresolver/
4 AssertStatusOK passed Created the xml file on /home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/varresolver/
5 AssertStatusOK passed Loaded varresolver module successfully from /home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/varresolver/
6 AssertTrue passed Module demo-module1 exists in IPM and version is 1.0.0
7 LogMessage passed List all modules
8 AssertStatusOK passed uninstalled module demo-module1 successfully.
9 AssertStatusOK passed Deleted the module.xml file from /home/irisowner/zpm/tests/integration_tests/Test/PM/Integration/_data/varresolver/
10 LogMessage passed Duration of execution: 1.193466 sec.

Copy link
Collaborator

@isc-dchui isc-dchui left a comment

Choose a reason for hiding this comment

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

A few comments and questions

for {
set param = $order(customParams(param), 1, data)
quit:param=""
continue:data'["${"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Both ${ and {$ syntax are allowed so you'll want to check for both

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for pointing the syntax. I've updated the code to support both formats

continue:data'["${"

set varExpr = "${" _ $piece($piece(data, "${", 2), "}") _ "}"
set resolved = ##class(%IPM.Utils.Module).%EvaluateSystemExpression(varExpr)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this not work if you pass the whole string into %EvaluateSystemExpression and let it resolve all the expressions at once instead of just extracting the first one?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've modified the logic to ensure the system variables and custom variables.

set module = ##class(%IPM.Storage.Module).NameOpen(..#ModuleName)
do $$$AssertTrue($isobject(module), "Module "_..#ModuleName_" exists in IPM and version is "_ module.Version.ToString())

do $$$LogMessage("List all modules")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: remove extra tab

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, fixed the tab alignment

<Default Name="ipmtest" Value="TESTING MY STRING"/>
<Default Name="ipmdir" Value="/usr/irissys/mgr/user/mts"/>
<Default Name="datapath" Value="${libdir}data/" />
<Default Name="datapath1" Value="${ipmdir}data/" />
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm a bit curious about how this will handle the rather pathological case of using defaults to create a new system expression from parts. So something like this:

<Default Name="start" Value="${" />
<Default Name="middle" Value="namespace" />
<Default Name="end" Value="}" />
<Default Name="frankenstein" Value="${start}${middle}${end}"/>

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for pointing out that excellent scenario. You are absolutely right—this is a 'pathological' but critical edge case for configuration flexibility. I have updated the implementation.

CHANGELOG.md Outdated
- #822: The CPF resource processor now supports system expressions and macros in CPF merge files
- #578 Added functionality to record and display IPM history of install, uninstall, load, and update
- #961: Adding creation of a lock file for a module by using the `-create-lockfile` flag on install.
- #1013: Implement recursive placeholder resolution in Default parameters
Copy link
Collaborator

Choose a reason for hiding this comment

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

Move this to the new 0.10.6 section please

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. I've moved to 0.10.6

Copy link
Collaborator

@isc-dchui isc-dchui left a comment

Choose a reason for hiding this comment

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

Thanks for the updates! Here's another pass of comments/questions

set found = 0
set maxLevels = maxLevels - 1
//Decrement levels and check for circular references
if '$increment(maxLevels, -1) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't think $increment will thrown an error if maxLevels <= 0. Only if it is (way) too large, so this error condition needs a bit of adjustment

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The ' negation catches when $increment returns 0, which happens exactly after 20 passes. However, I'll update it to an explicit if (maxLevels <= 0) check to ensure it's more robust and readable. Thank you!


set varExpr = "${" _ $piece($piece(data, "${", 2), "}") _ "}"
set resolved = ##class(%IPM.Utils.Module).%EvaluateSystemExpression(varExpr)
set initialData = data
Copy link
Collaborator

Choose a reason for hiding this comment

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

Hm, let me reword what I meant. Instead of all this manual parsing, can we not do something like this?

set param = $order(customParams(param), 1, data)
quit:param=""
//Skip if no placeholders remain
continue:data'["{"
set resolved=##class(%IPM.Utils.Module).%EvaluateSystemExpression(param)
// and then for (key, val) in customParams, set resolved = %RegExReplace(resolved, key, val)
...

Ideally we do not duplicate the SystemExpressions into another function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree. I initially considered using %EvaluateSystemExpression but opted for a local array approach to optimize the resolution process. Using a pre-fetched systemParams array avoids redundant scans and regex evaluations inside the nested loop, allowing for more efficient lookups. However, I have updated the code to invoke %EvaluateSystemExpression directly to maintain consistency with the existing API.
Thank you!

CHANGELOG.md Outdated

### Fixed
- #996: Ensure COS commands execute in exec under a dedicated, isolated context
- #1013: Implement recursive placeholder resolution in Default parameters
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should go into the "Added" section

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved to Added section

}

/// Create class definition at runtime to capute the resovled reference value through <invoke> in manifest
ClassMethod CreateClassdef() As %Status
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not sure I quite understand. Could you please explain why we need a new dynamically created class to capture frankenstein when the other defaults don't?

Copy link
Contributor Author

@AshokThangavel AshokThangavel Jan 23, 2026

Choose a reason for hiding this comment

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

The reason I created this class was to verify that placeholders are correctly translated and passed to the method. Additionally I captured other variables in args... and temple global verify those variables. However, I explicitly check frankenstein for an additional layer of confirmation to ensure everything works as expected during testing. Thank you!

…xpression

- Adopted %EvaluateSystemExpression for system variable resolution to align with existing APIs.
- Retained multi-pass logic to handle nested and "Frankenstein" placeholders.
- Implemented a priority gate: customParams take precedence over system defaults.
- Hardened maxLevels condition using an explicit guard clause for better robustness.
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.

Allow ${variables} in Default parameters

2 participants