Skip to content

Commit 9c02305

Browse files
(GH-538) Define newtypes for exit_codes fields
Prior to this change, the `exit_codes` fields for the resource and extension manifests defined the map of exit codes to their semantic meanings as `Option<HashMap<String, String>>`. As part of schema canonicalization, we need to provide a canonical JSON Schema for that object. Unfortunately, because we don't own either the `JsonSchema` trait or `HashMap<T>` type, we need to use a newtype wrapper. This change defines two new types: - `ExitCode` as a lightweight wrapper around `i32` where we can control serialization and deserialization with strings as the serialized format instead of an integer. - `ExitCodeMap` as a lightweight wrapper for `HashMap<ExitCode, String>`. It implements a helper function for determining whether the map is empty or default and defines the default map for exit `0` (success) and `1` (error). The `ExitCodeMap` includes a hand-implementation of `JsonSchema` to match the canonical JSON Schema, which `schemars` won't generate from the struct definition. This change not only enables us to provide the canonical JSON Schema for exit code fields in a reusable way, it also removes the need to maintain a converter function for transforming the exit codes map from `HashMap<String, String>` to `HashMap<i32, String>`.
1 parent 7d5eb02 commit 9c02305

9 files changed

Lines changed: 810 additions & 1 deletion

File tree

lib/dsc-lib/locales/en-us.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -729,6 +729,7 @@ forExecutable = "for executable"
729729
function = "Function"
730730
integerConversion = "Function integer argument conversion"
731731
invalidConfiguration = "Invalid configuration"
732+
invalidExitCode = "Invalid key in 'exitCodes' map"
732733
invalidTypeNamePrefix = "Invalid type name"
733734
invalidTypeNameSuffix = "valid resource type names must match the following pattern"
734735
unsupportedManifestVersion = "Unsupported manifest version"
@@ -777,6 +778,9 @@ notFoundSetting = "Setting '%{name}' not found in %{path}"
777778
failedToGetExePath = "Can't get 'dsc' executable path"
778779
settingNotFound = "Setting '%{name}' not found"
779780
failedToAbsolutizePath = "Failed to absolutize path '%{path}'"
780-
invalidExitCodeKey = "Invalid exit code key '%{key}'"
781781
executableNotFoundInWorkingDirectory = "Executable '%{executable}' not found with working directory '%{cwd}'"
782782
executableNotFound = "Executable '%{executable}' not found"
783+
784+
[types.exit_codes_map]
785+
successText = "Success"
786+
failureText = "Error"

lib/dsc-lib/locales/schemas.definitions.yaml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,36 @@
11
_version: 2
22
schemas:
33
definitions:
4+
exitCodes:
5+
title:
6+
en-us: Exit codes
7+
description:
8+
en-us: >-
9+
Defines a map of valid exit codes for operation commands.
10+
markdownDescription:
11+
en-us: |-
12+
Defines a map of valid exit codes for operation commands. DSC always interprets exit code
13+
`0` as a successful operation and any other exit code as an error. Use this property to
14+
indicate human-readable semantic meanings for the DSC resource's exit codes.
15+
16+
When this field isn't defined, DSC can only report "Success" (for exit code `0`) and
17+
"Error" (for all other exit codes).
18+
19+
Define the keys in this property as strings representing a valid 32-bit signed integer.
20+
You can't use alternate formats for the exit code. For example, instead of the
21+
hexadecimal value `0x80070005` for "Access denied", specify the exit code as
22+
`-2147024891`.
23+
24+
If you're authoring your resource manifest in YAML, be sure to wrap the exit code in
25+
single quotes, like `'0': Success` instead of `0: Success` to ensure the YAML file can be
26+
parsed correctly.
27+
28+
Define the value for each key as a string explaining what the exit code indicates.
29+
invalidKeyErrorMessage:
30+
en-us: >-
31+
Invalid exit code. Each exit code must be defined as a string representing a 32-bit
32+
signed integer, like `5` or `-2147024891`.
33+
434
resourceType:
535
title: Fully qualified type name
636
description: >-

