Skip to content

Commit a6df4e2

Browse files
committed
Implement generator for generating JSON schemas
1 parent c5a70d7 commit a6df4e2

File tree

10 files changed

+381
-1
lines changed

10 files changed

+381
-1
lines changed

languages/jsonschema/build.gradle

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

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ include 'languages:postman'
1111
include 'languages:typescript'
1212
include 'languages:python'
1313
include 'languages:go'
14+
include 'languages:jsonschema'
1415
include 'languages:ramldoc'
1516

1617
include 'ctp-validators'

tools/cli-application/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ String dir = "${buildDir.toString()}/gensrc/main/kotlin/io/vrap/rmf/codegen/cli/
7979

8080
dependencies {
8181
implementation project(':languages:javalang:builder-renderer:java-builder-client')
82+
implementation project(':languages:jsonschema')
8283
implementation project(':languages:typescript')
8384
implementation project(':languages:postman')
8485
implementation project(':languages:python')

0 commit comments

Comments
 (0)