Problem
Reading a typed answer repeats what the request already said, and a mismatch only fails at runtime:
.choice("dept", Dept.class, c -> ...) // key and enum at the request
...
response.choice("dept", Dept.class).choice(); // repeated at the response
A question can't carry this itself: the key lives in the request map, and the Choice<E> record can't hold Dept.class.
Proposal
A keyed handle, Ask<A>, that pairs a key, a question, and the type of its answer. Declare it once, ask it, and read the answer back through it:
static final Ask<Double> URGENT = Ask.noul("urgent", n -> n
.instructions("Does `email` need a reply today?")
.whenTrue("Explicit deadline or outage"));
static final Ask<ChoiceAnswer<Dept>> DEPT = Ask.choice("dept", Dept.class, c -> c
.instructions("Which team should handle `email`?")
.option(Dept.BILLING, "Invoices, refunds, payment methods")
.option(Dept.SECURITY, o -> o.what("Credential theft").notFor("Legitimate requests")));
static final Ask<ScoreAnswer> SEVERITY = Ask.score("severity", Score.of("How severe?", "Minor", "Major", "Outage"));
TypeSafeResponse response = client.systemOne(Map.of("email", email), URGENT, DEPT, SEVERITY);
// or: client.systemOne(r -> r.state("email", email).ask(URGENT, DEPT).noul("spam", n -> ...), options);
double urgent = response.answer(URGENT);
Dept dept = response.answer(DEPT).choice();
ScoreAnswer severity = response.answer(SEVERITY);
Shape
public final class Ask<A> {
public static Ask<Double> noul(String key, Noul question)
public static Ask<Double> noul(String key, Consumer<Noul.Builder> configure)
public static Ask<ChoiceAnswer<String>> choice(String key, Choice<String> question)
public static Ask<ChoiceAnswer<String>> choice(String key, Consumer<Choice.Builder<String>> configure)
public static <E extends Enum<E>> Ask<ChoiceAnswer<E>> choice(String key, Class<E> labels, Choice<E> question)
public static <E extends Enum<E>> Ask<ChoiceAnswer<E>> choice(String key, Class<E> labels, Consumer<Choice.Builder<E>> configure)
public static Ask<ScoreAnswer> score(String key, Score question)
public static Ask<ScoreAnswer> score(String key, Consumer<Score.Builder> configure)
public String key()
public TypeSafeQuestion question()
}
Plus TypeSafeResponse.answer(Ask<A>), TypeSafeRequest.Builder.ask(Ask<?>...), TypeSafeRequest.of(Object state, Ask<?>...), and systemOne / systemOneAsync(Object state, Ask<?>...). Per-call RequestOptions go through the builder form rather than another varargs overload.
Checked at compile time
Ask.choice("dept", Dept.class, Choice.of("…", Priority.class)): Choice<Priority> is not a Choice<Dept>.
Ask.choice("dept", Choice.of("…", Dept.class)): the String overload takes only Choice<String>, so an enum question can't be read back as Strings by accident.
response.answer(DEPT) is a ChoiceAnswer<Dept>; using it as anything else doesn't compile.
Still checked at runtime
- Reading an
Ask that wasn't in the request fails like reading a missing key does today.
- Two
Asks with the same key in one request should be rejected; check what Builder.question does with a repeated key and make ask consistent.
- A label that isn't a constant of the enum throws, as
ChoiceAnswer.as does now.
Compatibility
Additive; nothing existing changes. Minor release.
Open questions
- Name.
Ask reads well at the call site (r.ask(DEPT), response.answer(DEPT)); alternatives are Asked, KeyedQuestion, TypeSafeQuestion.Keyed.
- Noul answer type.
Ask<Double> matches response.noul(key); Ask<NoulAnswer> leaves room for threshold helpers such as answer(URGENT).atLeast(0.9).
CriteriaQuestionSet generates its keys at runtime and would stay on String keys.
Prior art
Reading an answer back through the question object comes from gudcks0305/jev-java, where result.answer(route).choice() returns the enum for a ChoiceQuestion built from it.
Problem
Reading a typed answer repeats what the request already said, and a mismatch only fails at runtime:
A question can't carry this itself: the key lives in the request map, and the
Choice<E>record can't holdDept.class.Proposal
A keyed handle,
Ask<A>, that pairs a key, a question, and the type of its answer. Declare it once, ask it, and read the answer back through it:Shape
Plus
TypeSafeResponse.answer(Ask<A>),TypeSafeRequest.Builder.ask(Ask<?>...),TypeSafeRequest.of(Object state, Ask<?>...), andsystemOne/systemOneAsync(Object state, Ask<?>...). Per-callRequestOptionsgo through the builder form rather than another varargs overload.Checked at compile time
Ask.choice("dept", Dept.class, Choice.of("…", Priority.class)):Choice<Priority>is not aChoice<Dept>.Ask.choice("dept", Choice.of("…", Dept.class)): the String overload takes onlyChoice<String>, so an enum question can't be read back as Strings by accident.response.answer(DEPT)is aChoiceAnswer<Dept>; using it as anything else doesn't compile.Still checked at runtime
Askthat wasn't in the request fails like reading a missing key does today.Asks with the same key in one request should be rejected; check whatBuilder.questiondoes with a repeated key and makeaskconsistent.ChoiceAnswer.asdoes now.Compatibility
Additive; nothing existing changes. Minor release.
Open questions
Askreads well at the call site (r.ask(DEPT),response.answer(DEPT)); alternatives areAsked,KeyedQuestion,TypeSafeQuestion.Keyed.Ask<Double>matchesresponse.noul(key);Ask<NoulAnswer>leaves room for threshold helpers such asanswer(URGENT).atLeast(0.9).CriteriaQuestionSetgenerates its keys at runtime and would stay on String keys.Prior art
Reading an answer back through the question object comes from gudcks0305/jev-java, where
result.answer(route).choice()returns the enum for aChoiceQuestionbuilt from it.