Azure / bicep

Bicep is a declarative language for describing and deploying Azure resources
MIT License
3.17k stars 730 forks source link

User-defined types -How to define default value #9636

Open slavizh opened 1 year ago

slavizh commented 1 year ago

Is your feature request related to a problem? Please describe. We can define the following type:

type test = {
  foo: true
}

But when you use intellisense on the foo property you get: image Instead what I want to get is that foo is boolean and default value is true.

What if I also want to define default value that uses Bicep function like:

type test = {
  foo: subscription().subscriptionId
}

Describe the solution you'd like See above.

jeskew commented 1 year ago

Default values on types came up at a previous team discussion, and the consensus at the time was that modifying a parameter or output based on its declared type wasn't something that should be done lightly, and the rules around which default value to apply and where defaults would be permitted got tricky very quickly.

This is some strawman syntax from that discussion:

type barString 'bar'|'baz' = 'bar'

@sealed()
type myObject {
  @minLength(3)
  foo: string = 'foo'
  bar: barString         // <-- uses default value from type
  baz: barString = 'baz' // <-- uses explicit default
  recur?: myObject // <-- recursive, so blocks a default for `myObject`
} // '= {}' would be a compiler error either:
    // because 'myObject' is directly recursive (if we apply property defaults to the object default), or
    // because required properties 'foo', 'bar', and 'baz' are missing

We looked at a few schema definitions languages that support some form of default value, and found that most schema definition languages either treat default values as purely informational or had some rules that made usage confusing:

As an alternative, a template author can always supply a default when they read a value. #9454 should help with this, as nullable types will be usable with the ?? (coalesce) operator.

slavizh commented 1 year ago

@jeskew Probably I should have explained it better. My main goal is not to define a default value in type so that default value to be used later in the Bicep deployment. I already have a mechanism in Bicep code where we are setting default values for certain properties if they are not defined. My main goal is that when folks use intellisense to build parameters file or use the module to see foo is a boolean and has default value of true. As you see above foo is reported as Type: true. I want to define foo in a way that it shows Type: bool with default value of true. With the example of type barString seems this can be achieved but only for enums. I have tried this:

type defaultTrue = bool

type test = {
  foo: defaultTrue = true
}

but it gives error on the equal sign to true but I guess this is something you are planning to implement. That looks ok to me.

jeskew commented 1 year ago

I should clarify that the strawman syntax from my previous comment is not planned to be implemented, though that could be revisited based on community feedback.

For the use case you describe where you are setting the default value but wish to communicate this to users, could you use a @description trait for that? type foo = true is not what you want; that is a literal type and means that the only accepted value for foo is true (similar to how type fizz = 'buzz' | 'pop' will cause any value other than 'buzz' or 'pop' to be rejected).

slavizh commented 1 year ago

@jeskew yes, description was my workaround but it is always good intellisense to show this separately from the description and also to be visible in code so when you make changes it is clear what needs to be changed. For example one version of the template foo can have default true but in another to be false.

pie-r commented 1 year ago

@jeskew In the example below, is it possible to set a default value to sizeGbType?

type sizeGbType = int

type storageProfileType = {
  @description('Optional. The storage size of the server.')
  sizeGB: sizeGbType
---
other keys
---
}

What I'm trying to achieve, is to move this:

@description('Optional. The storage size of the server.')
param storageSizeGB int = 32

inside a user-data type that contains all the storageProfile fields.

jeskew commented 1 year ago

@pie-r While you can't define a default value for a user-defined type at this time, you can make the property optional and apply a default value when reading it:

type sizeGbType = int

type storageProfileType = {
  @description('Optional. The storage size of the server.')
  sizeGB: int?
  ...
}

param storageProfile storageProfileType

var defaultSizeGB = 32

resource disk 'Microsoft.Compute/disks@2022-07-02' = {
  ...
  properties: {
    diskSizeGB = storageProfile.?sizeGB ?? defaultSizeGB
    ...
  }
}
pie-r commented 1 year ago

In this specific case, I think that writing 4 different statement, instead of a single row with the single scalar is not worth it.

