diff --git a/api/src/main/java/io/serverlessworkflow/api/ObjectMapperFactory.java b/api/src/main/java/io/serverlessworkflow/api/ObjectMapperFactory.java index 850e7da7..c8211586 100644 --- a/api/src/main/java/io/serverlessworkflow/api/ObjectMapperFactory.java +++ b/api/src/main/java/io/serverlessworkflow/api/ObjectMapperFactory.java @@ -21,6 +21,9 @@ import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.Feature; import io.serverlessworkflow.serialization.BeanDeserializerModifierWithValidation; +import io.serverlessworkflow.serialization.URIDeserializer; +import io.serverlessworkflow.serialization.URISerializer; +import java.net.URI; class ObjectMapperFactory { @@ -39,7 +42,10 @@ public static final ObjectMapper yamlMapper() { private static ObjectMapper configure(ObjectMapper mapper) { SimpleModule validationModule = new SimpleModule(); + validationModule.addDeserializer(URI.class, new URIDeserializer()); + validationModule.addSerializer(URI.class, new URISerializer()); validationModule.setDeserializerModifier(new BeanDeserializerModifierWithValidation()); + return mapper .configure(SerializationFeature.INDENT_OUTPUT, true) .configure(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS, false) diff --git a/api/src/main/java/io/serverlessworkflow/serialization/URIDeserializer.java b/api/src/main/java/io/serverlessworkflow/serialization/URIDeserializer.java new file mode 100644 index 00000000..a3269ab6 --- /dev/null +++ b/api/src/main/java/io/serverlessworkflow/serialization/URIDeserializer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.serialization; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonMappingException; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + +public class URIDeserializer extends JsonDeserializer { + @Override + public URI deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + try { + String uriStr = p.getValueAsString(); + if (uriStr == null) { + throw new JsonMappingException(p, "URI is not an string"); + } + return new URI(uriStr); + } catch (URISyntaxException ex) { + throw new JsonMappingException(p, ex.getMessage()); + } + } +} diff --git a/api/src/main/java/io/serverlessworkflow/serialization/URISerializer.java b/api/src/main/java/io/serverlessworkflow/serialization/URISerializer.java new file mode 100644 index 00000000..a36561d2 --- /dev/null +++ b/api/src/main/java/io/serverlessworkflow/serialization/URISerializer.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.serialization; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import java.io.IOException; +import java.net.URI; + +public class URISerializer extends JsonSerializer { + + @Override + public void serialize(URI value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeString(value.toString()); + } +} diff --git a/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java b/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java index 65a7e2a0..ab0c1a23 100644 --- a/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java +++ b/custom-generator/src/main/java/io/serverlessworkflow/generator/AllAnyOneOfSchemaRule.java @@ -23,6 +23,7 @@ import com.sun.codemodel.JClass; import com.sun.codemodel.JClassAlreadyExistsException; import com.sun.codemodel.JClassContainer; +import com.sun.codemodel.JConditional; import com.sun.codemodel.JDefinedClass; import com.sun.codemodel.JExpr; import com.sun.codemodel.JFieldVar; @@ -32,13 +33,17 @@ import com.sun.codemodel.JPackage; import com.sun.codemodel.JType; import com.sun.codemodel.JVar; +import jakarta.validation.ConstraintViolationException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; import java.util.List; import java.util.Optional; +import java.util.regex.Pattern; import org.jsonschema2pojo.Jsonschema2Pojo; import org.jsonschema2pojo.Schema; import org.jsonschema2pojo.exception.GenerationException; @@ -54,7 +59,35 @@ class AllAnyOneOfSchemaRule extends SchemaRule { this.ruleFactory = ruleFactory; } - private static class JTypeWrapper { + private static final String REF = "$ref"; + private static final String PATTERN = "pattern"; + + private enum Format { + URI_TEMPLATE("^[A-Za-z][A-Za-z0-9+\\-.]*://.*"); + + private final String pattern; + + Format(String pattern) { + this.pattern = pattern; + } + + public static Format parse(String str) { + if (str != null) { + switch (str) { + case "uri-template": + return URI_TEMPLATE; + } + } + return null; + } + + String pattern() { + + return pattern; + } + } + + private static class JTypeWrapper implements Comparable { private final JType type; private final JsonNode node; @@ -71,6 +104,27 @@ public JType getType() { public JsonNode getNode() { return node; } + + @Override + public int compareTo(JTypeWrapper other) { + return typeToNumber() - other.typeToNumber(); + } + + private int typeToNumber() { + if (type.name().equals("Object")) { + return 6; + } else if (type.name().equals("String")) { + return node.has(PATTERN) || node.has(REF) ? 4 : 5; + } else if (type.isPrimitive()) { + return 3; + } else if (type.isReference()) { + return 2; + } else if (type.isArray()) { + return 1; + } else { + return 0; + } + } } @Override @@ -82,12 +136,14 @@ public JType apply( Schema schema) { Optional refType = refType(nodeName, schemaNode, parent, generatableType, schema); - Collection unionTypes = new ArrayList<>(); + List unionTypes = new ArrayList<>(); unionType("oneOf", nodeName, schemaNode, parent, generatableType, schema, unionTypes); unionType("anyOf", nodeName, schemaNode, parent, generatableType, schema, unionTypes); unionType("allOf", nodeName, schemaNode, parent, generatableType, schema, unionTypes); + Collections.sort(unionTypes); + JType javaType; if (schemaNode.has("enum")) { javaType = @@ -101,11 +157,11 @@ public JType apply( .getTypeRule() .apply(nodeName, schemaNode, parent, generatableType.getPackage(), schema); if (javaType instanceof JDefinedClass) { - populateClass((JDefinedClass) javaType, refType, unionTypes); + populateClass(schema, (JDefinedClass) javaType, refType, unionTypes); } else if (!unionTypes.isEmpty()) { javaType = createUnionClass( - nodeName, schemaNode, generatableType.getPackage(), refType, unionTypes); + schema, nodeName, schemaNode, generatableType.getPackage(), refType, unionTypes); } schema.setJavaTypeIfEmpty(javaType); } @@ -113,7 +169,10 @@ public JType apply( } private JDefinedClass populateClass( - JDefinedClass definedClass, Optional refType, Collection unionTypes) { + Schema parentSchema, + JDefinedClass definedClass, + Optional refType, + Collection unionTypes) { JType clazzClass = definedClass.owner()._ref(Object.class); Optional valueField; @@ -144,9 +203,19 @@ private JDefinedClass populateClass( } catch (JClassAlreadyExistsException ex) { // already deserialized aware } + + Collection stringTypes = new ArrayList<>(); for (JTypeWrapper unionType : unionTypes) { - wrapIt(definedClass, valueField, unionType.getType(), Optional.of(unionType.getNode())); + if (isStringType(unionType.getType())) { + stringTypes.add(unionType); + } else { + wrapIt(parentSchema, definedClass, valueField, unionType.getType(), unionType.getNode()); + } } + if (!stringTypes.isEmpty()) { + wrapStrings(parentSchema, definedClass, valueField, stringTypes); + } + } else { valueField = Optional.empty(); } @@ -156,7 +225,7 @@ private JDefinedClass populateClass( if (type instanceof JClass) { definedClass._extends((JClass) type); } else { - wrapIt(definedClass, valueField, type, Optional.empty()); + wrapIt(parentSchema, definedClass, valueField, type, null); } }); @@ -167,6 +236,10 @@ private JDefinedClass populateClass( return definedClass; } + private static boolean isStringType(JType type) { + return type.name().equals("String"); + } + private JDefinedClass generateSerializer(JDefinedClass relatedClass) throws JClassAlreadyExistsException { JDefinedClass definedClass = GeneratorUtils.serializerClass(relatedClass); @@ -208,6 +281,7 @@ private JDefinedClass generateDeserializer( } private JDefinedClass createUnionClass( + Schema parentSchema, String nodeName, JsonNode schemaNode, JPackage container, @@ -215,6 +289,7 @@ private JDefinedClass createUnionClass( Collection unionTypes) { try { return populateClass( + parentSchema, container._class( ruleFactory.getNameHelper().getUniqueClassName(nodeName, schemaNode, container)), refType, @@ -225,30 +300,109 @@ private JDefinedClass createUnionClass( } private void wrapIt( + Schema parentSchema, JDefinedClass definedClass, Optional valueField, JType unionType, - Optional node) { - final String name = - node.map(n -> n.get("title")).map(JsonNode::asText).orElse(unionType.name()); + JsonNode node) { + JFieldVar instanceField = getInstanceField(parentSchema, definedClass, unionType, node); + JMethod constructor = definedClass.constructor(JMod.PUBLIC); + JVar instanceParam = constructor.param(unionType, instanceField.name()); + JBlock body = constructor.body(); + valueField.ifPresent(v -> body.assign(JExpr._this().ref(v), instanceParam)); + body.assign(JExpr._this().ref(instanceField), instanceParam); + } + + private void wrapStrings( + Schema parentSchema, + JDefinedClass definedClass, + Optional valueField, + Collection stringTypes) { + Iterator iter = stringTypes.iterator(); + JTypeWrapper first = iter.next(); + JMethod constructor = definedClass.constructor(JMod.PUBLIC); + + JBlock body = constructor.body(); + String pattern = pattern(first.getNode(), parentSchema); + if (pattern == null && iter.hasNext()) { + pattern = ".*"; + } + JFieldVar instanceField = + getInstanceField(parentSchema, definedClass, first.getType(), first.getNode()); + JVar instanceParam = constructor.param(first.type, instanceField.name()); + valueField.ifPresent(v -> body.assign(JExpr._this().ref(v), instanceParam)); + if (pattern != null) { + JConditional condition = + body._if(getPatternCondition(pattern, body, instanceField, instanceParam, definedClass)); + condition._then().assign(JExpr._this().ref(instanceField), instanceParam); + while (iter.hasNext()) { + JTypeWrapper item = iter.next(); + instanceField = + getInstanceField(parentSchema, definedClass, item.getType(), item.getNode()); + pattern = pattern(item.getNode(), parentSchema); + if (pattern == null) { + pattern = ".*"; + } + condition = + condition._elseif( + getPatternCondition(pattern, body, instanceField, instanceParam, definedClass)); + condition._then().assign(JExpr._this().ref(instanceField), instanceParam); + } + condition + ._else() + ._throw( + JExpr._new(definedClass.owner()._ref(ConstraintViolationException.class)) + .arg( + definedClass + .owner() + .ref(String.class) + .staticInvoke("format") + .arg("%s does not match any pattern") + .arg(instanceParam)) + .arg(JExpr._null())); + } else { + body.assign(JExpr._this().ref(instanceField), instanceParam); + } + } + + private JFieldVar getInstanceField( + Schema parentSchema, JDefinedClass definedClass, JType type, JsonNode node) { JFieldVar instanceField = definedClass.field( JMod.PRIVATE, - unionType, - ruleFactory.getNameHelper().getPropertyName(name, node.orElse(null))); - GeneratorUtils.buildMethod(definedClass, instanceField, ruleFactory.getNameHelper(), name); - - JMethod constructor = definedClass.getConstructor(new JType[] {unionType}); - JVar instanceParam; - if (constructor == null) { - constructor = definedClass.constructor(JMod.PUBLIC); - instanceParam = constructor.param(unionType, instanceField.name()); - } else { - instanceParam = constructor.listParams()[0]; + type, + ruleFactory + .getNameHelper() + .getPropertyName(getTypeName(node, type, parentSchema), node)); + GeneratorUtils.buildMethod( + definedClass, instanceField, ruleFactory.getNameHelper(), instanceField.name()); + return instanceField; + } + + private static String getFromNode(JsonNode node, String fieldName) { + if (node != null) { + JsonNode item = node.get(fieldName); + if (item != null) { + return item.asText(); + } } - JBlock body = constructor.body(); - valueField.ifPresent(v -> body.assign(JExpr._this().ref(v), instanceParam)); - body.assign(JExpr._this().ref(instanceField), instanceParam); + + return null; + } + + private JInvocation getPatternCondition( + String pattern, + JBlock body, + JFieldVar instanceField, + JVar instanceParam, + JDefinedClass definedClass) { + JFieldVar patternField = + definedClass.field( + JMod.PRIVATE | JMod.STATIC | JMod.FINAL, + Pattern.class, + instanceField.name() + "_" + "Pattern", + definedClass.owner().ref(Pattern.class).staticInvoke("compile").arg(pattern)); + return JExpr.invoke(JExpr.invoke(patternField, "matcher").arg(instanceParam), "matches"); } private void unionType( @@ -285,8 +439,8 @@ private Optional refType( JsonNode parent, JClassContainer generatableType, Schema parentSchema) { - if (schemaNode.has("$ref")) { - String ref = schemaNode.get("$ref").asText(); + if (schemaNode.has(REF)) { + String ref = schemaNode.get(REF).asText(); Schema schema = ruleFactory .getSchemaStore() @@ -308,6 +462,39 @@ private Optional refType( return Optional.empty(); } + private JsonNode schemaRef(JsonNode schemaNode, Schema parentSchema) { + String ref = getFromNode(schemaNode, REF); + return ref != null + ? ruleFactory + .getSchemaStore() + .create( + parentSchema, ref, ruleFactory.getGenerationConfig().getRefFragmentPathDelimiters()) + .getContent() + : null; + } + + private String getTypeName(JsonNode node, JType type, Schema parentSchema) { + final String title = "title"; + String name = getFromNode(node, title); + if (name == null) { + name = getFromNode(schemaRef(node, parentSchema), title); + } + if (name == null) { + name = type.name(); + } + return name; + } + + private String pattern(JsonNode node, Schema parentSchema) { + String pattern = pattern(node); + return pattern != null ? pattern : pattern(schemaRef(node, parentSchema)); + } + + private String pattern(JsonNode node) { + Format format = Format.parse(getFromNode(node, "format")); + return format != null ? format.pattern() : getFromNode(node, PATTERN); + } + private String nameFromRef(String ref, String nodeName) { if ("#".equals(ref)) { return nodeName; diff --git a/impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java new file mode 100644 index 00000000..806a3208 --- /dev/null +++ b/impl/src/main/java/io/serverlessworkflow/impl/DefaultTaskExecutorFactory.java @@ -0,0 +1,42 @@ +/* + * Copyright 2020-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.serverlessworkflow.impl; + +import io.serverlessworkflow.api.types.CallTask; +import io.serverlessworkflow.api.types.Task; +import io.serverlessworkflow.api.types.TaskBase; + +public class DefaultTaskExecutorFactory implements TaskExecutorFactory { + + protected DefaultTaskExecutorFactory() {} + + private static TaskExecutorFactory instance = new DefaultTaskExecutorFactory(); + + public static TaskExecutorFactory get() { + return instance; + } + + public TaskExecutor getTaskExecutor(Task task) { + + if (task.getCallTask() != null) { + CallTask callTask = task.getCallTask(); + if (callTask.getCallHTTP() != null) { + return new HttpExecutor(callTask.getCallHTTP()); + } + } + throw new UnsupportedOperationException(task.get().getClass().getName() + " not supported yet"); + } +} diff --git a/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java b/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java index 7d0f89ef..203ca15d 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/HttpExecutor.java @@ -21,12 +21,12 @@ import io.serverlessworkflow.api.types.HTTPArguments; import io.serverlessworkflow.api.types.WithHTTPHeaders; import io.serverlessworkflow.api.types.WithHTTPQuery; +import jakarta.ws.rs.HttpMethod; import jakarta.ws.rs.client.Client; import jakarta.ws.rs.client.ClientBuilder; import jakarta.ws.rs.client.Entity; import jakarta.ws.rs.client.Invocation.Builder; import jakarta.ws.rs.client.WebTarget; -import jakarta.ws.rs.core.MediaType; import java.util.Map; import java.util.Map.Entry; @@ -60,16 +60,16 @@ protected JsonNode internalExecute(JsonNode node) { target .resolveTemplates( JsonUtils.mapper().convertValue(node, new TypeReference>() {})) - .request(MediaType.APPLICATION_JSON); + .request(); WithHTTPHeaders headers = httpArgs.getHeaders(); if (headers != null) { headers.getAdditionalProperties().forEach(request::header); } - switch (httpArgs.getMethod().toLowerCase()) { - case "get": + switch (httpArgs.getMethod().toUpperCase()) { + case HttpMethod.GET: default: return request.get(JsonNode.class); - case "post": + case HttpMethod.POST: return request.post(Entity.json(httpArgs.getBody()), JsonNode.class); } } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutorFactory.java b/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutorFactory.java index a45f9455..69eaa0a0 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutorFactory.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/TaskExecutorFactory.java @@ -15,22 +15,9 @@ */ package io.serverlessworkflow.impl; -import io.serverlessworkflow.api.types.CallTask; import io.serverlessworkflow.api.types.Task; import io.serverlessworkflow.api.types.TaskBase; -public class TaskExecutorFactory { - - private TaskExecutorFactory() {} - - static TaskExecutor buildExecutor(Task task) { - - if (task.getCallTask() != null) { - CallTask callTask = task.getCallTask(); - if (callTask.getCallHTTP() != null) { - return new HttpExecutor(callTask.getCallHTTP()); - } - } - throw new UnsupportedOperationException(task + " not supported yet"); - } +public interface TaskExecutorFactory { + TaskExecutor getTaskExecutor(Task task); } diff --git a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java index bb453a81..8c8dad4f 100644 --- a/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java +++ b/impl/src/main/java/io/serverlessworkflow/impl/WorkflowDefinition.java @@ -31,18 +31,24 @@ public class WorkflowDefinition { - private WorkflowDefinition(Workflow workflow, Collection listeners) { + private WorkflowDefinition( + Workflow workflow, + TaskExecutorFactory factory, + Collection listeners) { this.workflow = workflow; + this.factory = factory; this.listeners = listeners; } private final Workflow workflow; private final Collection listeners; + private final TaskExecutorFactory factory; private final Map> taskExecutors = new ConcurrentHashMap<>(); public static class Builder { private final Workflow workflow; + private TaskExecutorFactory factory = DefaultTaskExecutorFactory.get(); private Collection listeners; private Builder(Workflow workflow) { @@ -57,9 +63,15 @@ public Builder withListener(WorkflowExecutionListener listener) { return this; } + public Builder withTaskExecutorFactory(TaskExecutorFactory factory) { + this.factory = factory; + return this; + } + public WorkflowDefinition build() { return new WorkflowDefinition( workflow, + factory, listeners == null ? Collections.emptySet() : Collections.unmodifiableCollection(listeners)); @@ -71,7 +83,7 @@ public static Builder builder(Workflow workflow) { } public WorkflowInstance execute(Object input) { - return new WorkflowInstance(JsonUtils.fromValue(input)); + return new WorkflowInstance(factory, JsonUtils.fromValue(input)); } enum State { @@ -85,14 +97,16 @@ public class WorkflowInstance { private final JsonNode input; private JsonNode output; private State state; + private TaskExecutorFactory factory; private JsonPointer currentPos; - private WorkflowInstance(JsonNode input) { + private WorkflowInstance(TaskExecutorFactory factory, JsonNode input) { this.input = input; this.output = object(); this.state = State.STARTED; this.currentPos = JsonPointer.compile("/"); + this.factory = factory; processDo(workflow.getDo()); } @@ -105,8 +119,7 @@ private void processDo(List tasks) { this.output = MergeUtils.merge( taskExecutors - .computeIfAbsent( - currentPos, k -> TaskExecutorFactory.buildExecutor(task.getTask())) + .computeIfAbsent(currentPos, k -> factory.getTaskExecutor(task.getTask())) .apply(input), output); listeners.forEach(l -> l.onTaskEnded(currentPos, task.getTask())); diff --git a/impl/src/main/resources/callHttp.yaml b/impl/src/main/resources/callHttp.yaml index 4022e38a..0fdeb10a 100644 --- a/impl/src/main/resources/callHttp.yaml +++ b/impl/src/main/resources/callHttp.yaml @@ -7,7 +7,8 @@ do: - getPet: call: http with: - method: get - endpoint: - uri: https://petstore.swagger.io/v2/pet/{petId} - output: response \ No newline at end of file + headers: + content-type: application/json + method: get + endpoint: + uri: https://petstore.swagger.io/v2/pet/{petId} \ No newline at end of file diff --git a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java b/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java index 91b1a259..f5feb513 100644 --- a/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java +++ b/impl/src/test/java/io/serverlessworkflow/impl/WorkflowDefinitionTest.java @@ -44,7 +44,7 @@ private static Stream provideParameters() { return Stream.of( Arguments.of( "callHttp.yaml", - Map.of("petId", 10), + Map.of("petId", 1), new Condition<>( o -> ((Map) o).containsKey("photoUrls"), "callHttpCondition"))); }