Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down Expand Up @@ -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);
}
Comment thread
christiangoerdes marked this conversation as resolved.
}

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<AbstractSchema<?>>();
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<AbstractSchema<?>> getSchemaObjects(Model m, MainInfo main, ChildElementInfo cei) {
var sos = new ArrayList<AbstractSchema<?>>();

Expand All @@ -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;
Comment thread
christiangoerdes marked this conversation as resolved.
}

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) {
Expand Down Expand Up @@ -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);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -226,7 +227,8 @@ private static <T> void populateObjectFields(ParsingContext<?> ctx, Class<T> cla
}

private static <T> T handleNoEnvelopeList(ParsingContext<?> ctx, Class<T> 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;
}

Expand Down Expand Up @@ -299,14 +301,18 @@ private static Object getReferenced(ParsingContext<?> ctx, JsonNode refNode) {
}

public static List<Object> parseListIncludingStartEvent(ParsingContext<?> context, JsonNode node) throws ParsingException {
return parseListIncludingStartEvent(context, node, null);
}

public static List<Object> parseListIncludingStartEvent(ParsingContext<?> context, JsonNode node, Class<?> elemType) throws ParsingException {
ensureArray(node);
return parseListExcludingStartEvent(context, node);
return parseListExcludingStartEvent(context, node, elemType);
}

private static @NotNull List<Object> parseListExcludingStartEvent(ParsingContext<?> context, JsonNode node) throws ParsingException {
private static @NotNull List<Object> parseListExcludingStartEvent(ParsingContext<?> context, JsonNode node, Class<?> elemType) throws ParsingException {
List<Object> 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;
}
Expand Down Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -189,9 +189,8 @@ private Object coerceNonTextual(ParsingContext ctx, JsonNode node, String key, C

private @Nullable List<Object> getObjectList(ParsingContext ctx, JsonNode node, String key, Class<?> wanted) {
if (Collection.class.isAssignableFrom(wanted)) {
List<Object> list = parseListIncludingStartEvent(ctx, node);

Class<?> elemType = getCollectionElementType(setter);
List<Object> list = parseListIncludingStartEvent(ctx, node, elemType);
if (elemType != null) {
for (Object o : list) {
if (o == null) continue;
Expand Down Expand Up @@ -250,7 +249,7 @@ private static <E extends Enum<E>> 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];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,7 @@ public void setAttr(String attr) {
demo:
child1:
child:
- child2:
attr: here
- attr: here
"""),
clazz("DemoElement",
property("child", clazz("Child1Element",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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<Child1Element> children;

public List<Child1Element> getChildren() { return children; }

@MCChildElement
public void setChildren(List<Child1Element> 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());
Expand Down
Loading
Loading