Skip to content

Commit 0204f9a

Browse files
add singleAttribute option for the @mcelement annotation (#2583)
* add `singleAttribute` option for the @mcelement annotation * refactor: move `isSingleAttribute` method to `McYamlIntrospector` * code optimization
1 parent 3776cb3 commit 0204f9a

7 files changed

Lines changed: 275 additions & 61 deletions

File tree

annot/src/main/java/com/predic8/membrane/annot/MCElement.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,12 @@
6464
* Whether the element should be configurable as part of the interceptor flow
6565
*/
6666
boolean excludeFromFlow() default false;
67+
68+
/**
69+
* Whether the element has only one attribute.
70+
* Enables inline yaml object configuration
71+
* e.g.
72+
* allow: foo
73+
*/
74+
boolean singleAttribute() default false;
6775
}

annot/src/main/java/com/predic8/membrane/annot/generator/JsonSchemaGenerator.java

Lines changed: 47 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -96,21 +96,6 @@ private void addTopLevelProperties(Model m, MainInfo main) {
9696
}
9797
}
9898

99-
private AbstractSchema<?> createTopLevelProperty(ElementInfo e, Model m) {
100-
101-
String name = e.getAnnotation().name();
102-
String refName = "#/$defs/" + e.getXSDTypeName(m);
103-
104-
schema.property(ref(name).ref(refName));
105-
106-
return object()
107-
.title(name)
108-
.additionalProperties(false)
109-
.property(ref(name)
110-
.ref(refName)
111-
.required(true));
112-
}
113-
11499
private void addParserDefinitions(Model m, MainInfo main) {
115100
for (ElementInfo elementInfo : main.getElements().values()) {
116101

@@ -159,9 +144,50 @@ private SchemaObject createParser(Model m, MainInfo main, ElementInfo elementInf
159144
.required(false));
160145
}
161146

147+
// allow scalar inline form for single-attribute elements
148+
if (elementInfo.getAnnotation().singleAttribute()) {
149+
return anyOf(List.of(
150+
parser,
151+
createSingleAttributeInlineVariant(elementInfo)
152+
)).name(parserName);
153+
}
154+
162155
return parser;
163156
}
164157

158+
private AbstractSchema<?> createSingleAttributeInlineVariant(ElementInfo ei) {
159+
// Only count attributes that are actually part of the schema.
160+
var attrs = ei.getAis().stream()
161+
.filter(ai -> !ai.excludedFromJsonSchema())
162+
.toList();
163+
164+
if (attrs.size() != 1) {
165+
throw new ProcessingException(
166+
"@MCElement(singleAttribute=true) requires exactly one @MCAttribute.",
167+
ei.getElement()
168+
);
169+
}
170+
if (ei.getTci() != null || !ei.getChildElementSpecs().isEmpty()) {
171+
throw new ProcessingException(
172+
"@MCElement(singleAttribute=true) must not declare text content or child elements.",
173+
ei.getElement()
174+
);
175+
}
176+
177+
AttributeInfo ai = attrs.getFirst();
178+
String type = ai.getSchemaType(processingEnv.getTypeUtils());
179+
180+
AbstractSchema<?> s = SchemaFactory.from(type)
181+
.type(type)
182+
.description(getDescriptionContent(ai));
183+
184+
if (ai.isEnum(processingEnv.getTypeUtils()) && !"boolean".equals(type)) {
185+
s.enumValues(ai.enumsAsLowerCaseList(processingEnv.getTypeUtils()));
186+
}
187+
188+
return s;
189+
}
190+
165191
private SchemaObject getParserSchemaObject(ElementInfo elementInfo, String parserName) {
166192
return object(parserName)
167193
.additionalProperties( elementInfo.isString())
@@ -255,8 +281,8 @@ private void processMCChilds(Model m, MainInfo main, ElementInfo i, AbstractSche
255281
}
256282
}
257283

