|
| 1 | +package io.vrap.codegen.languages.jsonschema.model |
| 2 | +import io.vrap.codegen.languages.extensions.discriminatorProperty |
| 3 | +import io.vrap.codegen.languages.extensions.isPatternProperty |
| 4 | +import io.vrap.rmf.codegen.di.AllAnyTypes |
| 5 | +import io.vrap.rmf.codegen.io.TemplateFile |
| 6 | +import io.vrap.rmf.codegen.rendring.FileProducer |
| 7 | +import io.vrap.rmf.codegen.types.VrapEnumType |
| 8 | +import io.vrap.rmf.codegen.types.VrapNilType |
| 9 | +import io.vrap.rmf.codegen.types.VrapObjectType |
| 10 | +import io.vrap.rmf.codegen.types.VrapScalarType |
| 11 | +import io.vrap.rmf.codegen.types.VrapType |
| 12 | +import io.vrap.rmf.codegen.types.VrapTypeProvider |
| 13 | +import io.vrap.rmf.raml.model.types.AnyType |
| 14 | +import io.vrap.rmf.raml.model.types.ArrayType |
| 15 | +import io.vrap.rmf.raml.model.types.BooleanType |
| 16 | +import io.vrap.rmf.raml.model.types.DateOnlyType |
| 17 | +import io.vrap.rmf.raml.model.types.DateTimeType |
| 18 | +import io.vrap.rmf.raml.model.types.IntegerType |
| 19 | +import io.vrap.rmf.raml.model.types.NumberType |
| 20 | +import io.vrap.rmf.raml.model.types.ObjectType |
| 21 | +import io.vrap.rmf.raml.model.types.Property |
| 22 | +import io.vrap.rmf.raml.model.types.StringInstance |
| 23 | +import io.vrap.rmf.raml.model.types.StringType |
| 24 | +import io.vrap.rmf.raml.model.types.TimeOnlyType |
| 25 | +import org.eclipse.emf.ecore.EObject |
| 26 | +import org.json.JSONObject |
| 27 | +class JsonSchemaRenderer constructor( |
| 28 | + val vrapTypeProvider: VrapTypeProvider, |
| 29 | + @AllAnyTypes var allAnyTypes: List<AnyType> |
| 30 | +) : FileProducer { |
| 31 | + |
| 32 | + override fun produceFiles(): List<TemplateFile> { |
| 33 | + var produced = allAnyTypes |
| 34 | + .map { |
| 35 | + when (it) { |
| 36 | + is ObjectType -> createObjectSchema(it) |
| 37 | + is StringType -> createStringSchema(it) |
| 38 | + else -> null |
| 39 | + } |
| 40 | + } |
| 41 | + .filterNotNull() |
| 42 | + .toList() |
| 43 | + |
| 44 | + return produced |
| 45 | + } |
| 46 | + |
| 47 | + private fun createObjectSchema(type: ObjectType): TemplateFile { |
| 48 | + val schema = LinkedHashMap<String, Any>() |
| 49 | + |
| 50 | + schema.put("${"$"}schema", "http://json-schema.org/draft-07/schema#") |
| 51 | + schema.put("${"$"}id", "https://example.com/${type.filename()}") |
| 52 | + schema.put("title", type.name) |
| 53 | + if (type.description != null) { |
| 54 | + schema.put("description", type.description.value.trim()) |
| 55 | + } |
| 56 | + schema.put("type", "object") |
| 57 | + |
| 58 | + val obj = LinkedHashMap<String, Any>() |
| 59 | + obj.put("properties", type.getObjectProperties()) |
| 60 | + |
| 61 | + val patterns = type.getPatternProperties() |
| 62 | + if (!patterns.isNullOrEmpty()) { |
| 63 | + obj.put("patternProperties", patterns) |
| 64 | + } |
| 65 | + |
| 66 | + val required = type.getRequiredPropertyNames() |
| 67 | + if (!required.isNullOrEmpty()) { |
| 68 | + obj.put("required", required) |
| 69 | + } |
| 70 | + |
| 71 | + obj.put("additionalProperties", false) |
| 72 | + |
| 73 | + val discriminatorProperty = type.discriminatorProperty() |
| 74 | + if (discriminatorProperty != null && type.discriminatorValue.isNullOrEmpty()) { |
| 75 | + schema.put( |
| 76 | + "discriminator", |
| 77 | + mapOf( |
| 78 | + "propertyName" to discriminatorProperty.name |
| 79 | + ) |
| 80 | + ) |
| 81 | + schema.put( |
| 82 | + "oneOf", |
| 83 | + allAnyTypes.getTypeInheritance(type) |
| 84 | + .filterIsInstance<ObjectType>() |
| 85 | + .map { |
| 86 | + mapOf("${"$"}ref" to "https://example.com/${it.filename()}") |
| 87 | + } |
| 88 | + ) |
| 89 | + } |
| 90 | + |
| 91 | + obj.forEach { (key, value) -> schema.put(key, value) } |
| 92 | + |
| 93 | + return TemplateFile(JSONObject(schema).toString(2), type.filename()) |
| 94 | + } |
| 95 | + |
| 96 | + private fun createStringSchema(type: StringType): TemplateFile? { |
| 97 | + val vrap = type.toVrapType() |
| 98 | + if (vrap !is VrapEnumType) { |
| 99 | + return null |
| 100 | + } |
| 101 | + |
| 102 | + val schema = mutableMapOf( |
| 103 | + "${"$"}schema" to "http://json-schema.org/draft-07/schema#", |
| 104 | + "${"$"}id" to "https://example.com/${type.filename()}", |
| 105 | + "title" to type.name, |
| 106 | + "type" to "string", |
| 107 | + "enum" to type.enumValues() |
| 108 | + ) |
| 109 | + |
| 110 | + if (type.description != null) { |
| 111 | + schema.put("description", type.description.value.trim()) |
| 112 | + } |
| 113 | + return TemplateFile(JSONObject(schema).toString(2), type.filename()) |
| 114 | + } |
| 115 | + |
| 116 | + private fun ObjectType.getObjectProperties(): LinkedHashMap<String, Any> { |
| 117 | + val result = LinkedHashMap<String, Any>() |
| 118 | + this.allProperties |
| 119 | + .filter { |
| 120 | + !it.isPatternProperty() |
| 121 | + } |
| 122 | + .forEach { |
| 123 | + result.put(it.name, it.type.toSchemaProperty(this, it)) |
| 124 | + } |
| 125 | + return result |
| 126 | + } |
| 127 | + |
| 128 | + private fun ObjectType.getPatternProperties(): LinkedHashMap<String, Any> { |
| 129 | + val result = LinkedHashMap<String, Any>() |
| 130 | + this.allProperties |
| 131 | + .filter { |
| 132 | + it.isPatternProperty() |
| 133 | + } |
| 134 | + .forEach { |
| 135 | + result.put(it.name.trim('/'), it.type.toSchemaProperty(this, it)) |
| 136 | + } |
| 137 | + return result |
| 138 | + } |
| 139 | + |
| 140 | + private fun ObjectType.getRequiredPropertyNames(): List<String> { |
| 141 | + return this.allProperties |
| 142 | + .filter { |
| 143 | + !it.isPatternProperty() && it.required |
| 144 | + } |
| 145 | + .map { |
| 146 | + it.name |
| 147 | + } |
| 148 | + } |
| 149 | + |
| 150 | + private fun AnyType.toSchemaProperty(parent: ObjectType, property: Property?): Map<String, Any> { |
| 151 | + |
| 152 | + if (parent.discriminatorValue != null && parent.discriminatorProperty() == property) { |
| 153 | + return mapOf( |
| 154 | + "enum" to listOf(parent.discriminatorValue) |
| 155 | + ) |
| 156 | + } |
| 157 | + |
| 158 | + val vrap = this.toVrapType() |
| 159 | + |
| 160 | + if (vrap is VrapEnumType && this is StringType) { |
| 161 | + return mapOf( |
| 162 | + "${"$"}ref" to vrap.filename() |
| 163 | + ) |
| 164 | + } |
| 165 | + |
| 166 | + return when (this) { |
| 167 | + is ObjectType -> this.toSchemaProperty(parent, property) |
| 168 | + is ArrayType -> this.toSchemaProperty(parent, property) |
| 169 | + is StringType -> this.toSchemaProperty(parent, property) |
| 170 | + is NumberType -> this.toSchemaProperty(parent, property) |
| 171 | + is BooleanType -> this.toSchemaProperty(parent, property) |
| 172 | + is DateTimeType -> this.toSchemaProperty(parent, property) |
| 173 | + is IntegerType -> this.toSchemaProperty(parent, property) |
| 174 | + is AnyType -> mapOf( |
| 175 | + "type" to listOf("number", "string", "boolean", "object", "array", "null") |
| 176 | + ) |
| 177 | + else -> { |
| 178 | + println("Missing case for " + this + property) |
| 179 | + |
| 180 | + return mapOf( |
| 181 | + "type" to listOf("number", "string", "boolean", "object", "array", "null") |
| 182 | + ) |
| 183 | + } |
| 184 | + } |
| 185 | + } |
| 186 | + |
| 187 | + private fun ObjectType.toSchemaProperty(parent: ObjectType, property: Property?): Map<String, Any> { |
| 188 | + |
| 189 | + val dProperty = this.discriminatorProperty() |
| 190 | + if (dProperty != null && !this.discriminatorValue.isNullOrEmpty()) { |
| 191 | + return mapOf( |
| 192 | + "${"$"}ref" to this.filename() |
| 193 | + ) |
| 194 | + } |
| 195 | + |
| 196 | + if (this.toVrapType() !is VrapObjectType) { |
| 197 | + // println(this.properties) |
| 198 | + return mapOf( |
| 199 | + "type" to "object" |
| 200 | + ) |
| 201 | + } else { |
| 202 | + return mapOf( |
| 203 | + "${"$"}ref" to this.filename() |
| 204 | + ) |
| 205 | + } |
| 206 | + } |
| 207 | + |
| 208 | + private fun StringType.toSchemaProperty(parent: ObjectType, property: Property?): Map<String, Any> { |
| 209 | + val vrapType = this.toVrapType() |
| 210 | + if (vrapType !is VrapScalarType) { |
| 211 | + throw Exception("Expected scalar") |
| 212 | + } |
| 213 | + val result = when (vrapType.scalarType) { |
| 214 | + "any" -> mapOf( |
| 215 | + "type" to listOf("number", "string", "boolean", "object", "array", "null") |
| 216 | + ) |
| 217 | + else -> mapOf( |
| 218 | + "type" to vrapType.scalarType |
| 219 | + ) |
| 220 | + } |
| 221 | + return result |
| 222 | + } |
| 223 | + |
| 224 | + private fun IntegerType.toSchemaProperty(parent: ObjectType, property: Property?) = mapOf( |
| 225 | + "type" to "number", |
| 226 | + "format" to "integer" |
| 227 | + ) |
| 228 | + |
| 229 | + private fun BooleanType.toSchemaProperty(parent: ObjectType, property: Property?) = mapOf( |
| 230 | + "type" to "boolean" |
| 231 | + ) |
| 232 | + |
| 233 | + private fun DateTimeType.toSchemaProperty(parent: ObjectType, property: Property?) = mapOf( |
| 234 | + "type" to "string", |
| 235 | + "format" to "date-time" |
| 236 | + ) |
| 237 | + |
| 238 | + private fun DateOnlyType.toSchemaProperty(parent: ObjectType, property: Property?) = mapOf( |
| 239 | + "type" to "string", |
| 240 | + "format" to "date" |
| 241 | + ) |
| 242 | + |
| 243 | + private fun TimeOnlyType.toSchemaProperty(parent: ObjectType, property: Property?) = mapOf( |
| 244 | + "type" to "string", |
| 245 | + "format" to "time" |
| 246 | + ) |
| 247 | + |
| 248 | + private fun NumberType.toSchemaProperty(parent: ObjectType, property: Property?) = mapOf( |
| 249 | + "type" to "number" |
| 250 | + ) |
| 251 | + |
| 252 | + private fun ArrayType.toSchemaProperty(parent: ObjectType, property: Property?): Map<String, Any> { |
| 253 | + return mapOf( |
| 254 | + "type" to "array", |
| 255 | + "items" to this.items.toSchemaProperty(parent, property) |
| 256 | + ) |
| 257 | + } |
| 258 | + |
| 259 | + private fun Property.isPatternProperty() = this.name.startsWith("/") && this.name.endsWith("/") |
| 260 | + |
| 261 | + fun EObject?.toVrapType(): VrapType { |
| 262 | + val vrapType = if (this != null) vrapTypeProvider.doSwitch(this) else VrapNilType() |
| 263 | + return vrapType |
| 264 | + } |
| 265 | + |
| 266 | + fun ObjectType.filename(): String { |
| 267 | + val type = this.toVrapType() |
| 268 | + if (type !is VrapObjectType) { |
| 269 | + return "unknown.json" |
| 270 | + } |
| 271 | + return type.filename() |
| 272 | + } |
| 273 | + |
| 274 | + fun StringType.filename(): String { |
| 275 | + val type = this.toVrapType() |
| 276 | + if (type !is VrapEnumType) { |
| 277 | + return "unknown.json" |
| 278 | + } |
| 279 | + return type.filename() |
| 280 | + } |
| 281 | + |
| 282 | + fun VrapObjectType.filename(): String { |
| 283 | + return this.simpleClassName + ".schema.json" |
| 284 | + } |
| 285 | + |
| 286 | + fun VrapEnumType.filename(): String { |
| 287 | + return this.simpleClassName + "Enum.schema.json" |
| 288 | + } |
| 289 | + |
| 290 | + fun List<AnyType>.getTypeInheritance(type: AnyType): List<AnyType> { |
| 291 | + return this |
| 292 | + .filter { it.type != null && it.type.name == type.name } |
| 293 | + // TODO: Shouldn't this be necessary? |
| 294 | + // .plus( |
| 295 | + // this |
| 296 | + // .filter { it.type != null && it.type.name == type.name } |
| 297 | + // .flatMap { this.getTypeInheritance(it.type) } |
| 298 | + // ) |
| 299 | + } |
| 300 | + |
| 301 | + fun StringType.enumValues(): List<String> = enum.filterIsInstance<StringInstance>() |
| 302 | + .map { it.value } |
| 303 | + .filterNotNull() |
| 304 | +} |
0 commit comments