001/*
002 * Copyright (C) 2017 Jens Reimann <jreimann@redhat.com>
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package de.dentrassi.asyncapi.internal.parser;
018
019import static de.dentrassi.asyncapi.AsyncApi.VERSION;
020import static de.dentrassi.asyncapi.internal.parser.Consume.asMap;
021import static de.dentrassi.asyncapi.internal.parser.Consume.asOptionalMap;
022import static de.dentrassi.asyncapi.internal.parser.Consume.asOptionalSet;
023import static de.dentrassi.asyncapi.internal.parser.Consume.asOptionalString;
024import static de.dentrassi.asyncapi.internal.parser.Consume.asSet;
025import static de.dentrassi.asyncapi.internal.parser.Consume.asString;
026
027import java.io.InputStream;
028import java.io.Reader;
029import java.time.ZonedDateTime;
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.Collections;
033import java.util.HashMap;
034import java.util.HashSet;
035import java.util.Iterator;
036import java.util.LinkedHashSet;
037import java.util.List;
038import java.util.Map;
039import java.util.Optional;
040import java.util.Set;
041
042import org.yaml.snakeyaml.Yaml;
043
044import de.dentrassi.asyncapi.ArrayType;
045import de.dentrassi.asyncapi.AsyncApi;
046import de.dentrassi.asyncapi.CoreType;
047import de.dentrassi.asyncapi.EnumType;
048import de.dentrassi.asyncapi.Information;
049import de.dentrassi.asyncapi.Message;
050import de.dentrassi.asyncapi.MessageReference;
051import de.dentrassi.asyncapi.ObjectType;
052import de.dentrassi.asyncapi.Property;
053import de.dentrassi.asyncapi.Topic;
054import de.dentrassi.asyncapi.Type;
055import de.dentrassi.asyncapi.TypeReference;
056
057/**
058 * Parser for AsyncAPI definitions encoded as YAML
059 */
060public class YamlParser {
061
062    private final Map<String, ?> document;
063
064    private final Map<String, Message> messages = new HashMap<>();
065
066    public YamlParser(final InputStream in) throws ParserException {
067        try {
068            this.document = asMap(new Yaml().load(in));
069        } catch (final Exception e) {
070            throw new ParserException("Failed to parse YAML document", e);
071        }
072    }
073
074    public YamlParser(final Reader reader) throws ParserException {
075        try {
076            this.document = asMap(new Yaml().load(reader));
077        } catch (final Exception e) {
078            throw new ParserException("Failed to parse YAML document", e);
079        }
080    }
081
082    public AsyncApi parse() {
083        final String version = asString("asyncapi", this.document);
084
085        if (!VERSION.equals(version)) {
086            throw new IllegalStateException(String.format("Only version '%s' is supported, this is version '%s'", VERSION, version));
087        }
088
089        final AsyncApi api = new AsyncApi();
090
091        api.setBaseTopic(asOptionalString("baseTopic", this.document).orElse(null));
092        api.setHost(asString("host", this.document));
093        api.setSchemes(asSet("schemes", this.document));
094        api.setInformation(parseInfo(asMap("info", this.document)));
095        api.setTopics(parseTopics(asMap("topics", this.document)));
096
097        final Map<String, ?> components = asMap("components", this.document);
098
099        api.setMessages(parseMessages(asOptionalMap("messages", components).orElse(null)));
100        api.setTypes(parseTypes(asOptionalMap("schemas", components).orElse(null)));
101
102        return api;
103    }
104
105    private Set<Type> parseTypes(final Map<String, ?> map) {
106        if (map == null || map.isEmpty()) {
107            return Collections.emptySet();
108        }
109
110        final Set<Type> result = new LinkedHashSet<>();
111
112        for (final Map.Entry<String, ?> entry : map.entrySet()) {
113            final String name = entry.getKey();
114            result.add(parseExplicitType("types", Collections.emptyList(), name, asMap(entry.getValue())));
115        }
116
117        return result;
118    }
119
120    private Set<Message> parseMessages(final Map<String, ?> map) {
121        if (map == null || map.isEmpty()) {
122            return Collections.emptySet();
123        }
124
125        final Set<Message> result = new LinkedHashSet<>();
126
127        for (final Map.Entry<String, ?> entry : map.entrySet()) {
128            final String name = entry.getKey();
129            result.add(parseExplicitMessage(name, asMap(entry.getValue())));
130        }
131
132        return result;
133    }
134
135    private static class Reference implements Iterable<String> {
136
137        private final List<String> tokens;
138
139        public Reference(final List<String> tokens) {
140            this.tokens = tokens;
141            if (tokens.isEmpty()) {
142                throw new IllegalArgumentException("Reference must not be empty");
143            }
144        }
145
146        @Override
147        public Iterator<String> iterator() {
148            return this.tokens.iterator();
149        }
150
151        public static Reference parse(final String ref) {
152            return new Reference(Arrays.asList(ref.split("/+")));
153        }
154
155        public String last() {
156            return last(0);
157        }
158
159        public String last(final int reverseIndex) {
160            return this.tokens.get(this.tokens.size() - (reverseIndex + 1));
161        }
162
163    }
164
165    private TypeReference parseType(final String namespace, final List<String> parents, final String name, final Map<String, ?> map) {
166        final Optional<String> ref = asOptionalString("$ref", map);
167
168        if (ref.isPresent()) {
169
170            final Reference to = Reference.parse(ref.get());
171
172            // FIXME: validate full ref syntax
173
174            return new TypeReference(mapPackageName(to.last(1)), to.last());
175        } else {
176            return parseExplicitType(namespace, parents, name, map);
177        }
178    }
179
180    private String mapPackageName(final String type) {
181        if ("schemas".equals(type)) {
182            return "types";
183        }
184        return type;
185    }
186
187    private Type parseExplicitType(final String namespace, final List<String> parents, final String name, final Map<String, ?> map) {
188
189        final String type = asString("type", map);
190        switch (type) {
191        case "boolean":
192            return addCommonTypeInfo(new CoreType(name, Boolean.class), map);
193        case "integer":
194            return addCommonTypeInfo(new CoreType(name, Integer.class), map);
195        case "number":
196            return addCommonTypeInfo(new CoreType(name, Double.class), map);
197        case "string": {
198            if (map.containsKey("enum")) {
199                return addCommonTypeInfo(parseEnumType(namespace, parents, name, map), map);
200            }
201            return addCommonTypeInfo(parseCoreType(name, map), map);
202        }
203        case "array":
204            return addCommonTypeInfo(parseArrayType(namespace, parents, name, map), map);
205        case "object":
206            return addCommonTypeInfo(parseObjectType(namespace, parents, name, map), map);
207        default:
208            throw new IllegalStateException(String.format("Unsupported type: %s", type));
209        }
210    }
211
212    private static List<String> push(final List<String> parents, final String name) {
213        final List<String> result = new ArrayList<>(parents);
214        result.add(name);
215        return result;
216    }
217
218    private Type parseArrayType(final String namespace, final List<String> parents, final String name, final Map<String, ?> map) {
219
220        final boolean uniqueItems = Consume.asBoolean(map, "uniqueItems");
221
222        final TypeReference itemType = parseType(namespace, parents, name + "Item", asMap("items", map));
223
224        final ArrayType type = new ArrayType(name, itemType, uniqueItems);
225
226        return type;
227    }
228
229    private Type parseEnumType(final String namespace, final List<String> parents, final String name, final Map<String, ?> map) {
230        final EnumType type = new EnumType(namespace, parents, name);
231
232        type.setLiterals(asSet("enum", map));
233
234        return type;
235    }
236
237    private CoreType parseCoreType(final String name, final Map<String, ?> map) {
238
239        final String format = asOptionalString("format", map).orElse(null);
240
241        if (format == null) {
242            return new CoreType(name, String.class);
243        }
244
245        switch (format) {
246        case "date-time":
247            return new CoreType(name, ZonedDateTime.class);
248        default:
249            throw new IllegalStateException(String.format("Unknown data format: " + format));
250        }
251    }
252
253    private Type parseObjectType(final String namespace, final List<String> parents, final String name, final Map<String, ?> map) {
254        final ObjectType type = new ObjectType(namespace, parents, name);
255
256        final Set<String> required = asOptionalSet("required", map).orElse(Collections.emptySet());
257
258        final Map<String, ?> prop = asMap("properties", map);
259
260        for (final Map.Entry<String, ?> entry : prop.entrySet()) {
261            final Property p = new Property();
262
263            final String propName = entry.getKey();
264            final Map<String, ?> propValues = asMap(entry.getValue());
265
266            p.setName(propName);
267            p.setDescription(asOptionalString("description", propValues).orElse(null));
268            p.setRequired(required.contains(propName));
269            p.setType(parseType(namespace, push(parents, name), entry.getKey(), propValues));
270
271            type.getProperties().add(p);
272        }
273
274        return type;
275    }
276
277    private Type addCommonTypeInfo(final Type type, final Map<String, ?> map) {
278        type.setTitle(asOptionalString("title", map).orElse(null));
279        type.setDescription(asOptionalString("description", map).orElse(null));
280        return type;
281    }
282
283    private Set<Topic> parseTopics(final Map<String, ?> topics) {
284        final Set<Topic> result = new HashSet<>();
285
286        for (final Map.Entry<String, ?> entry : topics.entrySet()) {
287            result.add(parseTopic(entry.getKey(), entry.getValue()));
288        }
289
290        return result;
291    }
292
293    private Topic parseTopic(final String key, final Object value) {
294        final Map<String, ?> map = asMap(value);
295
296        final Topic result = new Topic();
297
298        result.setName(key);
299        result.setPublish(asOptionalMap("publish", map).map(v -> parseMessage("Publish. " + key, v)).orElse(null));
300        result.setSubscribe(asOptionalMap("subscribe", map).map(v -> parseMessage("Subscribe." + key, v)).orElse(null));
301
302        return result;
303    }
304
305    private MessageReference parseMessage(final String name, final Map<String, ?> map) {
306        final Optional<String> ref = asOptionalString("$ref", map);
307
308        if (ref.isPresent()) {
309
310            final Reference to = Reference.parse(ref.get());
311
312            final String refName = to.last();
313
314            return new MessageReference(refName);
315        } else {
316            return parseExplicitMessage(name, map);
317        }
318    }
319
320    private Message parseExplicitMessage(final String name, final Map<String, ?> map) {
321
322        final Message message = new Message(name);
323
324        message.setDescription(asOptionalString("description", map).orElse(null));
325        message.setSummary(asOptionalString("summary", map).orElse(null));
326
327        message.setPayload(parseType("messages", Collections.singletonList(name), "payload", asMap("payload", map)));
328
329        this.messages.put(name, message);
330        return message;
331    }
332
333    private Information parseInfo(final Map<String, ?> map) {
334        final Information result = new Information();
335
336        result.setTitle(asOptionalString("title", map).orElse(null));
337        result.setVersion(asString("version", map));
338
339        return result;
340    }
341
342}