Skip to content

Commit 57bf997

Browse files
authored
feat: support custom validation errors for number-typed parameters (#12224)
1 parent 6414b7a commit 57bf997

File tree

7 files changed

+156
-9
lines changed

7 files changed

+156
-9
lines changed

cli/create_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,14 @@ func TestCreateValidateRichParameters(t *testing.T) {
556556
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: ptr.Ref(int32(3)), ValidationMax: ptr.Ref(int32(10))},
557557
}
558558

559+
numberCustomErrorRichParameters := []*proto.RichParameter{
560+
{
561+
Name: numberParameterName, Type: "number", Mutable: true,
562+
ValidationMin: ptr.Ref(int32(3)), ValidationMax: ptr.Ref(int32(10)),
563+
ValidationError: "These are values: {min}, {max}, and {value}.",
564+
},
565+
}
566+
559567
stringRichParameters := []*proto.RichParameter{
560568
{Name: stringParameterName, Type: "string", Mutable: true, ValidationRegex: "^[a-z]+$", ValidationError: "this is error"},
561569
}
@@ -644,6 +652,44 @@ func TestCreateValidateRichParameters(t *testing.T) {
644652
<-doneChan
645653
})
646654

655+
t.Run("ValidateNumber_CustomError", func(t *testing.T) {
656+
t.Parallel()
657+
658+
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
659+
owner := coderdtest.CreateFirstUser(t, client)
660+
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
661+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(numberCustomErrorRichParameters))
662+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
663+
664+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
665+
666+
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name)
667+
clitest.SetupConfig(t, member, root)
668+
doneChan := make(chan struct{})
669+
pty := ptytest.New(t).Attach(inv)
670+
go func() {
671+
defer close(doneChan)
672+
err := inv.Run()
673+
assert.NoError(t, err)
674+
}()
675+
676+
matches := []string{
677+
numberParameterName, "12",
678+
"These are values: 3, 10, and 12.", "",
679+
"Enter a value", "8",
680+
"Confirm create?", "yes",
681+
}
682+
for i := 0; i < len(matches); i += 2 {
683+
match := matches[i]
684+
value := matches[i+1]
685+
pty.ExpectMatch(match)
686+
if value != "" {
687+
pty.WriteLine(value)
688+
}
689+
}
690+
<-doneChan
691+
})
692+
647693
t.Run("ValidateBool", func(t *testing.T) {
648694
t.Parallel()
649695

codersdk/richparameters_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,24 @@ func TestParameterResolver_ValidateResolve_Ephemeral_UseEmptyDefault(t *testing.
376376
require.NoError(t, err)
377377
require.Equal(t, "", v)
378378
}
379+
380+
func TestParameterResolver_ValidateResolve_Number_CustomError(t *testing.T) {
381+
t.Parallel()
382+
uut := codersdk.ParameterResolver{}
383+
p := codersdk.TemplateVersionParameter{
384+
Name: "n",
385+
Type: "number",
386+
Mutable: true,
387+
DefaultValue: "5",
388+
389+
ValidationMin: ptr.Ref(int32(4)),
390+
ValidationMax: ptr.Ref(int32(6)),
391+
ValidationError: "These are values for testing purposes: {min}, {max}, and {value}.",
392+
}
393+
_, err := uut.ValidateResolve(p, &codersdk.WorkspaceBuildParameter{
394+
Name: "n",
395+
Value: "8",
396+
})
397+
require.Error(t, err)
398+
require.Contains(t, err.Error(), "These are values for testing purposes: 4, 6, and 8.")
399+
}

docs/templates/parameters.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,23 @@ data "coder_parameter" "instances" {
249249
}
250250
```
251251

252+
It is possible to override the default `error` message for a `number` parameter,
253+
along with its associated `min` and/or `max` properties. The following message
254+
placeholders are available `{min}`, `{max}`, and `{value}`.
255+
256+
```hcl
257+
data "coder_parameter" "instances" {
258+
name = "Instances"
259+
type = "number"
260+
description = "Number of compute instances"
261+
validation {
262+
min = 1
263+
max = 4
264+
error = "Sorry, we can't provision too many instances - maximum limit: {max}, wanted: {value}."
265+
}
266+
}
267+
```
268+
252269
### String
253270

254271
You can validate a `string` parameter to match a regular expression. The `regex`

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ require (
9696
github.com/coder/flog v1.1.0
9797
github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0
9898
github.com/coder/retry v1.5.1
99-
github.com/coder/terraform-provider-coder v0.17.0
99+
github.com/coder/terraform-provider-coder v0.18.0
100100
github.com/coder/wgtunnel v0.1.13-0.20231127054351-578bfff9b92a
101101
github.com/coreos/go-oidc/v3 v3.9.0
102102
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,8 @@ github.com/coder/ssh v0.0.0-20231128192721-70855dedb788 h1:YoUSJ19E8AtuUFVYBpXuO
204204
github.com/coder/ssh v0.0.0-20231128192721-70855dedb788/go.mod h1:aGQbuCLyhRLMzZF067xc84Lh7JDs1FKwCmF1Crl9dxQ=
205205
github.com/coder/tailscale v1.1.1-0.20240214140224-3788ab894ba1 h1:A7dZHNidAVH6Kxn5D3hTEH+iRO8slnM0aRer6/cxlyE=
206206
github.com/coder/tailscale v1.1.1-0.20240214140224-3788ab894ba1/go.mod h1:L8tPrwSi31RAMEMV8rjb0vYTGs7rXt8rAHbqY/p41j4=
207-
github.com/coder/terraform-provider-coder v0.17.0 h1:qwdLSbh6vPN+QDDvw1WNSYYEFlFwJFwzzP9vrvwr/ks=
208-
github.com/coder/terraform-provider-coder v0.17.0/go.mod h1:pACHRoXSHBGyY696mLeQ1hR/Ag1G2wFk5bw0mT5Zp2g=
207+
github.com/coder/terraform-provider-coder v0.18.0 h1:JWSBsOuzyiCev3C2Aj8Y1dvJkm5JMysIrIylMJtzPAY=
208+
github.com/coder/terraform-provider-coder v0.18.0/go.mod h1:pACHRoXSHBGyY696mLeQ1hR/Ag1G2wFk5bw0mT5Zp2g=
209209
github.com/coder/wgtunnel v0.1.13-0.20231127054351-578bfff9b92a h1:KhR9LUVllMZ+e9lhubZ1HNrtJDgH5YLoTvpKwmrGag4=
210210
github.com/coder/wgtunnel v0.1.13-0.20231127054351-578bfff9b92a/go.mod h1:QzfptVUdEO+XbkzMKx1kw13i9wwpJlfI1RrZ6SNZ0hA=
211211
github.com/coder/wireguard-go v0.0.0-20230807234434-d825b45ccbf5 h1:eDk/42Kj4xN4yfE504LsvcFEo3dWUiCOaBiWJ2uIH2A=

site/src/pages/CreateWorkspacePage/CreateWorkspacePage.test.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import { rest } from "msw";
2525
const nameLabelText = "Workspace Name";
2626
const createWorkspaceText = "Create Workspace";
2727
const validationNumberNotInRangeText = "Value must be between 1 and 3.";
28-
const validationPatternNotMatched = `${MockTemplateVersionParameter3.validation_error} (value does not match the pattern ^[a-z]{3}$)`;
2928

3029
const renderCreateWorkspacePage = () => {
3130
return renderWithAuth(<CreateWorkspacePage />, {
@@ -152,7 +151,36 @@ describe("CreateWorkspacePage", () => {
152151
fireEvent.submit(thirdParameterField);
153152

154153
const validationError = await screen.findByText(
155-
validationPatternNotMatched,
154+
MockTemplateVersionParameter3.validation_error as string,
155+
);
156+
expect(validationError).toBeInTheDocument();
157+
});
158+
159+
it("rich parameter: number validation fails with custom error", async () => {
160+
jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValueOnce([
161+
MockTemplateVersionParameter1,
162+
{
163+
...MockTemplateVersionParameter2,
164+
validation_error: "These are values: {min}, {max}, and {value}.",
165+
validation_monotonic: undefined, // only needs min-max rules
166+
},
167+
]);
168+
169+
renderCreateWorkspacePage();
170+
await waitForLoaderToBeRemoved();
171+
172+
const secondParameterField = await screen.findByLabelText(
173+
MockTemplateVersionParameter2.name,
174+
{ exact: false },
175+
);
176+
expect(secondParameterField).toBeDefined();
177+
fireEvent.change(secondParameterField, {
178+
target: { value: "4" },
179+
});
180+
fireEvent.submit(secondParameterField);
181+
182+
const validationError = await screen.findByText(
183+
"These are values: 1, 3, and 4.",
156184
);
157185
expect(validationError).toBeInTheDocument();
158186
});

site/src/utils/richParameters.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,9 @@ export const useValidationSchemaForRichParameters = (
7979
if (Number(val) < templateParameter.validation_min) {
8080
return ctx.createError({
8181
path: ctx.path,
82-
message: `Value must be greater than ${templateParameter.validation_min}.`,
82+
message:
83+
parameterError(templateParameter, val) ??
84+
`Value must be greater than ${templateParameter.validation_min}.`,
8385
});
8486
}
8587
} else if (
@@ -89,7 +91,9 @@ export const useValidationSchemaForRichParameters = (
8991
if (templateParameter.validation_max < Number(val)) {
9092
return ctx.createError({
9193
path: ctx.path,
92-
message: `Value must be less than ${templateParameter.validation_max}.`,
94+
message:
95+
parameterError(templateParameter, val) ??
96+
`Value must be less than ${templateParameter.validation_max}.`,
9397
});
9498
}
9599
} else if (
@@ -102,7 +106,9 @@ export const useValidationSchemaForRichParameters = (
102106
) {
103107
return ctx.createError({
104108
path: ctx.path,
105-
message: `Value must be between ${templateParameter.validation_min} and ${templateParameter.validation_max}.`,
109+
message:
110+
parameterError(templateParameter, val) ??
111+
`Value must be between ${templateParameter.validation_min} and ${templateParameter.validation_max}.`,
106112
});
107113
}
108114
}
@@ -149,7 +155,7 @@ export const useValidationSchemaForRichParameters = (
149155
if (val && !regex.test(val)) {
150156
return ctx.createError({
151157
path: ctx.path,
152-
message: `${templateParameter.validation_error} (value does not match the pattern ${templateParameter.validation_regex})`,
158+
message: parameterError(templateParameter, val),
153159
});
154160
}
155161
}
@@ -162,3 +168,32 @@ export const useValidationSchemaForRichParameters = (
162168
)
163169
.required();
164170
};
171+
172+
const parameterError = (
173+
parameter: TemplateVersionParameter,
174+
value?: string,
175+
): string | undefined => {
176+
if (!parameter.validation_error || !value) {
177+
return;
178+
}
179+
180+
const r = new Map<string, string>([
181+
[
182+
"{min}",
183+
parameter.validation_min !== undefined
184+
? parameter.validation_min.toString()
185+
: "",
186+
],
187+
[
188+
"{max}",
189+
parameter.validation_max !== undefined
190+
? parameter.validation_max.toString()
191+
: "",
192+
],
193+
["{value}", value],
194+
]);
195+
return parameter.validation_error.replace(
196+
/{min}|{max}|{value}/g,
197+
(match) => r.get(match) || "",
198+
);
199+
};

0 commit comments

Comments
 (0)