Skip to content

Commit

Permalink
Implement generator for generating JSON schemas
Browse files Browse the repository at this point in the history
  • Loading branch information
mvantellingen committed Jul 27, 2022
1 parent c5a70d7 commit a6df4e2
Show file tree
Hide file tree
Showing 10 changed files with 381 additions and 1 deletion.
5 changes: 5 additions & 0 deletions languages/jsonschema/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dependencies {
implementation 'org.json:json:20200518'
implementation project(':codegen-renderers')
implementation orgkotlin.stdlib
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.vrap.codegen.languages.jsonschema.model

import io.vrap.rmf.codegen.types.LanguageBaseTypes
import io.vrap.rmf.codegen.types.VrapScalarType

object JsonSchemaBaseTypes : LanguageBaseTypes(
anyType = nativeJsonSchemaType("any"),
objectType = nativeJsonSchemaType("object"),
integerType = nativeJsonSchemaType("integer"),
longType = nativeJsonSchemaType("integer"),
doubleType = nativeJsonSchemaType("number"),
stringType = nativeJsonSchemaType("string"),
booleanType = nativeJsonSchemaType("boolean"),
dateTimeType = nativeJsonSchemaType("date-time"),
dateOnlyType = nativeJsonSchemaType("date"),
timeOnlyType = nativeJsonSchemaType("time"),
file = nativeJsonSchemaType("string")
)

fun nativeJsonSchemaType(typeName: String): VrapScalarType = VrapScalarType(typeName)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package io.vrap.codegen.languages.jsonschema.model

fun create_schema_id (identifier: String): String {
return "https://api.commercetools.com/${identifier}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.vrap.codegen.languages.jsonschema.model

import io.vrap.rmf.codegen.di.RamlGeneratorModule
import io.vrap.rmf.codegen.di.Module
import io.vrap.rmf.codegen.rendering.CodeGenerator
import io.vrap.rmf.codegen.rendering.FileGenerator

object JsonSchemaModelModule : Module {
override fun configure(generatorModule: RamlGeneratorModule) = setOf<CodeGenerator>(
FileGenerator(
setOf(

JsonSchemaRenderer(
generatorModule.vrapTypeProvider(),
generatorModule.allAnyTypes()
)
)
)
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
package io.vrap.codegen.languages.jsonschema.model
import io.vrap.codegen.languages.extensions.discriminatorProperty
import io.vrap.codegen.languages.extensions.isPatternProperty
import io.vrap.rmf.codegen.di.AllAnyTypes
import io.vrap.rmf.codegen.io.TemplateFile
import io.vrap.rmf.codegen.rendering.FileProducer
import io.vrap.rmf.codegen.types.VrapEnumType
import io.vrap.rmf.codegen.types.VrapNilType
import io.vrap.rmf.codegen.types.VrapObjectType
import io.vrap.rmf.codegen.types.VrapScalarType
import io.vrap.rmf.codegen.types.VrapType
import io.vrap.rmf.codegen.types.VrapTypeProvider
import io.vrap.rmf.raml.model.types.AnyType
import io.vrap.rmf.raml.model.types.ArrayType
import io.vrap.rmf.raml.model.types.BooleanType
import io.vrap.rmf.raml.model.types.DateOnlyType
import io.vrap.rmf.raml.model.types.DateTimeType
import io.vrap.rmf.raml.model.types.IntegerType
import io.vrap.rmf.raml.model.types.NumberType
import io.vrap.rmf.raml.model.types.ObjectType
import io.vrap.rmf.raml.model.types.Property
import io.vrap.rmf.raml.model.types.StringInstance
import io.vrap.rmf.raml.model.types.StringType
import io.vrap.rmf.raml.model.types.TimeOnlyType
import org.eclipse.emf.ecore.EObject
import org.json.JSONObject


class JsonSchemaRenderer constructor(
val vrapTypeProvider: VrapTypeProvider,
@AllAnyTypes var allAnyTypes: List<AnyType>
) : FileProducer {

override fun produceFiles(): List<TemplateFile> {
var produced = allAnyTypes
.map {
when (it) {
is ObjectType -> createObjectSchema(it)
is StringType -> createStringSchema(it)
else -> null
}
}
.filterNotNull()
.toList()

return produced
}

private fun createObjectSchema(type: ObjectType): TemplateFile {
val schema = LinkedHashMap<String, Any>()

schema.put("${"$"}schema", "http://json-schema.org/draft/2020-12/schema")
schema.put("${"$"}id", create_schema_id(type.filename()))
schema.put("title", type.name)
if (type.description != null) {
schema.put("description", type.description.value.trim())
}
schema.put("type", "object")

val obj = LinkedHashMap<String, Any>()
obj.put("properties", type.getObjectProperties())

val patterns = type.getPatternProperties()
if (!patterns.isNullOrEmpty()) {
obj.put("patternProperties", patterns)
}

val required = type.getRequiredPropertyNames()
if (!required.isNullOrEmpty()) {
obj.put("required", required)
}

obj.put("additionalProperties", false)

val discriminatorProperty = type.discriminatorProperty()
if (discriminatorProperty != null && type.discriminatorValue.isNullOrEmpty()) {
schema.put(
"discriminator",
mapOf(
"propertyName" to discriminatorProperty.name
)
)
schema.put(
"oneOf",
allAnyTypes.getTypeInheritance(type)
.filterIsInstance<ObjectType>()
.map {
mapOf("${"$"}ref" to create_schema_id(it.filename()))
}
)
}

obj.forEach { (key, value) -> schema.put(key, value) }

return TemplateFile(JSONObject(schema).toString(2), type.filename())
}

private fun createStringSchema(type: StringType): TemplateFile? {
val vrap = type.toVrapType()
if (vrap !is VrapEnumType) {
return null
}

val schema = mutableMapOf(
"${"$"}schema" to "http://json-schema.org/draft-07/schema#",
"${"$"}id" to create_schema_id(type.filename()),
"title" to type.name,
"type" to "string",
"enum" to type.enumValues()
)

if (type.description != null) {
schema.put("description", type.description.value.trim())
}
return TemplateFile(JSONObject(schema).toString(2), type.filename())
}

private fun ObjectType.getObjectProperties(): LinkedHashMap<String, Any> {
val result = LinkedHashMap<String, Any>()
this.allProperties
.filter {
!it.isPatternProperty()
}
.forEach {
result.put(it.name, it.type.toSchemaProperty(this, it))
}
return result
}

private fun ObjectType.getPatternProperties(): LinkedHashMap<String, Any> {
val result = LinkedHashMap<String, Any>()
this.allProperties
.filter {
it.isPatternProperty()
}
.forEach {
result.put(it.name.trim('/'), it.type.toSchemaProperty(this, it))
}
return result
}

private fun ObjectType.getRequiredPropertyNames(): List<String> {
return this.allProperties
.filter {
!it.isPatternProperty() && it.required
}
.map {
it.name
}
}

private fun AnyType.toSchemaProperty(parent: ObjectType, property: Property?): Map<String, Any> {

if (parent.discriminatorValue != null && parent.discriminatorProperty() == property) {
return mapOf(
"enum" to listOf(parent.discriminatorValue)
)
}

val vrap = this.toVrapType()

if (vrap is VrapEnumType && this is StringType) {
return mapOf(
"${"$"}ref" to vrap.filename()
)
}

return when (this) {
is ObjectType -> this.toSchemaProperty(parent, property)
is ArrayType -> this.toSchemaProperty(parent, property)
is StringType -> this.toSchemaProperty(parent, property)
is NumberType -> this.toSchemaProperty(parent, property)
is BooleanType -> this.toSchemaProperty(parent, property)
is DateTimeType -> this.toSchemaProperty(parent, property)
is IntegerType -> this.toSchemaProperty(parent, property)
is AnyType -> mapOf(
"type" to listOf("number", "string", "boolean", "object", "array", "null")
)
else -> {
println("Missing case for " + this + property)

return mapOf(
"type" to listOf("number", "string", "boolean", "object", "array", "null")
)
}
}
}

private fun ObjectType.toSchemaProperty(parent: ObjectType, property: Property?): Map<String, Any> {

val dProperty = this.discriminatorProperty()
if (dProperty != null && !this.discriminatorValue.isNullOrEmpty()) {
return mapOf(
"${"$"}ref" to this.filename()
)
}

if (this.toVrapType() !is VrapObjectType) {
// println(this.properties)
return mapOf(
"type" to "object"
)
} else {
return mapOf(
"${"$"}ref" to this.filename()
)
}
}

private fun StringType.toSchemaProperty(parent: ObjectType, property: Property?): Map<String, Any> {
val vrapType = this.toVrapType()
if (vrapType !is VrapScalarType) {
throw Exception("Expected scalar")
}
val result = when (vrapType.scalarType) {
"any" -> mapOf(
"type" to listOf("number", "string", "boolean", "object", "array", "null")
)
else -> mapOf(
"type" to vrapType.scalarType
)
}
return result
}

private fun IntegerType.toSchemaProperty(parent: ObjectType, property: Property?) = mapOf(
"type" to "number",
"format" to "integer"
)

private fun BooleanType.toSchemaProperty(parent: ObjectType, property: Property?) = mapOf(
"type" to "boolean"
)

private fun DateTimeType.toSchemaProperty(parent: ObjectType, property: Property?) = mapOf(
"type" to "string",
"format" to "date-time"
)

private fun DateOnlyType.toSchemaProperty(parent: ObjectType, property: Property?) = mapOf(
"type" to "string",
"format" to "date"
)

private fun TimeOnlyType.toSchemaProperty(parent: ObjectType, property: Property?) = mapOf(
"type" to "string",
"format" to "time"
)

private fun NumberType.toSchemaProperty(parent: ObjectType, property: Property?) = mapOf(
"type" to "number"
)

private fun ArrayType.toSchemaProperty(parent: ObjectType, property: Property?): Map<String, Any> {
return mapOf(
"type" to "array",
"items" to this.items.toSchemaProperty(parent, property)
)
}

private fun Property.isPatternProperty() = this.name.startsWith("/") && this.name.endsWith("/")

fun EObject?.toVrapType(): VrapType {
val vrapType = if (this != null) vrapTypeProvider.doSwitch(this) else VrapNilType()
return vrapType
}

fun ObjectType.filename(): String {
val type = this.toVrapType()
if (type !is VrapObjectType) {
return "unknown.json"
}
return type.filename()
}

fun StringType.filename(): String {
val type = this.toVrapType()
if (type !is VrapEnumType) {
return "unknown.json"
}
return type.filename()
}

fun VrapObjectType.filename(): String {
return this.simpleClassName + ".schema.json"
}

fun VrapEnumType.filename(): String {
return this.simpleClassName + "Enum.schema.json"
}

fun List<AnyType>.getTypeInheritance(type: AnyType): List<AnyType> {
return this
.filter { it.type != null && it.type.name == type.name }
// TODO: Shouldn't this be necessary?
// .plus(
// this
// .filter { it.type != null && it.type.name == type.name }
// .flatMap { this.getTypeInheritance(it.type) }
// )
}

fun StringType.enumValues(): List<String> = enum.filterIsInstance<StringInstance>()
.map { it.value }
.filterNotNull()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.vrap.codegen.languages.jsonschema.model

import io.vrap.rmf.codegen.types.*

fun VrapType.simpleTypeName(): String {
return when (this) {
is VrapAnyType -> this.baseType
is VrapScalarType -> this.scalarType
is VrapEnumType -> this.simpleClassName
is VrapObjectType -> this.simpleClassName
is VrapArrayType -> "${this.itemType.simpleTypeName()}[]"
is VrapNilType -> this.name
}
}
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ include 'languages:postman'
include 'languages:typescript'
include 'languages:python'
include 'languages:go'
include 'languages:jsonschema'
include 'languages:ramldoc'

include 'ctp-validators'
Expand Down
1 change: 1 addition & 0 deletions tools/cli-application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ String dir = "${buildDir.toString()}/gensrc/main/kotlin/io/vrap/rmf/codegen/cli/

dependencies {
implementation project(':languages:javalang:builder-renderer:java-builder-client')
implementation project(':languages:jsonschema')
implementation project(':languages:typescript')
implementation project(':languages:postman')
implementation project(':languages:python')
Expand Down
Loading

0 comments on commit a6df4e2

Please sign in to comment.