Skip to content

Commit 1a55557

Browse files
committed
Implement generator for generating JSON schemas
1 parent 4106a8a commit 1a55557

File tree

9 files changed

+387
-1
lines changed

9 files changed

+387
-1
lines changed

languages/jsonschema/build.gradle

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
plugins {
2+
id 'org.jetbrains.kotlin.jvm' version '1.3.72'
3+
}
4+
5+
compileKotlin {
6+
kotlinOptions.jvmTarget = "1.8"
7+
}
8+
compileTestKotlin {
9+
kotlinOptions.jvmTarget = "1.8"
10+
}
11+
12+
dependencies {
13+
implementation 'org.json:json:20200518'
14+
15+
compile project(':codegen-renderers')
16+
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
17+
testCompile group: 'junit', name: 'junit', version: '4.13'
18+
}
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("typing.BinaryIO")
18+
)
19+
20+
fun nativeJsonSchemaType(typeName: String): VrapScalarType = VrapScalarType(typeName)
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.GeneratorModule
4+
import io.vrap.rmf.codegen.di.Module
5+
import io.vrap.rmf.codegen.rendring.CodeGenerator
6+
import io.vrap.rmf.codegen.rendring.FileGenerator
7+
8+
object JsonSchemaModelModule : Module {
9+
override fun configure(generatorModule: GeneratorModule) = setOf<CodeGenerator>(
10+
FileGenerator(
11+
setOf(
12+
13+
JsonSchemaRenderer(
14+
generatorModule.vrapTypeProvider(),
15+
generatorModule.allAnyTypes()
16+
)
17+
)
18+
)
19+
)
20+
}
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
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+
}
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
@@ -10,6 +10,7 @@ include 'languages:oas'
1010
include 'languages:postman'
1111
include 'languages:typescript'
1212
include 'languages:python'
13+
include 'languages:jsonschema'
1314
include 'languages:ramldoc'
1415

1516
include 'tools:cli-application'

tools/cli-application/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ dependencies {
9393
compile project(':languages:typescript')
9494
compile project(':languages:postman')
9595
compile project(':languages:python')
96+
compile project(':languages:jsonschema')
9697
compile project(':languages:php')
9798
compile project(':languages:csharp')
9899
compile project(':languages:ramldoc')

0 commit comments

Comments
 (0)