Add InputStreamNormalizer
[yangtools.git] / codec / yang-data-codec-gson / src / test / java / org / opendaylight / yangtools / yang / data / codec / gson / InputStreamNormalizerTest.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.yang.data.codec.gson;
9
10 import static org.junit.jupiter.api.Assertions.assertEquals;
11 import static org.junit.jupiter.api.Assertions.assertThrows;
12
13 import java.io.ByteArrayInputStream;
14 import java.io.InputStream;
15 import java.nio.charset.StandardCharsets;
16 import java.util.List;
17 import java.util.Map;
18 import org.eclipse.jdt.annotation.NonNull;
19 import org.junit.jupiter.api.Test;
20 import org.junit.jupiter.api.function.Executable;
21 import org.opendaylight.yangtools.yang.common.ErrorSeverity;
22 import org.opendaylight.yangtools.yang.common.ErrorTag;
23 import org.opendaylight.yangtools.yang.common.ErrorType;
24 import org.opendaylight.yangtools.yang.common.QName;
25 import org.opendaylight.yangtools.yang.common.Uint32;
26 import org.opendaylight.yangtools.yang.common.UnresolvedQName.Unqualified;
27 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
28 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
29 import org.opendaylight.yangtools.yang.data.api.YangNetconfError;
30 import org.opendaylight.yangtools.yang.data.api.schema.stream.InputStreamNormalizer;
31 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizationException;
32 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
33 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
34 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
35 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
36 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
37 import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
38
39 class InputStreamNormalizerTest {
40     private static final EffectiveModelContext MODEL_CONTEXT = YangParserTestUtils.parseYang("""
41             module foo {
42               yang-version 1.1;
43               prefix foo;
44               namespace foo;
45
46               container foo {
47                 leaf str {
48                   type string {
49                     length 3;
50                   }
51                 }
52               }
53
54               container bar {
55                 leaf uint {
56                   type uint32;
57                 }
58               }
59
60               list baz {
61                 key "one two";
62                 leaf one {
63                   type boolean;
64                 }
65                 leaf two {
66                   type string;
67                 }
68
69                 action qux {
70                   input {
71                     leaf str {
72                       type string;
73                     }
74                   }
75                 }
76               }
77
78               rpc thud {
79                 input {
80                   leaf uint {
81                     type uint32;
82                   }
83                 }
84               }
85
86               choice ch1 {
87                 choice ch2 {
88                   leaf str {
89                     type string;
90                   }
91                 }
92               }
93             }""");
94     private static final InputStreamNormalizer PARSER = JSONCodecFactorySupplier.RFC7951.getShared(MODEL_CONTEXT);
95     private static final QName FOO = QName.create("foo", "foo");
96     private static final QName BAR = QName.create("foo", "bar");
97     private static final QName BAZ = QName.create("foo", "baz");
98     private static final QName QUX = QName.create("foo", "qux");
99     private static final QName THUD = QName.create("foo", "thud");
100     private static final QName ONE = QName.create("foo", "one");
101     private static final QName TWO = QName.create("foo", "two");
102     private static final QName STR = QName.create("foo", "str");
103     private static final QName UINT = QName.create("foo", "uint");
104
105     private static final @NonNull NodeIdentifier DATA_NID = new NodeIdentifier(
106         QName.create("urn:ietf:params:xml:ns:yang:ietf-restconf", "2017-01-26", "data"));
107     private static final @NonNull Unqualified RESTCONF_MODULE = Unqualified.of("ietf-restconf");
108
109     @Test
110     void parseDatastore() throws Exception {
111         assertEquals(Builders.containerBuilder()
112             .withNodeIdentifier(DATA_NID)
113             .withChild(Builders.containerBuilder()
114                 .withNodeIdentifier(new NodeIdentifier(FOO))
115                 .withChild(ImmutableNodes.leafNode(STR, "str"))
116                 .build())
117             .withChild(Builders.containerBuilder()
118                 .withNodeIdentifier(new NodeIdentifier(BAR))
119                 .withChild(ImmutableNodes.leafNode(UINT, Uint32.TWO))
120                 .build())
121             .build(),
122             PARSER.parseDatastore(DATA_NID, RESTCONF_MODULE, stream("""
123                 {
124                   "ietf-restconf:data" : {
125                     "foo:foo" : {
126                       "str" : "str"
127                     },
128                     "foo:bar" : {
129                       "uint" : 2
130                     }
131                   }
132                 }""")).data());
133     }
134
135     @Test
136     void parseData() throws Exception {
137         assertEquals(Builders.containerBuilder()
138             .withNodeIdentifier(new NodeIdentifier(FOO))
139             .withChild(ImmutableNodes.leafNode(STR, "str"))
140             .build(),
141             PARSER.parseData(Inference.ofDataTreePath(MODEL_CONTEXT, FOO), stream("""
142                 {
143                   "foo:foo" : {
144                     "str" : "str"
145                   }
146                 }""")).data());
147     }
148
149     @Test
150     void parseDataBadType() throws Exception {
151         final var error = assertError(() -> PARSER.parseData(Inference.ofDataTreePath(MODEL_CONTEXT, FOO), stream("""
152             {
153               "foo:foo" : {
154                 "str" : "too long"
155               }
156             }""")));
157         assertEquals(ErrorType.APPLICATION, error.type());
158         assertEquals(ErrorTag.INVALID_VALUE, error.tag());
159     }
160
161     @Test
162     void parseDataBadRootElement() throws Exception {
163         assertMismatchedError("(foo)foo", "(foo)bar",
164             () -> PARSER.parseData(Inference.ofDataTreePath(MODEL_CONTEXT, FOO), stream("""
165                 {
166                   "foo:bar" : {
167                     "uint" : 23
168                   }
169                 }""")));
170     }
171
172     @Test
173     void parseDataBadInference() throws Exception {
174         final var stack = SchemaInferenceStack.of(MODEL_CONTEXT);
175         stack.enterSchemaTree(THUD);
176
177         final var ex = assertThrows(IllegalArgumentException.class,
178             () -> PARSER.parseData(stack.toInference(), stream("")));
179         assertEquals("Invalid inference statement RpcEffectiveStatementImpl{argument=(foo)thud}", ex.getMessage());
180     }
181
182     @Test
183     void parseDataEmptyInference() throws Exception {
184         final var inference = Inference.of(MODEL_CONTEXT);
185
186         final var ex = assertThrows(IllegalArgumentException.class, () -> PARSER.parseData(inference, stream("")));
187         assertEquals("Inference must not be empty", ex.getMessage());
188     }
189
190     @Test
191     void parseChildData() throws Exception {
192         final var prefixAndNode = PARSER.parseChildData(Inference.of(MODEL_CONTEXT), stream("""
193             {
194               "foo:foo" : {
195                 "str" : "str"
196               }
197             }"""));
198
199         assertEquals(List.of(), prefixAndNode.prefix());
200         assertEquals(Builders.containerBuilder()
201             .withNodeIdentifier(new NodeIdentifier(FOO))
202             .withChild(ImmutableNodes.leafNode(STR, "str"))
203             .build(), prefixAndNode.result().data());
204     }
205
206     @Test
207     void parseChildDataChoices() throws Exception {
208         final var prefixAndNode = PARSER.parseChildData(Inference.of(MODEL_CONTEXT), stream("""
209             {
210               "foo:str" : "str"
211             }"""));
212         assertEquals(List.of(
213             new NodeIdentifier(QName.create("foo", "ch1")),
214             new NodeIdentifier(QName.create("foo", "ch2"))), prefixAndNode.prefix());
215         assertEquals(ImmutableNodes.leafNode(STR, "str"), prefixAndNode.result().data());
216     }
217
218     @Test
219     void parseChildDataListEntry() throws Exception {
220         final var prefixAndNode = PARSER.parseChildData(Inference.of(MODEL_CONTEXT), stream("""
221             {
222               "foo:baz" : [
223                 {
224                   "one" : true,
225                   "two" : "two"
226                 }
227               ]
228             }"""));
229         assertEquals(List.of(new NodeIdentifier(BAZ)), prefixAndNode.prefix());
230         assertEquals(Builders.mapEntryBuilder()
231             .withNodeIdentifier(NodeIdentifierWithPredicates.of(BAZ, Map.of(ONE, Boolean.TRUE, TWO, "two")))
232             .withChild(ImmutableNodes.leafNode(ONE, Boolean.TRUE))
233             .withChild(ImmutableNodes.leafNode(TWO, "two"))
234             .build(), prefixAndNode.result().data());
235     }
236
237     @Test
238     void parseChildDataListEntryOnly() throws Exception {
239         // FIXME: this needs to be rejected, as it is an illegal format for a list resource, as per:
240         //
241         //        https://www.rfc-editor.org/rfc/rfc8040#section-4.4.1:
242         //
243         //        The message-body is expected to contain the
244         //        content of a child resource to create within the parent (target
245         //        resource).  The message-body MUST contain exactly one instance of the
246         //        expected data resource.  The data model for the child tree is the
247         //        subtree, as defined by YANG for the child resource.
248         //
249         //        https://www.rfc-editor.org/rfc/rfc7951#section-5.4:
250         //
251         //        the following is a valid JSON-encoded instance:
252         //
253         //            "bar": [
254         //              {
255         //                "foo": 123,
256         //                "baz": "zig"
257         //              },
258         //              {
259         //                "baz": "zag",
260         //                "foo": 0
261         //              }
262         //            ]
263         final var prefixAndNode = PARSER.parseChildData(Inference.of(MODEL_CONTEXT), stream("""
264             {
265               "foo:baz" : {
266                 "one" : true,
267                 "two" : "two"
268               }
269             }"""));
270         assertEquals(List.of(new NodeIdentifier(BAZ)), prefixAndNode.prefix());
271         assertEquals(Builders.mapEntryBuilder()
272             .withNodeIdentifier(NodeIdentifierWithPredicates.of(BAZ, Map.of(ONE, Boolean.TRUE, TWO, "two")))
273             .withChild(ImmutableNodes.leafNode(ONE, Boolean.TRUE))
274             .withChild(ImmutableNodes.leafNode(TWO, "two"))
275             .build(), prefixAndNode.result().data());
276     }
277
278     @Test
279     void parseChildDataListEntryNone() throws Exception {
280         final var error = assertError(() -> PARSER.parseChildData(Inference.of(MODEL_CONTEXT), stream("""
281             {
282               "foo:baz" : [
283               ]
284             }""")));
285         assertEquals(ErrorType.PROTOCOL, error.type());
286         assertEquals(ErrorTag.MALFORMED_MESSAGE, error.tag());
287         assertEquals("Exactly one instance of (foo)baz is required, 0 supplied", error.message());
288     }
289
290     @Test
291     void parseChildDataListEntryTwo() throws Exception {
292         final var error = assertError(() -> PARSER.parseChildData(Inference.of(MODEL_CONTEXT), stream("""
293             {
294               "foo:baz" : [
295                 {
296                   "one" : false,
297                   "two" : "two"
298                 },
299                 {
300                   "one" : true,
301                   "two" : "two"
302                 }
303               ]
304             }""")));
305         assertEquals(ErrorType.PROTOCOL, error.type());
306         assertEquals(ErrorTag.MALFORMED_MESSAGE, error.tag());
307         assertEquals("Exactly one instance of (foo)baz is required, 2 supplied", error.message());
308     }
309
310     @Test
311     void parseInputRpc() throws Exception {
312         final var stack = SchemaInferenceStack.of(MODEL_CONTEXT);
313         stack.enterSchemaTree(THUD);
314
315         assertEquals(Builders.containerBuilder()
316             .withNodeIdentifier(new NodeIdentifier(QName.create("foo", "input")))
317             .withChild(ImmutableNodes.leafNode(UINT, Uint32.TWO))
318             .build(),
319             PARSER.parseInput(stack.toInference(), stream("""
320                 {
321                   "foo:input" : {
322                     "uint" : 2
323                   }
324                 }""")).data());
325     }
326
327     @Test
328     void parseInputRpcBadRootElement() throws Exception {
329         final var stack = SchemaInferenceStack.of(MODEL_CONTEXT);
330         stack.enterSchemaTree(THUD);
331
332         assertMismatchedError("(foo)input", "(foo)output", () -> PARSER.parseInput(stack.toInference(), stream("""
333             {
334               "foo:output" : {
335               }
336             }""")));
337     }
338
339     @Test
340     void parseInputAction() throws Exception {
341         final var stack = SchemaInferenceStack.of(MODEL_CONTEXT);
342         stack.enterSchemaTree(BAZ);
343         stack.enterSchemaTree(QUX);
344
345         assertEquals(Builders.containerBuilder()
346             .withNodeIdentifier(new NodeIdentifier(QName.create("foo", "input")))
347             .withChild(ImmutableNodes.leafNode(STR, "str"))
348             .build(),
349             PARSER.parseInput(stack.toInference(), stream("""
350                 {
351                   "foo:input" : {
352                     "str" : "str"
353                   }
354                 }""")).data());
355     }
356
357     @Test
358     void parseInputBadInference() {
359         final var stack = SchemaInferenceStack.of(MODEL_CONTEXT);
360         stack.enterSchemaTree(BAZ);
361
362         final var ex = assertThrows(IllegalArgumentException.class,
363             () -> PARSER.parseInput(stack.toInference(), stream("")));
364         assertEquals("Invalid inference statement EmptyListEffectiveStatement{argument=(foo)baz}", ex.getMessage());
365     }
366
367     @Test
368     void parseOutputRpc() throws Exception {
369         final var stack = SchemaInferenceStack.of(MODEL_CONTEXT);
370         stack.enterSchemaTree(THUD);
371
372         assertEquals(Builders.containerBuilder()
373             .withNodeIdentifier(new NodeIdentifier(QName.create("foo", "output")))
374             .build(),
375             PARSER.parseOutput(stack.toInference(), stream("""
376                 {
377                   "foo:output" : {
378                   }
379                 }""")).data());
380     }
381
382     @Test
383     void parseOutputRpcBadRootElement() throws Exception {
384         final var stack = SchemaInferenceStack.of(MODEL_CONTEXT);
385         stack.enterSchemaTree(THUD);
386
387         assertMismatchedError("(foo)output", "(foo)input", () -> PARSER.parseOutput(stack.toInference(), stream("""
388             {
389               "foo:input" : {
390               }
391             }""")));
392     }
393
394     @Test
395     void parseOutputAction() throws Exception {
396         final var stack = SchemaInferenceStack.of(MODEL_CONTEXT);
397         stack.enterSchemaTree(BAZ);
398         stack.enterSchemaTree(QUX);
399
400         assertEquals(Builders.containerBuilder()
401             .withNodeIdentifier(new NodeIdentifier(QName.create("foo", "output")))
402             .build(),
403             PARSER.parseOutput(stack.toInference(), stream("""
404                 {
405                   "foo:output" : {
406                   }
407                 }""")).data());
408     }
409
410     @Test
411     void parseOutputBadInference() {
412         final var stack = SchemaInferenceStack.of(MODEL_CONTEXT);
413         stack.enterSchemaTree(BAZ);
414
415         final var ex = assertThrows(IllegalArgumentException.class,
416             () -> PARSER.parseOutput(stack.toInference(), stream("")));
417         assertEquals("Invalid inference statement EmptyListEffectiveStatement{argument=(foo)baz}", ex.getMessage());
418     }
419
420     private static @NonNull InputStream stream(final String str) {
421         return new ByteArrayInputStream(str.getBytes(StandardCharsets.UTF_8));
422     }
423
424     private static void assertMismatchedError(final String expected, final String actual, final Executable executable) {
425         final var error = assertError(executable);
426         assertEquals(ErrorType.PROTOCOL, error.type());
427         assertEquals(ErrorTag.MALFORMED_MESSAGE, error.tag());
428         assertEquals("Payload name " + actual + " is different from identifier name " + expected, error.message());
429     }
430
431     private static YangNetconfError assertError(final Executable executable) {
432         final var ex = assertThrows(NormalizationException.class, executable);
433         final var errors = ex.getNetconfErrors();
434         assertEquals(1, errors.size());
435         final var error = errors.get(0);
436         assertEquals(ErrorSeverity.ERROR, error.severity());
437         return error;
438     }
439 }