258-
private static @NotNull ArrayList<SchemaObject> getSchemaObjects(Model m, MainInfo main, ChildElementInfo cei) {
259-
var sos = new ArrayList<SchemaObject>();
284+
private static @NotNull ArrayList<AbstractSchema<?>> getSchemaObjects(Model m, MainInfo main, ChildElementInfo cei) {
285+
var sos = new ArrayList<AbstractSchema<?>>();
260286

261287
for (ElementInfo ei : main.getChildElementDeclarations().get(cei.getTypeDeclaration()).getElementInfo()) {
262288
if (ei.getAnnotation().excludeFromFlow())
@@ -293,7 +319,7 @@ boolean isFlowFromWebSocket(ChildElementInfo cei) {
293319
return "com.predic8.membrane.core.transport.ws.WebSocketInterceptorInterface".equals(cei.getTypeDeclaration().getQualifiedName().toString());
294320
}
295321

296-
private AbstractSchema<?> processList(ElementInfo i, AbstractSchema<?> so, ChildElementInfo cei, ArrayList<SchemaObject> sos) {
322+
private AbstractSchema<?> processList(ElementInfo i, AbstractSchema<?> so, ChildElementInfo cei, ArrayList<AbstractSchema<?>> sos) {
297323
SchemaObject items = object("items");
298324

299325
if (shouldGenerateFlowParserType(cei)) {
@@ -314,7 +340,7 @@ private AbstractSchema<?> processList(ElementInfo i, AbstractSchema<?> so, Child
314340
return items;
315341
}
316342

317-
private void addFlowParserRef(AbstractSchema<?> so, List<SchemaObject> sos) {
343+
private void addFlowParserRef(AbstractSchema<?> so, List<AbstractSchema<?>> sos) {
318344
if (!flowDefCreated) {
319345
schema.definition(array("flowParser").items(anyOf(sos)));
320346
flowDefCreated = true;
@@ -386,8 +412,8 @@ private SchemaObject createComponentsMapParser(Model m, MainInfo main, ElementIn
386412
return parser;
387413
}
388414

389-
private static @NotNull ArrayList<SchemaObject> getComponents(Model m, MainInfo main) {
390-
var variants = new ArrayList<SchemaObject>();
415+
private static @NotNull ArrayList<AbstractSchema<?>> getComponents(Model m, MainInfo main) {
416+
var variants = new ArrayList<AbstractSchema<?>>();
391417

392418
for (ElementInfo comp : main.getElements().values()) {
393419
if (!comp.getAnnotation().component()) continue;

annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/model/AnyOf.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@
2121

2222
public class AnyOf extends SchemaObject {
2323

24-
private final List<SchemaObject> anyOfs;
24+
private final List<AbstractSchema<?>> anyOfs;
2525

26-
AnyOf(List<SchemaObject> anyOfs) {
26+
AnyOf(List<AbstractSchema<?>> anyOfs) {
2727
super(null);
2828
this.anyOfs = anyOfs;
2929
}
@@ -35,7 +35,7 @@ public ObjectNode json(ObjectNode node) {
3535

3636
private ArrayNode getAnyNode() {
3737
ArrayNode list = jnf.arrayNode();
38-
for (SchemaObject anyOf : anyOfs) {
38+
for (AbstractSchema<?> anyOf : anyOfs) {
3939
list.add(anyOf.json(jnf.objectNode()));
4040
}
4141
return list;

annot/src/main/java/com/predic8/membrane/annot/generator/kubernetes/model/SchemaFactory.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public static SchemaRef ref(String ref) {
6161
return new SchemaRef(ref);
6262
}
6363

64-
public static AnyOf anyOf(List<SchemaObject> anyOfs) {
64+
public static AnyOf anyOf(List<AbstractSchema<?>> anyOfs) {
6565
var anyOf = new AnyOf(anyOfs);
6666
anyOf.name = "anyOf";
6767
return anyOf;

annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,37 +13,53 @@
1313
limitations under the License. */
1414
package com.predic8.membrane.annot.yaml;
1515

16-
import com.fasterxml.jackson.core.*;
17-
import com.fasterxml.jackson.databind.*;
18-
import com.fasterxml.jackson.databind.node.*;
19-
import com.networknt.schema.*;
16+
import com.fasterxml.jackson.core.JsonLocation;
17+
import com.fasterxml.jackson.core.JsonParseException;
18+
import com.fasterxml.jackson.databind.JsonNode;
19+
import com.fasterxml.jackson.databind.ObjectMapper;
20+
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
21+
import com.fasterxml.jackson.databind.node.ObjectNode;
2022
import com.networknt.schema.Error;
21-
import com.predic8.membrane.annot.*;
23+
import com.networknt.schema.Schema;
24+
import com.networknt.schema.SchemaLocation;
25+
import com.networknt.schema.SchemaRegistry;
26+
import com.predic8.membrane.annot.Grammar;
27+
import com.predic8.membrane.annot.MCAttribute;
28+
import com.predic8.membrane.annot.beanregistry.BeanDefinition;
29+
import com.predic8.membrane.annot.beanregistry.BeanLifecycleManager;
30+
import com.predic8.membrane.annot.beanregistry.BeanRegistry;
31+
import com.predic8.membrane.annot.beanregistry.BeanRegistryAware;
2232
import jakarta.annotation.PostConstruct;
2333
import jakarta.annotation.PreDestroy;
24-
import com.predic8.membrane.annot.beanregistry.*;
25-
import org.jetbrains.annotations.*;
26-
import org.slf4j.*;
34+
import org.jetbrains.annotations.NotNull;
35+
import org.slf4j.Logger;
36+
import org.slf4j.LoggerFactory;
2737
import org.springframework.util.ReflectionUtils;
2838

29-
import java.io.*;
30-
import java.lang.reflect.*;
31-
import java.util.*;
39+
import java.io.IOException;
40+
import java.io.InputStream;
41+
import java.lang.reflect.InvocationTargetException;
42+
import java.lang.reflect.Method;
43+
import java.util.ArrayList;
44+
import java.util.Iterator;
45+
import java.util.List;
3246

33-
import static com.networknt.schema.SpecificationVersion.*;
47+
import static com.networknt.schema.SpecificationVersion.DRAFT_2020_12;
3448
import static com.predic8.membrane.annot.yaml.McYamlIntrospector.*;
35-
import static com.predic8.membrane.annot.yaml.MethodSetter.*;
49+
import static com.predic8.membrane.annot.yaml.MethodSetter.getMethodSetter;
3650
import static com.predic8.membrane.annot.yaml.NodeValidationUtils.*;
37-
import static java.nio.charset.StandardCharsets.*;
51+
import static java.nio.charset.StandardCharsets.UTF_8;
3852
import static java.util.List.of;
39-
import static java.util.UUID.*;
53+
import static java.util.UUID.randomUUID;
4054

4155
public class GenericYamlParser {
4256
private static final Logger log = LoggerFactory.getLogger(GenericYamlParser.class);
4357
private static final String EMPTY_DOCUMENT_WARNING = "Skipping empty document. Maybe there are two --- separators but no configuration in between.";
4458

4559
private final List<BeanDefinition> beanDefs = new ArrayList<>();
4660

61+
private static final ObjectMapper SCALAR_MAPPER = new ObjectMapper();
62+
4763
/**
4864
* Parses one or more YAML documents into bean definitions.
4965
* <p>
@@ -171,6 +187,17 @@ public static <T> T createAndPopulateNode(ParsingContext<?> ctx, Class<T> clazz,
171187
method.invoke(configObj, parseListExcludingStartEvent(ctx, node));
172188
return configObj;
173189
}
190+
191+
// scalar inline form for @MCElement(singleAttribute=true)
192+
if (!node.isObject()) {
193+
if (isSingleAttribute(clazz)) {
194+
applySingleAttributeScalar(clazz, node, configObj);
195+
return handlePostConstructAndPreDestroy(ctx, configObj);
196+
}
197+
// anything non-object is invalid here
198+
ensureMappingStart(node);
199+
}
200+
174201
ensureMappingStart(node);
175202
if (isNoEnvelope(clazz))
176203
throw new RuntimeException("Class " + clazz.getName() + " is annotated with @MCElement(noEnvelope=true), but the YAML/JSON structure does not contain a list.");
@@ -334,4 +361,46 @@ private static <T> T handlePostConstructAndPreDestroy(ParsingContext<?> ctx, T b
334361
});
335362
return bean;
336363
}
364+
365+
private static <T> void applySingleAttributeScalar(Class<T> clazz, JsonNode node, T target) {
366+
if (node == null || node.isNull()) {
367+
throw new ParsingException("singleAttribute element must not be null.", node);
368+
}
369+
370+
Method setter = findSingleAttributeSetter(clazz);
371+
372+
Class<?> paramType = setter.getParameterTypes()[0];
373+
Object value;
374+
try {
375+
value = SCALAR_MAPPER.convertValue(node, paramType);
376+
} catch (IllegalArgumentException e) {
377+
throw new ParsingException("Cannot convert scalar value to " + paramType.getSimpleName() + ".", node);
378+
}
379+
380+
try {
381+
setter.setAccessible(true);
382+
setter.invoke(target, value);
383+
} catch (InvocationTargetException e) {
384+
throw new ParsingException(e.getTargetException(), node);
385+
} catch (Throwable t) {
386+
throw new ParsingException(t, node);
387+
}
388+
}
389+
390+
private static Method findSingleAttributeSetter(Class<?> clazz) {
391+
List<Method> setters = new ArrayList<>();
392+
ReflectionUtils.doWithMethods(clazz, m -> {
393+
if (m.isAnnotationPresent(MCAttribute.class) && m.getParameterCount() == 1) {
394+
setters.add(m);
395+
}
396+
});
397+
398+
if (setters.size() != 1) {
399+
throw new ParsingException(
400+
"@MCElement(singleAttribute=true) requires exactly one @MCAttribute setter, but found " + setters.size() + ".",
401+
JsonNodeFactory.instance.nullNode()
402+
);
403+
}
404+
return setters.getFirst();
405+
}
337406
}

annot/src/main/java/com/predic8/membrane/annot/yaml/McYamlIntrospector.java

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,15 @@
1515
package com.predic8.membrane.annot.yaml;
1616

1717
import com.predic8.membrane.annot.*;
18-
import org.jetbrains.annotations.*;
18+
import org.jetbrains.annotations.NotNull;
1919

20-
import java.lang.reflect.*;
21-
import java.util.*;
22-
import java.util.concurrent.atomic.AtomicInteger;
23-
import java.util.stream.*;
20+
import java.lang.reflect.Method;
21+
import java.util.List;
22+
import java.util.stream.Collectors;
2423

25-
import static java.lang.Character.*;
26-
import static java.util.Arrays.*;
27-
import static org.springframework.core.annotation.AnnotationUtils.*;
24+
import static java.lang.Character.toLowerCase;
25+
import static java.util.Arrays.stream;
26+
import static org.springframework.core.annotation.AnnotationUtils.findAnnotation;
2827

2928
public final class McYamlIntrospector {
3029

@@ -147,15 +146,15 @@ public static String getSetterName(Method setter) {
147146
}
148147

149148
public static boolean hasOtherAttributes(Class<?> clazz) {
150-
return stream(clazz.getMethods()).filter(m -> m.isAnnotationPresent(MCOtherAttributes.class)).count() > 0;
149+
return stream(clazz.getMethods()).anyMatch(m -> m.isAnnotationPresent(MCOtherAttributes.class));
151150
}
152151

153152
public static boolean hasAttributes(Class<?> clazz) {
154-
return stream(clazz.getMethods()).filter(m -> m.isAnnotationPresent(MCAttribute.class)).count() > 0;
153+
return stream(clazz.getMethods()).anyMatch(m -> m.isAnnotationPresent(MCAttribute.class));
155154
}
156155

157156
public static boolean hasChildren(Class<?> clazz) {
158-
return stream(clazz.getMethods()).filter(m -> m.isAnnotationPresent(MCChildElement.class)).count() > 0;
157+
return stream(clazz.getMethods()).anyMatch(m -> m.isAnnotationPresent(MCChildElement.class));
159158
}
160159

161160
public static <T> Method getAnySetter(Class<T> clazz) {
@@ -195,4 +194,9 @@ public static String getElementName(Class<?> type) {
195194
return type.getSimpleName();
196195
}
197196

197+
public static boolean isSingleAttribute(Class<?> clazz) {
198+
MCElement el = clazz.getAnnotation(MCElement.class);
199+
return el != null && el.singleAttribute();
200+
}
201+
198202
}

0 commit comments

Comments
 (0)