Skip to content

Typed answer lookup: read answers back through a keyed question handle #17

Description

@garretpremo

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

  1. Name. Ask reads well at the call site (r.ask(DEPT), response.answer(DEPT)); alternatives are Asked, KeyedQuestion, TypeSafeQuestion.Keyed.
  2. Noul answer type. Ask<Double> matches response.noul(key); Ask<NoulAnswer> leaves room for threshold helpers such as answer(URGENT).atLeast(0.9).
  3. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions