Skip to content

Commit 0e974e6

Browse files
authored
Merge pull request #3980 from github/mario-campos/add-optional-to-json
Support `undefined`, `number` in JSON module
2 parents 721b977 + cbf67da commit 0e974e6

4 files changed

Lines changed: 81 additions & 31 deletions

File tree

lib/entry-points.js

Lines changed: 16 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/json/index.test.ts

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ const testSchema = {
1010
requiredKey: json.string,
1111
};
1212

13-
const optionalSchema = {
14-
optionalKey: json.optional(json.string),
13+
const optionalOrNullSchema = {
14+
optionalKey: json.optionalOrNull(json.string),
1515
};
1616

1717
test("validateSchema - required properties are required", async (t) => {
@@ -28,13 +28,36 @@ test("validateSchema - required properties are required", async (t) => {
2828
t.true(json.validateSchema(testSchema, { requiredKey: "foo" }));
2929
});
3030

31-
test("validateSchema - optional properties are optional", async (t) => {
31+
test("validateSchema - optionalOrNullSchema properties are optional or null", async (t) => {
3232
// Optional fields may be absent
33+
t.true(json.validateSchema(optionalOrNullSchema, {}));
34+
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: undefined }));
35+
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: null }));
36+
37+
// But, if present, should have the expected type
38+
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: 0 }));
39+
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: 123 }));
40+
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: false }));
41+
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: true }));
42+
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: [] }));
43+
t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: {} }));
44+
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: "" }));
45+
t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: "foo" }));
46+
});
47+
48+
const optionalSchema = {
49+
optionalKey: json.optional(json.string),
50+
};
51+
52+
test("validateSchema - optional properties are optional", async (t) => {
53+
// Optional fields may be absent or explicitly undefined
3354
t.true(json.validateSchema(optionalSchema, {}));
3455
t.true(json.validateSchema(optionalSchema, { optionalKey: undefined }));
35-
t.true(json.validateSchema(optionalSchema, { optionalKey: null }));
3656

37-
// But, if present, should have the expected type
57+
// But should reject null
58+
t.false(json.validateSchema(optionalSchema, { optionalKey: null }));
59+
60+
// And, if present, should have the expected type
3861
t.false(json.validateSchema(optionalSchema, { optionalKey: 0 }));
3962
t.false(json.validateSchema(optionalSchema, { optionalKey: 123 }));
4063
t.false(json.validateSchema(optionalSchema, { optionalKey: false }));

src/json/index.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ export function isString(value: unknown): value is string {
3030
return typeof value === "string";
3131
}
3232

33+
/** Asserts that `value` is a number. */
34+
export function isNumber(value: unknown): value is number {
35+
return typeof value === "number";
36+
}
37+
3338
/** Asserts that `value` is either a string or undefined. */
3439
export function isStringOrUndefined(
3540
value: unknown,
@@ -55,8 +60,17 @@ export const string = {
5560
required: true,
5661
} as const satisfies Validator<string>;
5762

58-
/** Transforms a validator to be optional. */
59-
export function optional<T>(validator: Validator<T>) {
63+
/** A validator for number fields in schemas. */
64+
export const number = {
65+
validate: isNumber,
66+
required: true,
67+
} as const satisfies Validator<number>;
68+
69+
/**
70+
* Transforms a validator to be optional, accepting `undefined` or `null` for an
71+
* absent value.
72+
*/
73+
export function optionalOrNull<T>(validator: Validator<T>) {
6074
return {
6175
validate: (val: unknown) => {
6276
return val === undefined || val === null || validator.validate(val);
@@ -65,6 +79,19 @@ export function optional<T>(validator: Validator<T>) {
6579
} as const satisfies Validator<T | undefined | null>;
6680
}
6781

82+
/**
83+
* Transforms a validator to be optional, accepting `undefined` for an absent
84+
* value but, unlike `optionalOrNull`, rejecting `null`.
85+
*/
86+
export function optional<T>(validator: Validator<T>) {
87+
return {
88+
validate: (val: unknown): val is T | undefined => {
89+
return val === undefined || validator.validate(val);
90+
},
91+
required: false,
92+
} as const satisfies Validator<T | undefined>;
93+
}
94+
6895
/** Represents an arbitrary object schema. */
6996
export type Schema = Record<string, Validator<any>>;
7097

src/start-proxy/types.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type RawCredential = UnvalidatedObject<Credential>;
1212
/** A schema for credential objects with a username. */
1313
export const usernameSchema = {
1414
/** The username needed to authenticate to the package registry, if any. */
15-
username: json.optional(json.string),
15+
username: json.optionalOrNull(json.string),
1616
} as const satisfies json.Schema;
1717

1818
/** Usernames may be present for both authentication with tokens or passwords. */
@@ -29,7 +29,7 @@ export function hasUsername(config: AuthConfig): config is Username {
2929
/** A schema for credential objects with a username and password. */
3030
export const usernamePasswordSchema = {
3131
/** The password needed to authenticate to the package registry, if any. */
32-
password: json.optional(json.string),
32+
password: json.optionalOrNull(json.string),
3333
...usernameSchema,
3434
} as const satisfies json.Schema;
3535

@@ -52,7 +52,7 @@ export function hasUsernameAndPassword(
5252
/** A schema for credential objects for token-based authentication. */
5353
export const tokenSchema = {
5454
/** The token needed to authenticate to the package registry, if any. */
55-
token: json.optional(json.string),
55+
token: json.optionalOrNull(json.string),
5656
...usernameSchema,
5757
} as const satisfies json.Schema;
5858

@@ -100,7 +100,7 @@ export const awsConfigSchema = {
100100
"role-name": json.string,
101101
domain: json.string,
102102
"domain-owner": json.string,
103-
audience: json.optional(json.string),
103+
audience: json.optionalOrNull(json.string),
104104
} as const satisfies json.Schema;
105105

106106
/** Configuration for AWS OIDC. */
@@ -116,8 +116,8 @@ export function isAWSConfig(
116116
/** A schema for JFrog OIDC configurations. */
117117
export const jfrogConfigSchema = {
118118
"jfrog-oidc-provider-name": json.string,
119-
audience: json.optional(json.string),
120-
"identity-mapping-name": json.optional(json.string),
119+
audience: json.optionalOrNull(json.string),
120+
"identity-mapping-name": json.optionalOrNull(json.string),
121121
} as const satisfies json.Schema;
122122

123123
/** Configuration for JFrog OIDC. */
@@ -150,8 +150,8 @@ export function isCloudsmithConfig(
150150
/** A schema for GCP OIDC configurations. */
151151
export const gcpConfigSchema = {
152152
"workload-identity-provider": json.string,
153-
"service-account": json.optional(json.string),
154-
audience: json.optional(json.string),
153+
"service-account": json.optionalOrNull(json.string),
154+
audience: json.optionalOrNull(json.string),
155155
} as const satisfies json.Schema;
156156

157157
/** Configuration for GCP OIDC. */

0 commit comments

Comments
 (0)