diff --git a/dsc/locales/en-us.toml b/dsc/locales/en-us.toml index f1e47d6ec..b7957637d 100644 --- a/dsc/locales/en-us.toml +++ b/dsc/locales/en-us.toml @@ -44,6 +44,8 @@ failedReadingParametersFile = "Failed to read parameters file" readingParametersFromStdin = "Reading parameters from STDIN" generatingCompleter = "Generating completion script for" readingParametersFile = "Reading parameters from file" +mergingParameters = "Merging inline parameters with parameters file (inline takes precedence)" +failedMergingParameters = "Failed to merge parameters" usingDscVersion = "Running DSC version" foundProcesses = "Found processes" failedToGetPid = "Could not get current process id" @@ -158,5 +160,4 @@ failedToAbsolutizePath = "Error making config path absolute" failedToGetParentPath = "Error reading config path parent" dscConfigRootAlreadySet = "The current value of DSC_CONFIG_ROOT env var will be overridden" settingDscConfigRoot = "Setting DSC_CONFIG_ROOT env var as" -stdinNotAllowedForBothParametersAndInput = "Cannot read from STDIN for both parameters and input." removingUtf8Bom = "Removing UTF-8 BOM from input" diff --git a/dsc/src/args.rs b/dsc/src/args.rs index c3a769cd9..e7e514b7e 100644 --- a/dsc/src/args.rs +++ b/dsc/src/args.rs @@ -66,9 +66,9 @@ pub enum SubCommand { Config { #[clap(subcommand)] subcommand: ConfigSubCommand, - #[clap(short, long, help = t!("args.parameters").to_string(), conflicts_with = "parameters_file")] + #[clap(short, long, help = t!("args.parameters").to_string())] parameters: Option, - #[clap(short = 'f', long, help = t!("args.parametersFile").to_string(), conflicts_with = "parameters")] + #[clap(short = 'f', long, help = t!("args.parametersFile").to_string())] parameters_file: Option, #[clap(short = 'r', long, help = t!("args.systemRoot").to_string())] system_root: Option, diff --git a/dsc/src/main.rs b/dsc/src/main.rs index 7a1a7f876..68670e7ed 100644 --- a/dsc/src/main.rs +++ b/dsc/src/main.rs @@ -54,16 +54,63 @@ fn main() { generate(shell, &mut cmd, "dsc", &mut io::stdout()); }, SubCommand::Config { subcommand, parameters, parameters_file, system_root, as_group, as_assert, as_include } => { - if let Some(file_name) = parameters_file { + let merged_parameters = if parameters_file.is_some() && parameters.is_some() { + // Both parameters and parameters_file provided - merge them with inline taking precedence + let file_params = if let Some(file_name) = ¶meters_file { + if file_name == "-" { + info!("{}", t!("main.readingParametersFromStdin")); + let mut stdin = Vec::::new(); + match io::stdin().read_to_end(&mut stdin) { + Ok(_) => { + match String::from_utf8(stdin) { + Ok(input) => Some(input), + Err(err) => { + error!("{}: {err}", t!("util.invalidUtf8")); + exit(EXIT_INVALID_INPUT); + } + } + }, + Err(err) => { + error!("{}: {err}", t!("util.failedToReadStdin")); + exit(EXIT_INVALID_INPUT); + } + } + } else { + info!("{}: {file_name}", t!("main.readingParametersFile")); + match std::fs::read_to_string(file_name) { + Ok(content) => Some(content), + Err(err) => { + error!("{} '{file_name}': {err}", t!("main.failedReadingParametersFile")); + exit(util::EXIT_INVALID_INPUT); + } + } + } + } else { + None + }; + + // Parse both and merge + if let (Some(file_content), Some(inline_content)) = (file_params, parameters.as_ref()) { + info!("{}", t!("main.mergingParameters")); + match util::merge_parameters(&file_content, inline_content) { + Ok(merged) => Some(merged), + Err(err) => { + error!("{}: {err}", t!("main.failedMergingParameters")); + exit(EXIT_INVALID_INPUT); + } + } + } else { + parameters.clone() + } + } else if let Some(file_name) = parameters_file { + // Only parameters_file provided if file_name == "-" { info!("{}", t!("main.readingParametersFromStdin")); let mut stdin = Vec::::new(); - let parameters = match io::stdin().read_to_end(&mut stdin) { + match io::stdin().read_to_end(&mut stdin) { Ok(_) => { match String::from_utf8(stdin) { - Ok(input) => { - input - }, + Ok(input) => Some(input), Err(err) => { error!("{}: {err}", t!("util.invalidUtf8")); exit(EXIT_INVALID_INPUT); @@ -74,22 +121,22 @@ fn main() { error!("{}: {err}", t!("util.failedToReadStdin")); exit(EXIT_INVALID_INPUT); } - }; - subcommand::config(&subcommand, &Some(parameters), true, system_root.as_ref(), &as_group, &as_assert, &as_include, progress_format); - return; - } - info!("{}: {file_name}", t!("main.readingParametersFile")); - match std::fs::read_to_string(&file_name) { - Ok(parameters) => subcommand::config(&subcommand, &Some(parameters), false, system_root.as_ref(), &as_group, &as_assert, &as_include, progress_format), - Err(err) => { - error!("{} '{file_name}': {err}", t!("main.failedReadingParametersFile")); - exit(util::EXIT_INVALID_INPUT); + } + } else { + info!("{}: {file_name}", t!("main.readingParametersFile")); + match std::fs::read_to_string(&file_name) { + Ok(content) => Some(content), + Err(err) => { + error!("{} '{file_name}': {err}", t!("main.failedReadingParametersFile")); + exit(util::EXIT_INVALID_INPUT); + } } } - } - else { - subcommand::config(&subcommand, ¶meters, false, system_root.as_ref(), &as_group, &as_assert, &as_include, progress_format); - } + } else { + parameters + }; + + subcommand::config(&subcommand, &merged_parameters, system_root.as_ref(), &as_group, &as_assert, &as_include, progress_format); }, SubCommand::Extension { subcommand } => { subcommand::extension(&subcommand, progress_format); diff --git a/dsc/src/subcommand.rs b/dsc/src/subcommand.rs index 57d85cdc0..d9ee13090 100644 --- a/dsc/src/subcommand.rs +++ b/dsc/src/subcommand.rs @@ -276,7 +276,7 @@ fn initialize_config_root(path: Option<&String>) -> Option { #[allow(clippy::too_many_lines)] #[allow(clippy::too_many_arguments)] -pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, parameters_from_stdin: bool, mounted_path: Option<&String>, as_group: &bool, as_assert: &bool, as_include: &bool, progress_format: ProgressFormat) { +pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, mounted_path: Option<&String>, as_group: &bool, as_assert: &bool, as_include: &bool, progress_format: ProgressFormat) { let (new_parameters, json_string) = match subcommand { ConfigSubCommand::Get { input, file, .. } | ConfigSubCommand::Set { input, file, .. } | @@ -284,7 +284,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, parame ConfigSubCommand::Validate { input, file, .. } | ConfigSubCommand::Export { input, file, .. } => { let new_path = initialize_config_root(file.as_ref()); - let document = get_input(input.as_ref(), new_path.as_ref(), parameters_from_stdin); + let document = get_input(input.as_ref(), new_path.as_ref()); if *as_include { let (new_parameters, config_json) = match get_contents(&document) { Ok((parameters, config_json)) => (parameters, config_json), @@ -300,7 +300,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, parame }, ConfigSubCommand::Resolve { input, file, .. } => { let new_path = initialize_config_root(file.as_ref()); - let document = get_input(input.as_ref(), new_path.as_ref(), parameters_from_stdin); + let document = get_input(input.as_ref(), new_path.as_ref()); let (new_parameters, config_json) = match get_contents(&document) { Ok((parameters, config_json)) => (parameters, config_json), Err(err) => { @@ -398,7 +398,7 @@ pub fn config(subcommand: &ConfigSubCommand, parameters: &Option, parame }; if *as_include { let new_path = initialize_config_root(file.as_ref()); - let input = get_input(input.as_ref(), new_path.as_ref(), parameters_from_stdin); + let input = get_input(input.as_ref(), new_path.as_ref()); match serde_json::from_str::(&input) { Ok(_) => { // valid, so do nothing @@ -554,7 +554,7 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat }, ResourceSubCommand::Export { resource, version, input, file, output_format } => { dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); - let parsed_input = get_input(input.as_ref(), file.as_ref(), false); + let parsed_input = get_input(input.as_ref(), file.as_ref()); resource_command::export(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref()); }, ResourceSubCommand::Get { resource, version, input, file: path, all, output_format } => { @@ -567,23 +567,23 @@ pub fn resource(subcommand: &ResourceSubCommand, progress_format: ProgressFormat error!("{}", t!("subcommand.jsonArrayNotSupported")); exit(EXIT_INVALID_ARGS); } - let parsed_input = get_input(input.as_ref(), path.as_ref(), false); + let parsed_input = get_input(input.as_ref(), path.as_ref()); resource_command::get(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref()); } }, ResourceSubCommand::Set { resource, version, input, file: path, output_format } => { dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); - let parsed_input = get_input(input.as_ref(), path.as_ref(), false); + let parsed_input = get_input(input.as_ref(), path.as_ref()); resource_command::set(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref()); }, ResourceSubCommand::Test { resource, version, input, file: path, output_format } => { dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); - let parsed_input = get_input(input.as_ref(), path.as_ref(), false); + let parsed_input = get_input(input.as_ref(), path.as_ref()); resource_command::test(&mut dsc, resource, version.as_deref(), &parsed_input, output_format.as_ref()); }, ResourceSubCommand::Delete { resource, version, input, file: path } => { dsc.find_resources(&[DiscoveryFilter::new(resource, version.clone())], progress_format); - let parsed_input = get_input(input.as_ref(), path.as_ref(), false); + let parsed_input = get_input(input.as_ref(), path.as_ref()); resource_command::delete(&mut dsc, resource, version.as_deref(), &parsed_input); }, } diff --git a/dsc/src/util.rs b/dsc/src/util.rs index 7d2d07d1e..625fbb33f 100644 --- a/dsc/src/util.rs +++ b/dsc/src/util.rs @@ -432,7 +432,7 @@ pub fn enable_tracing(trace_level_arg: Option<&TraceLevel>, trace_format_arg: Op info!("Trace-level is {:?}", tracing_setting.level); } -pub fn get_input(input: Option<&String>, file: Option<&String>, parameters_from_stdin: bool) -> String { +pub fn get_input(input: Option<&String>, file: Option<&String>) -> String { trace!("Input: {input:?}, File: {file:?}"); let value = if let Some(input) = input { debug!("{}", t!("util.readingInput")); @@ -448,10 +448,6 @@ pub fn get_input(input: Option<&String>, file: Option<&String>, parameters_from_ // check if need to read from STDIN if path == "-" { info!("{}", t!("util.readingInputFromStdin")); - if parameters_from_stdin { - error!("{}", t!("util.stdinNotAllowedForBothParametersAndInput")); - exit(EXIT_INVALID_INPUT); - } let mut stdin = Vec::::new(); match std::io::stdin().read_to_end(&mut stdin) { Ok(_) => { @@ -582,3 +578,87 @@ pub fn in_desired_state(test_result: &ResourceTestResult) -> bool { } } } + +/// Merge two parameter sets, with inline parameters taking precedence over file parameters. +/// +/// # Arguments +/// +/// * `file_params` - Parameters from file (JSON or YAML format) +/// * `inline_params` - Inline parameters (JSON or YAML format) that take precedence +/// +/// # Returns +/// +/// * `Result` - Merged parameters as JSON string +/// +/// # Errors +/// +/// This function will return an error if: +/// - Either parameter set cannot be parsed as valid JSON or YAML +/// - The merged result cannot be serialized to JSON +pub fn merge_parameters(file_params: &str, inline_params: &str) -> Result { + use serde_json::Value; + + // Parse file parameters + let file_value: Value = match serde_json::from_str(file_params) { + Ok(json) => json, + Err(_) => { + // YAML + match serde_yaml::from_str::(file_params) { + Ok(yaml) => serde_json::to_value(yaml)?, + Err(err) => { + return Err(DscError::Parser(format!("Failed to parse file parameters: {err}"))); + } + } + } + }; + + // Parse inline parameters + let inline_value: Value = match serde_json::from_str(inline_params) { + Ok(json) => json, + Err(_) => { + // YAML + match serde_yaml::from_str::(inline_params) { + Ok(yaml) => serde_json::to_value(yaml)?, + Err(err) => { + return Err(DscError::Parser(format!("Failed to parse inline parameters: {err}"))); + } + } + } + }; + + // Both must be objects to merge + let Some(mut file_obj) = file_value.as_object().cloned() else { + return Err(DscError::Parser("File parameters must be a JSON object".to_string())); + }; + + let Some(inline_obj) = inline_value.as_object() else { + return Err(DscError::Parser("Inline parameters must be a JSON object".to_string())); + }; + + // Special handling for the "parameters" key - merge nested objects + if let (Some(file_params_value), Some(inline_params_value)) = (file_obj.get("parameters"), inline_obj.get("parameters")) { + if let (Some(mut file_params_obj), Some(inline_params_obj)) = (file_params_value.as_object().cloned(), inline_params_value.as_object()) { + // Merge the nested parameters objects + for (key, value) in inline_params_obj { + file_params_obj.insert(key.clone(), value.clone()); + } + file_obj.insert("parameters".to_string(), Value::Object(file_params_obj)); + } else { + // If one is not an object, inline takes precedence + file_obj.insert("parameters".to_string(), inline_params_value.clone()); + } + } else if let Some(inline_params_value) = inline_obj.get("parameters") { + // Only inline has parameters + file_obj.insert("parameters".to_string(), inline_params_value.clone()); + } + + // Merge other top-level keys: inline parameters override file parameters + for (key, value) in inline_obj { + if key != "parameters" { + file_obj.insert(key.clone(), value.clone()); + } + } + + let merged = Value::Object(file_obj); + Ok(serde_json::to_string(&merged)?) +} diff --git a/dsc/tests/dsc_parameters.tests.ps1 b/dsc/tests/dsc_parameters.tests.ps1 index d8b48b018..e44db6455 100644 --- a/dsc/tests/dsc_parameters.tests.ps1 +++ b/dsc/tests/dsc_parameters.tests.ps1 @@ -380,20 +380,6 @@ Describe 'Parameters tests' { $out.results[0].result.inDesiredState | Should -Be $IsWindows } - It 'Parameters and input cannot both be from STDIN' { - $params = @{ - parameters = @{ - osFamily = 'Windows' - } - } | ConvertTo-Json -Compress - - $out = $params | dsc config -f - test -f - 2> $TestDrive/error.log - $LASTEXITCODE | Should -Be 4 - $out | Should -BeNullOrEmpty - $errorMessage = Get-Content -Path $TestDrive/error.log -Raw - $errorMessage | Should -BeLike "*ERROR*Cannot read from STDIN for both parameters and input*" - } - It 'Invalid parameters read from STDIN result in error' { $params = @{ osFamily = 'Windows' @@ -577,4 +563,295 @@ Describe 'Parameters tests' { $LASTEXITCODE | Should -Be 4 $testError | Should -Match 'Circular dependency or unresolvable parameter references detected in parameters' } + + It 'Inline parameters and parameters file can be used together' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + message: + type: string + environment: + type: string + port: + type: int + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[concat('Message: ', parameters('message'), ', Environment: ', parameters('environment'), ', Port: ', string(parameters('port')))]" +"@ + $paramsFile = @{ + parameters = @{ + message = 'Hello from file' + environment = 'production' + port = 8080 + } + } | ConvertTo-Json + + $inlineParams = @{ + parameters = @{ + message = 'Hello from inline' + port = 3000 + } + } | ConvertTo-Json + + $file_path = "$TestDrive/test.parameters.json" + Set-Content -Path $file_path -Value $paramsFile + + $out = dsc config --parameters-file $file_path --parameters $inlineParams get -i $config_yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results[0].result.actualState.output | Should -BeExactly 'Message: Hello from inline, Environment: production, Port: 3000' + } + + It 'Inline parameters take precedence over parameters file' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + value1: + type: string + value2: + type: string + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[concat(parameters('value1'), '-', parameters('value2'))]" +"@ + $paramsFile = @{ + parameters = @{ + value1 = 'file1' + value2 = 'file2' + } + } | ConvertTo-Json + + $inlineParams = @{ + parameters = @{ + value1 = 'inline1' + } + } | ConvertTo-Json + + $file_path = "$TestDrive/test.parameters.json" + Set-Content -Path $file_path -Value $paramsFile + + $out = dsc config --parameters-file $file_path --parameters $inlineParams get -i $config_yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results[0].result.actualState.output | Should -BeExactly 'inline1-file2' + } + + It 'Parameters file from stdin can be merged with inline parameters' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + param1: + type: string + param2: + type: string + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[concat(parameters('param1'), '-', parameters('param2'))]" +"@ + $paramsFile = @{ + parameters = @{ + param1 = 'fromStdin' + param2 = 'alsoFromStdin' + } + } | ConvertTo-Json + + $inlineParams = @{ + parameters = @{ + param2 = 'fromInline' + } + } | ConvertTo-Json + + $out = $paramsFile | dsc config --parameters-file - --parameters $inlineParams get -i $config_yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results[0].result.actualState.output | Should -BeExactly 'fromStdin-fromInline' + } + + It 'Parameters file in YAML format can be merged with inline JSON parameters' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + name: + type: string + age: + type: int + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[concat(parameters('name'), ' is ', string(parameters('age')))]" +"@ + $paramsFileYaml = @" +parameters: + name: John + age: 30 +"@ + $inlineParams = @{ + parameters = @{ + age = 25 + } + } | ConvertTo-Json + + $file_path = "$TestDrive/test.parameters.yaml" + Set-Content -Path $file_path -Value $paramsFileYaml + + $out = dsc config --parameters-file $file_path --parameters $inlineParams get -i $config_yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results[0].result.actualState.output | Should -BeExactly 'John is 25' + } + + It 'Merged parameters work with default values' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + param1: + type: string + defaultValue: default1 + param2: + type: string + defaultValue: default2 + param3: + type: string + defaultValue: default3 + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[concat(parameters('param1'), '-', parameters('param2'), '-', parameters('param3'))]" +"@ + $paramsFile = @{ + parameters = @{ + param2 = 'fromFile' + } + } | ConvertTo-Json + + $inlineParams = @{ + parameters = @{ + param3 = 'fromInline' + } + } | ConvertTo-Json + + $file_path = "$TestDrive/test.parameters.json" + Set-Content -Path $file_path -Value $paramsFile + + $out = dsc config --parameters-file $file_path --parameters $inlineParams get -i $config_yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + $out.results[0].result.actualState.output | Should -BeExactly 'default1-fromFile-fromInline' + } + + It 'Merged parameters support all data types: ' -TestCases @( + @{ type = 'string'; fileValue = 'file'; inlineValue = 'inline' } + @{ type = 'int'; fileValue = 100; inlineValue = 200 } + @{ type = 'bool'; fileValue = $false; inlineValue = $true } + @{ type = 'array'; fileValue = @('a', 'b'); inlineValue = @('x', 'y', 'z') } + @{ type = 'object'; fileValue = @{key1='value1'}; inlineValue = @{key2='value2'} } + ) { + param($type, $fileValue, $inlineValue) + + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + fileParam: + type: $type + inlineParam: + type: $type + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[concat(string(parameters('fileParam')), '-', string(parameters('inlineParam')))]" +"@ + $paramsFile = @{ + parameters = @{ + fileParam = $fileValue + inlineParam = $fileValue + } + } | ConvertTo-Json + + $inlineParams = @{ + parameters = @{ + inlineParam = $inlineValue + } + } | ConvertTo-Json + + $file_path = "$TestDrive/test.parameters.json" + Set-Content -Path $file_path -Value $paramsFile + + $out = dsc config --parameters-file $file_path --parameters $inlineParams get -i $config_yaml | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + if ($type -eq 'bool') { + $expectedOutput = "{0}-{1}" -f $fileValue.ToString().ToLower(), $inlineValue.ToString().ToLower() + } + elseif ($type -eq 'array') { + $fileJson = $fileValue | ConvertTo-Json -Compress + $inlineJson = $inlineValue | ConvertTo-Json -Compress + $expectedOutput = "{0}-{1}" -f $fileJson, $inlineJson + } + elseif ($type -eq 'object') { + $fileJson = $fileValue | ConvertTo-Json -Compress + $inlineJson = $inlineValue | ConvertTo-Json -Compress + $expectedOutput = "{0}-{1}" -f $fileJson, $inlineJson + } + else { + $expectedOutput = "{0}-{1}" -f $fileValue, $inlineValue + } + $out.results[0].result.actualState.output | Should -BeExactly $expectedOutput + } + + It 'Invalid inline parameters format returns error when merging' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + param1: + type: string + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[parameters('param1')]" +"@ + $paramsFile = @{ + parameters = @{ + param1 = 'valid' + } + } | ConvertTo-Json + + $file_path = "$TestDrive/test.parameters.json" + Set-Content -Path $file_path -Value $paramsFile + + $invalidInlineParams = 'not valid json' + + $out = & {$config_yaml | dsc config --parameters-file $file_path --parameters $invalidInlineParams get -f - 2>&1} + $LASTEXITCODE | Should -Be 4 + $out | Should -Match 'Failed to merge parameters' + } + + It 'Invalid parameters file format returns error when merging' { + $config_yaml = @" + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + parameters: + param1: + type: string + resources: + - name: Echo + type: Microsoft.DSC.Debug/Echo + properties: + output: "[parameters('param1')]" +"@ + $inlineParams = @{ + parameters = @{ + param1 = 'valid' + } + } | ConvertTo-Json + + $file_path = "$TestDrive/test.parameters.json" + Set-Content -Path $file_path -Value 'not valid json' + + $out = & {$config_yaml | dsc config --parameters-file $file_path --parameters $inlineParams get -f - 2>&1} + $LASTEXITCODE | Should -Be 4 + $out | Should -Match 'Failed to merge parameters' + } }