Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow use of type properties as type expressions #13047

Merged
merged 20 commits into from
Jan 26, 2024
Merged

Conversation

jeskew
Copy link
Contributor

@jeskew jeskew commented Jan 19, 2024

Resolves #12920
Resolves #9229

This PR allows elements or properties of types to be used as standalone type expressions. This feature is compatible with both resource-derived types and compile-time imports, as shown in the following example:

types.bicep

@export()
type myObject = {
  quux: int
  saSku: resource<'Microsoft.Storage/storageAccounts@2022-09-01'>.sku
}

main.bicep

import * as types from 'types.bicep'

type test = {
  baz: types.myObject
}

type test2 = {
  foo: {
    bar: test
  }
}

type test3 = test2.foo.bar.baz.quux  
// ^^ compiles to {"$ref": "#/definitions/_1.myObject/properties/quux"}

type test4 = test2.foo.bar.baz.saSku.name 
// ^^ compiles to:
// {
//   "type": "string",
//   "metadata": {
//     "__bicep_resource_derived_type!": "Microsoft.Storage/storageAccounts@2022-09-01#properties/sku/properties/name"
//   }
// }

Accessing the following element kinds is supported:

  • Properties may be dereferenced via dot notation
  • Tuple items may be dereferenced via array index
  • An object's additional properties type declaration may be dereferenced via a special .* syntax
  • A typed array's element type declaration may be dereferenced via a special [*] syntax

For example:

type anObject = {
  property: int
  *: string
}

type strings = string[]

type tuple = [int, string]

param propertyDeref anObject.property = 10 // type compiles to {"$ref": "#/definitions/anObject/properties/property"}
param additionalPropertiesDeref anObject.* = 'foo' // type compiles to {"$ref": "#/definitions/anObject/additionalProperties"}
param elementDeref strings[*] = 'bar' // type compiles to {"$ref": "#/definitions/strings/items"}
param itemDeref tuple[1] = 'baz' // type compiles to {"$ref": "#/definitions/tuple/prefixItems/1"}
Microsoft Reviewers: Open in CodeFlow

@jeskew jeskew requested a review from a team January 19, 2024 22:58
Copy link
Contributor

github-actions bot commented Jan 19, 2024

Test this change out locally with the following install scripts (Action run 7662066748)

