Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
042462b
feat: implement recursive placeholder resolution in Default parameters
AshokThangavel Dec 30, 2025
0ea3b84
fix: update method name
AshokThangavel Dec 30, 2025
d31ed58
docs: update the CHANGELOG file
AshokThangavel Dec 30, 2025
40e6cb5
Merge branch 'main' into feat/unified-variable-interpolation
AshokThangavel Jan 20, 2026
f8c629b
fix: updated the code based on feedback
AshokThangavel Jan 22, 2026
c921fd7
refactor: Updated placeholder logic
AshokThangavel Jan 22, 2026
7272092
fix:updated the code based on feedback
AshokThangavel Jan 22, 2026
ab6ef64
refactor: implement robust variable resolution using %EvaluateSystemE…
AshokThangavel Jan 23, 2026
725f093
fix: code updated based on comments
AshokThangavel Feb 3, 2026
9328a2a
Merge remote-tracking branch 'origin/main' into feat/unified-variable…
AshokThangavel Feb 10, 2026
16d9e84
Merge branch 'main' into feat/unified-variable-interpolation
AshokThangavel Mar 8, 2026
efc2744
Merge branch 'main' into feat/unified-variable-interpolation
AshokThangavel Mar 8, 2026
26e2d84
docs: updated changelog
AshokThangavel Mar 8, 2026
e7e81f0
Merge branch 'main' into feat/unified-variable-interpolation
AshokThangavel Mar 9, 2026
b19390d
Merge branch 'main' into feat/unified-variable-interpolation
AshokThangavel Mar 10, 2026
94daff4
Merge branch 'feat/unified-variable-interpolation' of https://github.…
AshokThangavel Mar 10, 2026
390aa57
Merge branch 'main' into feat/unified-variable-interpolation
AshokThangavel Mar 19, 2026
aee802e
Merge branch 'main' into feat/unified-variable-interpolation
isc-dchui May 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [0.10.6] - Unreleased

### Added
- #1013: Implement recursive placeholder resolution in Default parameters

### Fixed
- #996: Ensure COS commands execute in exec under a dedicated, isolated context

Expand Down
1 change: 1 addition & 0 deletions src/cls/IPM/Storage/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -1779,6 +1779,7 @@ Method %Evaluate(
set customParams("verbose") = +$get(pParams("Verbose"))
set customParams("ipmDir") = ##class(%Library.File).NormalizeDirectory($system.Util.DataDirectory() _ "ipm/" _ ..Name _ "/" _ ..VersionString)
set tAttrValue = ##class(%IPM.Utils.Module).%EvaluateMacro(tAttrValue)
do ##class(%IPM.Storage.ModuleSetting.Default).ResolvePlaceholders(.customParams)
set tAttrValue = ##class(%IPM.Storage.ModuleSetting.Default).EvaluateAttribute(tAttrValue,.customParams)
set attrValue = ##class(%IPM.Utils.Module).%EvaluateSystemExpression(tAttrValue)

Expand Down
55 changes: 55 additions & 0 deletions src/cls/IPM/Storage/ModuleSetting/Default.cls
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,61 @@ ClassMethod EvaluateAttribute(
return attribute
}

/// Processes configuration values to resolve variable references.
/// Searches for placeholders using the ${var} or {$var} syntax and replaces them with
/// the value of the corresponding 'Default' or 'Resource' name.
ClassMethod ResolvePlaceholders(ByRef customParams)
Comment thread
isc-dchui marked this conversation as resolved.
{
set found = 1
// maxLevels is a safety guard to prevents infinite loops caused by circular references.
set maxLevels = 20
Comment thread
isc-dchui marked this conversation as resolved.

while (found && (maxLevels > 0)) {
set found = 0
//Decrement levels and check for circular references
if (maxLevels <= 0) {
$$$ThrowOnError($$$ERROR($$$GeneralError,"Circular reference or too many levels in placeholders"))
}
set maxLevels = maxLevels - 1
set param = ""
for {
set param = $order(customParams(param), 1, data)
quit:param=""
//Skip if no placeholders remain
continue:data'["{"

set initialData = data
Comment thread
AshokThangavel marked this conversation as resolved.
for delimiter = "${", "{$" {
continue:data'[delimiter
kill seen
set pCount = $length(data, delimiter)
for i=2:1:pCount {
set chunk = $piece(data, delimiter, i)
set key = $piece(chunk, "}", 1)
continue:key=""
continue:$data(seen(key))
set seen(key) = ""

set search = delimiter _ key _ "}"
if $data(customParams(key), val) {
set data = $replace(data, search, val)
}
else {
Comment thread
AshokThangavel marked this conversation as resolved.
Outdated
set resolved = ##class(%IPM.Utils.Module).%EvaluateSystemExpression(search)
if (resolved '= search) {
set data = $replace(data, search, resolved)
}
}
}
}
if data '= initialData {
set customParams(param) = data
set found = 1
}
}
}
}

