Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Added support for `[ValidatePattern()]` attributes on DSC properties, emitting the regex as a `pattern` keyword in the generated JSON schema.
- Added `-AllowNonEcmaPattern` switch to `New-DscAdaptedResourceManifest` to force-emit patterns containing .NET-specific regex constructs that are not ECMA 262 compatible.
- Added `-ModuleManifestPath` to `New-DscAdaptedResourceManifest` to take the module name, version, author, description and manifest path from a built module manifest while the resource classes are parsed from the file given in `-Path`, so manifests can be generated from resource source files.
- Added support for fully qualified type names such as `System.Boolean` and `System.Int32`, for `Nullable[T]`, for generic collections and for additional CLR types (`Guid`, `TimeSpan`, `SecureString`, unsigned integers, ...) in the generated JSON schema. These previously fell back to `string`.
- Added support for class types defined in the same file. They are emitted once under `$defs` in the embedded schema and referenced with `$ref`, including arrays of a class and classes nested in other classes. A class without `[DscProperty()]` members is described by its public instance properties.
- Added a shared `PSCredential` definition under `$defs` for credential properties, with `username` and `password` as the PowerShell adapter expects.
- Added property descriptions from `[System.ComponentModel.Description()]` attributes when the class comment-based help has no entry for the property. The missing-help warnings are only written when neither source describes a property.
- Added `ConvertTo-DscPropertyOverrideFromConfig` as a public command so the `Create_DscAdaptedResourceManifests` and `Create_DscResourceManifestsList` build tasks can apply `PropertyOverrides` from `build.yaml`.

### Changed

- The default `$schema` of generated adapted resource manifests is now `https://aka.ms/dsc/schemas/v3/bundled/resource/adapted/manifest.json`. DSC 3.3 reports the previous URI as deprecated.
- The `export` capability is only emitted for a `static Export()` method, because the PowerShell adapter invokes `Export` on the type rather than on an instance.
- A four-part `ModuleVersion` is emitted as `Major.Minor.Build` because the adapted resource manifest requires a semantic version.
- `New-DscPropertyOverride -JsonSchema` and `DscPropertyOverride.JsonSchema` accept any `IDictionary`, including ordered hashtables.
- `ToJson()` on `DscAdaptedResourceManifest` and `DscResourceManifestList` serializes with a depth of 20 to allow nested definitions and overrides.
- The build tasks write the manifest files as UTF-8 without a byte order mark on every PowerShell edition.
- Added `.OUTPUTS` to the comment-based help of every private function.

### Fixed

- Fixed build task import so module aliases are correctly exported when the module is loaded.
- Fixed `[ValidateSet()]` attributes on `[string]` DSC properties now being correctly emitted as `enum` in the generated JSON schema.
- Fixed `[ValidateSet()]` attributes on array properties now being emitted under `items` instead of as a top-level `enum`.
- Fixed `[ValidateSet()]` attributes on numeric and boolean properties now keeping the mapped JSON type with the values converted (`"enum": [0, 1]` for an `[int]`) instead of turning the property into a string enum.
- Fixed the Windows PowerShell test job by moving the Pester settings in `build.yaml` to the advanced configuration with `CodeCoverage.UseBreakpoints: true`. The profiler-based tracer that Pester 6 uses by default fails with `Index was out of range` as soon as a PowerShell class is instantiated under Windows PowerShell 5.1; breakpoint-based coverage works on every edition.
- Fixed `UTF8BOM` issue on new script.
- Fixed tasks not being updated in the manifest.
- Added `Configuration` to `RequiredModules.psd1` for latest `ModuleBuilder` version.
Expand Down
50 changes: 30 additions & 20 deletions build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,32 +77,42 @@ BuildWorkflow:
####################################################

Pester:
OutputFormat: NUnitXML
# Pester advanced configuration. A key that is not set uses the Sampler pipeline default.
Configuration:
# If no path is defined the default is to use all the tests under the project's
# tests folder. Paths can be defined to only run tests in certain folders, to run
# specific test files, or to specify the order tests are run.
Run:
Path:
# - tests/QA/module.tests.ps1
# - tests/QA
# - tests/Unit
# - tests/Integration
Filter:
Tag:
ExcludeTag:
# - helpQuality
# - FunctionalQuality
# - TestQuality
Output:
Verbosity: Detailed
CodeCoverage:
CoveragePercentTarget: 85 # Set to 0 to bypass
# The profiler-based tracer that Pester 6 uses by default fails with 'Index was out of
# range' as soon as a PowerShell class is instantiated under Windows PowerShell 5.1.
# Breakpoint-based coverage works on every edition.
UseBreakpoints: true
#OutputPath: JaCoCo_$OsShortName.xml
#OutputEncoding: ascii
TestResult:
OutputFormat: NUnitXML
# Excludes one or more paths from being used to calculate code coverage.
ExcludeFromCodeCoverage:
- Assets
- tasks