VSCode
  • Mac/Linux
    bash <(curl -Ls https://aka.ms/bicep/nightly-vsix.sh) --run-id 7662066748
  • Windows
    iex "& { $(irm https://aka.ms/bicep/nightly-vsix.ps1) } -RunId 7662066748"
Azure CLI
  • Mac/Linux
    bash <(curl -Ls https://aka.ms/bicep/nightly-cli.sh) --run-id 7662066748
  • Windows
    iex "& { $(irm https://aka.ms/bicep/nightly-cli.ps1) } -RunId 7662066748"

Copy link
Contributor

github-actions bot commented Jan 19, 2024

Test Results

    66 files   -     33      66 suites   - 33   26m 26s ⏱️ - 28m 12s
10 781 tests +   125  10 781 ✅ +   125  0 💤 ±0  0 ❌ ±0 
25 825 runs   - 12 830  25 825 ✅  - 12 830  0 💤 ±0  0 ❌ ±0 

Results for commit 52fd4db. ± Comparison against base commit 4d311c1.

♻️ This comment has been updated with latest results.

Comment on lines +216 to +226
if (!isResourceBodyType)
{
// strict validation on top-level resource properties, as 'custom' top-level properties are not supported by the platform
return TypeSymbolValidationFlags.Default;
flags |= TypeSymbolValidationFlags.WarnOnPropertyTypeMismatch;
}

// in all other places, we should allow some wiggle room so that we don't block compilation if there are any swagger inaccuracies
return TypeSymbolValidationFlags.WarnOnTypeMismatch;
// strict validation on top-level resource properties, as 'custom' top-level properties are not supported by the platform
if (!isResourceBodyType && !isResourceBodyTopLevelPropertyType)
{
// in all other places, we should allow some wiggle room so that we don't block compilation if there are any swagger inaccuracies
flags |= TypeSymbolValidationFlags.WarnOnTypeMismatch;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conceptually, isn't the goal the same (error on top-level properties, warn on everything else)? Why do you need the new flag here?

Copy link
Contributor Author

@jeskew jeskew Jan 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The flags are similar but handle slightly different cases. Consider the properties property on any resource: we know that this must be an object, and since it's part of the ARM RPC contract, we want to emit an error diagnostic if a template supplies a string or array instead of an object, but warn if there's a mismatch within the properties property:

resource res 'My.RP/widgets@2024-12-31' = {
  ...
  properties: 'foo' // <-- should be an error
}

resource res 'My.RP/widgets@2024-12-31' = {
  ...
  properties: {
    stringWithMinLength3: 'ab' // <-- should be a warning (string is too short)
    unknownProperty: 'foo' // <-- should be a warning (unexpected property)
  }
}

We're currently figuring out whether to warn or error for a property based on the WarnOnTypeMismatch flag on the enclosing object. So properties: 'foo' above is an error because the body type for "My.RP/widgets@2024-12-31" resources does not have the WarnOnTypeMismatch flag, while the issues within properties are warnings because the properties property of the resource body type does have that flag. This presented an issue when I wanted to reuse a type that should generate a warning on mismatch (resource<'My.RP/widgets@2024-12-31'>.properties.knownPropertyOfTypeString) inside of an object that will not have the WarnOnTypeMismatch flag:

// mod.bicep
param p resource<'My.RP/widgets@2024-12-31'>.properties.stringWithMinLength3

// main.bicep
module mod 'mod.bicep' = {
  name: 'mod'
  params: {
    p: 'ab'
  }
}

Since we don't want to set the WarnOnTypeMismatch flag on the params property of a module, I needed to update the property validator to look at a flag on the property type rather than on the enclosing object. Because that wouldn't cover unexpected properties, I added a secondary WarnOnPropertyTypeMismatch flag. For the "My.RP/widgets@2024-12-31" resource, the body type gets no flags, each top level property gets a WarnOnPropertyTypeMismatch flag, and everything else gets both WarnOnPropertyTypeMismatch and WarnOnTypeMismatch:

                                                // these are the flags assigned today
resource res 'My.RP/widgets@2024-12-31' = {     // <-- None
  name: ...                                     // <-- WarnOnTypeMismatch
  location: ...                                 // <-- WarnOnTypeMismatch
  properties: {                                 // <-- WarnOnTypeMismatch
    property: ...                               // <-- WarnOnTypeMismatch
  }
}

                                                // these are the flags that would be assigned with this PR
resource res 'My.RP/widgets@2024-12-31' = {     // <-- None
  name: ...                                     // <-- WarnOnPropertyTypeMismatch
  location: ...                                 // <-- WarnOnPropertyTypeMismatch
  properties: {                                 // <-- WarnOnPropertyTypeMismatch
    property: ...                               // <-- WarnOnPropertyTypeMismatch | WarnOnTypeMismatch
  }
}

@@ -13,13 +14,11 @@ namespace Bicep.Core.TypeSystem.Types;
/// </summary>
public interface IUnboundResourceDerivedType
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] The 'unbound' terminology has been confusing me (since we already have a separate unrelated concept of binding), and it seems like it's being used to refer specifically just to resource derived types. Have you considered calling this just IResourceDerivedType?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe IUnresolvedResourceDerivedType is better? At this point, the compiler hasn't yet looked up the type as defined by the provider.

@jeskew jeskew merged commit 7f8aac1 into main Jan 26, 2024
44 checks passed
@jeskew jeskew deleted the jeskew/type-descent branch January 26, 2024 00:10
asilverman pushed a commit that referenced this pull request Jan 26, 2024
Resolves #12920
Resolves #9229

This PR allows elements or properties of types to be used as standalone
type expressions. This feature is compatible with both resource-derived
types and compile-time imports, as shown in the following example:

types.bicep
```bicep
@export()
type myObject = {
  quux: int
  saSku: resource<'Microsoft.Storage/storageAccounts@2022-09-01'>.sku
}
```

main.bicep
```bicep
import * as types from 'types.bicep'

type test = {
  baz: types.myObject
}

type test2 = {
  foo: {
    bar: test
  }
}

type test3 = test2.foo.bar.baz.quux
// ^^ compiles to {"$ref": "#/definitions/_1.myObject/properties/quux"}

type test4 = test2.foo.bar.baz.saSku.name
// ^^ compiles to:
// {
//   "type": "string",
//   "metadata": {
//     "__bicep_resource_derived_type!": "Microsoft.Storage/storageAccounts@2022-09-01#properties/sku/properties/name"
//   }
// }
```

Accessing the following element kinds is supported:
* Properties may be dereferenced via dot notation
* Tuple items may be dereferenced via array index
* An object's additional properties type declaration may be dereferenced
via a special `.*` syntax
* A typed array's element type declaration may be dereferenced via a
special `[*]` syntax

For example:

```bicep
type anObject = {
  property: int
  *: string
}

type strings = string[]

type tuple = [int, string]

param propertyDeref anObject.property = 10 // type compiles to {"$ref": "#/definitions/anObject/properties/property"}
param additionalPropertiesDeref anObject.* = 'foo' // type compiles to {"$ref": "#/definitions/anObject/additionalProperties"}
param elementDeref strings[*] = 'bar' // type compiles to {"$ref": "#/definitions/strings/items"}
param itemDeref tuple[1] = 'baz' // type compiles to {"$ref": "#/definitions/tuple/prefixItems/1"}
```
CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/Azure/bicep/pull/13047)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Error not surfaced when attempting to "dot" into type expression Use resource types for parameter validation
2 participants