diff --git a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java index 76060c4cfd..a98b57181f 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java +++ b/annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java @@ -198,7 +198,7 @@ private AbstractSchema createCollapsedInlineParser(ElementInfo ei, String par private SchemaObject getParserSchemaObject(ElementInfo elementInfo, String parserName) { return object(parserName) - .additionalProperties( elementInfo.isString()) + .additionalProperties(elementInfo.isString()) .description(getDescriptionContent(elementInfo)); } @@ -277,18 +277,51 @@ private void collectTextContent(ElementInfo i, SchemaObject so) { private void processMCChilds(Model m, MainInfo main, ElementInfo i, AbstractSchema so) { for (ChildElementInfo cei : i.getChildElementSpecs()) { - AbstractSchema parent2 = so; - if (cei.isList()) { - if (shouldGenerateFlowParserType(cei)) { - processList(i, so, cei, getSchemaObjects(m, main, cei)); - continue; - } - parent2 = processList(i, so, cei, null); + + if (!cei.isList()) { + addChildsAsProperties(m, main, cei, (SchemaObject) so, isComponentsList(i, cei), false); + continue; + } + + if (shouldGenerateFlowParserType(cei)) { + processList(i, so, cei, getSchemaObjects(m, main, cei)); + continue; } - addChildsAsProperties(m, main, cei, (SchemaObject) parent2, isComponentsList(i, cei), cei.isList()); + + if (shouldInlineListItems(main, cei)) { + processInlineList(m, main, i, so, cei); + continue; + } + + AbstractSchema parent2 = processList(i, so, cei, null); + addChildsAsProperties(m, main, cei, (SchemaObject) parent2, isComponentsList(i, cei), true); } } + private void processInlineList(Model m, MainInfo main, ElementInfo i, AbstractSchema so, ChildElementInfo cei) { + var decl = getChildElementDeclarationInfo(main, cei); + + ElementInfo itemEi = decl.getElementInfo().stream() + .filter(ei -> !ei.getAnnotation().topLevel()) + .findFirst() + .orElseThrow(); // should never happen due to shouldInlineListItems + + AbstractSchema itemsSchema = ref(itemEi.getAnnotation().name()).ref("#/$defs/" + itemEi.getXSDTypeName(m)); + + // keep "- $ref: ..." alternative for component items + if (!isComponentsList(i, cei) && itemEi.getAnnotation().component()) { + var variants = new ArrayList>(); + variants.add(itemsSchema); + variants.add(object() + .title("componentRef") + .additionalProperties(false) + .property(string("$ref").required(false))); + itemsSchema = anyOf(variants); + } + + attachArrayItems(i, so, cei, itemsSchema); + } + private static @NotNull ArrayList> getSchemaObjects(Model m, MainInfo main, ChildElementInfo cei) { var sos = new ArrayList>(); @@ -308,14 +341,14 @@ private void processMCChilds(Model m, MainInfo main, ElementInfo i, AbstractSche sos.add(object() .title("componentRef") .additionalProperties(false) - .property( string("$ref"))); + .property(string("$ref"))); return sos; } private boolean isComponentsList(ElementInfo parent, ChildElementInfo cei) { return COMPONENTS.equals(parent.getAnnotation().name()) - && parent.getAnnotation().noEnvelope() - && COMPONENTS.equals(cei.getPropertyName()); + && parent.getAnnotation().noEnvelope() + && COMPONENTS.equals(cei.getPropertyName()); } private boolean shouldGenerateFlowParserType(ChildElementInfo cei) { @@ -450,6 +483,51 @@ private boolean hasComponentChild(ElementInfo parent, MainInfo main) { return false; } + private boolean shouldInlineListItems(MainInfo main, ChildElementInfo cei) { + if (!cei.isList()) return false; + if (cei.getAnnotation().allowForeign()) return false; + + var decl = getChildElementDeclarationInfo(main, cei); + if (decl == null) return false; + + var eis = decl.getElementInfo().stream().filter(ei -> !ei.getAnnotation().topLevel()).toList(); + + // Only inline if there is exactly ONE possible list-item element type (no inheritance etc.) + if (eis.size() != 1) return false; + + var ei = eis.getFirst(); + + if (ei.getAnnotation().collapsed()) return false; + if (ei.getAnnotation().noEnvelope()) return false; + if (ei.isString()) return false; + + return hasAnyConfigurableProperty(ei, main); + } + + private void attachArrayItems(ElementInfo parentEi, AbstractSchema parentSchema, ChildElementInfo cei, AbstractSchema itemsSchema) { + // noEnvelope list: parent is an array already + if (parentEi.getAnnotation().noEnvelope() && parentSchema instanceof SchemaArray sa) { + sa.items(itemsSchema); + return; + } + + if (parentSchema instanceof SchemaObject so) { + so.property(array(cei.getPropertyName()) + .items(itemsSchema) + .required(cei.isRequired()) + .description(getDescriptionContent(cei))); + } + } + + private boolean hasAnyConfigurableProperty(ElementInfo ei, MainInfo main) { + return ei.getAis().stream() + .filter(ai -> !ai.excludedFromJsonSchema()) + .anyMatch(ai -> !"id".equals(ai.getXMLName())) + || ei.getTci() != null + || !ei.getChildElementSpecs().isEmpty() + || hasComponentChild(ei, main); + } + // For description. Probably we'll include that later. (Temporarily deactivated!) private String getDescriptionAsText(AbstractJavadocedInfo elementInfo) { return escapeJsonContent(getDescriptionContent(elementInfo).replaceAll("<[^>]+>", "").replaceAll("\\s+", " ").trim()); diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java index 891578015f..8b3291d1c3 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java +++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java @@ -31,17 +31,18 @@ import java.io.IOException; import java.io.InputStream; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; +import java.lang.reflect.*; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Objects; import static com.predic8.membrane.annot.yaml.McYamlIntrospector.*; +import static com.predic8.membrane.annot.yaml.MethodSetter.getCollectionElementType; import static com.predic8.membrane.annot.yaml.MethodSetter.getMethodSetter; import static com.predic8.membrane.annot.yaml.NodeValidationUtils.*; import static com.predic8.membrane.annot.yaml.YamlParsingUtils.*; +import static java.lang.reflect.Modifier.isAbstract; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.List.of; import static java.util.UUID.randomUUID; @@ -226,7 +227,8 @@ private static void populateObjectFields(ParsingContext ctx, Class cla } private static T handleNoEnvelopeList(ParsingContext ctx, Class clazz, JsonNode node, T configObj) throws IllegalAccessException, InvocationTargetException { - getSingleChildSetter(clazz).invoke(configObj, parseListExcludingStartEvent(ctx, node)); + Method childSetter = getSingleChildSetter(clazz); + childSetter.invoke(configObj, parseListExcludingStartEvent(ctx, node, getCollectionElementType(childSetter))); return configObj; } @@ -299,14 +301,18 @@ private static Object getReferenced(ParsingContext ctx, JsonNode refNode) { } public static List parseListIncludingStartEvent(ParsingContext context, JsonNode node) throws ParsingException { + return parseListIncludingStartEvent(context, node, null); + } + + public static List parseListIncludingStartEvent(ParsingContext context, JsonNode node, Class elemType) throws ParsingException { ensureArray(node); - return parseListExcludingStartEvent(context, node); + return parseListExcludingStartEvent(context, node, elemType); } - private static @NotNull List parseListExcludingStartEvent(ParsingContext context, JsonNode node) throws ParsingException { + private static @NotNull List parseListExcludingStartEvent(ParsingContext context, JsonNode node, Class elemType) throws ParsingException { List res = new ArrayList<>(); for (int i = 0; i < node.size(); i++) { - res.add(parseMapToObj(context, node.get(i))); + res.add(parseListItem(context, node.get(i), elemType)); } return res; } @@ -360,4 +366,37 @@ static Object convertScalarOrSpel(JsonNode node, Class targetType) { if (node == null || !node.isTextual()) return SCALAR_MAPPER.convertValue(node, targetType); return resolveSpelValue(node.asText(), targetType, node); } + + private static Object parseListItem(ParsingContext ctx, JsonNode item, Class elemType) throws ParsingException { + if (item == null || item.isNull()) throw new ParsingException("List items must not be null.", item); + + // Non-object items (scalar/array): only supported for typed element lists (e.g. collapsed items). + if (!item.isObject()) { + return parseInlineListItem(ctx, item, elemType); + } + + // $ref-only object is allowed, but mixing $ref with other fields is not. + JsonNode ref = item.get("$ref"); + if (ref != null) { + if (item.size() == 1) return parseMapToObj(ctx, item); + throw new ParsingException("Cannot mix '$ref' with other fields in a list item.", ref); + } + + // Single-key object: treat as inline if it matches a setter of the element type, otherwise wrapper form. + if (item.size() == 1) { + if (elemType != null && findSetterForKey(elemType, item.fieldNames().next()) != null) { + return parseInlineListItem(ctx, item, elemType); + } + return parseMapToObj(ctx, item); + } + + return parseInlineListItem(ctx, item, elemType); + } + + private static Object parseInlineListItem(ParsingContext ctx, JsonNode node, Class elemType) { + if (elemType == null) throw new ParsingException("Inline list item form requires a typed list element.", node); + if (elemType.isInterface() || isAbstract(elemType.getModifiers())) throw new ParsingException("Inline list item form requires a concrete element type, but found: %s.".formatted(elemType.getName()), node); + return createAndPopulateNode(ctx.updateContext(getElementName(elemType)), elemType, node); + } + } \ No newline at end of file diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/MethodSetter.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/MethodSetter.java index dda8d48f42..5ddf18ef51 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/yaml/MethodSetter.java +++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/MethodSetter.java @@ -189,9 +189,8 @@ private Object coerceNonTextual(ParsingContext ctx, JsonNode node, String key, C private @Nullable List getObjectList(ParsingContext ctx, JsonNode node, String key, Class wanted) { if (Collection.class.isAssignableFrom(wanted)) { - List list = parseListIncludingStartEvent(ctx, node); - Class elemType = getCollectionElementType(setter); + List list = parseListIncludingStartEvent(ctx, node, elemType); if (elemType != null) { for (Object o : list) { if (o == null) continue; @@ -250,7 +249,7 @@ private static > E parseEnum(Class enumClass, JsonNode node } } - private static Class getCollectionElementType(Method setter) { + static Class getCollectionElementType(Method setter) { Type t = setter.getGenericParameterTypes()[0]; if (!(t instanceof ParameterizedType pt)) return null; Type arg = pt.getActualTypeArguments()[0]; diff --git a/annot/src/test/java/com/predic8/membrane/annot/YAMLBeanParsingTest.java b/annot/src/test/java/com/predic8/membrane/annot/YAMLBeanParsingTest.java index 406547a282..6d73d84b2c 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/YAMLBeanParsingTest.java +++ b/annot/src/test/java/com/predic8/membrane/annot/YAMLBeanParsingTest.java @@ -39,12 +39,12 @@ void beanComponentIsInstantiatedAndInjectedViaRefInList() { class: com.predic8.membrane.demo.MyBean scope: singleton constructorArgs: - - constructorArg: { value: "8080" } - - constructorArg: { ref: "#/components/dep" } + - { value: "8080" } + - { ref: "#/components/dep" } properties: - - property: { name: "name", value: "abc" } - - property: { name: "l", value: "7" } - - property: { name: "d", value: "1.5" } + - { name: "name", value: "abc" } + - { name: "l", value: "7" } + - { name: "d", value: "1.5" } --- holder: items: diff --git a/annot/src/test/java/com/predic8/membrane/annot/YAMLParsingTest.java b/annot/src/test/java/com/predic8/membrane/annot/YAMLParsingTest.java index e5b4e379da..7749ed6319 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/YAMLParsingTest.java +++ b/annot/src/test/java/com/predic8/membrane/annot/YAMLParsingTest.java @@ -262,8 +262,7 @@ public void setAttr(String attr) { demo: child1: child: - - child2: - attr: here + - attr: here """), clazz("DemoElement", property("child", clazz("Child1Element", @@ -314,10 +313,8 @@ public void setAttr(String attr) { assertStructure( parseYAML(result, """ demo: - - child1: - attr: here - - child1: - attr: here2 + - attr: here + - attr: here2 """), clazz("DemoElement", property("children", list( @@ -414,9 +411,8 @@ public void setContent(String content) { - demo: child: child: - - child2: - attr: here - content: here2 + - attr: here + content: here2 """), clazz("OuterElement", property("flow", list( @@ -714,6 +710,82 @@ public void destroy() throws Exception { } + @Test + public void noEnvelopeListItemWithNonListChild() { + var sources = splitSources(MC_MAIN_DEMO + """ + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import java.util.List; + + @MCElement(name="demo", noEnvelope=true, topLevel=true, component=false) + public class DemoElement { + List children; + + public List getChildren() { return children; } + + @MCChildElement + public void setChildren(List children) { this.children = children; } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + + @MCElement(name="child1", component=false) + public class Child1Element { + String attr; + ValidatorElement validator; + + public String getAttr() { return attr; } + public ValidatorElement getValidator() { return validator; } + + @MCAttribute + public void setAttr(String attr) { this.attr = attr; } + + @MCChildElement + public void setValidator(ValidatorElement validator) { this.validator = validator; } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + + @MCElement(name="validator", component=false) + public class ValidatorElement { + String type; + + public String getType() { return type; } + + @MCAttribute + public void setType(String type) { this.type = type; } + } + """); + + var result = CompilerHelper.compile(sources, false); + assertCompilerResult(true, result); + + assertStructure( + parseYAML(result, """ + demo: + - attr: here + validator: + type: regex + - attr: here2 + validator: + type: notNull + """), + clazz("DemoElement", + property("children", list( + clazz("Child1Element", + property("attr", value("here")), + property("validator", clazz("ValidatorElement", + property("type", value("regex"))))), + clazz("Child1Element", + property("attr", value("here2")), + property("validator", clazz("ValidatorElement", + property("type", value("notNull"))))) + ))) + ); + } + private Throwable getCause(Throwable e) { if (e.getCause() != null) return getCause(e.getCause()); diff --git a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/SecurityUtils.java b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/SecurityUtils.java index 9131011779..24efb25b92 100644 --- a/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/SecurityUtils.java +++ b/core/src/main/java/com/predic8/membrane/core/interceptor/authentication/SecurityUtils.java @@ -1,3 +1,17 @@ +/* Copyright 2026 predic8 GmbH, www.predic8.com + + 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 com.predic8.membrane.core.interceptor.authentication; import org.apache.commons.codec.digest.Crypt; diff --git a/core/src/test/java/com/predic8/membrane/core/interceptor/authentication/SecurityUtilsTest.java b/core/src/test/java/com/predic8/membrane/core/interceptor/authentication/SecurityUtilsTest.java index 80b9d9992b..9a8f4320d4 100644 --- a/core/src/test/java/com/predic8/membrane/core/interceptor/authentication/SecurityUtilsTest.java +++ b/core/src/test/java/com/predic8/membrane/core/interceptor/authentication/SecurityUtilsTest.java @@ -1,3 +1,17 @@ +/* Copyright 2026 predic8 GmbH, www.predic8.com + + 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 com.predic8.membrane.core.interceptor.authentication; import org.junit.jupiter.api.Test; diff --git a/distribution/examples/loadbalancing/1-static/apis.yaml b/distribution/examples/loadbalancing/1-static/apis.yaml index 22dd38b71b..06fe0a1af3 100644 --- a/distribution/examples/loadbalancing/1-static/apis.yaml +++ b/distribution/examples/loadbalancing/1-static/apis.yaml @@ -4,18 +4,14 @@ api: flow: - balancer: clusters: - - cluster: - name: Production - nodes: # Replace these with your backend nodes. - - node: - host: localhost - port: 4000 - - node: - host: localhost - port: 4001 - - node: - host: localhost - port: 4002 + - name: Production + nodes: # Replace these with your backend nodes. + - host: localhost + port: 4000 + - host: localhost + port: 4001 + - host: localhost + port: 4002 --- # Mock nodes for testing. Remove them in production. diff --git a/distribution/examples/loadbalancing/4-session/apis.yaml b/distribution/examples/loadbalancing/4-session/apis.yaml index 364311e466..73ec45afc7 100644 --- a/distribution/examples/loadbalancing/4-session/apis.yaml +++ b/distribution/examples/loadbalancing/4-session/apis.yaml @@ -8,18 +8,14 @@ api: sessionSource: $.id language: jsonpath clusters: - - cluster: - name: Production - nodes: # Replace these with your backend nodes. - - node: - host: localhost - port: 4000 - - node: - host: localhost - port: 4001 - - node: - host: localhost - port: 4002 + - name: Production + nodes: # Replace these with your backend nodes. + - host: localhost + port: 4000 + - host: localhost + port: 4001 + - host: localhost + port: 4002 --- # Mock nodes for testing. Remove them in production. diff --git a/distribution/examples/loadbalancing/5-multiple/apis.yaml b/distribution/examples/loadbalancing/5-multiple/apis.yaml index 9d7abc8f21..120a9ba7a7 100644 --- a/distribution/examples/loadbalancing/5-multiple/apis.yaml +++ b/distribution/examples/loadbalancing/5-multiple/apis.yaml @@ -8,15 +8,12 @@ api: - balancer: name: balancer1 clusters: - - cluster: - name: Default - nodes: # target 1 and 2 that are balanced by balancer 1 - - node: - host: localhost - port: 4000 - - node: - host: localhost - port: 4001 + - name: Default + nodes: # target 1 and 2 that are balanced by balancer 1 + - host: localhost + port: 4000 + - host: localhost + port: 4001 --- @@ -29,15 +26,12 @@ api: - balancer: name: balancer2 clusters: - - cluster: - name: Default - nodes: # target 3 and 4 that are balanced by balancer 2 - - node: - host: localhost - port: 4002 - - node: - host: localhost - port: 4003 + - name: Default + nodes: # target 3 and 4 that are balanced by balancer 2 + - host: localhost + port: 4002 + - host: localhost + port: 4003 --- diff --git a/distribution/examples/logging/access/apis.yaml b/distribution/examples/logging/access/apis.yaml index 5873dc9eeb..cd0258e7dc 100644 --- a/distribution/examples/logging/access/apis.yaml +++ b/distribution/examples/logging/access/apis.yaml @@ -4,12 +4,10 @@ api: flow: - accessLog: additionalPatternList: - - additionalVariable: - name: res.contentType - expression: response?.header['Content-Type'] - - additionalVariable: - name: forwarded - expression: headers['x-forwarded-for'] + - name: res.contentType + expression: response?.header['Content-Type'] + - name: forwarded + expression: headers['x-forwarded-for'] - return: status: 200 contentType: application/json diff --git a/distribution/examples/logging/jdbc-database/apis.yaml b/distribution/examples/logging/jdbc-database/apis.yaml index ec58379c9b..700a3b6b87 100644 --- a/distribution/examples/logging/jdbc-database/apis.yaml +++ b/distribution/examples/logging/jdbc-database/apis.yaml @@ -5,18 +5,14 @@ components: bean: class: org.apache.commons.dbcp2.BasicDataSource properties: - - property: - name: driverClassName - value: org.h2.Driver - - property: - name: url - value: jdbc:h2:./membranedb;AUTO_SERVER=TRUE - - property: - name: username - value: membrane - - property: - name: password - value: secret + - name: driverClassName + value: org.h2.Driver + - name: url + value: jdbc:h2:./membranedb;AUTO_SERVER=TRUE + - name: username + value: membrane + - name: password + value: secret --- diff --git a/distribution/examples/offline/apis.yaml b/distribution/examples/offline/apis.yaml index 9924e2473b..80d0fa3e1d 100644 --- a/distribution/examples/offline/apis.yaml +++ b/distribution/examples/offline/apis.yaml @@ -26,11 +26,10 @@ api: api: port: 2000 specs: - - openapi: - location: ../../conf/openapi/fruitshop-v2-2-0.oas.yml - rewrite: - port: 3000 - host: localhost + - location: ../../conf/openapi/fruitshop-v2-2-0.oas.yml + rewrite: + port: 3000 + host: localhost # Use this target instead of any server URL in the OpenAPI spec target: url: http://localhost:3000 diff --git a/distribution/examples/openapi/jwt-auth/apis.yaml b/distribution/examples/openapi/jwt-auth/apis.yaml index 070c08d66c..f48d5c4387 100644 --- a/distribution/examples/openapi/jwt-auth/apis.yaml +++ b/distribution/examples/openapi/jwt-auth/apis.yaml @@ -22,14 +22,12 @@ api: name: Protected API port: 2001 specs: - - openapi: - location: secure-shop-api.yml - validateSecurity: true + - location: secure-shop-api.yml + validateSecurity: true flow: - jwtAuth: expectedAud: shop jwks: jwks: - - jwk: - location: jwk.json + - location: jwk.json - openapiValidator: {} diff --git a/distribution/examples/openapi/openapi-proxy/apis.yaml b/distribution/examples/openapi/openapi-proxy/apis.yaml index 6199dbe9fc..b9e3ccd7fc 100644 --- a/distribution/examples/openapi/openapi-proxy/apis.yaml +++ b/distribution/examples/openapi/openapi-proxy/apis.yaml @@ -2,8 +2,7 @@ api: port: 2000 specs: - - openapi: - location: fruitshop-api.yml - validateRequests: yes - validateResponses: yes - validationDetails: yes + - location: fruitshop-api.yml + validateRequests: yes + validateResponses: yes + validationDetails: yes diff --git a/distribution/examples/openapi/validation-simple/apis.yaml b/distribution/examples/openapi/validation-simple/apis.yaml index f9d1d57269..5c6e298bbf 100644 --- a/distribution/examples/openapi/validation-simple/apis.yaml +++ b/distribution/examples/openapi/validation-simple/apis.yaml @@ -3,9 +3,8 @@ api: port: 2000 specs: - - openapi: - location: contacts-api-v1.yml - validateRequests: yes + - location: contacts-api-v1.yml + validateRequests: yes --- # This proxy provides a mock backend implementation for the API. diff --git a/distribution/examples/openapi/validation/apis.yaml b/distribution/examples/openapi/validation/apis.yaml index c0d1484696..e9fe3810a2 100644 --- a/distribution/examples/openapi/validation/apis.yaml +++ b/distribution/examples/openapi/validation/apis.yaml @@ -3,11 +3,10 @@ api: port: 2000 specs: - - openapi: - location: contacts-xxl-api-v1.yml - validateRequests: yes - validateResponses: no - validationDetails: yes + - location: contacts-xxl-api-v1.yml + validateRequests: yes + validateResponses: no + validationDetails: yes --- # These proxies provides mock backend implementations for the API in this demo. diff --git a/distribution/examples/routing-traffic/rewriter/openapi/apis.yaml b/distribution/examples/routing-traffic/rewriter/openapi/apis.yaml index d1397b371f..ab2d86fa76 100644 --- a/distribution/examples/routing-traffic/rewriter/openapi/apis.yaml +++ b/distribution/examples/routing-traffic/rewriter/openapi/apis.yaml @@ -2,10 +2,9 @@ api: port: 2000 specs: - - openapi: - location: demo-api-v1.yml - validateRequests: yes - rewrite: - host: predic8.de - port: 3000 - basePath: /foo + - location: demo-api-v1.yml + validateRequests: yes + rewrite: + host: predic8.de + port: 3000 + basePath: /foo diff --git a/distribution/examples/routing-traffic/rewriter/regex/apis.yaml b/distribution/examples/routing-traffic/rewriter/regex/apis.yaml index 58064abf2c..f2f865b9a1 100644 --- a/distribution/examples/routing-traffic/rewriter/regex/apis.yaml +++ b/distribution/examples/routing-traffic/rewriter/regex/apis.yaml @@ -3,9 +3,8 @@ api: port: 2000 flow: - rewriter: - - map: - from: ^/store/(.*) - to: /shop/v2/$1 + - from: ^/store/(.*) + to: /shop/v2/$1 target: host: api.predic8.de port: 443 diff --git a/distribution/examples/security/api-key/apikey-openapi/apis.yaml b/distribution/examples/security/api-key/apikey-openapi/apis.yaml index 00e7aa775f..9e3d22d151 100644 --- a/distribution/examples/security/api-key/apikey-openapi/apis.yaml +++ b/distribution/examples/security/api-key/apikey-openapi/apis.yaml @@ -9,9 +9,8 @@ components: api: port: 2000 specs: - - openapi: - location: fruitshop-api-v2-openapi-3-security.yml - validateSecurity: true + - location: fruitshop-api-v2-openapi-3-security.yml + validateSecurity: true flow: - apiKey: # API keys are validated in the OpenAPI validator with validateSecurity: true. See the OpenAPI document for details. diff --git a/distribution/examples/security/jwt/apikey-to-jwt-conversion/apis.yaml b/distribution/examples/security/jwt/apikey-to-jwt-conversion/apis.yaml index 5a4850c92d..2290f89c12 100644 --- a/distribution/examples/security/jwt/apikey-to-jwt-conversion/apis.yaml +++ b/distribution/examples/security/jwt/apikey-to-jwt-conversion/apis.yaml @@ -36,8 +36,7 @@ api: expectedAud: order jwks: jwks: - - jwk: - location: jwk.json + - location: jwk.json - template: src: | You accessed protected content! diff --git a/distribution/examples/security/jwt/verification/apis.yaml b/distribution/examples/security/jwt/verification/apis.yaml index 4baf297090..70c2cd39e0 100644 --- a/distribution/examples/security/jwt/verification/apis.yaml +++ b/distribution/examples/security/jwt/verification/apis.yaml @@ -7,8 +7,7 @@ api: expectedAud: order jwks: jwks: - - jwk: - location: jwk.json + - location: jwk.json - template: src: | You accessed protected content! diff --git a/distribution/examples/security/oauth2/api/authorization_server/apis.yaml b/distribution/examples/security/oauth2/api/authorization_server/apis.yaml index 2962753733..07ef2539d2 100644 --- a/distribution/examples/security/oauth2/api/authorization_server/apis.yaml +++ b/distribution/examples/security/oauth2/api/authorization_server/apis.yaml @@ -14,20 +14,17 @@ api: email: john@predic8.de staticClientList: clients: - - client: - clientId: abc - clientSecret: def - callbackUrl: http://localhost:2000/oauth2callback + - clientId: abc + clientSecret: def + callbackUrl: http://localhost:2000/oauth2callback bearerToken: {} claims: value: aud email iss sub username scopes: - - scope: - id: username - claims: username - - scope: - id: profile - claims: username email + - id: username + claims: username + - id: profile + claims: username email --- diff --git a/distribution/examples/security/oauth2/credentials/authorization_server/apis.yaml b/distribution/examples/security/oauth2/credentials/authorization_server/apis.yaml index f7a80c78fc..61355e051d 100644 --- a/distribution/examples/security/oauth2/credentials/authorization_server/apis.yaml +++ b/distribution/examples/security/oauth2/credentials/authorization_server/apis.yaml @@ -14,22 +14,19 @@ api: email: john@predic8.de staticClientList: clients: - - client: - clientId: abc - clientSecret: def - callbackUrl: http://localhost:2000/oauth2callback + - clientId: abc + clientSecret: def + callbackUrl: http://localhost:2000/oauth2callback # Generates tokens in the given format bearerToken: {} # Scopes are defined from the claims exposed above claims: value: aud email iss sub username scopes: - - scope: - id: username - claims: username - - scope: - id: profile - claims: username email + - id: username + claims: username + - id: profile + claims: username email --- diff --git a/distribution/examples/security/oauth2/membrane/authorization_server/apis.yaml b/distribution/examples/security/oauth2/membrane/authorization_server/apis.yaml index f70475ff29..dcb667f770 100644 --- a/distribution/examples/security/oauth2/membrane/authorization_server/apis.yaml +++ b/distribution/examples/security/oauth2/membrane/authorization_server/apis.yaml @@ -16,22 +16,19 @@ api: email: john@predic8.de staticClientList: clients: - - client: - clientId: abc - clientSecret: def - callbackUrl: http://localhost:2000/oauth2callback + - clientId: abc + clientSecret: def + callbackUrl: http://localhost:2000/oauth2callback # Generates tokens in the given format bearerToken: {} claims: value: aud email iss sub username # Scopes are defined from the claims exposed above scopes: - - scope: - id: username - claims: username - - scope: - id: profile - claims: username email + - id: username + claims: username + - id: profile + claims: username email --- diff --git a/distribution/examples/security/ssl-tls/api-with-tls-pem/apis.yaml b/distribution/examples/security/ssl-tls/api-with-tls-pem/apis.yaml index 5c44007712..565c236ac9 100644 --- a/distribution/examples/security/ssl-tls/api-with-tls-pem/apis.yaml +++ b/distribution/examples/security/ssl-tls/api-with-tls-pem/apis.yaml @@ -8,8 +8,7 @@ api: private: location: membrane-key.pem certificates: - - certificate: - location: membrane.pem + - location: membrane.pem # Route here to your target target: host: localhost diff --git a/distribution/examples/validation/form/apis.yaml b/distribution/examples/validation/form/apis.yaml index 1850daab0c..9a284f9e79 100644 --- a/distribution/examples/validation/form/apis.yaml +++ b/distribution/examples/validation/form/apis.yaml @@ -4,8 +4,7 @@ api: flow: - formValidation: fields: - - field: - name: name - regex: '[a-zA-Z]+' + - name: name + regex: '[a-zA-Z]+' target: url: https://api.predic8.de/shop/v2/products diff --git a/distribution/examples/validation/json-schema/apis.yaml b/distribution/examples/validation/json-schema/apis.yaml index 7611cb8fad..bc3c6f10ee 100644 --- a/distribution/examples/validation/json-schema/apis.yaml +++ b/distribution/examples/validation/json-schema/apis.yaml @@ -20,12 +20,10 @@ api: jsonSchema: schemas/schema2001.json schemaMappings: schemas: - - schema: - id: urn:app:base_def - location: schemas/base.json - - schema: - id: urn:app:meta_def - location: schemas/meta.json + - id: urn:app:base_def + location: schemas/base.json + - id: urn:app:meta_def + location: schemas/meta.json target: host: localhost port: 2002 diff --git a/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/ConfigurationExampleTest.java b/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/ConfigurationExampleTest.java index 29b0c3b42c..0397496cd3 100644 --- a/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/ConfigurationExampleTest.java +++ b/distribution/src/test/java/com/predic8/membrane/examples/withinternet/test/ConfigurationExampleTest.java @@ -1,3 +1,17 @@ +/* Copyright 2026 predic8 GmbH, www.predic8.com + + 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 com.predic8.membrane.examples.withinternet.test; import com.predic8.membrane.examples.util.AbstractSampleMembraneStartStopTestcase; diff --git a/distribution/tutorials/advanced/30-Path-Rewriting.yaml b/distribution/tutorials/advanced/30-Path-Rewriting.yaml index bbc3b14fe3..774740b318 100644 --- a/distribution/tutorials/advanced/30-Path-Rewriting.yaml +++ b/distribution/tutorials/advanced/30-Path-Rewriting.yaml @@ -14,10 +14,9 @@ api: - log: message: Requested ${path} - rewriter: - - map: - # Put values in quotes when using special characters - from: "/fruits/(.*)" - to: "/shop/v2/products/$1" + # Put values in quotes when using special characters + - from: "/fruits/(.*)" + to: "/shop/v2/products/$1" - log: message: Rewritten ${path} target: diff --git a/distribution/tutorials/advanced/50-Redirects.yaml b/distribution/tutorials/advanced/50-Redirects.yaml index e891bd581e..cdb856f778 100644 --- a/distribution/tutorials/advanced/50-Redirects.yaml +++ b/distribution/tutorials/advanced/50-Redirects.yaml @@ -21,7 +21,6 @@ api: - log: message: Requested ${path} - rewriter: - - map: - from: "/fruits/(.*)" - to: "https://api.predic8.de/shop/v2/products/$1" - do: redirect_permanent + - from: "/fruits/(.*)" + to: "https://api.predic8.de/shop/v2/products/$1" + do: redirect_permanent diff --git a/distribution/tutorials/getting-started/80-OpenAPI.yaml b/distribution/tutorials/getting-started/80-OpenAPI.yaml index 54567b89eb..a47ecc4d78 100644 --- a/distribution/tutorials/getting-started/80-OpenAPI.yaml +++ b/distribution/tutorials/getting-started/80-OpenAPI.yaml @@ -18,7 +18,5 @@ api: port: 2000 specs: - - openapi: - location: https://api.predic8.de/api-docs/dlp-field-information-api-v1-0-0 - - openapi: - location: ../../conf/openapi/fruitshop-v2-2-0.oas.yml + - location: https://api.predic8.de/api-docs/dlp-field-information-api-v1-0-0 + - location: ../../conf/openapi/fruitshop-v2-2-0.oas.yml diff --git a/distribution/tutorials/getting-started/90-OpenAPI-Validation.yaml b/distribution/tutorials/getting-started/90-OpenAPI-Validation.yaml index 67bf04ff76..697531971f 100644 --- a/distribution/tutorials/getting-started/90-OpenAPI-Validation.yaml +++ b/distribution/tutorials/getting-started/90-OpenAPI-Validation.yaml @@ -10,8 +10,7 @@ api: port: 2000 specs: - - openapi: - location: ../../conf/openapi/fruitshop-v2-2-0.oas.yml - validateRequests: true - validateResponses: true - validationDetails: true + - location: ../../conf/openapi/fruitshop-v2-2-0.oas.yml + validateRequests: true + validateResponses: true + validationDetails: true diff --git a/distribution/tutorials/orchestration/40-Authentication-Call.yaml b/distribution/tutorials/orchestration/40-Authentication-Call.yaml index b882dcb815..3ee8dd6006 100644 --- a/distribution/tutorials/orchestration/40-Authentication-Call.yaml +++ b/distribution/tutorials/orchestration/40-Authentication-Call.yaml @@ -56,9 +56,8 @@ api: # Place authentication here: Basic, API key, OAuth2, ... - response: - setCookies: - - cookie: - name: SESSION - value: akj34 + - name: SESSION + value: akj34 - log: message: Login successful. Issue SESSION cookie. - return: diff --git a/distribution/tutorials/security/10-TLS-Termination.yaml b/distribution/tutorials/security/10-TLS-Termination.yaml index 9c08313f83..afa3ec3676 100644 --- a/distribution/tutorials/security/10-TLS-Termination.yaml +++ b/distribution/tutorials/security/10-TLS-Termination.yaml @@ -24,8 +24,7 @@ api: private: location: membrane-key.pem certificates: - - certificate: - location: membrane.pem + - location: membrane.pem flow: - request: # Traffic is decrypted and can be logged or transformed diff --git a/distribution/tutorials/security/20-Central-SSL-Config.yaml b/distribution/tutorials/security/20-Central-SSL-Config.yaml index 5459c833dd..934966ff6b 100644 --- a/distribution/tutorials/security/20-Central-SSL-Config.yaml +++ b/distribution/tutorials/security/20-Central-SSL-Config.yaml @@ -13,8 +13,7 @@ components: private: location: membrane-key.pem certificates: - - certificate: - location: membrane.pem + - location: membrane.pem --- api: diff --git a/distribution/tutorials/soap/90-Manual-SOAPProxy.yaml b/distribution/tutorials/soap/90-Manual-SOAPProxy.yaml index 0443d9aa76..a635ae0611 100644 --- a/distribution/tutorials/soap/90-Manual-SOAPProxy.yaml +++ b/distribution/tutorials/soap/90-Manual-SOAPProxy.yaml @@ -21,10 +21,9 @@ api: flow: # The wsdlRewriter must be the first of the following interceptors. - rewriter: - - map: - # Put values in quotes when using special characters - from: "/my-service/(.*)" - to: "/city-service/$1" + # Put values in quotes when using special characters + - from: "/my-service/(.*)" + to: "/city-service/$1" - wsdlRewriter: host: my.host.example.com protocol: https diff --git a/distribution/tutorials/transformation/60-SOAP-Array-to-JSON.yaml b/distribution/tutorials/transformation/60-SOAP-Array-to-JSON.yaml index 72fc786510..aa41a99c57 100644 --- a/distribution/tutorials/transformation/60-SOAP-Array-to-JSON.yaml +++ b/distribution/tutorials/transformation/60-SOAP-Array-to-JSON.yaml @@ -31,9 +31,8 @@ components: ns: xmlConfig: namespaces: - - namespace: - prefix: f - uri: https://predic8.de/fruits + - prefix: f + uri: https://predic8.de/fruits --- api: diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a10db5a545..47ea69247b 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -67,6 +67,29 @@ PRIO 3: - headerFilter YAML format has changed. - Choose Interceptor configuration - Chain (ChainDef) configuration +- **YAML configuration in list elements**: + * List items can now be written in *inline form* if the list accepts exactly one concrete element type (no polymorphic candidates) and the element is not `collapsed`, not `noEnvelope`, and not string-like. + * Old wrapper form remains supported: `- : { ... }` (Only when schema validation is deactivated). + + Old: + ```yaml + properties: + - property: + name: driverClassName + value: org.h2.Driver + - property: + name: url + value: jdbc:h2:./membranedb;AUTO_SERVER=TRUE + ``` + New: + ```yaml + properties: + - name: driverClassName + value: org.h2.Driver + - name: url + value: jdbc:h2:./membranedb;AUTO_SERVER=TRUE + ``` + ## Bug Fixes - `xml2json`: Ensuring content type alignment and better exception handling.