# If no scripts are defined the default is to use all the tests under the project's
# tests folder or source folder (if present). Test script paths can be defined to
# only run tests in certain folders, or run specific test files, or can be use to
# specify the order tests are run.
Script:
# - tests/QA/module.tests.ps1
# - tests/QA
# - tests/Unit
# - tests/Integration
ExcludeTag:
# - helpQuality
# - FunctionalQuality
# - TestQuality
Tag:
CodeCoverageThreshold: 85 # Set to 0 to bypass
#CodeCoverageOutputFile: JaCoCo_$OsShortName.xml
#CodeCoverageOutputFileEncoding: ascii
# Use this if code coverage should be merged from several pipeline test jobs.
# Any existing keys above should be replaced. See also CodeCoverage below.
# CodeCoverageOutputFile is the file that is created for each pipeline test job.
# CodeCoverage.OutputPath is the file that is created for each pipeline test job.
#CodeCoverageOutputFile: JaCoCo_Merge.xml

# Use this to merged code coverage from several pipeline test jobs.
Expand Down
2 changes: 1 addition & 1 deletion source/Classes/002.DscAdaptedResourceManifest.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class DscAdaptedResourceManifest
embedded = $this.ManifestSchema.Embedded
}
}
return $manifest | ConvertTo-Json -Depth 10
return $manifest | ConvertTo-Json -Depth 20
}

[hashtable] ToHashtable()
Expand Down
2 changes: 1 addition & 1 deletion source/Classes/003.DscPropertyOverride.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ class DscPropertyOverride
[string] $Name
[string] $Description
[string] $Title
[hashtable] $JsonSchema
[System.Collections.IDictionary] $JsonSchema
[string[]] $RemoveKeys
[object] $Required

Expand Down
2 changes: 1 addition & 1 deletion source/Classes/004.DscResourceManifestList.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ class DscResourceManifestList
$result['extensions'] = @($this.Extensions)
}

return $result | ConvertTo-Json -Depth 15
return $result | ConvertTo-Json -Depth 20
}
}
124 changes: 112 additions & 12 deletions source/Private/Add-AstProperty.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,41 @@
first so that derived class properties override them when the list is
consumed.

For every property the element type is resolved by stripping an array
suffix and unwrapping Nullable[T]. When that element type is an enum
defined in the same file, or a .NET enum that can be reflected, the enum
member names are returned as EnumValues. When it is a class defined in
the same file, its name is returned as ComplexTypeName so the schema can
reference a shared definition for it.

.PARAMETER AllTypeDefinitions
All type definition AST nodes discovered in the script. Used to resolve
base class types and enum types defined in the same file.
base class types, enum types and complex class types defined in the same
file.

.PARAMETER TypeAst
The type definition AST to collect properties from.

.PARAMETER Properties
The list to which property hashtables are added. Each hashtable contains
the property Name, TypeName, IsKey, IsMandatory, IsNotConfigurable and EnumValues.
the property Name, TypeName, IsArray, IsKey, IsMandatory,
IsNotConfigurable, EnumValues, PatternValue, ComplexTypeName and
Description.

.PARAMETER AllProperties
Collect every public instance property instead of only those decorated
with [DscProperty()]. Used for complex classes that describe nested
values and therefore carry no DSC attributes.

.EXAMPLE
$properties = [System.Collections.Generic.List[hashtable]]::new()
Add-AstProperty -AllTypeDefinitions $allTypes -TypeAst $typeAst -Properties $properties

Collects all [DscProperty()] decorated properties from $typeAst into $properties.

.OUTPUTS
Returns no object. The property hashtables are added to the list object
passed in the Properties parameter.
#>
function Add-AstProperty
{
Expand All @@ -42,22 +61,33 @@ function Add-AstProperty
[Parameter(Mandatory = $true)]
[AllowEmptyCollection()]
[System.Collections.Generic.List[hashtable]]
$Properties
$Properties,

[Parameter()]
[System.Management.Automation.SwitchParameter]
$AllProperties
)

$descriptionAttributeNames = @(
'Description',
'DescriptionAttribute',
'System.ComponentModel.Description',
'System.ComponentModel.DescriptionAttribute'
)

