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}