001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.juneau.rest.server.mcp.v20260728;
018
019
020import java.util.*;
021
022import org.apache.juneau.bean.mcp.v20260728.*;
023import org.apache.juneau.marshall.marshaller.*;
024import static org.apache.juneau.commons.utils.Shorts.*;
025
026/**
027 * Static helper that parses typed {@link ElicitResult}s out of an {@link McpMrtrResumeContext} (MCP
028 * {@code 2026-07-28} SEP-2322 elicitation, riding the Multi-Round-Trip Requests loop).
029 *
030 * <p>
031 * {@link McpMrtrResumeContext#inputResponses()} values are generic JSON (a {@code JsonMap}/{@code JsonList}/
032 * boxed primitive/{@code String} after wire deserialization, never a typed bean automatically); this helper
033 * converts each to {@link ElicitResult} via the same {@code Json.to(Json.of(value), type)} technique
034 * {@link McpMrtrResumeContext#continuationAs(Class)} already uses for the continuation side, just applied
035 * per-key across the map instead of to the single continuation value.
036 */
037public final class ElicitationResponses {
038
039   private ElicitationResponses() {}
040
041   /**
042    * Returns the typed answer for a single question id.
043    *
044    * @param ctx The resume context.  Must not be <jk>null</jk>.
045    * @param id The server-assigned id this question was posed under.  Must not be <jk>null</jk>.
046    * @return The typed answer, or <jk>null</jk> if {@code id} is absent from {@code ctx}'s responses, or maps
047    *    to <jk>null</jk>.
048    * @throws IllegalArgumentException If {@code ctx} or {@code id} is <jk>null</jk>.
049    * @throws RuntimeException If the answer's decoded shape cannot be converted to {@link ElicitResult}.
050    */
051   public static ElicitResult get(McpMrtrResumeContext ctx, String id) {
052      reqnn("ctx", ctx);
053      reqnn("id", id);
054      var value = ctx.inputResponses().get(id);
055      if (value == null)
056         return null;
057      return Json.to(Json.of(value), ElicitResult.class);
058   }
059
060   /**
061    * Returns every answer in {@code ctx}, converted to {@link ElicitResult}.
062    *
063    * @param ctx The resume context.  Must not be <jk>null</jk>.
064    * @return A keyed map of typed answers.  Never <jk>null</jk> (empty if {@code ctx} carried none). Values
065    *    may be <jk>null</jk> when the corresponding raw entry is absent/<jk>null</jk> (see {@link #get}).
066    * @throws IllegalArgumentException If {@code ctx} is <jk>null</jk>.
067    * @throws RuntimeException If any answer's decoded shape cannot be converted to {@link ElicitResult}.
068    */
069   public static Map<String,ElicitResult> all(McpMrtrResumeContext ctx) {
070      reqnn("ctx", ctx);
071      Map<String,ElicitResult> out = new LinkedHashMap<>();
072      ctx.inputResponses().keySet().forEach(id -> out.put(id, get(ctx, id)));
073      return out;
074   }
075
076   /**
077    * Returns a boolean field's value from a single question's answer, decline-safe.
078    *
079    * <p>
080    * Treats a missing id, a missing field, or a non-{@link ElicitAction#ACCEPT} action (declined, cancelled)
081    * as <jk>false</jk> rather than requiring the caller to null-check {@link #get} and its content map by
082    * hand &mdash; the safe default for a confirmation question is to never proceed on ambiguity.
083    *
084    * @param ctx The resume context.  Must not be <jk>null</jk>.
085    * @param id The server-assigned id this question was posed under.  Must not be <jk>null</jk>.
086    * @param field The field name within the answer's content map.  Must not be <jk>null</jk>.
087    * @return <jk>true</jk> only if {@code id} is present, the action is {@link ElicitAction#ACCEPT}, and
088    *    {@code field} is present in the content map with value {@link Boolean#TRUE}. <jk>false</jk> in every
089    *    other case.
090    * @throws IllegalArgumentException If {@code ctx}, {@code id}, or {@code field} is <jk>null</jk>.
091    * @throws RuntimeException If the answer's decoded shape cannot be converted to {@link ElicitResult}.
092    */
093   public static boolean getBoolean(McpMrtrResumeContext ctx, String id, String field) {
094      reqnn("ctx", ctx);
095      reqnn("id", id);
096      reqnn("field", field);
097      var answer = get(ctx, id);
098      if (answer == null || answer.getAction() != ElicitAction.ACCEPT)
099         return false;
100      var content = answer.getContent();
101      return content != null && Boolean.TRUE.equals(content.get(field));
102   }
103
104   /**
105    * Returns a string field's value from a single question's answer, decline-safe.
106    *
107    * <p>
108    * Treats a missing id, a missing field, or a non-{@link ElicitAction#ACCEPT} action (declined, cancelled)
109    * as <jk>null</jk> rather than requiring the caller to null-check {@link #get} and its content map by hand.
110    *
111    * @param ctx The resume context.  Must not be <jk>null</jk>.
112    * @param id The server-assigned id this question was posed under.  Must not be <jk>null</jk>.
113    * @param field The field name within the answer's content map.  Must not be <jk>null</jk>.
114    * @return The field's value as a {@link String}, or <jk>null</jk> if {@code id} is absent, the action is
115    *    not {@link ElicitAction#ACCEPT}, {@code field} is absent, or its value is not a {@link String}.
116    * @throws IllegalArgumentException If {@code ctx}, {@code id}, or {@code field} is <jk>null</jk>.
117    * @throws RuntimeException If the answer's decoded shape cannot be converted to {@link ElicitResult}.
118    */
119   public static String getString(McpMrtrResumeContext ctx, String id, String field) {
120      reqnn("ctx", ctx);
121      reqnn("id", id);
122      reqnn("field", field);
123      var answer = get(ctx, id);
124      if (answer == null || answer.getAction() != ElicitAction.ACCEPT)
125         return null;
126      var content = answer.getContent();
127      if (content == null)
128         return null;
129      return content.get(field) instanceof String s ? s : null;
130   }
131}