param diskSizeGB int = 32 

But I see use cases where your solution is the proper approach. Thanks for sharing!

pie-r commented 1 year ago

My syntax suggestion to enable default values in the context of user-defined types is to have the ability to set them in the param. I don't think exist a programming language that allows you to put the default in a data type.

That means define a type as:

type storageProfileType = {
  key1: string
  sizeGB: int
  key2: string
}

A param like:

param storageProfile storageProfileType = {
key1: ='xxx'
sizeGB:  = 32 # default, use this value if not overridden from inputparam
key2: ='yyy'
}

And now given an input

storageProfile = {
key1:  'fooo'
}

At build time bicep convert it with:

param storageProfile storageProfileType = {
key1: 'fooo'
sizeGB:   32    --> use default
key2: 'yyy'      --> use default
}

OR given an input

storageProfile = {
key1:  'fooo'
sizeGB: 16
}

At build time bicep convert it with:

param storageProfile storageProfileType = {
key1: 'fooo'
sizeGB:   16  
key2: 'yyy'      --> use default
}
jeskew commented 7 months ago

Porting some syntax suggestions for this from #12661: (original author: @mattias-fjellstrom)

I would like to be able to provide default values to optional fields in a user-defined type parameter.

I see two options, the first is to be able to provide the default value in the type definition:

param myParameter {
  field1: string
  field2: string? = 'default value'
}

The second option is to allow partial default value like the following, the default values should be merged with the values passed as a value for the parameter (with the passed value taking precedence):

param myParameter {
  field1: string
  field2: string?
} = {
  field2: 'default value'
}
jeskew commented 7 months ago

One other option would be to add some form of deepMerge function. To take @mattias-fjellstrom's example above, this might look like:

param myParameter {
  field1: string
  field2: string?
}

var myParameterDefaults = {
  field2: 'default value'
}

var withDefaults = deepMerge(myParameterDefaults, myParameter)
slavizh commented 7 months ago

This one seems the most easy one to use to me:

param myParameter {
  field1: string
  field2: string? = 'default value'
}

Sometimes the default value is something that can only be calculated at deployment time. For example the subscription ID so being able to just type the words: 'current subscription' instead the actual GUID as that one can only known at deployment time is good for us. So basically being relaxed as much as possible syntax. And of course when you use intellisense that default value to be listed along the type and the description with some text like: Default value: current subscription.

jeskew commented 7 months ago

One other option would be to add some form of deepMerge function. To take @mattias-fjellstrom's example above, this might look like:

param myParameter {
  field1: string
  field2: string?
}

var myParameterDefaults = {
  field2: 'default value'
}

var withDefaults = deepMerge(myParameterDefaults, myParameter)

It turns out that the union function will already perform a deep merge, so you can use the following as a workaround today:

param myParameter {
  field1: string
  field2: string?
}

var myParameterDefaults = {
  field2: 'default value'
}

var withDefaults = union(myParameterDefaults, myParameter)
mattias-fjellstrom commented 7 months ago

@jeskew That's a good workaround, I had not thought of trying that 👍🏻

slavizh commented 6 months ago

Be careful with union(). It does not merge arrays within object and null is never merged (if you have default value 'str' and you provide null, the end value will be 'str'.

ChristopherGLewis commented 4 months ago

@pie-r While you can't define a default value for a user-defined type at this time, you can make the property optional and apply a default value when reading it:

type sizeGbType = int

type storageProfileType = {
  @description('Optional. The storage size of the server.')
  sizeGB: int?
  ...
}

param storageProfile storageProfileType

var defaultSizeGB = 32

resource disk 'Microsoft.Compute/disks@2022-07-02' = {
  ...
  properties: {
    diskSizeGB = storageProfile.?sizeGB ?? defaultSizeGB
    ...
  }
}

I'd rather have my type be smarter rather than my code...

This is primarily to match what some resource providers do currently. What happens when we get to import types from resource providers and they have default values?

dharnil commented 1 month ago

Is there any progress of defining defaults?