diff --git a/src/main/java/com/google/genai/AfcUtil.java b/src/main/java/com/google/genai/AfcUtil.java index 8ff11ce0ac8..7d1a53e558c 100644 --- a/src/main/java/com/google/genai/AfcUtil.java +++ b/src/main/java/com/google/genai/AfcUtil.java @@ -17,7 +17,6 @@ package com.google.genai; import static com.google.common.collect.ImmutableList.toImmutableList; -import static java.util.Arrays.stream; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.collect.ImmutableList; @@ -28,9 +27,12 @@ import com.google.genai.types.Part; import com.google.genai.types.Tool; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.logging.Logger; @@ -95,6 +97,15 @@ static ImmutableMap getFunctionMap(GenerateContentConfig config) for (Tool tool : config.tools().get()) { if (tool.functions().isPresent() && !tool.functions().get().isEmpty()) { for (Method method : tool.functions().get()) { + if (!Modifier.isStatic(method.getModifiers()) + && (!tool.functionInstances().isPresent() + || !tool.functionInstances().get().containsKey(method) + || tool.functionInstances().get().get(method) == null)) { + throw new IllegalArgumentException( + "Instance methods are not supported without an instance. Please provide an" + + " instance for method: " + + method.getName()); + } functionMapBuilder.put(method.getName(), method); } } @@ -103,8 +114,26 @@ static ImmutableMap getFunctionMap(GenerateContentConfig config) return functionMapBuilder.buildOrThrow(); } + static ImmutableMap getFunctionInstanceMap(GenerateContentConfig config) { + ImmutableMap.Builder functionInstanceMapBuilder = ImmutableMap.builder(); + if (config != null && config.tools().isPresent() && !config.tools().get().isEmpty()) { + for (Tool tool : config.tools().get()) { + if (tool.functionInstances().isPresent() && !tool.functionInstances().get().isEmpty()) { + for (Map.Entry entry : tool.functionInstances().get().entrySet()) { + if (entry.getValue() != null) { + functionInstanceMapBuilder.put(entry.getKey().getName(), entry.getValue()); + } + } + } + } + } + return functionInstanceMapBuilder.buildOrThrow(); + } + static ImmutableList getFunctionResponseParts( - GenerateContentResponse response, ImmutableMap functionMap) { + GenerateContentResponse response, + ImmutableMap functionMap, + ImmutableMap functionInstanceMap) { ImmutableList.Builder functionResponsePartsBuilder = ImmutableList.builder(); ImmutableList responseParts = response.parts(); ImmutableList functionCalls = response.functionCalls(); @@ -115,14 +144,21 @@ static ImmutableList getFunctionResponseParts( return functionResponsePartsBuilder.build(); } for (FunctionCall functionCall : functionCalls) { + if (!functionCall.name().isPresent() || functionCall.name().get().isEmpty()) { + continue; + } String funcName = functionCall.name().get(); - if (funcName == null || !functionMap.containsKey(funcName)) { + if (!functionMap.containsKey(funcName)) { continue; } Method method = functionMap.get(funcName); - ImmutableMap args = ImmutableMap.copyOf(functionCall.args().get()); + Object instance = functionInstanceMap != null ? functionInstanceMap.get(funcName) : null; + Map args = functionCall.args().orElse(Collections.emptyMap()); + if (args == null) { + args = Collections.emptyMap(); + } try { - Object funcResponse = getFunctionResponse(method, args); + Object funcResponse = getFunctionResponse(method, args, instance); if (funcResponse == null) { functionResponsePartsBuilder.add( Part.fromFunctionResponse(funcName, ImmutableMap.of("result", ""))); @@ -138,6 +174,11 @@ static ImmutableList getFunctionResponseParts( return functionResponsePartsBuilder.build(); } + static ImmutableList getFunctionResponseParts( + GenerateContentResponse response, ImmutableMap functionMap) { + return getFunctionResponseParts(response, functionMap, ImmutableMap.of()); + } + static boolean shouldDisableAfc(GenerateContentConfig config) { if (config == null) { return false; @@ -206,11 +247,10 @@ static boolean shouldAppendAfcHistory(GenerateContentConfig config) { } private static Object getFunctionResponse( - Method method, ImmutableMap argsFromModel) throws Exception { + Method method, Map argsFromModel, Object instance) throws Exception { List argsListFromModel = new ArrayList<>(); - ImmutableList methodParameterNames = - stream(method.getParameters()).map(Parameter::getName).collect(toImmutableList()); - for (String parameterName : methodParameterNames) { + for (Parameter parameter : method.getParameters()) { + String parameterName = parameter.getName(); if (!argsFromModel.containsKey(parameterName)) { throw new IllegalArgumentException( "The parameter \"" @@ -220,17 +260,25 @@ private static Object getFunctionResponse( + argsFromModel); } Object argValueFromModel = argsFromModel.get(parameterName); - String className = argValueFromModel.getClass().getName(); - - if (className.equals("java.lang.String")) { + if (argValueFromModel == null) { + if (parameter.getType().isPrimitive()) { + throw new IllegalArgumentException( + "The parameter \"" + + parameterName + + "\" is a primitive type " + + parameter.getType().getName() + + " but received null from the model."); + } + argsListFromModel.add(null); + } else if (argValueFromModel instanceof String) { argsListFromModel.add(argValueFromModel); - } else if (className.equals("java.lang.Integer")) { + } else if (argValueFromModel instanceof Integer) { argsListFromModel.add(Integer.parseInt(argValueFromModel.toString())); - } else if (className.equals("java.lang.Double")) { + } else if (argValueFromModel instanceof Double) { argsListFromModel.add(Double.parseDouble(argValueFromModel.toString())); - } else if (className.equals("java.lang.Float")) { + } else if (argValueFromModel instanceof Float) { argsListFromModel.add(Float.parseFloat(argValueFromModel.toString())); - } else if (className.equals("java.lang.Boolean")) { + } else if (argValueFromModel instanceof Boolean) { argsListFromModel.add(Boolean.parseBoolean(argValueFromModel.toString())); } else if (argValueFromModel instanceof List) { argsListFromModel.add(argValueFromModel); @@ -243,7 +291,7 @@ private static Object getFunctionResponse( } } - return method.invoke(null, argsListFromModel.toArray()); + return method.invoke(instance, argsListFromModel.toArray()); } private AfcUtil() {} diff --git a/src/main/java/com/google/genai/AsyncModels.java b/src/main/java/com/google/genai/AsyncModels.java index 8f4d660a3b1..e2f45ea0714 100644 --- a/src/main/java/com/google/genai/AsyncModels.java +++ b/src/main/java/com/google/genai/AsyncModels.java @@ -624,6 +624,7 @@ private CompletableFuture privateGenerateContentLoopAsync( List contents, GenerateContentConfig transformedConfig, ImmutableMap functionMap, + ImmutableMap functionInstanceMap, List automaticFunctionCallingHistory, int remainingRemoteCalls, int initialMaxCalls) { @@ -652,7 +653,7 @@ private CompletableFuture privateGenerateContentLoopAsync( } ImmutableList functionResponseParts = - AfcUtil.getFunctionResponseParts(response, functionMap); + AfcUtil.getFunctionResponseParts(response, functionMap, functionInstanceMap); if (functionResponseParts.isEmpty()) { return CompletableFuture.completedFuture( new AfcLoopResult(response, automaticFunctionCallingHistory)); @@ -671,6 +672,7 @@ private CompletableFuture privateGenerateContentLoopAsync( newHistory, transformedConfig, functionMap, + functionInstanceMap, newHistory, remainingRemoteCalls - 1, initialMaxCalls); @@ -710,6 +712,7 @@ public CompletableFuture generateContent( return privateGenerateContent(model, contents, transformedConfig); } ImmutableMap functionMap = AfcUtil.getFunctionMap(config); + ImmutableMap functionInstanceMap = AfcUtil.getFunctionInstanceMap(config); if (functionMap.isEmpty()) { return privateGenerateContent(model, contents, transformedConfig); } @@ -725,6 +728,7 @@ public CompletableFuture generateContent( contents, transformedConfig, functionMap, + functionInstanceMap, automaticFunctionCallingHistory, maxRemoteCalls, maxRemoteCalls) diff --git a/src/main/java/com/google/genai/Models.java b/src/main/java/com/google/genai/Models.java index f698a1f497b..94bfbba0b6a 100644 --- a/src/main/java/com/google/genai/Models.java +++ b/src/main/java/com/google/genai/Models.java @@ -7227,6 +7227,7 @@ public GenerateContentResponse generateContent( return privateGenerateContent(model, contents, transformedConfig); } ImmutableMap functionMap = AfcUtil.getFunctionMap(config); + ImmutableMap functionInstanceMap = AfcUtil.getFunctionInstanceMap(config); if (functionMap.isEmpty()) { return privateGenerateContent(model, contents, transformedConfig); } @@ -7254,7 +7255,7 @@ public GenerateContentResponse generateContent( break; } ImmutableList functionResponseParts = - AfcUtil.getFunctionResponseParts(response, functionMap); + AfcUtil.getFunctionResponseParts(response, functionMap, functionInstanceMap); if (functionResponseParts.isEmpty()) { break; } diff --git a/src/main/java/com/google/genai/types/FunctionDeclaration.java b/src/main/java/com/google/genai/types/FunctionDeclaration.java index cca7a2e9ed0..20bcd42d5a3 100644 --- a/src/main/java/com/google/genai/types/FunctionDeclaration.java +++ b/src/main/java/com/google/genai/types/FunctionDeclaration.java @@ -25,7 +25,6 @@ import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.genai.JsonSerializable; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; @@ -332,7 +331,6 @@ public static FunctionDeclaration fromJson(String jsonString) { * Creates a FunctionDeclaration instance from a {@link Method} instance. * * @param method The {@link Method} instance to be parsed into the FunctionDeclaration instance. - * Only static method is supported. * @param orderedParameterNames Optional ordered parameter names. If not provided, parameter names * will be retrieved via reflection. * @return A FunctionDeclaration instance. @@ -346,18 +344,12 @@ public static FunctionDeclaration fromMethod(Method method, String... orderedPar * * @param functionDescription Description of the function. * @param method The {@link Method} instance to be parsed into the FunctionDeclaration instance. - * Only static method is supported. * @param orderedParameterNames Optional ordered parameter names. If not provided, parameter names * will be retrieved via reflection. * @return A FunctionDeclaration instance. */ public static FunctionDeclaration fromMethod( String functionDescription, Method method, String... orderedParameterNames) { - if (!Modifier.isStatic(method.getModifiers())) { - throw new IllegalArgumentException( - "Instance methods are not supported. Please use static methods."); - } - Schema.Builder parametersBuilder = Schema.builder().type("OBJECT"); Parameter[] parameters = method.getParameters(); diff --git a/src/main/java/com/google/genai/types/Tool.java b/src/main/java/com/google/genai/types/Tool.java index 68121d26195..81c53d8e4e6 100644 --- a/src/main/java/com/google/genai/types/Tool.java +++ b/src/main/java/com/google/genai/types/Tool.java @@ -25,11 +25,16 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.auto.value.AutoValue; +import com.google.common.base.VerifyException; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.genai.JsonSerializable; +import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; /** Tool details of a tool that the model may use to generate a response. */ @@ -127,6 +132,13 @@ public abstract class Tool extends JsonSerializable { @JsonProperty("exaAiSearch") public abstract Optional exaAiSearch(); + /** + * The java.lang.reflect.Method instance to target instance mapping. If provided, it will be used + * to invoke instance methods during automatic function calling. + */ + @JsonIgnore + public abstract Optional> functionInstances(); + /** Instantiates a builder for Tool. */ @ExcludeFromGeneratedCoverageReport public static Builder builder() { @@ -608,6 +620,56 @@ public Builder clearExaAiSearch() { return exaAiSearch(Optional.empty()); } + /** + * Setter for functionInstances. + * + *

functionInstances: The java.lang.reflect.Method instance to target instance mapping. If + * provided, it will be used to invoke instance methods during automatic function calling. + */ + @JsonIgnore + public abstract Builder functionInstances(Map functionInstances); + + @ExcludeFromGeneratedCoverageReport + abstract Builder functionInstances(Optional> functionInstances); + + /** Clears the value of functionInstances field. */ + @ExcludeFromGeneratedCoverageReport + @CanIgnoreReturnValue + public Builder clearFunctionInstances() { + return functionInstances(Optional.empty()); + } + + public Builder functionWithInstance(Method method, Object instance) { + try { + Field fFunctions = this.getClass().getDeclaredField("functions"); + fFunctions.setAccessible(true); + @SuppressWarnings("unchecked") + Optional> optFunctions = (Optional>) fFunctions.get(this); + List currentFunctions = + new ArrayList<>( + optFunctions != null && optFunctions.isPresent() + ? optFunctions.get() + : new ArrayList<>()); + currentFunctions.add(method); + functions(currentFunctions); + + Field fInstances = this.getClass().getDeclaredField("functionInstances"); + fInstances.setAccessible(true); + @SuppressWarnings("unchecked") + Optional> optInstances = + (Optional>) fInstances.get(this); + Map currentInstances = + new HashMap<>( + optInstances != null && optInstances.isPresent() + ? optInstances.get() + : new HashMap<>()); + currentInstances.put(method, instance); + return functionInstances(currentInstances); + } catch (Exception e) { + throw new VerifyException("Failed to add function with instance", e); + } + } + public abstract Tool build(); } diff --git a/src/test/java/com/google/genai/AfcUtilTest.java b/src/test/java/com/google/genai/AfcUtilTest.java index a1c0397f87c..82339cca352 100644 --- a/src/test/java/com/google/genai/AfcUtilTest.java +++ b/src/test/java/com/google/genai/AfcUtilTest.java @@ -17,6 +17,8 @@ package com.google.genai; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -42,6 +44,7 @@ import com.google.genai.types.UrlContext; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Test; @@ -50,10 +53,22 @@ public static String testFunction1(String input) { return input + "testFunction1"; } + public String instanceFunction1(String input) { + return input + "instanceFunction1"; + } + public static Integer testFunction2(Integer a, Integer b) { return a / b; } + public static String testFunctionNoArgs() { + return "noArgs"; + } + + public static int testFunctionPrimitive(int a) { + return a * 2; + } + public static String testFunctionJoinListOfStrings(List items) { return String.join(",", items); } @@ -164,6 +179,71 @@ public void getFunctionMap_emptyConfig_returnsEmptyFunctionMap() { assertEquals(ImmutableMap.of(), actualFunctionMap); } + @Test + public void getFunctionInstanceMap_withInstance_returnsFunctionInstanceMap() + throws NoSuchMethodException { + Method instanceMethod = AfcUtilTest.class.getMethod("instanceFunction1", String.class); + AfcUtilTest instance = new AfcUtilTest(); + GenerateContentConfig config = + GenerateContentConfig.builder() + .tools( + Tool.builder() + .functions(instanceMethod) + .functionInstances(ImmutableMap.of(instanceMethod, instance)) + .build()) + .build(); + ImmutableMap actualInstanceMap = AfcUtil.getFunctionInstanceMap(config); + ImmutableMap expectedInstanceMap = + ImmutableMap.of("instanceFunction1", instance); + assertEquals(expectedInstanceMap, actualInstanceMap); + } + + @Test + public void getFunctionInstanceMap_withStaticMethod_returnsEmptyInstanceMap() + throws NoSuchMethodException { + Method staticMethod = AfcUtilTest.class.getMethod("testFunction1", String.class); + GenerateContentConfig config = + GenerateContentConfig.builder() + .tools(Tool.builder().functions(staticMethod).build()) + .build(); + ImmutableMap actualInstanceMap = AfcUtil.getFunctionInstanceMap(config); + assertEquals(ImmutableMap.of(), actualInstanceMap); + } + + @Test + public void getFunctionMap_withInstanceMethodAndInstance_succeeds() throws NoSuchMethodException { + Method instanceMethod = AfcUtilTest.class.getMethod("instanceFunction1", String.class); + AfcUtilTest instance = new AfcUtilTest(); + GenerateContentConfig config = + GenerateContentConfig.builder() + .tools( + Tool.builder() + .functions(instanceMethod) + .functionInstances(ImmutableMap.of(instanceMethod, instance)) + .build()) + .build(); + ImmutableMap actualFunctionMap = AfcUtil.getFunctionMap(config); + assertEquals(ImmutableMap.of("instanceFunction1", instanceMethod), actualFunctionMap); + } + + @Test + public void getFunctionMap_withInstanceMethodWithoutInstance_throwsIllegalArgumentException() + throws NoSuchMethodException { + Method instanceMethod = AfcUtilTest.class.getMethod("instanceFunction1", String.class); + GenerateContentConfig config = + GenerateContentConfig.builder() + .tools(Tool.builder().functions(instanceMethod).build()) + .build(); + IllegalArgumentException thrown = + assertThrows(IllegalArgumentException.class, () -> AfcUtil.getFunctionMap(config)); + assertTrue( + thrown + .getMessage() + .contains( + "Instance methods are not supported without an instance. Please provide an instance" + + " for method: instanceFunction1")); + } + @Test public void shouldDisableAfc_nullConfig_returnsFalse() { boolean shouldDisableAfc = AfcUtil.shouldDisableAfc(null); @@ -287,6 +367,117 @@ public void getFunctionResponseParts_returnsFunctionResponseParts() throws NoSuc assertEquals(expectedFunctionResponseParts.toString(), functionResponseParts.toString()); } + @Test + public void getFunctionResponseParts_missingName_skipsGracefully() throws NoSuchMethodException { + FunctionCall functionCall = + FunctionCall.builder().args(ImmutableMap.of("input", "test")).build(); + ImmutableMap functionMap = + ImmutableMap.of( + "testFunction1", AfcUtilTest.class.getMethod("testFunction1", String.class)); + Content content = Content.fromParts(Part.builder().functionCall(functionCall).build()); + GenerateContentResponse response = + GenerateContentResponse.builder().candidates(Candidate.builder().content(content)).build(); + ImmutableList functionResponseParts = + AfcUtil.getFunctionResponseParts(response, functionMap); + assertEquals(0, functionResponseParts.size()); + } + + @Test + public void getFunctionResponseParts_missingArgs_invokesNoArgFunction() + throws NoSuchMethodException { + FunctionCall functionCall = FunctionCall.builder().name("testFunctionNoArgs").build(); + ImmutableMap functionMap = + ImmutableMap.of("testFunctionNoArgs", AfcUtilTest.class.getMethod("testFunctionNoArgs")); + Content content = Content.fromParts(Part.builder().functionCall(functionCall).build()); + GenerateContentResponse response = + GenerateContentResponse.builder().candidates(Candidate.builder().content(content)).build(); + ImmutableList functionResponseParts = + AfcUtil.getFunctionResponseParts(response, functionMap); + ImmutableList expected = + ImmutableList.of( + Part.fromFunctionResponse("testFunctionNoArgs", ImmutableMap.of("result", "noArgs"))); + assertEquals(expected, functionResponseParts); + } + + @Test + public void getFunctionResponseParts_nullArgForObjectParam_passesNullSuccessfully() + throws NoSuchMethodException { + FunctionCall functionCall = + FunctionCall.builder() + .name("testFunction1") + .args(Collections.singletonMap("input", null)) + .build(); + ImmutableMap functionMap = + ImmutableMap.of( + "testFunction1", AfcUtilTest.class.getMethod("testFunction1", String.class)); + Content content = Content.fromParts(Part.builder().functionCall(functionCall).build()); + GenerateContentResponse response = + GenerateContentResponse.builder().candidates(Candidate.builder().content(content)).build(); + ImmutableList functionResponseParts = + AfcUtil.getFunctionResponseParts(response, functionMap); + ImmutableList expected = + ImmutableList.of( + Part.fromFunctionResponse( + "testFunction1", ImmutableMap.of("result", "nulltestFunction1"))); + assertEquals(expected, functionResponseParts); + } + + @Test + public void getFunctionResponseParts_nullArgForPrimitiveParam_returnsErrorResponse() + throws NoSuchMethodException { + FunctionCall functionCall = + FunctionCall.builder() + .name("testFunctionPrimitive") + .args(Collections.singletonMap("a", null)) + .build(); + ImmutableMap functionMap = + ImmutableMap.of( + "testFunctionPrimitive", + AfcUtilTest.class.getMethod("testFunctionPrimitive", int.class)); + Content content = Content.fromParts(Part.builder().functionCall(functionCall).build()); + GenerateContentResponse response = + GenerateContentResponse.builder().candidates(Candidate.builder().content(content)).build(); + ImmutableList functionResponseParts = + AfcUtil.getFunctionResponseParts(response, functionMap); + assertEquals(1, functionResponseParts.size()); + assertTrue( + functionResponseParts + .get(0) + .functionResponse() + .get() + .response() + .get() + .get("error") + .toString() + .contains("primitive type int but received null")); + } + + @Test + public void getFunctionResponseParts_withInstanceMethod_returnsFunctionResponseParts() + throws NoSuchMethodException { + Method instanceMethod = AfcUtilTest.class.getMethod("instanceFunction1", String.class); + AfcUtilTest instance = new AfcUtilTest(); + FunctionCall functionCall = + FunctionCall.builder() + .name("instanceFunction1") + .args(ImmutableMap.of("input", "test")) + .build(); + ImmutableMap functionMap = ImmutableMap.of("instanceFunction1", instanceMethod); + ImmutableMap functionInstanceMap = + ImmutableMap.of("instanceFunction1", instance); + Content content = Content.builder().parts(Part.builder().functionCall(functionCall)).build(); + GenerateContentResponse response = + GenerateContentResponse.builder().candidates(Candidate.builder().content(content)).build(); + ImmutableList functionResponseParts = + AfcUtil.getFunctionResponseParts(response, functionMap, functionInstanceMap); + ImmutableList expectedFunctionResponseParts = + ImmutableList.of( + Part.fromFunctionResponse( + "instanceFunction1", ImmutableMap.of("result", "testinstanceFunction1"))); + assertEquals(1, functionResponseParts.size()); + assertEquals(expectedFunctionResponseParts.toString(), functionResponseParts.toString()); + } + @Test public void getFunctionResponseParts_emptyResponse_returnsEmptyFunctionResponseParts() { ImmutableMap functionMap = ImmutableMap.of(); diff --git a/src/test/java/com/google/genai/types/FunctionDeclarationTest.java b/src/test/java/com/google/genai/types/FunctionDeclarationTest.java index 9e14d231ecd..468f8c2d537 100644 --- a/src/test/java/com/google/genai/types/FunctionDeclarationTest.java +++ b/src/test/java/com/google/genai/types/FunctionDeclarationTest.java @@ -186,18 +186,6 @@ public void fromMethodWithParameterNames_returnsFunctionDeclaration() assertEquals(EXPECTED_FUNCTION_DECLARATION.toString(), functionDeclaration.toString()); } - @Test - public void fromMethodWithInstanceMethod_throwsIllegalArgumentException() - throws NoSuchMethodException { - Method method = FunctionDeclarationTest.class.getMethod("instanceMethod", String.class); - - IllegalArgumentException thrown = - assertThrows( - IllegalArgumentException.class, - () -> FunctionDeclaration.fromMethod(FUNCTION_DESCRIPTION, method, STRING_PARAM_NAME)); - assertEquals( - "Instance methods are not supported. Please use static methods.", thrown.getMessage()); - } @Test public void fromMethodWithInvalidParameterType_throwsIllegalArgumentException() @@ -240,7 +228,7 @@ public void fromMethodWithInvalidReturnType_returnsFunctionDeclaration() .type(Type.Known.STRING) .title(STRING_PARAM_NAME) .build())) - .required(STRING_PARAM_NAME)) + .required(ImmutableList.of(STRING_PARAM_NAME))) .response(Schema.builder().type(Type.Known.OBJECT).title("return type")) .build(); @@ -271,6 +259,34 @@ public void fromMethodWithUnmatchedParameterNames_throwsIllegalArgumentException thrown.getMessage()); } + @Test + public void fromMethodWithInstanceMethod_returnsFunctionDeclaration() + throws NoSuchMethodException { + Method method = FunctionDeclarationTest.class.getMethod("instanceMethod", String.class); + FunctionDeclaration functionDeclaration = + FunctionDeclaration.fromMethod(FUNCTION_DESCRIPTION, method, STRING_PARAM_NAME); + + FunctionDeclaration expectedFunctionDeclaration = + FunctionDeclaration.builder() + .name("instanceMethod") + .description(FUNCTION_DESCRIPTION) + .parameters( + Schema.builder() + .type(Type.Known.OBJECT) + .properties( + ImmutableMap.of( + STRING_PARAM_NAME, + Schema.builder() + .type(Type.Known.STRING) + .title(STRING_PARAM_NAME) + .build())) + .required(ImmutableList.of(STRING_PARAM_NAME))) + .response(Schema.builder().type(Type.Known.INTEGER).title("return type")) + .build(); + + assertEquals(expectedFunctionDeclaration, functionDeclaration); + } + @Test public void testClearMethods() { FunctionDeclaration functionDeclaration =