├── settings.gradle ├── .github └── FUNDING.yml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ └── kotlin │ │ └── org │ │ └── stvad │ │ ├── kask │ │ ├── model │ │ │ ├── constants.kt │ │ │ ├── generator │ │ │ │ ├── common.kt │ │ │ │ ├── KaskGenerator.kt │ │ │ │ ├── SlotGenerator.kt │ │ │ │ ├── SlotVendor.kt │ │ │ │ ├── AlexaModelGenerator.kt │ │ │ │ ├── IntentGenerator.kt │ │ │ │ └── IntentCompanionGenerator.kt │ │ │ ├── intent.kt │ │ │ ├── InteractionModel.kt │ │ │ └── slot.kt │ │ ├── exceptions.kt │ │ ├── request │ │ │ ├── IntentRequestHandler.kt │ │ │ ├── BasicIntentRequestHandler.kt │ │ │ └── handlers.kt │ │ └── extensions.kt │ │ └── verse │ │ └── extensions.kt └── test │ └── kotlin │ └── org │ └── stvad │ └── kask │ ├── model │ ├── generator │ │ ├── utils │ │ │ └── slot.kt │ │ ├── PoeticSlotGeneratorSpec.kt │ │ ├── IntentGeneratorSpec.kt │ │ ├── AlexaModelGeneratorSpec.kt │ │ └── PoeticSlotVendorSpec.kt │ └── SlotSpec.kt │ ├── ExtensionsSpec.kt │ └── request │ └── HandlerSpec.kt ├── .travis.yml ├── gradlew.bat ├── gradlew ├── README.md └── LICENSE /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'kask' 2 | 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Stvad 4 | patreon: stvad 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Stvad/kask/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/model/constants.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model 2 | 3 | const val amazonPrefix = "AMAZON." -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /src/test/kotlin/org/stvad/kask/model/generator/utils/slot.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model.generator.utils 2 | 3 | import org.stvad.kask.model.DurationSlot 4 | import org.stvad.kask.model.SlotDefinition 5 | 6 | fun createDummySlot(name: String = "dummyName", type: String = DurationSlot.type) = SlotDefinition(name, type, emptyList()) 7 | -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/verse/extensions.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.verse 2 | 3 | import com.squareup.kotlinpoet.ParameterSpec 4 | import com.squareup.kotlinpoet.PropertySpec 5 | 6 | fun ParameterSpec.toProperty() = PropertySpec.builder(name, type).initializer(name).build() 7 | 8 | fun Iterable.toInvocation() = map(ParameterSpec::name).joinToString() 9 | fun Iterable.toProperties() = map { it.toProperty() } 10 | 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | install: true 3 | 4 | jdk: 5 | - oraclejdk8 6 | 7 | before_cache: 8 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 9 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 10 | 11 | cache: 12 | directories: 13 | - $HOME/.gradle/caches/ 14 | - $HOME/.gradle/wrapper/ 15 | - $HOME/.m2/repository/ 16 | 17 | script: 18 | - ./gradlew build -s 19 | 20 | after_success: 21 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/model/generator/common.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model.generator 2 | 3 | import com.squareup.kotlinpoet.ParameterSpec 4 | import org.stvad.kask.model.Intent 5 | import org.stvad.kask.model.Slot 6 | 7 | val askIntentParameter = ParameterSpec.builder(Intent::askIntent.name, com.amazon.ask.model.Intent::class).build() 8 | val askSlotParameter = ParameterSpec.builder(Slot::askSlot.name, com.amazon.ask.model.Slot::class).build() 9 | -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/exceptions.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask 2 | 3 | open class KaskException(message: String? = null, cause: Throwable? = null) : RuntimeException(message, cause) 4 | 5 | class InvalidModelException(message: String? = null, cause: Throwable? = null) : KaskException(message, cause) 6 | 7 | class SlotMissingException(message: String? = null, cause: Throwable? = null) : KaskException(message, cause) 8 | 9 | class CodeGenerationError(message: String? = null, cause: Throwable? = null) : KaskException(message, cause) -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/model/intent.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model 2 | 3 | import com.amazon.ask.model.IntentConfirmationStatus 4 | 5 | abstract class Intent(val askIntent: com.amazon.ask.model.Intent) { 6 | val confirmationStatus: IntentConfirmationStatus get() = askIntent.confirmationStatus 7 | val name: String get() = askIntent.name 8 | } 9 | 10 | abstract class BuiltInIntent(askIntent: com.amazon.ask.model.Intent) : Intent(askIntent) 11 | 12 | interface IntentCompanion { 13 | val name: String 14 | fun fromAskIntent(askIntent: com.amazon.ask.model.Intent): T 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/request/IntentRequestHandler.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.request 2 | 3 | import com.amazon.ask.dispatcher.request.handler.HandlerInput 4 | import com.amazon.ask.model.Response 5 | import org.stvad.kask.model.Intent 6 | import org.stvad.kask.model.IntentCompanion 7 | import java.util.Optional 8 | 9 | abstract class IntentRequestHandler(protected val intentCompanion: IntentCompanion) : BasicIntentRequestHandler(intentCompanion.name) { 10 | override fun handle(input: HandlerInput) = handle(input, intentCompanion.fromAskIntent(input.intent)) 11 | abstract fun handle(input: HandlerInput, intent: T): Optional 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/extensions.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask 2 | 3 | import com.amazon.ask.model.Intent 4 | import org.joda.time.DateTime.now 5 | import org.joda.time.Duration 6 | import org.joda.time.Period 7 | 8 | fun Intent.requireSlot(slotName: Any) = slots[slotName.toString()] 9 | ?: throw SlotMissingException("The slot $slotName is missing. Please check validity of your model.") 10 | 11 | val Period.duration: Duration 12 | get() = toDurationFrom(now()) 13 | 14 | fun String.removeFartsFoundPrefix(prefixes: List) = 15 | prefixes.find { this.startsWith(it) }?.let { this.removePrefix(it) } ?: this 16 | 17 | fun String.isAlphaNum() = matches("[A-Za-z0-9]+".toRegex()) -------------------------------------------------------------------------------- /src/test/kotlin/org/stvad/kask/ExtensionsSpec.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask 2 | 3 | import com.amazon.ask.model.Intent 4 | import com.amazon.ask.model.Slot 5 | import io.kotlintest.shouldBe 6 | import io.kotlintest.shouldThrow 7 | import io.kotlintest.specs.WordSpec 8 | 9 | class ExtensionsSpec : WordSpec({ 10 | "requireSlot" should { 11 | val testSlot = Slot.builder().withName("test_slot_name").build() 12 | val otherSlotName = "other_slot_name" 13 | 14 | "throw if slot is missing" { 15 | val testIntent = Intent.builder().putSlotsItem(otherSlotName, testSlot).build() 16 | shouldThrow { 17 | testIntent.requireSlot(testSlot.name) 18 | } 19 | } 20 | 21 | "return slot if present" { 22 | val testIntent = Intent.builder().putSlotsItem(testSlot.name, testSlot).build() 23 | testIntent.requireSlot(testSlot.name) shouldBe testSlot 24 | } 25 | } 26 | }) -------------------------------------------------------------------------------- /src/test/kotlin/org/stvad/kask/model/generator/PoeticSlotGeneratorSpec.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model.generator 2 | 3 | import io.kotlintest.shouldBe 4 | import io.kotlintest.specs.WordSpec 5 | import org.stvad.kask.model.StringSlot 6 | import org.stvad.kask.model.generator.utils.createDummySlot 7 | 8 | class PoeticSlotGeneratorSpec : WordSpec({ 9 | "PoeticSlotGenerator" should { 10 | "return typespec with the name where specified prefix was removed" { 11 | val prefix = "prefix." 12 | val suffix = "suffix" 13 | val slotDefinition = createDummySlot(type = prefix + suffix) 14 | 15 | PoeticSlotGenerator(listOf(prefix)).generate(slotDefinition).name shouldBe suffix 16 | } 17 | 18 | "generated typespec should have the StringSlot as supertype" { 19 | println(PoeticSlotGenerator().generate(createDummySlot()).superclass) 20 | PoeticSlotGenerator().generate(createDummySlot()).superclass.toString() shouldBe StringSlot::class.qualifiedName 21 | } 22 | } 23 | }) -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/request/BasicIntentRequestHandler.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.request 2 | 3 | import com.amazon.ask.dispatcher.request.handler.HandlerInput 4 | import com.amazon.ask.dispatcher.request.handler.RequestHandler 5 | import com.amazon.ask.model.DialogState 6 | import com.amazon.ask.model.Intent 7 | import com.amazon.ask.model.IntentRequest 8 | import com.amazon.ask.model.Response 9 | import java.util.Optional 10 | 11 | abstract class BasicIntentRequestHandler(private vararg val intents: String) : RequestHandler { 12 | override fun canHandle(input: HandlerInput) = canHandleIntents(input, intents) 13 | 14 | val HandlerInput.intentRequest get() = (requestEnvelope.request as IntentRequest) 15 | 16 | fun HandlerInput.delegateDialog(): Optional = responseBuilder.addDelegateDirective(intent).build() 17 | 18 | val HandlerInput.dialogState: DialogState 19 | get() = intentRequest.dialogState 20 | 21 | val DialogState.isCompleted get() = this == DialogState.COMPLETED 22 | 23 | val HandlerInput.intent: Intent 24 | get() = intentRequest.intent 25 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/model/InteractionModel.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model 2 | 3 | import com.squareup.moshi.Json 4 | import com.squareup.moshi.JsonClass 5 | 6 | @JsonClass(generateAdapter = true) 7 | data class InteractionModelEnvelope(val interactionModel: InteractionModel) { 8 | companion object 9 | } 10 | 11 | @JsonClass(generateAdapter = true) 12 | data class InteractionModel(val languageModel: LanguageModel, val prompts: List = emptyList()) 13 | 14 | @JsonClass(generateAdapter = true) 15 | data class Dialog(val intents: List = emptyList()) 16 | 17 | @JsonClass(generateAdapter = true) 18 | data class LanguageModel(val invocationName: String, 19 | @Json(name = "intents") val intentDefinitions: List = emptyList(), 20 | val types: List = emptyList()) 21 | 22 | @JsonClass(generateAdapter = true) 23 | data class IntentDefinition(val name: String, 24 | val samples: List = emptyList(), 25 | @Json(name = "slots") val slotDefinitions: List = emptyList()) 26 | 27 | @JsonClass(generateAdapter = true) 28 | data class SlotDefinition(val name: String, val type: String, val samples: List = emptyList()) -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/model/generator/KaskGenerator.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model.generator 2 | 3 | import com.squareup.moshi.Moshi 4 | import org.stvad.kask.InvalidModelException 5 | import org.stvad.kask.model.IntentDefinition 6 | import org.stvad.kask.model.InteractionModelEnvelope 7 | import org.stvad.kask.model.jsonAdapter 8 | import java.io.File 9 | import kotlin.reflect.KClass 10 | 11 | object KaskGenerator { 12 | fun generateAlexaModel(packageName: String, 13 | modelPath: File, 14 | outputPath: File, 15 | slotOverrideMap: Map> = emptyMap()) { 16 | val intentDefinitions = getIntentDefinitions(modelPath) 17 | 18 | AlexaModelGenerator(intentDefinitions, packageName, slotOverrideMap) 19 | .generate().writeTo(outputPath) 20 | } 21 | 22 | private fun getIntentDefinitions(modelPath: File): List { 23 | val interactionModelEnvelope = 24 | InteractionModelEnvelope.jsonAdapter(Moshi.Builder().build()).fromJson(modelPath.readText()) 25 | ?: throw InvalidModelException("The supplied alexa language model is not valid") 26 | 27 | return interactionModelEnvelope.interactionModel.languageModel.intentDefinitions 28 | } 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/model/generator/SlotGenerator.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model.generator 2 | 3 | import com.squareup.kotlinpoet.FunSpec.Companion.constructorBuilder 4 | import com.squareup.kotlinpoet.TypeSpec 5 | import org.stvad.kask.model.SlotDefinition 6 | import org.stvad.kask.model.StringSlot 7 | import org.stvad.kask.model.amazonPrefix 8 | import org.stvad.kask.removeFartsFoundPrefix 9 | import org.stvad.verse.toInvocation 10 | 11 | interface SlotGenerator { 12 | fun generate(slotDefinition: SlotDefinition): TypeSpec 13 | } 14 | 15 | class PoeticSlotGenerator(private val prefixesToRemove: List = listOf(amazonPrefix)) : SlotGenerator { 16 | override fun generate(slotDefinition: SlotDefinition) = 17 | TypeSpec.classBuilder(classNameFor(slotDefinition)) 18 | .superclass(StringSlot::class) 19 | .addSuperclassConstructorParameter(superclassParameters().toInvocation()) 20 | .primaryConstructor(constructor()) 21 | .build() 22 | 23 | private fun classNameFor(slotDefinition: SlotDefinition) = slotDefinition.type.removeFartsFoundPrefix(prefixesToRemove) 24 | private fun constructor() = constructorBuilder().addParameters(superclassParameters()).build() 25 | 26 | private fun superclassParameters() = listOf(askSlotParameter) 27 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/stvad/kask/model/generator/IntentGeneratorSpec.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model.generator 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | import io.kotlintest.shouldThrow 5 | import io.kotlintest.specs.WordSpec 6 | import io.mockk.every 7 | import io.mockk.mockk 8 | import io.mockk.verify 9 | import org.stvad.kask.CodeGenerationError 10 | import org.stvad.kask.model.IntentDefinition 11 | import org.stvad.kask.model.generator.utils.createDummySlot 12 | 13 | class IntentGeneratorSpec : WordSpec({ 14 | "IntentGenerator" should { 15 | val dummySlot = createDummySlot() 16 | 17 | "throw validation exception if non-alphanumeric name is provided as intent name" { 18 | shouldThrow { 19 | IntentGenerator(IntentDefinition("test.Int", slotDefinitions = listOf(dummySlot))).generate() 20 | } 21 | } 22 | 23 | "call vendor to retrieve slot type if intent has slots" { 24 | val slotVendor = mockk() 25 | 26 | every { slotVendor.classNameForSlot(dummySlot) } returns ClassName("", "dummyName") 27 | 28 | IntentGenerator( 29 | IntentDefinition("testIntent", slotDefinitions = listOf(dummySlot)), 30 | slotVendor).generate() 31 | 32 | verify { slotVendor.classNameForSlot(dummySlot) } 33 | } 34 | } 35 | }) -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/model/generator/SlotVendor.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model.generator 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | import com.squareup.kotlinpoet.TypeSpec 5 | import com.squareup.kotlinpoet.asClassName 6 | import org.stvad.kask.CodeGenerationError 7 | import org.stvad.kask.model.SlotDefinition 8 | import org.stvad.kask.model.supportedAmazonSlots 9 | import kotlin.reflect.KClass 10 | 11 | interface SlotVendor { 12 | fun classNameForSlot(slotDefinition: SlotDefinition): ClassName 13 | val generatedSlots: Set 14 | } 15 | 16 | class PoeticSlotVendor(private val slotGenerator: SlotGenerator = PoeticSlotGenerator(), 17 | private val overrideMap: Map> = emptyMap()) : SlotVendor { 18 | 19 | 20 | override fun classNameForSlot(slotDefinition: SlotDefinition): ClassName { 21 | return overrideMap.getOrElse(slotDefinition.type) { supportedAmazonSlots[slotDefinition.type] }?.asClassName() 22 | ?: generateSlot(slotDefinition) 23 | } 24 | 25 | override val generatedSlots = mutableSetOf() 26 | 27 | private fun generateSlot(slotDefinition: SlotDefinition): ClassName { 28 | val generatedSpec = slotGenerator.generate(slotDefinition) 29 | generatedSlots.add(generatedSpec) 30 | return ClassName("", generatedSpec.name ?: throw CodeGenerationError("Slot should have a name")) 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/model/generator/AlexaModelGenerator.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model.generator 2 | 3 | import arrow.core.Try 4 | import com.squareup.kotlinpoet.FileSpec 5 | import org.stvad.kask.model.IntentDefinition 6 | import kotlin.reflect.KClass 7 | 8 | class AlexaModelGenerator(private val intentDefinitions: List, 9 | packageName: String, 10 | slotOverrideMap: Map> = emptyMap(), 11 | private val modelSpecBuilder: FileSpec.Builder = FileSpec.builder(packageName, fileName), 12 | private val slotVendor: SlotVendor = PoeticSlotVendor(overrideMap = slotOverrideMap)) { 13 | companion object { 14 | private const val fileName = "model" 15 | private const val generatedNotificationComment = 16 | "This code is generated by the Kask model generation plugin. Any changes made would be overridden during next build." 17 | } 18 | 19 | fun generate(): FileSpec { 20 | addImports() 21 | addIntents() 22 | return modelSpecBuilder 23 | .addComment(generatedNotificationComment) 24 | .build() 25 | } 26 | 27 | private fun addImports() = 28 | IntentGenerator.requiredImports.forEach { modelSpecBuilder.addImport(it.first, it.second) } 29 | 30 | private fun addIntents() { 31 | intentDefinitions.map { Try { IntentGenerator(it, slotVendor).generate() } }.forEach { type -> 32 | type.fold({ modelSpecBuilder.addComment("Failed to generate an Intent type with the following error: $it\n") }, 33 | { modelSpecBuilder.addType(it) }) 34 | } 35 | 36 | addGeneratedSlots() 37 | } 38 | 39 | private fun addGeneratedSlots() = 40 | slotVendor.generatedSlots.forEach { modelSpecBuilder.addType(it) } 41 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/stvad/kask/model/generator/AlexaModelGeneratorSpec.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model.generator 2 | 3 | import com.squareup.kotlinpoet.FileSpec 4 | import com.squareup.kotlinpoet.TypeSpec 5 | import io.kotlintest.Description 6 | import io.kotlintest.Spec 7 | import io.kotlintest.extensions.TestListener 8 | import io.kotlintest.specs.WordSpec 9 | import io.mockk.clearConstructorMockk 10 | import io.mockk.every 11 | import io.mockk.mockk 12 | import io.mockk.mockkConstructor 13 | import io.mockk.spyk 14 | import io.mockk.verify 15 | import org.stvad.kask.model.IntentDefinition 16 | 17 | class AlexaModelGeneratorSpec : WordSpec({ 18 | "AlexaModelGenerator" should { 19 | val builderSpy = spyk(FileSpec.builder("", "")) 20 | val subject = AlexaModelGenerator(listOf(IntentDefinition("simpleIntent")), "", modelSpecBuilder = builderSpy) 21 | 22 | "add one type for intent with no custom slots" { 23 | subject.generate() 24 | verify(exactly = 1) { builderSpy.addType(any()) } 25 | } 26 | 27 | "gracefully handle error in intent generation" { 28 | mockkConstructor(IntentGenerator::class) 29 | every { anyConstructed().generate() } throws IllegalStateException() 30 | 31 | subject.generate() 32 | 33 | verify(exactly = 0) { builderSpy.addType(any()) } 34 | } 35 | 36 | "adds generated slots to the spec" { 37 | val dummyTypeSpec = TypeSpec.classBuilder("dummyType").build() 38 | 39 | val slotVendor = mockk() 40 | every { slotVendor.generatedSlots } returns setOf(dummyTypeSpec) 41 | 42 | AlexaModelGenerator(emptyList(), "", modelSpecBuilder = builderSpy, slotVendor = slotVendor).generate() 43 | 44 | verify { builderSpy.addType(dummyTypeSpec) } 45 | } 46 | } 47 | }) { 48 | override fun isInstancePerTest() = true 49 | override fun listeners(): List = listOf(object : TestListener { 50 | override fun afterSpec(description: Description, spec: Spec) = clearConstructorMockk(IntentGenerator::class) 51 | }) 52 | } -------------------------------------------------------------------------------- /src/test/kotlin/org/stvad/kask/model/generator/PoeticSlotVendorSpec.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model.generator 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | import com.squareup.kotlinpoet.TypeSpec 5 | import com.squareup.kotlinpoet.asClassName 6 | import io.kotlintest.matchers.collections.shouldContain 7 | import io.kotlintest.shouldBe 8 | import io.kotlintest.specs.WordSpec 9 | import io.mockk.every 10 | import io.mockk.mockk 11 | import io.mockk.verify 12 | import org.stvad.kask.model.DurationSlot 13 | import org.stvad.kask.model.generator.utils.createDummySlot 14 | 15 | class PoeticSlotVendorSpec : WordSpec({ 16 | "PoeticSlotVendor" should { 17 | val slotGenerator = mockk() 18 | 19 | "return a ClassName for the slot that is directly supported" { 20 | PoeticSlotVendor(slotGenerator).classNameForSlot(createDummySlot(type = DurationSlot.type)) shouldBe DurationSlot::class.asClassName() 21 | } 22 | 23 | "return a ClassName for the slot that is provided by an override" { 24 | val overriddenSlot = createDummySlot(type = "overriddenType") 25 | val slotVendor = PoeticSlotVendor(slotGenerator, mapOf(overriddenSlot.type to PoeticSlotVendorSpec::class)) 26 | 27 | slotVendor.classNameForSlot(overriddenSlot) shouldBe PoeticSlotVendorSpec::class.asClassName() 28 | } 29 | 30 | "if slot is not in default supported list - generate a slot type based on String slot type and return ClassName for it" { 31 | val unsupportedSlot = createDummySlot(type = "unsupported_slot") 32 | val unsupportedClassName = ClassName("", unsupportedSlot.type) 33 | val generatedSpec = TypeSpec.classBuilder(unsupportedClassName).build() 34 | 35 | every { slotGenerator.generate(unsupportedSlot) } returns generatedSpec 36 | 37 | val slotVendor = PoeticSlotVendor(slotGenerator) 38 | slotVendor.classNameForSlot(unsupportedSlot) shouldBe unsupportedClassName 39 | 40 | verify { slotGenerator.generate(unsupportedSlot) } 41 | slotVendor.generatedSlots.shouldContain(generatedSpec) 42 | } 43 | } 44 | }) 45 | 46 | -------------------------------------------------------------------------------- /src/test/kotlin/org/stvad/kask/model/SlotSpec.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model 2 | 3 | import com.amazon.ask.model.Slot 4 | import com.github.debop.kodatimes.months 5 | import com.github.debop.kodatimes.toLocalTime 6 | import io.kotlintest.forAll 7 | import io.kotlintest.matchers.collections.shouldHaveSize 8 | import io.kotlintest.shouldBe 9 | import io.kotlintest.specs.WordSpec 10 | import org.stvad.kask.model.Slot as KaskSlot 11 | 12 | 13 | class SlotSpec : WordSpec({ 14 | val emptySlot = Slot.builder().build() 15 | 16 | "AllSlots" should { 17 | "handle null values in askSlot.value" { 18 | val supportedSlotTypes = 19 | listOf(::NumberSlot, 20 | ::FourDigitNumberSlot, 21 | ::DurationSlot, 22 | ::TimeSlot, 23 | ::PhoneNumberSlot, 24 | ::StringSlot) 25 | 26 | supportedSlotTypes.shouldHaveSize(supportedAmazonSlots.size + 1) 27 | 28 | forAll(supportedSlotTypes) { SlotConstructor -> 29 | SlotConstructor.call(emptySlot).value shouldBe null 30 | } 31 | } 32 | } 33 | 34 | "DurationSlot" should { 35 | "parse the duration value when it's present" { 36 | val oneMonthSlot = Slot.builder().withValue(1.months().toString()).build() 37 | 38 | DurationSlot(oneMonthSlot).value shouldBe 1.months() 39 | } 40 | } 41 | 42 | "NumberSlot" should { 43 | "parse Long value from the slot" { 44 | val longSlot = Slot.builder().withValue(Long.MAX_VALUE.toString()).build() 45 | 46 | NumberSlot(longSlot).value shouldBe Long.MAX_VALUE 47 | } 48 | } 49 | 50 | "TimeSlot" should { 51 | "be able to parse ISO date" { 52 | val testTime = "12:00" 53 | val middaySlot = Slot.builder().withValue(testTime).build() 54 | 55 | TimeSlot(middaySlot).value shouldBe testTime.toLocalTime() 56 | } 57 | 58 | "be able to interpret the Fuzzy Amazon time" { 59 | val timeOfDay = "MO" 60 | val morningSlot = Slot.builder().withValue(timeOfDay).build() 61 | 62 | TimeSlot(morningSlot).value shouldBe TimeSlot.defaultFuzzyTimeMap[timeOfDay]?.toLocalTime() 63 | } 64 | } 65 | }) 66 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/model/generator/IntentGenerator.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model.generator 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | import com.squareup.kotlinpoet.FunSpec.Companion.constructorBuilder 5 | import com.squareup.kotlinpoet.ParameterSpec 6 | import com.squareup.kotlinpoet.TypeSpec.Companion.classBuilder 7 | import org.stvad.kask.CodeGenerationError 8 | import org.stvad.kask.isAlphaNum 9 | import org.stvad.kask.model.BuiltInIntent 10 | import org.stvad.kask.model.Intent 11 | import org.stvad.kask.model.IntentDefinition 12 | import org.stvad.kask.model.amazonPrefix 13 | import org.stvad.kask.removeFartsFoundPrefix 14 | import org.stvad.kask.requireSlot 15 | import org.stvad.verse.toInvocation 16 | import org.stvad.verse.toProperties 17 | import com.amazon.ask.model.Intent as ASKIntent 18 | 19 | class IntentGenerator(private val intentDefinition: IntentDefinition, 20 | private val slotVendor: SlotVendor = PoeticSlotVendor(), 21 | private val prefixesToRemove: List = listOf(amazonPrefix)) { 22 | companion object { 23 | val requiredImports = listOf("org.stvad.kask" to ASKIntent::requireSlot.name) 24 | } 25 | 26 | private fun className(): ClassName { 27 | val name = intentDefinition.name.removeFartsFoundPrefix(prefixesToRemove) 28 | if (!name.isAlphaNum()) throw CodeGenerationError("$name is not a valid intent name") 29 | 30 | return ClassName("", name) 31 | } 32 | 33 | fun generate() = 34 | classBuilder(className()) 35 | .primaryConstructor(constructor()) 36 | .addProperties(slotParameters().toProperties()) 37 | .superclass(superclass()) 38 | .addSuperclassConstructorParameter(superclassParameters().toInvocation()) 39 | .addType(IntentCompanionGenerator(intentDefinition, className(), slotVendor).generate()) 40 | .build() 41 | 42 | private fun superclass() = 43 | if (intentDefinition.name.startsWith(amazonPrefix)) BuiltInIntent::class 44 | else Intent::class 45 | 46 | 47 | private fun constructor() = constructorBuilder() 48 | .addParameters(superclassParameters()) 49 | .addParameters(slotParameters()) 50 | .build() 51 | 52 | //todo consider camelcase for names 53 | private fun slotParameters() = 54 | intentDefinition.slotDefinitions.map { 55 | ParameterSpec.builder(it.name, slotVendor.classNameForSlot(it)).build() 56 | } 57 | 58 | private fun superclassParameters() = listOf(askIntentParameter) 59 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/model/generator/IntentCompanionGenerator.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model.generator 2 | 3 | import com.amazon.ask.model.Intent 4 | import com.squareup.kotlinpoet.ClassName 5 | import com.squareup.kotlinpoet.CodeBlock 6 | import com.squareup.kotlinpoet.FunSpec 7 | import com.squareup.kotlinpoet.KModifier 8 | import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy 9 | import com.squareup.kotlinpoet.PropertySpec 10 | import com.squareup.kotlinpoet.TypeSpec 11 | import com.squareup.kotlinpoet.asClassName 12 | import org.stvad.kask.model.IntentCompanion 13 | import org.stvad.kask.model.IntentDefinition 14 | import org.stvad.kask.model.SlotDefinition 15 | import org.stvad.kask.requireSlot 16 | 17 | class IntentCompanionGenerator(private val intentDefinition: IntentDefinition, 18 | private val intentClassName: ClassName, 19 | private val slotVendor: SlotVendor) { 20 | 21 | fun generate(): TypeSpec = TypeSpec.companionObjectBuilder() 22 | .addSuperinterface(companionInterface) 23 | .addProperty(companionNameProperty) 24 | .addFunction(companionIntentIntentInitializer) 25 | .build() 26 | 27 | private val companionInterface = IntentCompanion::class.asClassName() 28 | .parameterizedBy(intentClassName) 29 | 30 | private val companionNameProperty = 31 | PropertySpec.builder(IntentCompanion::name.name, String::class, KModifier.OVERRIDE) 32 | .initializer("%S", intentDefinition.name) 33 | .build() 34 | 35 | private val intentInitializerCode = 36 | CodeBlock.builder() 37 | .add("return %T(", intentClassName) 38 | .add(askIntentParameter.name) 39 | .add(slotInitializers()) 40 | .add(")") 41 | .build() 42 | 43 | private val companionIntentIntentInitializer = FunSpec.builder(IntentCompanion::fromAskIntent.name) 44 | .addParameter(askIntentParameter) 45 | .addModifiers(KModifier.OVERRIDE) 46 | .addCode(intentInitializerCode) 47 | .returns(intentClassName) 48 | .build() 49 | 50 | private fun slotInitializerInvocation(slotDefinition: SlotDefinition) = 51 | CodeBlock.of("%T(${askIntentParameter.name}.${Intent::requireSlot.name}(%S))", 52 | slotVendor.classNameForSlot(slotDefinition), 53 | slotDefinition.name) 54 | 55 | private fun slotInitializers() = 56 | intentDefinition.slotDefinitions 57 | .map(this::slotInitializerInvocation) 58 | .fold(CodeBlock.of("")) { acc, block -> acc.toBuilder().add(", ").add(block).build() } 59 | } -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/request/handlers.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.request 2 | 3 | import com.amazon.ask.dispatcher.request.handler.HandlerInput 4 | import com.amazon.ask.dispatcher.request.handler.RequestHandler 5 | import com.amazon.ask.model.Request 6 | import com.amazon.ask.model.Response 7 | import com.amazon.ask.request.Predicates 8 | import com.amazon.ask.request.Predicates.requestType 9 | import com.amazon.ask.response.ResponseBuilder 10 | import org.stvad.kask.model.IntentCompanion 11 | import java.util.Optional 12 | import java.util.function.Predicate 13 | import kotlin.reflect.KClass 14 | 15 | typealias ResponseContext = ResponseBuilder.(HandlerInput) -> Unit 16 | 17 | abstract class LambdaRequestHandler(private val handler: (HandlerInput) -> Optional) : RequestHandler { 18 | override fun handle(input: HandlerInput) = handler(input) 19 | } 20 | 21 | fun handle(vararg intents: String, handler: (HandlerInput) -> Optional) = object : LambdaRequestHandler(handler) { 22 | override fun canHandle(input: HandlerInput) = canHandleIntents(input, intents) 23 | } 24 | 25 | fun handle(handler: (HandlerInput) -> Optional, vararg intents: String) = handle(*intents, handler = handler) 26 | 27 | fun respond(vararg intents: String, responseContext: ResponseContext) = 28 | handle(*intents) { it.respond(responseContext) } 29 | 30 | fun handle(vararg intents: IntentCompanion, handler: (HandlerInput) -> Optional) = object : LambdaRequestHandler(handler) { 31 | override fun canHandle(input: HandlerInput) = canHandleIntents(input, intents.map { it.name }.toTypedArray()) 32 | } 33 | 34 | fun handle(handler: (HandlerInput) -> Optional, vararg intents: IntentCompanion) = handle(*intents, handler = handler) 35 | 36 | fun respond(vararg intents: IntentCompanion, responseContext: ResponseContext) = 37 | handle(*intents) { it.respond(responseContext) } 38 | 39 | fun handle(vararg requestTypes: KClass, handler: (HandlerInput) -> Optional) = object : LambdaRequestHandler(handler) { 40 | override fun canHandle(input: HandlerInput) = 41 | input.matches(requestTypes 42 | .map { requestType(it.java) } 43 | .reduce(Predicate::or)) 44 | } 45 | 46 | fun handle(handler: (HandlerInput) -> Optional, vararg requestTypes: Class) = 47 | handle(*requestTypes.map { it.kotlin }.toTypedArray(), handler = handler) 48 | 49 | fun respond(vararg requestTypes: KClass, responseContext: ResponseContext) = 50 | handle(*requestTypes) { it.respond(responseContext) } 51 | 52 | fun canHandleIntents(input: HandlerInput, intents: Array) = 53 | input.matches(intents.map(Predicates::intentName).reduce(Predicate::or)) 54 | 55 | fun HandlerInput.respond(responseContext: ResponseContext): Optional = 56 | responseBuilder.apply { responseContext(this@respond) }.build() 57 | 58 | -------------------------------------------------------------------------------- /src/test/kotlin/org/stvad/kask/request/HandlerSpec.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.request 2 | 3 | import com.amazon.ask.dispatcher.request.handler.HandlerInput 4 | import com.amazon.ask.dispatcher.request.handler.RequestHandler 5 | import com.amazon.ask.model.Intent 6 | import com.amazon.ask.model.IntentRequest 7 | import com.amazon.ask.model.LaunchRequest 8 | import com.amazon.ask.model.Request 9 | import com.amazon.ask.model.RequestEnvelope 10 | import com.amazon.ask.response.ResponseBuilder 11 | import io.kotlintest.Matcher 12 | import io.kotlintest.Result 13 | import io.kotlintest.should 14 | import io.kotlintest.shouldBe 15 | import io.kotlintest.specs.WordSpec 16 | import java.util.Optional.empty 17 | 18 | fun handlerInputForIntent(intentName: String): HandlerInput = 19 | handlerInputForRequest(IntentRequest.builder().withIntent( 20 | Intent.builder().withName(intentName) 21 | .build()).build()) 22 | 23 | fun handlerInputForRequest(request: Request): HandlerInput = 24 | HandlerInput.builder().withRequestEnvelope( 25 | RequestEnvelope.builder().withRequest(request).build() 26 | ).build() 27 | 28 | fun handleRequestsWithIntents(vararg intents: String) = object : Matcher { 29 | override fun test(value: RequestHandler) = 30 | Result(intents.map { value.canHandle(handlerInputForIntent(it)) }.reduce { acc, next -> acc && next }, 31 | "The $value handler should be able to handle all the intents in ${intents.contentToString()}") 32 | } 33 | 34 | fun handleRequestsWithIntents(vararg requestTypes: Request) = object : Matcher { 35 | override fun test(value: RequestHandler) = 36 | Result(requestTypes.map { value.canHandle(handlerInputForRequest(it)) }.reduce { acc, next -> acc && next }, 37 | "The $value handler should be able to handle all the request types in ${requestTypes.contentToString()}") 38 | } 39 | 40 | class HandlerSpec : WordSpec({ 41 | "Intent based handlers" should { 42 | "match inputs with appropriate intent" { 43 | val intents = arrayOf("firstIntent", "secondIntent") 44 | handle(*intents) { empty() } should handleRequestsWithIntents(*intents) 45 | } 46 | } 47 | 48 | "Request type based handlers" should { 49 | "match inputs with appropriate request type" { 50 | val requestTypes = arrayOf(IntentRequest::class, LaunchRequest::class) 51 | val requests = arrayOf(IntentRequest.builder().build(), LaunchRequest.builder().build()) 52 | handle(*requestTypes) { empty() } should handleRequestsWithIntents(*requests) 53 | } 54 | } 55 | 56 | "Handlers with ResponseContext" should { 57 | "build response with data provided in the context" { 58 | val intentName = "testIntent" 59 | val testSpeech = "testSpeech" 60 | 61 | val response = respond(intentName) { 62 | withSpeech(testSpeech) 63 | }.handle(handlerInputForIntent(intentName)) 64 | 65 | response shouldBe ResponseBuilder().withSpeech(testSpeech).build() 66 | } 67 | } 68 | }) -------------------------------------------------------------------------------- /src/main/kotlin/org/stvad/kask/model/slot.kt: -------------------------------------------------------------------------------- 1 | package org.stvad.kask.model 2 | 3 | import com.amazon.ask.model.SlotConfirmationStatus 4 | import com.github.debop.kodatimes.toLocalTime 5 | import org.joda.time.LocalTime 6 | import org.joda.time.Period 7 | 8 | abstract class Slot(val askSlot: com.amazon.ask.model.Slot) { 9 | val name: String get() = askSlot.name 10 | val confirmationStatus: SlotConfirmationStatus get() = askSlot.confirmationStatus 11 | val stringValue: String? get() = askSlot.value 12 | 13 | abstract val value: T? 14 | //TODO(resolutions?) 15 | } 16 | 17 | interface SlotCompanion { 18 | val type: String 19 | } 20 | 21 | class PhoneNumber(private val number: String) : CharSequence by number 22 | 23 | /** 24 | * AMAZON.DATE - is non standard and needs a custom code to parse it (https://developer.amazon.com/docs/custom-skills/slot-type-reference.html#date). 25 | * As I didn't see anything that does that for Java/Kotlin atm - not implementing it for now. 26 | * 27 | * The Phrase slot types (AMAZON.SearchQuery & List Slot types) are not implemented explicitly, but the intention is to 28 | * generate them as needed based on StringSlot. 29 | */ 30 | val supportedAmazonSlots = mapOf(DurationSlot.type to DurationSlot::class, 31 | NumberSlot.type to NumberSlot::class, 32 | FourDigitNumberSlot.type to FourDigitNumberSlot::class, 33 | TimeSlot.type to TimeSlot::class, 34 | PhoneNumberSlot.type to PhoneNumberSlot::class) 35 | 36 | /** 37 | * https://developer.amazon.com/docs/custom-skills/slot-type-reference.html#duration 38 | */ 39 | class DurationSlot(askSlot: com.amazon.ask.model.Slot) : Slot(askSlot) { 40 | companion object : SlotCompanion { 41 | override val type = "AMAZON.DURATION" 42 | } 43 | 44 | override val value get() = stringValue?.let { Period.parse(it) } 45 | } 46 | 47 | /** 48 | * https://developer.amazon.com/docs/custom-skills/slot-type-reference.html#time 49 | */ 50 | class TimeSlot(askSlot: com.amazon.ask.model.Slot, 51 | val fuzzyTimeMap: Map = defaultFuzzyTimeMap) : Slot(askSlot) { 52 | companion object : SlotCompanion { 53 | override val type = "AMAZON.TIME" 54 | 55 | val defaultFuzzyTimeMap = mapOf( 56 | "NI" to "21:00", 57 | "MO" to "09:00", 58 | "AF" to "12:00", 59 | "EV" to "18:00" 60 | ) 61 | } 62 | 63 | constructor(askSlot: com.amazon.ask.model.Slot) : this(askSlot, defaultFuzzyTimeMap) 64 | 65 | override val value get() = fuzzyTimeMap.getOrDefault(stringValue, stringValue)?.toLocalTime() 66 | } 67 | 68 | /** 69 | * https://developer.amazon.com/docs/custom-skills/slot-type-reference.html#number 70 | */ 71 | open class NumberSlot(askSlot: com.amazon.ask.model.Slot) : Slot(askSlot) { 72 | companion object : SlotCompanion { 73 | override val type = "AMAZON.NUMBER" 74 | } 75 | 76 | override val value get() = stringValue?.toLong() 77 | } 78 | 79 | /** 80 | * https://developer.amazon.com/docs/custom-skills/slot-type-reference.html#four_digit_number 81 | */ 82 | class FourDigitNumberSlot(askSlot: com.amazon.ask.model.Slot) : NumberSlot(askSlot) { 83 | companion object : SlotCompanion { 84 | override val type = "AMAZON.FOUR_DIGIT_NUMBER" 85 | } 86 | } 87 | 88 | class PhoneNumberSlot(askSlot: com.amazon.ask.model.Slot) : Slot(askSlot) { 89 | companion object : SlotCompanion { 90 | override val type = "AMAZON.PhoneNumber" 91 | } 92 | 93 | override val value get() = stringValue?.let { PhoneNumber(it) } 94 | } 95 | 96 | open class StringSlot(askSlot: com.amazon.ask.model.Slot) : Slot(askSlot) { 97 | override val value get() = stringValue 98 | 99 | } 100 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kask 2 | [![Build Status](https://travis-ci.org/Stvad/kask.svg?branch=master)](https://travis-ci.org/Stvad/kask) 3 | [![codecov](https://codecov.io/gh/Stvad/kask/branch/master/graph/badge.svg)](https://codecov.io/gh/Stvad/kask) 4 | 5 | 6 | A Kotlin library designed to improve an experience of developing Alexa skills on JVM. 7 | It's based on official [ASK SDK for Java](https://github.com/alexa/alexa-skills-kit-sdk-for-java). 8 | You can find the complete working example (Kotlin and Java) here: https://github.com/Stvad/hello-kask 9 | 10 | --- 11 | 12 | ## Why would you want to use this 13 | 14 | 1. [Static, automatically instantiated version of your Skill model](#static-automatically-instantiated-version-of-your-skill-model) 15 | 1. [Functional handlers](#functional-request-handlers) 16 | 1. [Concise intent handler classes](#concise-intent-handler-classes) 17 | 18 | 19 | --- 20 | 21 | ### Static, automatically instantiated version of your Skill model 22 | 23 | Kask can generate code based on your skill model, creating representations for the Intents and Slots in your model. 24 | It will also perform instantiation of those classes for incoming request. 25 | 26 | So your model is expressed explicitly in the code and you don't have to handle it's recreation from incoming request yourself. 27 | 28 | #### Example 29 | 30 | Say you have model with the following intent defined in it: 31 | 32 | ```json 33 | { 34 | "name": "SetTimerIntent", 35 | "slots": [ 36 | { 37 | "name": "timerDuration", 38 | "type": "AMAZON.DURATION", 39 | ... 40 | } 41 | ] 42 | } 43 | ``` 44 | 45 | To handle this intent, if you are to use the vanilla ASK SDK for Java, you'd need write something like this: 46 | 47 | ```kotlin 48 | class SetTimerIntentHandler : RequestHandler { 49 | override fun canHandle(input: HandlerInput) = input.matches(intentName("SetTimerIntent")) 50 | 51 | override fun handle(input: HandlerInput): Optional { 52 | val intentRequest = input.requestEnvelope.request as IntentRequest 53 | val period = parsePeriod(intentRequest.intent.slots["timerDuration"]?.value) 54 | //set timer... 55 | val speechText = "Timer was successfully set" 56 | return input.responseBuilder 57 | .withSpeech(speechText) 58 | .withSimpleCard("TimerSet", speechText) 59 | .build() 60 | } 61 | 62 | private fun parsePeriod(duration: String?): Period { 63 | TODO("...") 64 | } 65 | } 66 | ``` 67 | 68 | If you're to use Kask you can achieve the same functionality in the following way: 69 | 70 | ```kotlin 71 | class SetTimerIntentHandler : IntentRequestHandler(SetTimerIntent) { 72 | override fun handle(input: HandlerInput, intent: SetTimerIntent): Optional { 73 | val duration = intent.timerDuration.value 74 | //set timer... 75 | val speechText = "Timer was successfully set" 76 | return input.respond { 77 | withSpeech(speechText) 78 | withSimpleCard("TimerSet", speechText) 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | To unpack this example a little: 85 | * `SetTimerIntent` is an intent class generated by Kask. It has `timerDuration` property of `DurationSlot` type; 86 | * `handle` function now accepts the instance of `SetTimerIntent` (it's instantiated by base class); 87 | * You can now get the value of the slot by calling `intent.timerDuration.value` which would give you an instance of `Period?` (you don't need to parse it yourself). 88 | * The `canHandle` function implementation is done for you by Kask. You can override it if you need to though. 89 | 90 | 91 | ### Functional request handlers 92 | 93 | Kask provides you an ability to concisely handle various requests or intents via Functional Handlers. 94 | 95 | #### Examples 96 | **Handle LaunchRequest** 97 | *Kotlin* 98 | ```kotlin 99 | respond(LaunchRequest::class) { 100 | val welcomeSpeech = "Hey there!" 101 | withSpeech(welcomeSpeech) 102 | withReprompt(welcomeSpeech) 103 | withSimpleCard("Hello Kask", welcomeSpeech) 104 | } 105 | //or 106 | handle(LaunchRequest::class) { 107 | val welcomeSpeech = "Hey there!" 108 | it.respond { 109 | withSpeech(welcomeSpeech) 110 | withReprompt(welcomeSpeech) 111 | withSimpleCard("Hello Kask", welcomeSpeech) 112 | } 113 | } 114 | ``` 115 | *Java* 116 | ```java 117 | handle(input -> { 118 | String welcomeSpeech = "Hey There!"; 119 | return input.getResponseBuilder() 120 | .withSpeech(welcomeSpeech) 121 | .withReprompt(welcomeSpeech) 122 | .withSimpleCard("Hello Kask", welcomeSpeech) 123 | .build(); 124 | }, 125 | LaunchRequest.class); 126 | 127 | ``` 128 | 129 | **Handle AMAZON.CancelIntent and AMAZON.StopIntent** 130 | *Kotlin* 131 | ```kotlin 132 | respond("AMAZON.StopIntent", "AMAZON.CancelIntent") { withSpeech("OK!") } 133 | ``` 134 | 135 | *Java* 136 | ```java 137 | handle(input -> input.getResponseBuilder() 138 | .withSpeech("OK!") 139 | .build(), 140 | "AMAZON.CancelIntent", "AMAZON.StopIntent"); 141 | ``` 142 | 143 | ### Concise intent handler classes 144 | 145 | As in Functional Handlers above Kask can help you with defining `canHandle` function for common cases. 146 | 147 | *Kotlin* 148 | ```kotlin 149 | class FallBackIntentHandler : BasicIntentRequestHandler("AMAZON.FallbackIntent") { 150 | override fun handle(input: HandlerInput) = input.respond { 151 | withSpeech("I didn't quite catch that!") 152 | } 153 | } 154 | ``` 155 | *Java* 156 | ```java 157 | public class FallBackIntentHandler extends BasicIntentRequestHandler { 158 | public FallBackIntentHandler() { 159 | super("AMAZON.FallbackIntent"); 160 | } 161 | 162 | @Override 163 | public Optional handle(HandlerInput input) { 164 | return input.getResponseBuilder().withSpeech("I didn't quite catch that!").build(); 165 | } 166 | } 167 | ``` 168 | 169 | ## How to get it (Gradle) 170 | 171 | ### As a plugin 172 | 173 | If you'd like to take advantage of code generation capabilities - you'd need to use Kask gradle plugin. (If you'd like to write a plugin for other build system or just want to use code generator as a library - refer to `KaskGenerator.kt`). 174 | 175 | To do that you need make two changes to your gradle project: 176 | 177 | 1. **Add jitpack repository to plugin repository configuration.** 178 | If you didn't have this configuration explicitly specified before - you need to explicitly add `gradlePluginPortal` as well. 179 | So a complete working example would look like: 180 | **settings.gradle.kts**: 181 | 182 | ```kotlin 183 | pluginManagement { 184 | repositories { 185 | gradlePluginPortal() 186 | maven(url = "https://jitpack.io") 187 | } 188 | } 189 | ``` 190 | 191 | 1. **Add plugin configuration to the `plugins` section** 192 | 193 | **build.gradle.kts:** 194 | ```kotlin 195 | plugins{ 196 | id("org.stvad.kask") version "0.1.5" 197 | } 198 | ``` 199 | 200 | #### Configure code generation 201 | 202 | To configure code generation from your skill model, you need to set a few settings on the `kask` gradle task: 203 | * `packageName` - Package name for generated code 204 | * `modelPath` - The path where your skill model is located 205 | 206 | **Example from [hello-kask](https://github.com/Stvad/hello-kask/blob/master/build.gradle.kts)** 207 | *Kotlin DSL* 208 | ```kotlin 209 | tasks.withType { 210 | packageName = "org.stvad.kask.example.model" 211 | modelPath.set(layout.projectDirectory.dir("models").file("en-US.json")) 212 | } 213 | ``` 214 | 215 | *Groovy DSL* 216 | ```groovy 217 | kask { 218 | packageName = "org.stvad.kask.example.model" 219 | modelPath.set(layout.projectDirectory.dir("models").file("en-US.json")) 220 | } 221 | ``` 222 | 223 | ### As a library 224 | 225 | If you want to use Kask only as a library (e.g. to play with functional handlers or code generator as a library). 226 | You need to **add jitpack repository to your repositories** and **add dependency declaration for Kask**. 227 | 228 | Example: 229 | ```kotlin 230 | repositories { 231 | maven(url = "https://jitpack.io") 232 | } 233 | 234 | dependencies { 235 | implementation("com.github.Stvad:kask:0.1.0") 236 | 237 | } 238 | ``` 239 | 240 | 241 | ## References 242 | 243 | 1. [Complete working example of using Kask](https://github.com/Stvad/hello-kask) 244 | 1. [A skill I'm developing using Kask](https://github.com/Stvad/alexa-life-advice) 245 | 1. [Alexa Skills Kit SDK for Java](https://github.com/alexa/alexa-skills-kit-sdk-for-java) 246 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------