lib/dsc-lib/src/dscerror.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ pub enum DscError {
5252
#[error("{t} '{0}', {t2} {1}, {t3} {2}", t = t!("dscerror.invalidFunctionParameterCount"), t2 = t!("dscerror.expected"), t3 = t!("dscerror.got"))]
5353
InvalidFunctionParameterCount(String, usize, usize),
5454

55+
#[error("{t} '{0}': {1}", t = t!("dscerror.invalidExitCode"))]
56+
InvalidExitCode(String, core::num::ParseIntError),
57+
5558
#[error("{0}")]
5659
InvalidManifest(String),
5760

lib/dsc-lib/src/types/exit_code.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use std::{borrow::Borrow, fmt::Display, ops::Deref, str::FromStr};
5+
6+
use serde::{Deserialize, Serialize};
7+
8+
use crate::dscerror::DscError;
9+
10+
/// Defines a program exit code as a 32-bit integer ([`i32`]).
11+
///
12+
/// DSC uses exit codes to determine whether invoked commands, including resource and extension
13+
/// operations, are successful. DSC treats exit code `0` as successful and all other exit codes
14+
/// as indicating a failure.
15+
#[derive(Debug, Copy, Clone, Hash, Eq, PartialOrd, Ord, Serialize, Deserialize)]
16+
#[serde(try_from = "String", into = "String")]
17+
pub struct ExitCode(i32);
18+
19+
impl ExitCode {
20+
/// Creates an instance of [`ExitCode`] from an [`i32`].
21+
pub fn new(code: i32) -> Self {
22+
Self(code)
23+
}
24+
25+
/// Parses a string into an [`ExitCode`].
26+
///
27+
/// If the string can be parsed as an [`i32`], the function returns an [`ExitCode`]. Otherwise,
28+
/// the function raises the [`DscError::InvalidExitCode`] error.
29+
pub fn parse(text: &str) -> Result<ExitCode, DscError> {
30+
match i32::from_str(text) {
31+
Ok(code) => Ok(Self(code)),
32+
Err(err) => Err(DscError::InvalidExitCode(text.to_string(), err)),
33+
}
34+
}
35+
}
36+
37+
impl AsRef<i32> for ExitCode {
38+
fn as_ref(&self) -> &i32 {
39+
&self.0
40+
}
41+
}
42+
43+
impl Deref for ExitCode {
44+
type Target = i32;
45+
fn deref(&self) -> &Self::Target {
46+
&self.0
47+
}
48+
}
49+
50+
impl Borrow<i32> for ExitCode {
51+
fn borrow(&self) -> &i32 {
52+
&self.0
53+
}
54+
}
55+
56+
impl Display for ExitCode {
57+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58+
self.0.fmt(f)
59+
}
60+
}
61+
62+
impl FromStr for ExitCode {
63+
type Err = DscError;
64+
fn from_str(s: &str) -> Result<Self, Self::Err> {
65+
Self::parse(s)
66+
}
67+
}
68+
69+
impl TryFrom<String> for ExitCode {
70+
type Error = DscError;
71+
fn try_from(value: String) -> Result<Self, Self::Error> {
72+
Self::parse(value.as_str())
73+
}
74+
}
75+
76+
impl TryFrom<&str> for ExitCode {
77+
type Error = DscError;
78+
fn try_from(value: &str) -> Result<Self, Self::Error> {
79+
Self::parse(value)
80+
}
81+
}
82+
83+
impl From<ExitCode> for String {
84+
fn from(value: ExitCode) -> Self {
85+
value.to_string()
86+
}
87+
}
88+
89+
impl From<i32> for ExitCode {
90+
fn from(value: i32) -> Self {
91+
Self(value)
92+
}
93+
}
94+
95+
impl From<ExitCode> for i32 {
96+
fn from(value: ExitCode) -> Self {
97+
value.0
98+
}
99+
}
100+
101+
impl PartialEq for ExitCode {
102+
fn eq(&self, other: &Self) -> bool {
103+
self.0.eq(&other.0)
104+
}
105+
}
106+
107+
impl PartialEq<i32> for ExitCode {
108+
fn eq(&self, other: &i32) -> bool {
109+
self.0.eq(other)
110+
}
111+
}
112+
113+
impl PartialEq<ExitCode> for i32 {
114+
fn eq(&self, other: &ExitCode) -> bool {
115+
self.eq(&other.0)
116+
}
117+
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
use std::{collections::HashMap, ops::{Deref, DerefMut}};
5+
6+
use rust_i18n::t;
7+
use schemars::{JsonSchema, json_schema};
8+
use serde::{Deserialize, Serialize};
9+
10+
use crate::{schemas::dsc_repo::DscRepoSchema, types::ExitCode};
11+
12+
/// Defines a map of exit codes to their semantic meaning for operation commands.
13+
///
14+
/// DSC resources and extensions may define any number of operation commands like `get` or
15+
/// `secret`. DSC always considers commands that exit with code `0` to be successful operations and
16+
/// commands that exit with any nonzero code to have failed.
17+
///
18+
/// Resource and extension authors can provide more useful information to users by defining an
19+
/// [`ExitCodesMap`]. When a resource or extension defines the `exitCodes` field in its manifest,
20+
/// DSC surfaces the associated string as part of the error message.
21+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, DscRepoSchema)]
22+
#[dsc_repo_schema(base_name = "exitCodes", folder_path = "definitions")]
23+
pub struct ExitCodesMap(HashMap<ExitCode, String>);
24+
25+
impl ExitCodesMap {
26+
/// Defines the regular expression for validating a string as an exit code.
27+
///
28+
/// The string must consist only of ASCII digits (`[0-9]`) with an optional leading hyphen
29+
/// (`-`). If the string can't be parsed as an [`i32`], the value is invalid.
30+
///
31+
/// This value is only used in the JSON Schema for validating the property names for the map
32+
/// of exit codes to their descriptions. For JSON and YAML, DSC expects the keys to always be
33+
/// strings but they _must_ map to 32-bit integers.
34+
pub const KEY_VALIDATING_PATTERN: &str = r"^-?[0-9]+$";
35+
36+
/// Creates a new instance of [`ExitCodesMap`] with the default capacity.
37+
pub fn new() -> Self {
38+
Self(HashMap::new())
39+
}
40+
41+
/// Creates a new instance of [`ExitCodesMap`] with the given capacity.
42+
pub fn with_capacity(capacity: usize) -> Self {
43+
Self(HashMap::with_capacity(capacity))
44+
}
45+
46+
/// Looks up an [`ExitCode`] in the map and returns a reference to its description, if the map
47+
/// contains the given exit code.
48+
pub fn get_code(&self, code: i32) -> Option<&String> {
49+
self.0.get(&ExitCode::new(code))
50+
}
51+
52+
/// Looks up an [`ExitCode`] in the map and returns its description, if the map contains the
53+
/// given exit code, or the default description.
54+
///
55+
/// The default description is retrieved from the `default()` map:
56+
///
57+
/// - Exit code `0` returns the description for `0` in the default map.
58+
/// - All other exit codes return the description for `1` in the default map.
59+
pub fn get_code_or_default(&self, code: i32) -> String {
60+
match self.0.get(&ExitCode::new(code)) {
61+
Some(description) => description.clone(),
62+
None => match code {
63+
0 => Self::default().get_code(0).expect("default always defines exit code 0").clone(),
64+
_ => Self::default().get_code(1).expect("default always defines exit code 1").clone(),
65+
}
66+
}
67+
}
68+
69+
/// Indicates whether the [`ExitCodesMap`] is identical to the default map.
70+
pub fn is_default(&self) -> bool {
71+
self == &Self::default()
72+
}
73+
74+
/// Indicates whether the [`ExitCodesMap`] is empty or identical to the default map.
75+
///
76+
/// Use this method with the `skip_serializing_if` attribute for serde to avoid serializing
77+
/// empty and default maps.
78+
pub fn is_empty_or_default(&self) -> bool {
79+
self.is_empty() || self.is_default()
80+
}
81+
}
82+
83+
impl Default for ExitCodesMap {
84+
fn default() -> Self {
85+
let mut map: HashMap<ExitCode, String> = HashMap::with_capacity(2);
86+
map.insert(ExitCode::new(0), t!("types.exit_codes_map.successText").into());
87+
map.insert(ExitCode::new(1), t!("types.exit_codes_map.failureText").into());
88+
89+
Self(map)
90+
}
91+
}
92+
93+
impl JsonSchema for ExitCodesMap {
94+
fn schema_name() -> std::borrow::Cow<'static, str> {
95+
Self::default_schema_id_uri().into()
96+
}
97+
98+
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
99+
json_schema!({
100+
"$schema": "https://json-schema.org/draft/2020-12/schema",
101+
"title": t!("schemas.definitions.exitCodes.title"),
102+
"description": t!("schemas.definitions.exitCodes.description"),
103+
"markdownDescription": t!("schemas.definitions.exitCodes.markdownDescription"),
104+
"type": "object",
105+
"minProperties": 1,
106+
"propertyNames": {
107+
"pattern": Self::KEY_VALIDATING_PATTERN,
108+
"patternErrorMessage": t!("schemas.definitions.exitCodes.invalidKeyErrorMessage")
109+
},
110+
"patternProperties": {
111+
Self::KEY_VALIDATING_PATTERN: {
112+
"type": "string"
113+
}
114+
},
115+
"unevaluatedProperties": false,
116+
"default": Self::default(),
117+
"examples": [{
118+
"0": "Success",
119+
"1": "Invalid parameter",
120+
"2": "Invalid input",
121+
"3": "Registry error",
122+
"4": "JSON serialization failed"
123+
}]
124+
})
125+
}
126+
}
127+
128+
impl AsRef<ExitCodesMap> for ExitCodesMap {
129+
fn as_ref(&self) -> &ExitCodesMap {
130+
&self
131+
}
132+
}
133+
134+
impl Deref for ExitCodesMap {
135+
type Target = HashMap<ExitCode, String>;
136+
fn deref(&self) -> &Self::Target {
137+
&self.0
138+
}
139+
}
140+
141+
impl DerefMut for ExitCodesMap {
142+
fn deref_mut(&mut self) -> &mut Self::Target {
143+
&mut self.0
144+
}
145+
}

lib/dsc-lib/src/types/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
mod exit_code;
5+
pub use exit_code::ExitCode;
6+
mod exit_codes_map;
7+
pub use exit_codes_map::ExitCodesMap;
48
mod fully_qualified_type_name;
59
pub use fully_qualified_type_name::FullyQualifiedTypeName;
610
mod resource_version;

0 commit comments

Comments
 (0)