From 042462b38f8228bb8cdf88ac1b6fd7ecab152274 Mon Sep 17 00:00:00 2001 From: AshokThangavel Date: Tue, 30 Dec 2025 22:06:55 +0530 Subject: [PATCH 1/8] feat: implement recursive placeholder resolution in Default parameters --- src/cls/IPM/Storage/Module.cls | 1 + src/cls/IPM/Storage/ModuleSetting/Default.cls | 34 +++++++ .../Test/PM/Integration/Module.cls | 88 +++++++++++++++++++ tests/unit_tests/Test/PM/Unit/Module.cls | 43 +++++++++ 4 files changed, 166 insertions(+) create mode 100644 tests/integration_tests/Test/PM/Integration/Module.cls diff --git a/src/cls/IPM/Storage/Module.cls b/src/cls/IPM/Storage/Module.cls index ce8ba82cd..6215a3a7e 100644 --- a/src/cls/IPM/Storage/Module.cls +++ b/src/cls/IPM/Storage/Module.cls @@ -1775,6 +1775,7 @@ Method %Evaluate( set customParams("version") = ..VersionString set customParams("verbose") = +$get(pParams("Verbose")) 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) diff --git a/src/cls/IPM/Storage/ModuleSetting/Default.cls b/src/cls/IPM/Storage/ModuleSetting/Default.cls index 543500c34..e4544437e 100644 --- a/src/cls/IPM/Storage/ModuleSetting/Default.cls +++ b/src/cls/IPM/Storage/ModuleSetting/Default.cls @@ -57,6 +57,40 @@ ClassMethod EvaluateAttribute( return attribute } +ClassMethod ResolvePlaceholders(ByRef customParams) +{ + set found = 1 + set maxLevels = 20 + + while (found && (maxLevels > 0)) { + set found = 0 + set maxLevels = maxLevels - 1 + set param = "" + + for { + set param = $order(customParams(param), 1, data) + quit:param="" + continue:data'["${" + + set varExpr = "${" _ $piece($piece(data, "${", 2), "}") _ "}" + set resolved = ##class(%IPM.Utils.Module).%EvaluateSystemExpression(varExpr) + + if resolved = varExpr { + set internalVar = $piece($piece(data, "${", 2), "}") + set resolved = $get(customParams(internalVar), varExpr) + } + + if resolved '= varExpr { + set prefix = $piece(data, "${", 1) + set suffix = $piece(data, "}", 2, *) + set customParams(param) = prefix _ resolved _ suffix + set found = 1 + } + + } + } +} + Storage Default { diff --git a/tests/integration_tests/Test/PM/Integration/Module.cls b/tests/integration_tests/Test/PM/Integration/Module.cls new file mode 100644 index 000000000..e9508b5ab --- /dev/null +++ b/tests/integration_tests/Test/PM/Integration/Module.cls @@ -0,0 +1,88 @@ +Class Test.PM.Integration.Module Extends Test.PM.Integration.Base +{ + +Parameter CommonPathPrefix As STRING = "varresolver"; + +Parameter ModuleName As STRING = "demo-module1"; + +/// 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 TestNestedPlaceholderIntegration() +{ + 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) + + 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") + + 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) +} + +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 ] +{ + + + + + demo-module1 + 1.0.0 + testing the name resolved + module + + + + + + + + + + + + + + src + Module installed successfully! + + + +} + +} diff --git a/tests/unit_tests/Test/PM/Unit/Module.cls b/tests/unit_tests/Test/PM/Unit/Module.cls index bffe7cd12..447f42e1c 100644 --- a/tests/unit_tests/Test/PM/Unit/Module.cls +++ b/tests/unit_tests/Test/PM/Unit/Module.cls @@ -60,4 +60,47 @@ 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}" + + 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" + + do ##class(%IPM.Storage.ModuleSetting.Default).ResolvePlaceholders(.customParams) + + do $$$LogMessage("Validate all placholder variables") + + set variables = $listbuild("count","dataDefaultTest2","dataDefaultTest3","datadefaultcspdir","datadefaultmgrdir","datapath","datapath1","dataversion","ipmdir","mTestPlaceHolder","mTestVersion","version") + + set ptr=0 + while $listnext(variables, ptr, variable){ + do $$$AssertEquals(customParams(variable), customParamsOut(variable), variable_" is resolved correctly and the placeholder "_customParamsIn(variable)_" value is "_customParamsOut(variable)) + } +} + } From 0ea3b844c1109c1e53c9a92ecb19477157b91dc2 Mon Sep 17 00:00:00 2001 From: AshokThangavel Date: Tue, 30 Dec 2025 22:17:22 +0530 Subject: [PATCH 2/8] fix: update method name --- tests/integration_tests/Test/PM/Integration/Module.cls | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration_tests/Test/PM/Integration/Module.cls b/tests/integration_tests/Test/PM/Integration/Module.cls index e9508b5ab..945b93363 100644 --- a/tests/integration_tests/Test/PM/Integration/Module.cls +++ b/tests/integration_tests/Test/PM/Integration/Module.cls @@ -10,7 +10,7 @@ Parameter ModuleName As STRING = "demo-module1"; /// 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 TestNestedPlaceholderIntegration() +Method TestNestedPlaceholderVar() { do $$$LogMessage(" start Loading the "_..#ModuleName_" module") set status = ..CreateModuleXml(.moduleDir) From d31ed585fca189cdb361110e4f9c21e734663f51 Mon Sep 17 00:00:00 2001 From: AshokThangavel Date: Tue, 30 Dec 2025 22:31:11 +0530 Subject: [PATCH 3/8] docs: update the CHANGELOG file --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7f7f5679..ff1f274f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #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 ### Changed - #316: All parameters, except developer mode, included with a `load`, `install` or `update` command will be propagated to dependencies From f8c629b12227022e0c3bc51aaecdd5aff14bbaaf Mon Sep 17 00:00:00 2001 From: AshokThangavel Date: Thu, 22 Jan 2026 15:56:37 +0530 Subject: [PATCH 4/8] fix: updated the code based on feedback --- CHANGELOG.md | 2 +- src/cls/IPM/Storage/ModuleSetting/Default.cls | 47 +++++++++----- src/cls/IPM/Utils/Module.cls | 20 ++++++ .../Test/PM/Integration/Module.cls | 65 ++++++++++++++++++- tests/unit_tests/Test/PM/Unit/Module.cls | 24 +++++-- 5 files changed, 134 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3318fff3d..04aaca68a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - #996: Ensure COS commands execute in exec under a dedicated, isolated context +- #1013: Implement recursive placeholder resolution in Default parameters ## [0.10.5] - 2026-01-15 @@ -23,7 +24,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #959: In ORAS repos, external name can now be used interchangeably with (default) name for `install` and `update`, i.e. a module published with its (default) name can be installed using its external name. - #951: The `unpublish` command will skip user confirmation prompt if the `-force` flag is provided. - #1018: Require module name for uninstall when not using the -all flag -- #1013: Implement recursive placeholder resolution in Default parameters ### Changed - #316: All parameters, except developer mode, included with a `load`, `install` or `update` command will be propagated to dependencies diff --git a/src/cls/IPM/Storage/ModuleSetting/Default.cls b/src/cls/IPM/Storage/ModuleSetting/Default.cls index e4544437e..310251d34 100644 --- a/src/cls/IPM/Storage/ModuleSetting/Default.cls +++ b/src/cls/IPM/Storage/ModuleSetting/Default.cls @@ -57,36 +57,51 @@ ClassMethod EvaluateAttribute( return attribute } +/// Processes configuration values to resolve variable references. +/// Searches for placeholders using the ${var} syntax and replaces them with +/// the value of the corresponding 'Default' or 'Resource' name. ClassMethod ResolvePlaceholders(ByRef customParams) { + // found tracks if any changes were made in the current pass. set found = 1 + // maxLevels is a safety guard to prevents infinite loops caused by circular references. set maxLevels = 20 - + do ##class(%IPM.Utils.Module).GetSystemExpressions(.systemPrams) while (found && (maxLevels > 0)) { set found = 0 set maxLevels = maxLevels - 1 + if 'maxLevels { + $$$ThrowOnError($$$ERROR($$$GeneralError,"Circular reference or too many levels in placeholders")) + } set param = "" - for { set param = $order(customParams(param), 1, data) quit:param="" continue:data'["${" - - set varExpr = "${" _ $piece($piece(data, "${", 2), "}") _ "}" - set resolved = ##class(%IPM.Utils.Module).%EvaluateSystemExpression(varExpr) - - if resolved = varExpr { - set internalVar = $piece($piece(data, "${", 2), "}") - set resolved = $get(customParams(internalVar), varExpr) + set lookup = "" + // user defined placeholder + for { + set lookup = $order(customParams(lookup), 1, val) + quit:lookup="" + set search = "${" _ lookup _ "}" + if data[search { + set data = $replace(data, search, val) + set customParams(param) = data + set found = 1 + } } - - if resolved '= varExpr { - set prefix = $piece(data, "${", 1) - set suffix = $piece(data, "}", 2, *) - set customParams(param) = prefix _ resolved _ suffix - set found = 1 + set lookup = "" + // system placeholder + for { + set lookup = $order(systemPrams(lookup), 1, val) + quit:lookup="" + set search = "${" _ lookup _ "}" + if data[search { + set data = $replace(data, search, val) + set customParams(param) = data + set found = 1 + } } - } } } diff --git a/src/cls/IPM/Utils/Module.cls b/src/cls/IPM/Utils/Module.cls index d19b92efe..2e2fdbb19 100644 --- a/src/cls/IPM/Utils/Module.cls +++ b/src/cls/IPM/Utils/Module.cls @@ -2349,6 +2349,26 @@ ClassMethod %EvaluateSystemExpression(pString As %String) As %String [ Internal return result } +ClassMethod GetSystemExpressions(ByRef SystemParams) As %String [ Internal ] +{ + do ..GetDatabaseInfoForNamespace($namespace, .properties) + set SystemParams("namespace") = $namespace + set SystemParams("namespacelower") = $zconvert($namespace,"L") + set SystemParams("namespaceroutinedb") = $get(properties("Routines")) + set SystemParams("namespaceglobalsdb") = $get(properties("Globals")) + set SystemParams("installdir") = $system.Util.InstallDirectory() + set SystemParams("datadir") = $system.Util.DataDirectory() + set SystemParams("mgrdir") = ##class(%Library.File).ManagerDirectory() + set SystemParams("cspdir") = ##class(%Library.File).NormalizeDirectory($system.Util.InstallDirectory()_"csp") + set SystemParams("bindir") = $system.Util.BinaryDirectory() + set SystemParams("libdir") = ##class(%Library.File).NormalizeDirectory($system.Util.InstallDirectory()_"lib") + do ##class(%Studio.General).GetWebServerPort(,,,.urlRoot) + set SystemParams("webroot") = urlRoot + set dbRole = ..GetDatabaseRole() + set SystemParams("dbrole") = dbRole + set SystemParams("globalsbbrole") = dbRole +} + ClassMethod %EvaluateMacro(pString As %String) As %String [ Internal ] { if pString '[ "$$$" { diff --git a/tests/integration_tests/Test/PM/Integration/Module.cls b/tests/integration_tests/Test/PM/Integration/Module.cls index 945b93363..26a0002f0 100644 --- a/tests/integration_tests/Test/PM/Integration/Module.cls +++ b/tests/integration_tests/Test/PM/Integration/Module.cls @@ -5,6 +5,8 @@ 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. @@ -12,6 +14,10 @@ Parameter ModuleName As STRING = "demo-module1"; /// 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) @@ -19,10 +25,23 @@ Method TestNestedPlaceholderVar() 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) + } + 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) + } 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") + do $$$LogMessage("List all modules") set status = ##class(%IPM.Main).Shell("list") set status = ##class(%IPM.Main).Shell("uninstall "_..#ModuleName) @@ -30,6 +49,31 @@ Method TestNestedPlaceholderVar() 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 in manifest +ClassMethod CreateClassdef() As %Status +{ + 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 @@ -77,8 +121,27 @@ XData TestModuleXML [ MimeType = application/xml ] + + + + + src + + ${frankenstein} + ${dataversion} + "12121212" + "12121212ASHOK" + ${datadefaultcspdir} + ${dataDefaultTest2} + ${dataDefaultTest3} + ${mTestVersion} + ${ipmtest} + ${datapath1} + ${ipmdir} + ${datadefaultmgrdir} + Module installed successfully! diff --git a/tests/unit_tests/Test/PM/Unit/Module.cls b/tests/unit_tests/Test/PM/Unit/Module.cls index 447f42e1c..1e0e7ecd5 100644 --- a/tests/unit_tests/Test/PM/Unit/Module.cls +++ b/tests/unit_tests/Test/PM/Unit/Module.cls @@ -76,6 +76,16 @@ Method TestResolveAllVariables() 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 @@ -90,15 +100,17 @@ Method TestResolveAllVariables() 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 variables = $listbuild("count","dataDefaultTest2","dataDefaultTest3","datadefaultcspdir","datadefaultmgrdir","datapath","datapath1","dataversion","ipmdir","mTestPlaceHolder","mTestVersion","version") - - set ptr=0 - while $listnext(variables, ptr, variable){ + 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)) } } From c921fd7bd4bac00f2e212bd5ec0e13b7c2bb49a3 Mon Sep 17 00:00:00 2001 From: AshokThangavel Date: Thu, 22 Jan 2026 20:57:46 +0530 Subject: [PATCH 5/8] refactor: Updated placeholder logic -Support nested ${var} and {$var} delimiters and handled Frankenstein-case --- src/cls/IPM/Storage/ModuleSetting/Default.cls | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/cls/IPM/Storage/ModuleSetting/Default.cls b/src/cls/IPM/Storage/ModuleSetting/Default.cls index 310251d34..8308df414 100644 --- a/src/cls/IPM/Storage/ModuleSetting/Default.cls +++ b/src/cls/IPM/Storage/ModuleSetting/Default.cls @@ -58,49 +58,52 @@ ClassMethod EvaluateAttribute( } /// Processes configuration values to resolve variable references. -/// Searches for placeholders using the ${var} syntax and replaces them with +/// 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) { - // found tracks if any changes were made in the current pass. set found = 1 // maxLevels is a safety guard to prevents infinite loops caused by circular references. set maxLevels = 20 do ##class(%IPM.Utils.Module).GetSystemExpressions(.systemPrams) + while (found && (maxLevels > 0)) { set found = 0 - set maxLevels = maxLevels - 1 - if 'maxLevels { + //Decrement levels and check for circular references + if '$increment(maxLevels, -1) { $$$ThrowOnError($$$ERROR($$$GeneralError,"Circular reference or too many levels in placeholders")) - } + } set param = "" for { set param = $order(customParams(param), 1, data) quit:param="" - continue:data'["${" - set lookup = "" - // user defined placeholder - for { - set lookup = $order(customParams(lookup), 1, val) - quit:lookup="" - set search = "${" _ lookup _ "}" - if data[search { - set data = $replace(data, search, val) - set customParams(param) = data - set found = 1 + //Skip if no placeholders remain + continue:data'["{" + + set initialData = data + for delimiter = "${", "{$" { + continue:data'[delimiter + set pCount = $length(data, delimiter) + for i=2:1:pCount { + kill seen + 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) + } elseif $data(systemPrams($$$lcase(key)), val) { + // system variables handled as Case-Insensitive + set data = $replace(data, search, val) + } } } - set lookup = "" - // system placeholder - for { - set lookup = $order(systemPrams(lookup), 1, val) - quit:lookup="" - set search = "${" _ lookup _ "}" - if data[search { - set data = $replace(data, search, val) - set customParams(param) = data - set found = 1 - } + if data '= initialData { + set customParams(param) = data + set found = 1 } } } From 72720928a5f04614b21112101585e62acb1b0242 Mon Sep 17 00:00:00 2001 From: AshokThangavel Date: Thu, 22 Jan 2026 21:04:45 +0530 Subject: [PATCH 6/8] fix:updated the code based on feedback --- tests/integration_tests/Test/PM/Integration/Module.cls | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration_tests/Test/PM/Integration/Module.cls b/tests/integration_tests/Test/PM/Integration/Module.cls index 26a0002f0..dfc19b2f8 100644 --- a/tests/integration_tests/Test/PM/Integration/Module.cls +++ b/tests/integration_tests/Test/PM/Integration/Module.cls @@ -43,6 +43,7 @@ Method TestNestedPlaceholderVar() do $$$LogMessage("List all modules") set status = ##class(%IPM.Main).Shell("list") + do $$$AssertStatusOK(status,"List all modules") set status = ##class(%IPM.Main).Shell("uninstall "_..#ModuleName) do $$$AssertStatusOK(status,"uninstalled module "_..#ModuleName_" successfully.") From ab6ef642686ec69c13cab7465daab770490faefa Mon Sep 17 00:00:00 2001 From: AshokThangavel Date: Fri, 23 Jan 2026 23:59:40 +0530 Subject: [PATCH 7/8] refactor: implement robust variable resolution using %EvaluateSystemExpression - 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. --- CHANGELOG.md | 4 +++- src/cls/IPM/Storage/ModuleSetting/Default.cls | 15 ++++++++------ src/cls/IPM/Utils/Module.cls | 20 ------------------- 3 files changed, 12 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04aaca68a..96e983e89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ 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 -- #1013: Implement recursive placeholder resolution in Default parameters ## [0.10.5] - 2026-01-15 diff --git a/src/cls/IPM/Storage/ModuleSetting/Default.cls b/src/cls/IPM/Storage/ModuleSetting/Default.cls index 8308df414..174aefbdb 100644 --- a/src/cls/IPM/Storage/ModuleSetting/Default.cls +++ b/src/cls/IPM/Storage/ModuleSetting/Default.cls @@ -65,14 +65,14 @@ ClassMethod ResolvePlaceholders(ByRef customParams) set found = 1 // maxLevels is a safety guard to prevents infinite loops caused by circular references. set maxLevels = 20 - do ##class(%IPM.Utils.Module).GetSystemExpressions(.systemPrams) while (found && (maxLevels > 0)) { set found = 0 //Decrement levels and check for circular references - if '$increment(maxLevels, -1) { + 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) @@ -83,9 +83,9 @@ ClassMethod ResolvePlaceholders(ByRef customParams) set initialData = data for delimiter = "${", "{$" { continue:data'[delimiter + kill seen set pCount = $length(data, delimiter) for i=2:1:pCount { - kill seen set chunk = $piece(data, delimiter, i) set key = $piece(chunk, "}", 1) continue:key="" @@ -95,9 +95,12 @@ ClassMethod ResolvePlaceholders(ByRef customParams) set search = delimiter _ key _ "}" if $data(customParams(key), val) { set data = $replace(data, search, val) - } elseif $data(systemPrams($$$lcase(key)), val) { - // system variables handled as Case-Insensitive - set data = $replace(data, search, val) + } + else { + set resolved = ##class(%IPM.Utils.Module).%EvaluateSystemExpression(search) + if (resolved '= search) { + set data = $replace(data, search, resolved) + } } } } diff --git a/src/cls/IPM/Utils/Module.cls b/src/cls/IPM/Utils/Module.cls index 2e2fdbb19..d19b92efe 100644 --- a/src/cls/IPM/Utils/Module.cls +++ b/src/cls/IPM/Utils/Module.cls @@ -2349,26 +2349,6 @@ ClassMethod %EvaluateSystemExpression(pString As %String) As %String [ Internal return result } -ClassMethod GetSystemExpressions(ByRef SystemParams) As %String [ Internal ] -{ - do ..GetDatabaseInfoForNamespace($namespace, .properties) - set SystemParams("namespace") = $namespace - set SystemParams("namespacelower") = $zconvert($namespace,"L") - set SystemParams("namespaceroutinedb") = $get(properties("Routines")) - set SystemParams("namespaceglobalsdb") = $get(properties("Globals")) - set SystemParams("installdir") = $system.Util.InstallDirectory() - set SystemParams("datadir") = $system.Util.DataDirectory() - set SystemParams("mgrdir") = ##class(%Library.File).ManagerDirectory() - set SystemParams("cspdir") = ##class(%Library.File).NormalizeDirectory($system.Util.InstallDirectory()_"csp") - set SystemParams("bindir") = $system.Util.BinaryDirectory() - set SystemParams("libdir") = ##class(%Library.File).NormalizeDirectory($system.Util.InstallDirectory()_"lib") - do ##class(%Studio.General).GetWebServerPort(,,,.urlRoot) - set SystemParams("webroot") = urlRoot - set dbRole = ..GetDatabaseRole() - set SystemParams("dbrole") = dbRole - set SystemParams("globalsbbrole") = dbRole -} - ClassMethod %EvaluateMacro(pString As %String) As %String [ Internal ] { if pString '[ "$$$" { From 26e2d84435fa8fb5895576c3fe0b0437d9b2e8ea Mon Sep 17 00:00:00 2001 From: AshokThangavel Date: Sun, 8 Mar 2026 18:03:14 +0530 Subject: [PATCH 8/8] docs: updated changelog --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f1d0cfe8..6dd27218c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,8 +10,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - #1013: Implement recursive placeholder resolution in Default parameters -## [0.10.6] - Unreleased - ## [0.10.6] - 2026-02-24 ### Added