foreach ($typeConstraint in $TypeAst.BaseTypes)
{
$baseType = $AllTypeDefinitions | Where-Object { $_.Name -eq $typeConstraint.TypeName.Name }
if ($baseType)
{
Add-AstProperty -AllTypeDefinitions $AllTypeDefinitions -TypeAst $baseType -Properties $Properties
Add-AstProperty -AllTypeDefinitions $AllTypeDefinitions -TypeAst $baseType -Properties $Properties -AllProperties:$AllProperties
}
}

foreach ($member in $TypeAst.Members)
{
$propertyAst = $member -as [System.Management.Automation.Language.PropertyMemberAst]
if (($null -eq $propertyAst) -or ($propertyAst.IsStatic))
if (($null -eq $propertyAst) -or ($propertyAst.IsStatic) -or ($propertyAst.IsHidden))
{
continue
}
Expand All @@ -68,6 +98,7 @@ function Add-AstProperty
$isNotConfigurable = $false
$validateSetValues = $null
$validatePatternValue = $null
$description = $null
foreach ($attr in $propertyAst.Attributes)
{
if ($attr.TypeName.Name -eq 'DscProperty')
Expand All @@ -93,44 +124,113 @@ function Add-AstProperty
{
$validatePatternValue = $attr.PositionalArguments[0].Value
}

if ($attr.TypeName.Name -in $descriptionAttributeNames -and $attr.PositionalArguments.Count -gt 0)
{
$argument = $attr.PositionalArguments[0]
if ($argument -is [System.Management.Automation.Language.StringConstantExpressionAst] -or
$argument -is [System.Management.Automation.Language.ExpandableStringExpressionAst])
{
$description = $argument.Value
}
}
}

if (-not $isDscProperty)
if (-not $isDscProperty -and -not $AllProperties)
{
continue
}

$typeName = if ($propertyAst.PropertyType)
$typeName = 'string'
$elementTypeAst = $null
if ($propertyAst.PropertyType)
{
$typeName = $propertyAst.PropertyType.TypeName.Name
$elementTypeAst = $propertyAst.PropertyType.TypeName
}

# Resolve the element type: strip an array suffix, then unwrap Nullable[T].
$isArray = $false
$elementTypeName = $typeName
if ($elementTypeName -match '^(.+)\[\]$')
{
$isArray = $true
$elementTypeName = $Matches[1]
}
elseif ($elementTypeName -match '^(?:System\.Collections\.Generic\.)?(?:I?List|IEnumerable|ICollection|HashSet)\[(.+)\]$')
{
$isArray = $true
$elementTypeName = $Matches[1]
}

if ($elementTypeName -match '^(?:System\.)?Nullable\[(.+)\]$')
{
$propertyAst.PropertyType.TypeName.Name
$elementTypeName = $Matches[1]
}
else

if ($elementTypeAst -is [System.Management.Automation.Language.ArrayTypeName])
{
$elementTypeAst = $elementTypeAst.ElementType
}

if ($elementTypeAst -is [System.Management.Automation.Language.GenericTypeName] -and
$elementTypeAst.GenericArguments.Count -eq 1)
{
'string'
$elementTypeAst = $elementTypeAst.GenericArguments[0]
}

# check if the type is an enum defined in the same file
# check if the type is an enum or a class defined in the same file
$enumValues = $null
$complexTypeName = $null
$enumAst = $AllTypeDefinitions | Where-Object {
$_.Name -eq $typeName -and $_.IsEnum
$_.Name -eq $elementTypeName -and $_.IsEnum
}
$classAst = $AllTypeDefinitions | Where-Object {
$_.Name -eq $elementTypeName -and $_.IsClass
}

if ($enumAst)
{
$enumValues = @($enumAst.Members | ForEach-Object { $_.Name })
}
elseif ($classAst)
{
$complexTypeName = @($classAst)[0].Name
}
elseif ($validateSetValues)
{
$enumValues = $validateSetValues
}
elseif ($null -ne $elementTypeAst)
{
# A .NET enum that is not defined in the file can still be reflected.
$reflectionType = $null
try
{
$reflectionType = $elementTypeAst.GetReflectionType()
}
catch
{
$reflectionType = $null
}

if ($null -ne $reflectionType -and $reflectionType.IsEnum)
{
$enumValues = @([System.Enum]::GetNames($reflectionType))
}
}

$Properties.Add(@{
Name = $propertyAst.Name
TypeName = $typeName
IsArray = $isArray
IsKey = $isKey
IsMandatory = $isMandatory -or $isKey
IsNotConfigurable = $isNotConfigurable
EnumValues = $enumValues
PatternValue = $validatePatternValue
ComplexTypeName = $complexTypeName
Description = $description
})
}
}
Loading