All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.pwall.json.schema.codegen.Configurator.kt Maven / Gradle / Ivy

There is a newer version: 0.118
Show newest version
/*
 * @(#) Configurator.kt
 *
 * json-kotlin-schema-codegen  JSON Schema Code Generation
 * Copyright (c) 2021, 2022, 2023 Peter Wall
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package net.pwall.json.schema.codegen

import java.net.URI

import net.pwall.json.JSONBoolean
import net.pwall.json.JSONFunctions.displayString
import net.pwall.json.JSONMapping
import net.pwall.json.JSONSequence
import net.pwall.json.JSONString
import net.pwall.json.JSONValue
import net.pwall.json.pointer.JSONPointer
import net.pwall.json.pointer.JSONReference
import net.pwall.json.schema.JSONSchema
import net.pwall.json.schema.codegen.CodeGenerator.Companion.fatal
import net.pwall.json.schema.output.BasicErrorEntry
import net.pwall.json.schema.validation.FormatValidator
import net.pwall.mustache.Template

object Configurator {

    fun configure(generator: CodeGenerator, ref: JSONReference, uri: URI? = null) {
        // TODO validate against schema?
        val extensionValidators = mutableMapOf>()
        val nonStandardFormats = mutableMapOf()
        val parser = generator.schemaParser
        if (ref.value !is JSONMapping<*>)
            fatal("Config must be object")
        ref.ifPresent("title") {}
        ref.ifPresent("version") {}
        ref.ifPresent("description") {}
        ref.ifPresent("packageName") {
            generator.basePackageName = it?.let { nonEmptyString(it) }
        }
        ref.ifPresent("markerInterface") {
            generator.markerInterface = it?.let { ClassName.of(nonEmptyString(it)) }
        }
        ref.ifPresent("generatorComment") {
            generator.generatorComment = it?.let { nonEmptyString(it) }
        }
        ref.ifPresent("commentTemplate") {
            generator.commentTemplate = it?.let { Template.parse(nonEmptyString(it)) }
        }
        ref.ifPresent("targetLanguage") {
            generator.targetLanguage = when (it.value) {
                "kotlin" -> TargetLanguage.KOTLIN
                "java" -> TargetLanguage.JAVA
                "typescript" -> TargetLanguage.TYPESCRIPT
                else -> invalid(it)
            }
        }
        ref.ifPresent("nestedClassNameOption") {
            generator.nestedClassNameOption = when (it.value) {
                "property" -> CodeGenerator.NestedClassNameOption.USE_NAME_FROM_PROPERTY
                "refSchema" -> CodeGenerator.NestedClassNameOption.USE_NAME_FROM_REF_SCHEMA
                else -> invalid(it)
            }
        }
        ref.ifPresent("derivePackageFromStructure") {
            generator.derivePackageFromStructure = it.value
        }
        ref.ifPresent>("extensionValidations") {
            forEachKey { ext ->
                if (ext.value !is JSONMapping<*>)
                    fatal("Config entry ${ext.pointer} invalid entry")
                ext.forEachKey {
                    if (it.value !is JSONMapping<*>)
                        fatal("Config entry ${it.pointer} invalid entry")
                    extensionValidators.getOrPut(ext.current) { mutableMapOf() }[it.current] =
                        CustomValidator(uri, it.pointer, parser.parseSchema(ref.base, it, null))
                }
            }
        }
        ref.ifPresent>("nonStandardFormat") {
            forEachKey {
                if (it.value !is JSONMapping<*>)
                    fatal("Config entry ${it.pointer} invalid entry")
                nonStandardFormats[it.current] =
                        CustomValidator(uri, it.pointer, parser.parseSchema(ref.base, it, null))
            }
        }
        ref.ifPresent>("customClasses") {
            ifPresent>("format") {
                forEachKey {
                    (it.value as? JSONString)?.let { s ->
                        generator.addCustomClassByFormat(it.current, it.nonEmptyString(s))
                    } ?: it.invalid(it.value)
                }
            }
            ifPresent>("uri") {
                forEachKey {
                    (it.value as? JSONString)?.let { s ->
                        generator.addCustomClassByURI(URI(it.current), it.nonEmptyString(s))
                    } ?: it.invalid(it.value)
                }
            }
            ifPresent>("extension") {
                forEachKey { ext ->
                    if (ext.value !is JSONMapping<*>)
                        fatal("Config entry ${ext.pointer} invalid entry")
                    ext.forEachKey {
                        (it.value as? JSONString)?.let { s ->
                            generator.addCustomClassByExtension(ext.current, it.current, it.nonEmptyString(s))
                        } ?: it.invalid(it.value)
                    }
                }
            }
            forEachKey {
                if (it.current !in listOf("format", "uri", "extension"))
                    fatal("Config entry ${it.pointer} unrecognised mapping type")
            }
        }
        ref.ifPresent>("classNames") {
            forEachKey {
                (it.value as? JSONString)?.let { s ->
                    generator.addClassNameMapping(URI(it.current), it.nonEmptyString(s))
                } ?: it.invalid(it.value)
            }
        }
        ref.ifPresent>("annotations") {
            ifPresent>("classes") {
                forEachKey { entry ->
                    when (val value = entry.value) {
                        null -> generator.addClassAnnotation(entry.current)
                        is JSONString -> generator.addClassAnnotation(entry.current, Template.parse(value.value))
                        else -> fatal("Config entry ${entry.pointer} invalid entry")
                    }
                }
            }
            ifPresent>("fields") {
                forEachKey { entry ->
                    when (val value = entry.value) {
                        null -> generator.addFieldAnnotation(entry.current)
                        is JSONString -> generator.addFieldAnnotation(entry.current, Template.parse(value.value))
                        else -> fatal("Config entry ${entry.pointer} invalid entry")
                    }
                }
            }
            forEachKey {
                if (it.current != "classes" && it.current != "fields")
                    fatal("Config entry ${it.pointer} unrecognised annotation type")
            }
        }
        ref.ifPresent("companionObject") {
            when (it) {
                is JSONBoolean -> generator.companionObjectForAll = it.value
                is JSONSequence<*> -> {
                    generator.companionObjectForClasses.addAll(it.map { v ->
                        if (v !is JSONString)
                            fatal("Config entry companionObject invalid value")
                        v.value
                    })
                }
                else -> fatal("Config entry companionObject invalid value")
            }
        }
        if (extensionValidators.isNotEmpty()) {
            parser.customValidationHandler = { key, _, _, value ->
                extensionValidators[key]?.get((value as? JSONString)?.value)
            }
        }
        if (nonStandardFormats.isNotEmpty()) {
            parser.nonstandardFormatHandler = { keyword ->
                nonStandardFormats[keyword]?.let { FormatValidator.DelegatingFormatChecker(keyword, it) }
            }
        }
    }

    private fun JSONReference.forEachKey(block: (JSONReference) -> Unit) {
        value.let {
            if (it !is JSONMapping<*>)
                fatal("Config entry ${this.pointer} must be object - ${it.displayValue()}")
            for (key in it.keys)
                block(child(key))
        }
    }

    private inline fun  JSONReference.ifPresent(
        name: String,
        block: JSONReference.(T) -> Unit
    ): ConditionDSL {
        child(name).let {
            if (it.isValid) {
                it.checkType(it.value) { v ->
                    it.block(v)
                    return ConditionDSL(this@ifPresent, true)
                }
            }
        }
        return ConditionDSL(this@ifPresent, false)
    }

    class ConditionDSL(val ref: JSONReference, private val condition: Boolean) {

        fun andAlso(block: JSONReference.() -> Unit): ConditionDSL {
            if (condition)
                ref.block()
            return this
        }

        fun orElse(block: JSONReference.() -> Unit): ConditionDSL {
            if (!condition)
                ref.block()
            return ConditionDSL(ref, !condition)
        }

    }

    private inline fun  JSONReference.checkType(
        value: JSONValue?,
        block: JSONReference.(T) -> V
    ): V = when (value) {
        is T -> block(value)
        else -> fatal("Config entry $pointer incorrect type - ${value.displayValue()}")
    }

    private fun JSONReference.nonEmptyString(s: JSONString): String = s.value.takeIf { it.isNotEmpty() } ?:
            fatal("Config entry $pointer must not be empty")

    private fun JSONValue?.displayValue(): String {
        return when (this) {
            null -> "null"
            is JSONString -> displayString(value, 21)
            is JSONSequence<*> -> when (size) {
                0 -> "[]"
                1 -> "[${this[0].displayValue()}]"
                else -> "[...]"
            }
            is JSONMapping<*> -> when (size) {
                0 -> "{}"
                1 -> entries.iterator().next().let { "{${displayString(it.key, 21)}:${it.value.displayValue()}}" }
                else -> "{...}"
            }
            else -> toString()
        }
    }

    private fun JSONReference.invalid(value: JSONValue?): Nothing {
        fatal("Config entry $pointer invalid - ${value.displayValue()}")
    }

    class CustomValidator(uri: URI?, location: JSONPointer, val schema: JSONSchema) :
            JSONSchema.Validator(uri, location) {

        override fun getErrorEntry(
            relativeLocation: JSONPointer,
            json: JSONValue?,
            instanceLocation: JSONPointer,
        ): BasicErrorEntry? {
            fatal("Config error - validation for code generation only")
        }

        override fun validate(json: JSONValue?, instanceLocation: JSONPointer): Boolean {
            fatal("Config error - validation for code generation only")
        }

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy