Skip to content

Commit

Permalink
Merge branch 'feature/detect-optional-fields' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
SMILEY4 committed Aug 3, 2024
2 parents 08974f5 + 9a3c1bb commit f95fcd7
Show file tree
Hide file tree
Showing 16 changed files with 690 additions and 22 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.smiley4.schemakenerator.core.annotations


/**
* Specifies that the annotated object is optional, i.e. not required.
*/
@Target(
AnnotationTarget.PROPERTY,
AnnotationTarget.FIELD,
AnnotationTarget.FUNCTION
)
@Retention(AnnotationRetention.RUNTIME)
annotation class Optional
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.github.smiley4.schemakenerator.core.annotations


/**
* Specifies that the annotated object is required.
*/
@Target(
AnnotationTarget.PROPERTY,
AnnotationTarget.FIELD,
AnnotationTarget.FUNCTION
)
@Retention(AnnotationRetention.RUNTIME)
annotation class Required
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ data class PropertyData(
* whether the (return) type is nullable
*/
var nullable: Boolean,
/**
* whether the property is optional (i.e. when a default value is provided)
*/
var optional: Boolean,
/**
* the general visibility of this property
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.github.smiley4.schemakenerator.core.data.Bundle
import io.github.smiley4.schemakenerator.core.data.ObjectTypeData
import io.github.smiley4.schemakenerator.core.data.PropertyData
import io.github.smiley4.schemakenerator.core.data.PropertyType
import javax.swing.text.html.HTML.Tag.U

/**
* Merges getters with their matching property;
Expand Down Expand Up @@ -35,21 +36,24 @@ class MergeGettersStep {
.filter { it.kind == PropertyType.GETTER }
.forEach { getter ->

// find matching property
val propertyName = getterNameToPropertyName(getter.name)

val property = typeData.members
.filter { it.kind == PropertyType.PROPERTY }
.find { it.name == propertyName && it.type == getter.type }

if(property != null) {
// copy some information from getter to property
property.annotations.addAll(getter.annotations)
property.nullable = getter.nullable
property.visibility = getter.visibility
} else {
// create new property from getter
toAdd.add(PropertyData(
name = propertyName,
type = getter.type,
nullable = getter.nullable,
optional = getter.optional,
visibility = getter.visibility,
kind = PropertyType.PROPERTY,
annotations = getter.annotations
Expand All @@ -58,6 +62,7 @@ class MergeGettersStep {

}

// remove all getters, add created members
typeData.members.removeIf { it.kind == PropertyType.GETTER }
typeData.members.addAll(toAdd)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,39 @@ import io.github.smiley4.schemakenerator.jsonschema.steps.JsonSchemaCoreAnnotati
import io.github.smiley4.schemakenerator.jsonschema.steps.JsonSchemaCoreAnnotationDeprecatedStep
import io.github.smiley4.schemakenerator.jsonschema.steps.JsonSchemaCoreAnnotationDescriptionStep
import io.github.smiley4.schemakenerator.jsonschema.steps.JsonSchemaCoreAnnotationExamplesStep
import io.github.smiley4.schemakenerator.jsonschema.steps.JsonSchemaCoreAnnotationOptionalAndRequiredStep
import io.github.smiley4.schemakenerator.jsonschema.steps.JsonSchemaCoreAnnotationTitleStep
import io.github.smiley4.schemakenerator.jsonschema.steps.JsonSchemaCustomizeStep
import io.github.smiley4.schemakenerator.jsonschema.steps.JsonSchemaGenerationStep

enum class OptionalHandling {
REQUIRED,
NON_REQUIRED
}

class JsonSchemaGenerationStepConfig {
/**
* How to handle optional parameters
*
* Example:
* ```
* class MyExample(val someValue: String = "hello")
* ```
* - with `optionalHandling = REQUIRED` => "someValue" is required (because is not nullable)
* - with `optionalHandling = NON_REQUIRED` => "someValue" is not required (because a default value is provided)
*/
var optionalHandling = OptionalHandling.REQUIRED
}


/**
* See [JsonSchemaGenerationStep]
*/
fun Bundle<BaseTypeData>.generateJsonSchema(): Bundle<JsonSchema> {
return JsonSchemaGenerationStep().generate(this)
fun Bundle<BaseTypeData>.generateJsonSchema(configBlock: JsonSchemaGenerationStepConfig.() -> Unit = {}): Bundle<JsonSchema> {
val config = JsonSchemaGenerationStepConfig().apply(configBlock)
return JsonSchemaGenerationStep(
optionalAsNonRequired = config.optionalHandling == OptionalHandling.NON_REQUIRED,
).generate(this)
}


Expand All @@ -39,10 +63,11 @@ fun Bundle<JsonSchema>.withAutoTitle(type: TitleType = TitleType.FULL): Bundle<J

/**
* See [JsonSchemaCoreAnnotationDefaultStep], [JsonSchemaCoreAnnotationDeprecatedStep], [JsonSchemaCoreAnnotationDescriptionStep],
* [JsonSchemaCoreAnnotationExamplesStep], [JsonSchemaCoreAnnotationTitleStep]
* [JsonSchemaCoreAnnotationExamplesStep], [JsonSchemaCoreAnnotationTitleStep], [JsonSchemaCoreAnnotationOptionalAndRequiredStep]
*/
fun Bundle<JsonSchema>.handleCoreAnnotations(): Bundle<JsonSchema> {
return this
.let { JsonSchemaCoreAnnotationOptionalAndRequiredStep().process(this) }
.let { JsonSchemaCoreAnnotationDefaultStep().process(this) }
.let { JsonSchemaCoreAnnotationDeprecatedStep().process(this) }
.let { JsonSchemaCoreAnnotationDescriptionStep().process(this) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.github.smiley4.schemakenerator.jsonschema.steps

import io.github.smiley4.schemakenerator.core.annotations.Optional
import io.github.smiley4.schemakenerator.core.annotations.Required
import io.github.smiley4.schemakenerator.core.data.Bundle
import io.github.smiley4.schemakenerator.core.data.PropertyData
import io.github.smiley4.schemakenerator.jsonschema.data.JsonSchema
import io.github.smiley4.schemakenerator.jsonschema.jsonDsl.JsonArray
import io.github.smiley4.schemakenerator.jsonschema.jsonDsl.JsonNode
import io.github.smiley4.schemakenerator.jsonschema.jsonDsl.JsonObject
import io.github.smiley4.schemakenerator.jsonschema.jsonDsl.JsonTextValue
import io.github.smiley4.schemakenerator.jsonschema.steps.JsonSchemaAnnotationUtils.iterateProperties

/**
* Sets properties as optional/required from core [Optional] and [Required]-annotation.
*/
class JsonSchemaCoreAnnotationOptionalAndRequiredStep {

fun process(bundle: Bundle<JsonSchema>): Bundle<JsonSchema> {
return bundle.also { schema ->
process(schema.data)
schema.supporting.forEach { process(it) }
}
}

private fun process(schema: JsonSchema) {
iterateProperties(schema) { prop, data ->
determineRequired(data)?.also { required ->
if (required) {
addRequired(schema, data.name)
} else {
removeRequired(schema, data.name)
}
}
}
}

private fun determineRequired(typeData: PropertyData): Boolean? {
if (typeData.annotations.any { it.name == Required::class.qualifiedName }) {
return true
}
if (typeData.annotations.any { it.name == Optional::class.qualifiedName }) {
return false
}
return null
}

private fun getRequiredList(schema: JsonSchema): MutableList<JsonNode> {
val json = schema.json
if (json is JsonObject) {
val required = json.properties["required"]
if (required is JsonArray) {
return required.items
}
}
return mutableListOf()
}

private fun addRequired(schema: JsonSchema, propertyName: String) {
val list = getRequiredList(schema)
if (list.none { (it as JsonTextValue).value == propertyName }) {
list.add(JsonTextValue(propertyName))
}
}

private fun removeRequired(schema: JsonSchema, propertyName: String) {
val list = getRequiredList(schema)
list.removeIf { (it as JsonTextValue).value == propertyName }
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import io.github.smiley4.schemakenerator.jsonschema.jsonDsl.JsonNode
* Generates json-schemas from the given type data. All types in the schema are provisionally referenced by the full type-id.
* Result needs to be "compiled" to get the final json-schema.
*/
class JsonSchemaGenerationStep {
class JsonSchemaGenerationStep(private val optionalAsNonRequired: Boolean = false) {

private val schemaUtils = JsonSchemaUtils()

Expand Down Expand Up @@ -168,7 +168,9 @@ class JsonSchemaGenerationStep {

collectMembers(typeData, typeDataList).forEach { member ->
propertySchemas[member.name] = schemaUtils.referenceSchema(member.type)
if (!member.nullable) {
val nullable = member.nullable
val optional = member.optional && optionalAsNonRequired
if(!nullable && !optional) {
requiredProperties.add(member.name)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ class ReflectionTypeProcessingStep(
name = "item",
type = it.type,
nullable = it.nullable,
optional = false,
visibility = Visibility.PUBLIC,
kind = PropertyType.PROPERTY,
annotations = mutableListOf()
Expand All @@ -259,6 +260,7 @@ class ReflectionTypeProcessingStep(
name = "item",
type = it.type,
nullable = it.nullable,
optional = false,
visibility = Visibility.PUBLIC,
kind = PropertyType.PROPERTY,
annotations = mutableListOf()
Expand All @@ -268,6 +270,7 @@ class ReflectionTypeProcessingStep(
name = "item",
type = it.value.type,
nullable = it.value.nullable,
optional = false,
visibility = Visibility.PUBLIC,
kind = PropertyType.PROPERTY,
annotations = mutableListOf()
Expand All @@ -287,6 +290,7 @@ class ReflectionTypeProcessingStep(
name = "key",
type = it.type,
nullable = it.nullable,
optional = false,
visibility = Visibility.PUBLIC,
kind = PropertyType.PROPERTY,
annotations = mutableListOf()
Expand All @@ -297,6 +301,7 @@ class ReflectionTypeProcessingStep(
name = "value",
type = it.type,
nullable = it.nullable,
optional = false,
visibility = Visibility.PUBLIC,
kind = PropertyType.PROPERTY,
annotations = mutableListOf()
Expand Down Expand Up @@ -371,7 +376,7 @@ class ReflectionTypeProcessingStep(
.filter { filterMember(it) }
.mapNotNull { member ->
when (member) {
is KProperty<*> -> parseProperty(member, resolvedTypeParameters, typeData)
is KProperty<*> -> parseProperty(member, resolvedTypeParameters, typeData, clazz)
is KFunction<*> -> parseFunction(member, resolvedTypeParameters, typeData)
else -> null
}
Expand Down Expand Up @@ -432,12 +437,21 @@ class ReflectionTypeProcessingStep(
private fun parseProperty(
member: KProperty<*>,
resolvedTypeParameters: Map<String, TypeParameterData>,
typeData: MutableList<BaseTypeData>
typeData: MutableList<BaseTypeData>,
clazz: KClass<*>
): PropertyData {

val isOptional = clazz.constructors.any { constructor ->
val ctorParameter = constructor.parameters.find { parameter ->
parameter.name == member.name && parameter.type == member.returnType
}
ctorParameter?.isOptional ?: false
}
return PropertyData(
name = member.name,
type = resolveMemberType(member.returnType, resolvedTypeParameters, typeData).id,
nullable = member.returnType.isMarkedNullable,
optional = isOptional,
annotations = parseAnnotations(member).toMutableList(),
kind = PropertyType.PROPERTY,
visibility = determinePropertyVisibility(member)
Expand All @@ -453,9 +467,10 @@ class ReflectionTypeProcessingStep(
name = member.name,
type = resolveMemberType(member.returnType, resolvedTypeParameters, typeData).id,
nullable = member.returnType.isMarkedNullable,
optional = false,
annotations = parseAnnotations(member).toMutableList(),
kind = determineFunctionPropertyType(member),
visibility = determinePropertyVisibility(member)
visibility = determinePropertyVisibility(member),
)
}

Expand Down Expand Up @@ -639,6 +654,7 @@ class ReflectionTypeProcessingStep(
name = name,
type = TypeId.wildcard(),
nullable = false,
optional = false,
visibility = Visibility.PUBLIC,
kind = PropertyType.PROPERTY,
annotations = mutableListOf()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ class KotlinxSerializationTypeProcessingStep(
name = "item",
type = itemType.id,
nullable = itemDescriptor.isNullable,
optional = false,
kind = PropertyType.PROPERTY,
visibility = Visibility.PUBLIC,
),
Expand Down Expand Up @@ -220,6 +221,7 @@ class KotlinxSerializationTypeProcessingStep(
name = "key",
type = keyType.id,
nullable = keyDescriptor.isNullable,
optional = false,
kind = PropertyType.PROPERTY,
visibility = Visibility.PUBLIC,

Expand All @@ -228,6 +230,7 @@ class KotlinxSerializationTypeProcessingStep(
name = "value",
type = valueType.id,
nullable = valueDescriptor.isNullable,
optional = false,
kind = PropertyType.PROPERTY,
visibility = Visibility.PUBLIC,
),
Expand Down Expand Up @@ -258,6 +261,7 @@ class KotlinxSerializationTypeProcessingStep(
name = fieldName,
type = fieldType.id,
nullable = fieldDescriptor.isNullable,
optional = false,
kind = PropertyType.PROPERTY,
visibility = Visibility.PUBLIC,
)
Expand Down
Loading

0 comments on commit f95fcd7

Please sign in to comment.