mirror of
https://github.com/go-task/task.git
synced 2026-06-11 09:51:50 +00:00
feat(requires): support variable references in enum constraints (#2678)
This commit is contained in:
@@ -351,6 +351,41 @@ func TestRequires(t *testing.T) {
|
|||||||
),
|
),
|
||||||
WithTask("var-defined-in-task"),
|
WithTask("var-defined-in-task"),
|
||||||
)
|
)
|
||||||
|
NewExecutorTest(t,
|
||||||
|
WithName("enum ref - passes validation"),
|
||||||
|
WithExecutorOptions(
|
||||||
|
task.WithDir("testdata/requires"),
|
||||||
|
),
|
||||||
|
WithTask("validation-var-ref"),
|
||||||
|
WithVar("ENV", "dev"),
|
||||||
|
)
|
||||||
|
NewExecutorTest(t,
|
||||||
|
WithName("enum ref - fails validation"),
|
||||||
|
WithExecutorOptions(
|
||||||
|
task.WithDir("testdata/requires"),
|
||||||
|
),
|
||||||
|
WithTask("validation-var-ref"),
|
||||||
|
WithVar("ENV", "invalid"),
|
||||||
|
WithRunError(),
|
||||||
|
)
|
||||||
|
NewExecutorTest(t,
|
||||||
|
WithName("enum ref - ref to non-list"),
|
||||||
|
WithExecutorOptions(
|
||||||
|
task.WithDir("testdata/requires"),
|
||||||
|
),
|
||||||
|
WithTask("validation-var-ref-invalid"),
|
||||||
|
WithVar("VALUE", "test"),
|
||||||
|
WithRunError(),
|
||||||
|
)
|
||||||
|
NewExecutorTest(t,
|
||||||
|
WithName("enum ref - ref to nonexistent var"),
|
||||||
|
WithExecutorOptions(
|
||||||
|
task.WithDir("testdata/requires"),
|
||||||
|
),
|
||||||
|
WithTask("validation-var-ref-nonexistent"),
|
||||||
|
WithVar("ENV", "dev"),
|
||||||
|
WithRunError(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: mock fs
|
// TODO: mock fs
|
||||||
|
|||||||
@@ -247,15 +247,17 @@ func printTaskRequires(l *logger.Logger, t *ast.Task) {
|
|||||||
l.Outf(logger.Default, " vars:\n")
|
l.Outf(logger.Default, " vars:\n")
|
||||||
|
|
||||||
for _, v := range t.Requires.Vars {
|
for _, v := range t.Requires.Vars {
|
||||||
// If the variable has enum constraints, format accordingly
|
if v.Enum != nil && len(v.Enum.Value) > 0 {
|
||||||
if len(v.Enum) > 0 {
|
|
||||||
l.Outf(logger.Yellow, " - %s:\n", v.Name)
|
l.Outf(logger.Yellow, " - %s:\n", v.Name)
|
||||||
l.Outf(logger.Yellow, " enum:\n")
|
l.Outf(logger.Yellow, " enum:\n")
|
||||||
for _, enumValue := range v.Enum {
|
for _, enumValue := range v.Enum.Value {
|
||||||
l.Outf(logger.Yellow, " - %s\n", enumValue)
|
l.Outf(logger.Yellow, " - %s\n", enumValue)
|
||||||
}
|
}
|
||||||
|
} else if v.Enum != nil && v.Enum.Ref != "" {
|
||||||
|
l.Outf(logger.Yellow, " - %s:\n", v.Name)
|
||||||
|
l.Outf(logger.Yellow, " enum:\n")
|
||||||
|
l.Outf(logger.Yellow, " ref: %s\n", v.Enum.Ref)
|
||||||
} else {
|
} else {
|
||||||
// Simple required variable
|
|
||||||
l.Outf(logger.Yellow, " - %s\n", v.Name)
|
l.Outf(logger.Yellow, " - %s\n", v.Name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
18
requires.go
18
requires.go
@@ -81,7 +81,7 @@ func (e *Executor) promptDepsVars(calls []*Call) error {
|
|||||||
e.promptedVars = ast.NewVars()
|
e.promptedVars = ast.NewVars()
|
||||||
|
|
||||||
for _, v := range varsMap {
|
for _, v := range varsMap {
|
||||||
value, err := prompter.Prompt(v.Name, v.Enum)
|
value, err := prompter.Prompt(v.Name, getEnumValues(v.Enum))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, input.ErrCancelled) {
|
if errors.Is(err, input.ErrCancelled) {
|
||||||
return &errors.TaskCancelledByUserError{TaskName: "interactive prompt"}
|
return &errors.TaskCancelledByUserError{TaskName: "interactive prompt"}
|
||||||
@@ -120,7 +120,7 @@ func (e *Executor) promptTaskVars(t *ast.Task, call *Call) (bool, error) {
|
|||||||
prompter := e.newPrompter()
|
prompter := e.newPrompter()
|
||||||
|
|
||||||
for _, v := range missing {
|
for _, v := range missing {
|
||||||
value, err := prompter.Prompt(v.Name, v.Enum)
|
value, err := prompter.Prompt(v.Name, getEnumValues(v.Enum))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, input.ErrCancelled) {
|
if errors.Is(err, input.ErrCancelled) {
|
||||||
return false, &errors.TaskCancelledByUserError{TaskName: t.Name()}
|
return false, &errors.TaskCancelledByUserError{TaskName: t.Name()}
|
||||||
@@ -168,7 +168,7 @@ func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
|
|||||||
for i, v := range missing {
|
for i, v := range missing {
|
||||||
missingVars[i] = errors.MissingVar{
|
missingVars[i] = errors.MissingVar{
|
||||||
Name: v.Name,
|
Name: v.Name,
|
||||||
AllowedValues: v.Enum,
|
AllowedValues: getEnumValues(v.Enum),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,11 +187,12 @@ func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
|
|||||||
for _, requiredVar := range t.Requires.Vars {
|
for _, requiredVar := range t.Requires.Vars {
|
||||||
varValue, _ := t.Vars.Get(requiredVar.Name)
|
varValue, _ := t.Vars.Get(requiredVar.Name)
|
||||||
|
|
||||||
|
enumValues := getEnumValues(requiredVar.Enum)
|
||||||
value, isString := varValue.Value.(string)
|
value, isString := varValue.Value.(string)
|
||||||
if isString && requiredVar.Enum != nil && !slices.Contains(requiredVar.Enum, value) {
|
if isString && len(enumValues) > 0 && !slices.Contains(enumValues, value) {
|
||||||
notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{
|
notAllowedValuesVars = append(notAllowedValuesVars, errors.NotAllowedVar{
|
||||||
Value: value,
|
Value: value,
|
||||||
Enum: requiredVar.Enum,
|
Enum: enumValues,
|
||||||
Name: requiredVar.Name,
|
Name: requiredVar.Name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -206,3 +207,10 @@ func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getEnumValues(e *ast.Enum) []string {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return e.Value
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,9 +22,56 @@ func (r *Requires) DeepCopy() *Requires {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enum represents an enum constraint for a required variable.
|
||||||
|
// It can either be a static list of values or a reference to another variable.
|
||||||
|
type Enum struct {
|
||||||
|
Ref string
|
||||||
|
Value []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Enum) DeepCopy() *Enum {
|
||||||
|
if e == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &Enum{
|
||||||
|
Ref: e.Ref,
|
||||||
|
Value: deepcopy.Slice(e.Value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalYAML implements yaml.Unmarshaler interface.
|
||||||
|
func (e *Enum) UnmarshalYAML(node *yaml.Node) error {
|
||||||
|
switch node.Kind {
|
||||||
|
case yaml.SequenceNode:
|
||||||
|
// Static list of values: enum: ["a", "b"]
|
||||||
|
var values []string
|
||||||
|
if err := node.Decode(&values); err != nil {
|
||||||
|
return errors.NewTaskfileDecodeError(err, node)
|
||||||
|
}
|
||||||
|
e.Value = values
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case yaml.MappingNode:
|
||||||
|
// Reference to another variable: enum: { ref: .VAR }
|
||||||
|
var refStruct struct {
|
||||||
|
Ref string
|
||||||
|
}
|
||||||
|
if err := node.Decode(&refStruct); err != nil {
|
||||||
|
return errors.NewTaskfileDecodeError(err, node)
|
||||||
|
}
|
||||||
|
if refStruct.Ref == "" {
|
||||||
|
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("enum")
|
||||||
|
}
|
||||||
|
e.Ref = refStruct.Ref
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("enum")
|
||||||
|
}
|
||||||
|
|
||||||
type VarsWithValidation struct {
|
type VarsWithValidation struct {
|
||||||
Name string
|
Name string
|
||||||
Enum []string
|
Enum *Enum
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
|
func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
|
||||||
@@ -33,7 +80,7 @@ func (v *VarsWithValidation) DeepCopy() *VarsWithValidation {
|
|||||||
}
|
}
|
||||||
return &VarsWithValidation{
|
return &VarsWithValidation{
|
||||||
Name: v.Name,
|
Name: v.Name,
|
||||||
Enum: v.Enum,
|
Enum: v.Enum.DeepCopy(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +100,7 @@ func (v *VarsWithValidation) UnmarshalYAML(node *yaml.Node) error {
|
|||||||
case yaml.MappingNode:
|
case yaml.MappingNode:
|
||||||
var vv struct {
|
var vv struct {
|
||||||
Name string
|
Name string
|
||||||
Enum []string
|
Enum *Enum
|
||||||
}
|
}
|
||||||
if err := node.Decode(&vv); err != nil {
|
if err := node.Decode(&vv); err != nil {
|
||||||
return errors.NewTaskfileDecodeError(err, node)
|
return errors.NewTaskfileDecodeError(err, node)
|
||||||
|
|||||||
28
testdata/requires/Taskfile.yml
vendored
28
testdata/requires/Taskfile.yml
vendored
@@ -1,5 +1,9 @@
|
|||||||
version: '3'
|
version: '3'
|
||||||
|
|
||||||
|
vars:
|
||||||
|
ALLOWED_ENVS: ["dev", "staging", "prod"]
|
||||||
|
NOT_A_LIST: "this is a string"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
default:
|
default:
|
||||||
- task: missing-var
|
- task: missing-var
|
||||||
@@ -41,3 +45,27 @@ tasks:
|
|||||||
{{range .MY_VAR | splitList " " }}
|
{{range .MY_VAR | splitList " " }}
|
||||||
echo {{.}}
|
echo {{.}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
|
validation-var-ref:
|
||||||
|
requires:
|
||||||
|
vars:
|
||||||
|
- name: ENV
|
||||||
|
enum:
|
||||||
|
ref: .ALLOWED_ENVS
|
||||||
|
cmd: echo "{{.ENV}}"
|
||||||
|
|
||||||
|
validation-var-ref-invalid:
|
||||||
|
requires:
|
||||||
|
vars:
|
||||||
|
- name: VALUE
|
||||||
|
enum:
|
||||||
|
ref: .NOT_A_LIST
|
||||||
|
cmd: echo "{{.VALUE}}"
|
||||||
|
|
||||||
|
validation-var-ref-nonexistent:
|
||||||
|
requires:
|
||||||
|
vars:
|
||||||
|
- name: ENV
|
||||||
|
enum:
|
||||||
|
ref: .NONEXISTENT_VAR
|
||||||
|
cmd: echo "{{.ENV}}"
|
||||||
|
|||||||
2
testdata/requires/testdata/TestRequires-enum_ref_-_fails_validation-err-run.golden
vendored
Normal file
2
testdata/requires/testdata/TestRequires-enum_ref_-_fails_validation-err-run.golden
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
task: Task "validation-var-ref" cancelled because it is missing required variables:
|
||||||
|
- ENV has an invalid value : 'invalid' (allowed values : [dev staging prod])
|
||||||
0
testdata/requires/testdata/TestRequires-enum_ref_-_fails_validation.golden
vendored
Normal file
0
testdata/requires/testdata/TestRequires-enum_ref_-_fails_validation.golden
vendored
Normal file
2
testdata/requires/testdata/TestRequires-enum_ref_-_passes_validation.golden
vendored
Normal file
2
testdata/requires/testdata/TestRequires-enum_ref_-_passes_validation.golden
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
task: [validation-var-ref] echo "dev"
|
||||||
|
dev
|
||||||
1
testdata/requires/testdata/TestRequires-enum_ref_-_ref_to_non-list-err-run.golden
vendored
Normal file
1
testdata/requires/testdata/TestRequires-enum_ref_-_ref_to_non-list-err-run.golden
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
enum reference ".NOT_A_LIST" must resolve to a list
|
||||||
0
testdata/requires/testdata/TestRequires-enum_ref_-_ref_to_non-list.golden
vendored
Normal file
0
testdata/requires/testdata/TestRequires-enum_ref_-_ref_to_non-list.golden
vendored
Normal file
1
testdata/requires/testdata/TestRequires-enum_ref_-_ref_to_nonexistent_var-err-run.golden
vendored
Normal file
1
testdata/requires/testdata/TestRequires-enum_ref_-_ref_to_nonexistent_var-err-run.golden
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
enum reference ".NONEXISTENT_VAR" must resolve to a list
|
||||||
0
testdata/requires/testdata/TestRequires-enum_ref_-_ref_to_nonexistent_var.golden
vendored
Normal file
0
testdata/requires/testdata/TestRequires-enum_ref_-_ref_to_nonexistent_var.golden
vendored
Normal file
42
variables.go
42
variables.go
@@ -99,6 +99,17 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
|
|||||||
}
|
}
|
||||||
|
|
||||||
cache := &templater.Cache{Vars: vars}
|
cache := &templater.Cache{Vars: vars}
|
||||||
|
|
||||||
|
// Resolve enum refs only when dynamic variables have been evaluated,
|
||||||
|
// since enum refs may depend on shell-derived variables (e.g. fromJson)
|
||||||
|
requires := origTask.Requires
|
||||||
|
if evaluateShVars {
|
||||||
|
requires = origTask.Requires.DeepCopy()
|
||||||
|
if err := resolveEnumRefs(requires, cache); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
new := ast.Task{
|
new := ast.Task{
|
||||||
Task: origTask.Task,
|
Task: origTask.Task,
|
||||||
Label: templater.Replace(origTask.Label, cache),
|
Label: templater.Replace(origTask.Label, cache),
|
||||||
@@ -126,7 +137,7 @@ func (e *Executor) compiledTask(call *Call, evaluateShVars bool) (*ast.Task, err
|
|||||||
Platforms: origTask.Platforms,
|
Platforms: origTask.Platforms,
|
||||||
If: templater.Replace(origTask.If, cache),
|
If: templater.Replace(origTask.If, cache),
|
||||||
Location: origTask.Location,
|
Location: origTask.Location,
|
||||||
Requires: origTask.Requires,
|
Requires: requires,
|
||||||
Watch: origTask.Watch,
|
Watch: origTask.Watch,
|
||||||
Failfast: origTask.Failfast,
|
Failfast: origTask.Failfast,
|
||||||
Namespace: origTask.Namespace,
|
Namespace: origTask.Namespace,
|
||||||
@@ -432,6 +443,35 @@ func resolveMatrixRefs(matrix *ast.Matrix, cache *templater.Cache) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveEnumRefs(requires *ast.Requires, cache *templater.Cache) error {
|
||||||
|
if requires == nil || len(requires.Vars) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, v := range requires.Vars {
|
||||||
|
if v.Enum == nil || v.Enum.Ref == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resolved := templater.ResolveRef(v.Enum.Ref, cache)
|
||||||
|
if cache.Err() != nil {
|
||||||
|
return cache.Err()
|
||||||
|
}
|
||||||
|
arr, ok := resolved.([]any)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("enum reference %q must resolve to a list", v.Enum.Ref)
|
||||||
|
}
|
||||||
|
strValues := make([]string, 0, len(arr))
|
||||||
|
for _, item := range arr {
|
||||||
|
s, ok := item.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("enum reference %q must contain only strings", v.Enum.Ref)
|
||||||
|
}
|
||||||
|
strValues = append(strValues, s)
|
||||||
|
}
|
||||||
|
v.Enum.Value = strValues
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// product generates the cartesian product of the input map of slices.
|
// product generates the cartesian product of the input map of slices.
|
||||||
func product(matrix *ast.Matrix) []map[string]any {
|
func product(matrix *ast.Matrix) []map[string]any {
|
||||||
if matrix.Len() == 0 {
|
if matrix.Len() == 0 {
|
||||||
|
|||||||
@@ -1233,6 +1233,71 @@ This is supported only for string variables.
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
### Using variable references for enum values
|
||||||
|
|
||||||
|
Instead of hardcoding enum values, you can reference a variable containing the
|
||||||
|
allowed values. This is useful when you want to define allowed values once and
|
||||||
|
reuse them, or when the values are computed dynamically.
|
||||||
|
|
||||||
|
Use the `ref` key to reference a variable:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
vars:
|
||||||
|
ALLOWED_ENVS: [dev, staging, prod]
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
deploy:
|
||||||
|
requires:
|
||||||
|
vars:
|
||||||
|
- name: ENV
|
||||||
|
enum:
|
||||||
|
ref: .ALLOWED_ENVS
|
||||||
|
cmds:
|
||||||
|
- echo "Deploying to {{.ENV}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use template expressions to transform the value:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
vars:
|
||||||
|
CONFIG:
|
||||||
|
sh: cat config.json
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
deploy:
|
||||||
|
requires:
|
||||||
|
vars:
|
||||||
|
- name: ENV
|
||||||
|
enum:
|
||||||
|
ref: ( .CONFIG | fromJson ).allowed_environments
|
||||||
|
cmds:
|
||||||
|
- echo "Deploying to {{.ENV}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or generate values dynamically from a shell command:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
vars:
|
||||||
|
AVAILABLE_SERVICES:
|
||||||
|
sh: ls services/
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
deploy:
|
||||||
|
requires:
|
||||||
|
vars:
|
||||||
|
- name: SERVICE
|
||||||
|
enum:
|
||||||
|
ref: .AVAILABLE_SERVICES | splitLines | compact
|
||||||
|
cmds:
|
||||||
|
- echo "Deploying {{.SERVICE}}"
|
||||||
|
```
|
||||||
|
|
||||||
### Prompting for missing variables interactively
|
### Prompting for missing variables interactively
|
||||||
|
|
||||||
If you want Task to prompt users for missing required variables instead of
|
If you want Task to prompt users for missing required variables instead of
|
||||||
|
|||||||
@@ -674,14 +674,12 @@ tasks:
|
|||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
tasks:
|
tasks:
|
||||||
# Simple requirements
|
|
||||||
deploy:
|
deploy:
|
||||||
requires:
|
requires:
|
||||||
vars: [API_KEY, ENVIRONMENT]
|
vars: [API_KEY, ENVIRONMENT]
|
||||||
cmds:
|
cmds:
|
||||||
- ./deploy.sh
|
- ./deploy.sh
|
||||||
|
|
||||||
# Requirements with enum validation
|
|
||||||
advanced-deploy:
|
advanced-deploy:
|
||||||
requires:
|
requires:
|
||||||
vars:
|
vars:
|
||||||
@@ -693,6 +691,17 @@ tasks:
|
|||||||
cmds:
|
cmds:
|
||||||
- echo "Deploying to {{.ENVIRONMENT}} with log level {{.LOG_LEVEL}}"
|
- echo "Deploying to {{.ENVIRONMENT}} with log level {{.LOG_LEVEL}}"
|
||||||
- ./deploy.sh
|
- ./deploy.sh
|
||||||
|
|
||||||
|
|
||||||
|
# Requirements with enum from variable reference
|
||||||
|
reusable-deploy:
|
||||||
|
requires:
|
||||||
|
vars:
|
||||||
|
- name: ENVIRONMENT
|
||||||
|
enum:
|
||||||
|
ref: .ALLOWED_ENVS
|
||||||
|
cmds:
|
||||||
|
- ./deploy.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
See [Prompting for missing variables interactively](/docs/guide#prompting-for-missing-variables-interactively)
|
See [Prompting for missing variables interactively](/docs/guide#prompting-for-missing-variables-interactively)
|
||||||
|
|||||||
@@ -633,7 +633,19 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": { "type": "string" },
|
"name": { "type": "string" },
|
||||||
"enum": { "type": "array", "items": { "type": "string" } }
|
"enum": {
|
||||||
|
"oneOf": [
|
||||||
|
{ "type": "array", "items": { "type": "string" } },
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ref": { "type": "string" }
|
||||||
|
},
|
||||||
|
"required": ["ref"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"required": ["name"],
|
"required": ["name"],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|||||||
Reference in New Issue
Block a user