Storage Default
{
<Data name="DefaultState">
Expand Down
152 changes: 152 additions & 0 deletions tests/integration_tests/Test/PM/Integration/Module.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
Class Test.PM.Integration.Module Extends Test.PM.Integration.Base
{

Parameter CommonPathPrefix As STRING = "varresolver";

Parameter ModuleName As STRING = "demo-module1";

Parameter ClsDefName As STRING = "Test.TrackPlaceHolders";

/// This test validates that the IPM engine can handle "chained" ${variables}
/// (placeholders that resolve to other placeholders).
/// 1. Generates a 'module.xml' from XData with deep variable dependencies.
/// 2. Executes IPM Shell 'load' to verify multi-pass expansion.
/// 3. Ensures no unresolved placeholders remain after the load process.
Method TestNestedPlaceholderVar()
{
do $$$LogMessage("Create '"_..#ClsDefName_"' class definition and method 'CaptureResolvedPlaceHolders' to capturing the placeholder variables")
set status = ..CreateClassdef()
do $$$AssertStatusOK(status,"Class definition created successfully")

do $$$LogMessage(" start Loading the "_..#ModuleName_" module")
set status = ..CreateModuleXml(.moduleDir)
do $$$AssertStatusOK(status,"Created the xml file on "_moduleDir)

set status = ##class(%IPM.Main).Shell("load "_moduleDir)
do $$$AssertStatusOK(status,"Loaded "_..#CommonPathPrefix_" module successfully from "_moduleDir)

do $$$LogMessage("Validate the place holder variable resolved values in ^IRIS.Temp.IPMVarTest global")
set data = $get(^IRIS.Temp.IPMVarTest("frankenstein"))
if data'="" {
do $$$AssertTrue(1,"frankenstein place holder value is resolved to "_data)
Comment thread
isc-dchui marked this conversation as resolved.
Outdated
}
do $$$LogMessage("Verifying other parameter values")
do $$$LogMessage(" ^IRIS.Temp.IPMVarTest set on Namespace: "_$namespace)
set sub=""
for {
set sub = $order(^IRIS.Temp.IPMVarTest(sub),1,data)
quit:sub=""
do $$$LogMessage("^IRIS.Temp.IPMVarTest("_sub_") and value : "_data)
Comment thread
isc-dchui marked this conversation as resolved.
Outdated
}
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")
set status = ##class(%IPM.Main).Shell("list")
Comment thread
isc-dchui marked this conversation as resolved.
do $$$AssertStatusOK(status,"List all modules")

set status = ##class(%IPM.Main).Shell("uninstall "_..#ModuleName)
do $$$AssertStatusOK(status,"uninstalled module "_..#ModuleName_" successfully.")

set status = ##class(%File).Delete(##class(%File).NormalizeFilename("module.xml",moduleDir))
do $$$AssertStatusOK(status,"Deleted the module.xml file from "_moduleDir)

do $$$LogMessage("Deleting the "_..#ClsDefName_" generated test class")
set status = ##class(%Dictionary.ClassDefinition).%DeleteId(..#ClsDefName)
do $$$AssertStatusOK(status,"class "_..#ClsDefName_" is deleted successfully")

do $$$LogMessage("Deleting the ^IRIS.Temp.IPMVarTest global")
kill ^IRIS.Temp.IPMVarTest
}

/// Create class definition at runtime to capute the resovled reference value through <invoke> in manifest
Comment thread
AshokThangavel marked this conversation as resolved.
Outdated
ClassMethod CreateClassdef() As %Status
Comment thread
isc-dchui marked this conversation as resolved.
Outdated
{
set cls = ##class(%Dictionary.ClassDefinition).%New()
set cls.Name = ..#ClsDefName
// create sample method
set method = ##class(%Dictionary.MethodDefinition).%New()
set method.Name = "CaptureResolvedPlaceHolders"
set method.ClassMethod = 1
set method.FormalSpec = "frankenstein,args..."
do method.Implementation.WriteLine($char(9)_"set ^IRIS.Temp.IPMVarTest(""frankenstein"")= frankenstein")
do method.Implementation.WriteLine($char(9)_"merge ^IRIS.Temp.IPMVarTest = args")
do cls.Methods.Insert(method)
set sc = cls.%Save()
do $system.OBJ.Compile(..#ClsDefName)
return sc
}

Method CreateModuleXml(Output pModuleDir) As %Status
{
#define NormalizeDirectory(%path) ##class(%File).NormalizeDirectory(%path)
#define UTRoot ^UnitTestRoot

set sc = 1
set testRoot = $$$NormalizeDirectory($get($$$UTRoot))
set pModuleDir = $$$NormalizeDirectory(##class(%File).GetDirectory(testRoot)_"/_data/"_..#CommonPathPrefix_"/")

if '##class(%File).DirectoryExists(pModuleDir) {
set sc = ##class(%File).CreateDirectoryChain(pModuleDir)
}
do $$$AssertStatusOK(sc,"Directory created "_pModuleDir)
set stream = ##class(%Dictionary.CompiledXData).%OpenId(..%ClassName(1)_"||TestModuleXML").Data
set fileStream = ##class(%Stream.FileBinary).%New()
set fileStream.Filename=##class(%File).NormalizeFilename("module.xml",pModuleDir)
set sc = fileStream.CopyFromAndSave(stream)
do $$$AssertStatusOK(1,"module.xml File created on "_pModuleDir)

return sc
}

/// Sample module file
XData TestModuleXML [ MimeType = application/xml ]
{
<?xml version="1.0" encoding="UTF-8"?>
<Export generator="Cache" version="25">
<Document name="demo-module1.ZPM">
<Module>
<Name>demo-module1</Name>
<Version>1.0.0</Version>
<Description>testing the name resolved</Description>
<Packaging>module</Packaging>
<Default Name="count" Value="7"/>
<Default Name="dataversion" Value="${version}/" />
<Default Name="datadefaultmgrdir" Value="${mgrdir}/" />
<Default Name="mTestPlaceHolder" Value="${dataDefaultTest2}/xtest/${version}/${mgrdir}/${mTestVersion}/${datapath1}" />
<Default Name="datadefaultcspdir" Value="${cspdir}/" />
<Default Name="dataDefaultTest2" Value="${datadefaultcspdir}/xdata" />
<Default Name="dataDefaultTest3" Value="${dataDefaultTest2}/xtest" />
<Default Name="mTestVersion" Value="${dataDefaultTest2}/xtest/${version}" />
<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/" />
Comment thread
isc-dchui marked this conversation as resolved.

<Default Name="start" Value="${" />
<Default Name="middle" Value="namespace" />
<Default Name="end" Value="}" />
<Default Name="frankenstein" Value="${start}${middle}${end}"/>
<SystemRequirements Version=">=2020.1" Interoperability="enabled"/>
<SourcesRoot>src</SourcesRoot>
<Invoke Class="Test.TrackPlaceHolders" Method="CaptureResolvedPlaceHolders">
<Arg>${frankenstein}</Arg>
<Arg>${dataversion} </Arg>
<Arg>"12121212" </Arg>
<Arg>"12121212ASHOK"</Arg>
<Arg>${datadefaultcspdir}</Arg>
<Arg>${dataDefaultTest2}</Arg>
<Arg>${dataDefaultTest3} </Arg>
<Arg>${mTestVersion}</Arg>
<Arg>${ipmtest}</Arg>
<Arg>${datapath1}</Arg>
<Arg>${ipmdir} </Arg>
<Arg>${datadefaultmgrdir}</Arg>
</Invoke>
<AfterInstallMessage>Module installed successfully!</AfterInstallMessage>
</Module>
</Document>
</Export>
}

}
55 changes: 55 additions & 0 deletions tests/unit_tests/Test/PM/Unit/Module.cls
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,59 @@ Method TestFixUndefinedCLIGenCommand()
do $$$AssertStatusOK(sc, "AddWebApps method must now process web app list without error.")
}

Method TestResolveAllVariables()
{
//Setup Test Data
set customParams("count") = 7
set customParams("version") = "1.0.0"
set customParams("datadefaultcspdir") = "${cspdir}"
set customParams("datadefaultmgrdir") = "${mgrdir}"
set customParams("dataversion") = "${version}"
set customParams("dataDefaultTest2") = "${datadefaultcspdir}xdata"
set customParams("dataDefaultTest3") = "${dataDefaultTest2}xtest"
set customParams("datapath") = "${libdir}data/"
set customParams("ipmdir") = "/usr/irissys/mgr/user/mts"
set customParams("datapath1") = "${ipmdir}data/"
set customParams("mTestVersion") = "${dataDefaultTest2}/xtest/${version}"
set customParams("mTestPlaceHolder") = "${dataDefaultTest2}/xtest/${version}${mgrdir}${mTestVersion}${datapath1}"

// Frankenstein Variable Assembly
// These variables test the "Multi-Pass" capability of the resolver.
// It must first resolve 'start', 'middle', and 'end' to build a new
// valid placeholder string ("${namespace}"), and then resolve that
// string against the System Dictionary in a subsequent pass.
set customParams("start") = "${"
set customParams("middle") = "namespace"
set customParams("end") = "}"
set customParams("frankenstein") = "${start}${middle}${end}"

merge customParamsIn = customParams
// output
set customParamsOut("count")=7
set customParamsOut("dataDefaultTest2")="/usr/irissys/csp/xdata"
set customParamsOut("dataDefaultTest3")="/usr/irissys/csp/xdataxtest"
set customParamsOut("datadefaultcspdir")=##class(%IPM.Utils.Module).%EvaluateSystemExpression("${cspdir}")
set customParamsOut("datadefaultmgrdir")=##class(%IPM.Utils.Module).%EvaluateSystemExpression("${mgrdir}")
set customParamsOut("datapath")=##class(%IPM.Utils.Module).%EvaluateSystemExpression("${libdir}")_"data/"
set customParamsOut("datapath1")="/usr/irissys/mgr/user/mtsdata/"
set customParamsOut("dataversion")="1.0.0"
set customParamsOut("ipmdir")="/usr/irissys/mgr/user/mts"
set customParamsOut("mTestPlaceHolder")="/usr/irissys/csp/xdata/xtest/1.0.0/usr/irissys/mgr//usr/irissys/csp/xdata/xtest/1.0.0/usr/irissys/mgr/user/mtsdata/"
set customParamsOut("mTestVersion")="/usr/irissys/csp/xdata/xtest/1.0.0"
set customParamsOut("version")="1.0.0"
set customParamsOut("start") = "${"
set customParamsOut("middle") = "namespace"
set customParamsOut("end") = "}"
set customParamsOut("frankenstein") = "USER"
do ##class(%IPM.Storage.ModuleSetting.Default).ResolvePlaceholders(.customParams)

do $$$LogMessage("Validate all placholder variables")
set variable=""
for {
set variable = $order(customParams(variable))
quit:variable=""
do $$$AssertEquals(customParams(variable), customParamsOut(variable), variable_" is resolved correctly and the placeholder "_customParamsIn(variable)_" value is "_customParamsOut(variable))
}
}

}