Skip to content

Commit be1c13a

Browse files
authored
Merge pull request #34 from githubnext/rig-sampler/14-changelog-categorizer-da7cc4a21e1f6f8a
[rig-sampler] Add s.literal helper for single-value enum constraints
2 parents 98a216e + 3f104c2 commit be1c13a

2 files changed

Lines changed: 50 additions & 0 deletions

File tree

skills/rig/rig.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
* s.object(props,desc?) ObjectSchema; s.optional(inner) marks field optional; s.nullable(inner) accepts inner|null; use for fixed-key shapes
3232
* s.record(valSchema,desc?) RecordSchema keyed by string; use for open-ended key→value maps
3333
* s.enum(...values|values,desc) EnumSchema
34+
* s.literal(value,desc?) EnumSchema with a single value; clearer than s.enum for single-value constraints
3435
* s.unknown unconstrained JSON; call as value or s.unknown("description")
3536
* p`...` PromptBuilder template tag; interpolates PromptIntent|string|PromptBuilder
3637
* p.bash(cmd,opts?) PromptIntent bash execution declaration (not run in-process)
@@ -274,6 +275,18 @@ export const s = {
274275
? { nullable: true, inner: schema, description }
275276
: { nullable: true, inner: schema });
276277
},
278+
/**
279+
* Schema for a single exact literal value. More expressive than `s.enum` when
280+
* only one value is valid. The inferred TypeScript type is the literal itself.
281+
*
282+
* @example
283+
* s.literal("done") // accepts only "done"; infers as "done"
284+
* s.literal(42) // accepts only 42; infers as 42
285+
* s.literal(true, "must be true") // with description
286+
*/
287+
literal<const T extends Json>(value: T, description?: string): EnumSchema<[T]> {
288+
return markAsSchema(description !== undefined ? { enum: [value], description } : { enum: [value] });
289+
},
277290
/** Converts a rig `Schema` to a plain JSON Schema object. */
278291
toJsonSchema,
279292
};

src/rig.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,6 +1022,12 @@ describe("toJsonSchema", () => {
10221022
expect(toJsonSchema(s.enum(["x", "y"], "A choice"))).toEqual({ type: "string", enum: ["x", "y"], description: "A choice" });
10231023
});
10241024

1025+
it("converts literal schemas", () => {
1026+
expect(toJsonSchema(s.literal("done"))).toEqual({ type: "string", enum: ["done"] });
1027+
expect(toJsonSchema(s.literal(42))).toEqual({ enum: [42] });
1028+
expect(toJsonSchema(s.literal(true, "must be true"))).toEqual({ enum: [true], description: "must be true" });
1029+
});
1030+
10251031
it("adds type:string to all-string enums but not mixed enums", () => {
10261032
expect(toJsonSchema(s.enum("low", "medium", "high"))).toEqual({ type: "string", enum: ["low", "medium", "high"] });
10271033
expect(toJsonSchema(s.enum(1, 2, 3))).toEqual({ enum: [1, 2, 3] });
@@ -1180,3 +1186,34 @@ describe("s.null", () => {
11801186
expect(result.ok).toBe(true);
11811187
});
11821188
});
1189+
1190+
describe("s.literal", () => {
1191+
it("serializes a string literal to an all-string enum schema", () => {
1192+
expect(toJsonSchema(s.literal("done"))).toEqual({ type: "string", enum: ["done"] });
1193+
});
1194+
1195+
it("serializes a number literal to an enum schema without type:string", () => {
1196+
expect(toJsonSchema(s.literal(42))).toEqual({ enum: [42] });
1197+
});
1198+
1199+
it("accepts the exact literal value in validation", () => {
1200+
const result = analyzeResponse(JSON.stringify("done"), s.literal("done"), "test", 1);
1201+
expect(result.ok).toBe(true);
1202+
});
1203+
1204+
it("rejects a different value in validation", () => {
1205+
const result = analyzeResponse(JSON.stringify("pending"), s.literal("done"), "test", 1);
1206+
expect(result.ok).toBe(false);
1207+
if (!result.ok) {
1208+
expect(result.error.message).toContain('"done"');
1209+
}
1210+
});
1211+
1212+
it("supports an optional description", () => {
1213+
expect(toJsonSchema(s.literal("active", "must be active"))).toEqual({
1214+
type: "string",
1215+
enum: ["active"],
1216+
description: "must be active",
1217+
});
1218+
});
1219+
});

0 commit comments

Comments
 (0)