App

Overview

Slate Kit uses a somewhat new, yet familiar paradigm to building out APIs by enriching normal Kotlin methods and making them easily discoverable and accessible across a range of hosts. This will resemble an RPC type approach, but contains some support and concepts from REST. More specifically, APIs in Slate Kit can be hosted and made available as Web/HTTP APIs, on the CLI, or called from requests from queues or files for automation purposes. Under the hood, Slate Kit simply leverages existing HTTP servers ( currently Ktor ), to host, discover, manage, and access Slate Kit APIs. Our CLI also supports the ability to host Slate Kit APIs. This specific approach to API development in Slate Kit is referred to as Universal APIs.

    
    slatekit new api -name="SampleAPI" -package="mycompany.apps"
    



Goals

Goal Description
1. Accessable Write once and have APIs hosted and/or run anywhere ( e.g. Web, CLI, from File/Queue sources )
2. Discoverable Folder like heirarchical and drill-down discovery of APIS using a 3 part routing format { area / api / action }
3. Natural Allow normal methods to be easily turned to API actions with strong types, automatic docs and accurate Results


Status

This component is currently stable and uses JetBrains Ktor as the underlying HTTP server for hosting Slate Kit APIs as Web/HTTP APIs.

Feature Status Description
Open-API Upcoming Auto-generate Open-APIs docs from Actions
Postman Upcoming Auto-generate Postman scripts from Actions
Serialization Upcoming Replace internal serializer with Jackson, Moshi, or kotlinx.serialization
Streaming Upcoming Support for streaming

Back to top



Install

    repositories {
        // other repositories
        maven { url  "http://dl.bintray.com/codehelixinc/slatekit" }
    }

    dependencies {
        // other dependencies ...

        compile 'com.slatekit:slatekit-apis:1.0.0'
    }
package slatekit.apis
jar slatekit.apis.jar
git slatekit/src/lib/kotlin/slatekit-apis
docs APIs
uses slatekit.results, slatekit.common
license Apache 2.0
example Example_APIs.kt

Back to top



Requires

This component uses the following other Slate Kit and/or third-party components.

Component Description
Slate Kit - Results To model successes and failures with optional status codes
Slate Kit - Common Common utilities for both android + server


Back to top



Sample

This is a quick and simple example of creating an API using the Slate Kit Universal API paradigm. This API is then accessible on the CLI and Web

      
    import slatekit.apis.*
    import slatekit.common.DateTime
    import slatekit.common.Request

    @Api(area = "app", name = "movies", desc = "Create and manage movies", auth = AuthModes.NONE)
    class MovieApi {
        // Using explicit fields
        @Action()
        fun createSample(title:String, playing:Boolean, cost:Int, released: DateTime):Movie {
            return Movie.of(title, playing, cost, released)
        }

        // Using request object
        @Action()
        fun createWithRequest(req:Request):Movie {
            return Movie.of(req.getString("title"), req.getBool("playing"), req.getInt("cost"), req.getDateTime("released"))
        }
    }


Back to top



Guide

Name Description More
1. Setup Description of feature more
2. Routes How routes work actions are called more
3. Config Configuration options including auth, verbs, sources and more more
4. Requests How to handle requests and parameters more
5. Responses How to return responses more
6. Errors Modeling of successes, failures and dealing with status codes more
7. Middleware Incorporate middle ware globally or per API more
8. Web How to make APIs hosted in a Web Server (Ktor) more
9. CLI How to make APIs hosted in a CLI ( Slate Kit CLI ) more
10. Files How to handle requests from Files more
11. REST How to set up partial REST compatible actions more

Back to top



Setup

APIs are developed as normal Kotlin methods. The only difference is that they are enriched with annotations and/or configuration during registration, to provided metadata to the Slate Kit API Server indicated how they should be accessed and managed.

1: Annotations

This approach is convenient and puts all relevant metadata at the source.

      
    import slatekit.apis.*
    import slatekit.common.DateTime
    import slatekit.common.Request

    @Api(area = "manage", name = "movies", desc = "Create and manage movies", auth = AuthModes.NONE)
    class MovieApi {
        // Using explicit fields
        @Action()
        fun createSample(title:String, playing:Boolean, cost:Int, released: DateTime):Movie {
            return Movie.of(title, playing, cost, released)
        }

        // Using request object
        @Action()
        fun createWithRequest(req:Request):Movie {
            return Movie.of(req.getString("title"), req.getBool("playing"), req.getInt("cost"), req.getDateTime("released"))
        }
    }

2: Registration

This approach reduces the dependency on Slate Kit, and requires that the metadata be supplied during registration of the API, and all the actions assume that the annotation values are inherited from the parent Api metadata.

      
    import slatekit.apis.*
    import slatekit.common.DateTime
    import slatekit.common.Request

    class MovieApi {
        // Using explicit fields
        fun createSample(title:String, playing:Boolean, cost:Int, released: DateTime):Movie {
            return Movie.of(title, playing, cost, released)
        }

        // Using request object
        fun createWithRequest(req:Request):Movie {
            return Movie.of(req.getString("title"), req.getBool("playing"), req.getInt("cost"), req.getDateTime("released"))
        }
    }

    // ... Registration code ( see other sections for more details )
    val api = slatekit.apis.core.Api(
                    instance = MovieApi(ctx), 
                    area = "manage", 
                    name = "movies", 
                    auth = AuthMode.None
                )

Back to features Back to top


Routes

API routes consist of a 3 part ( {AREA} / {API} / {ACTION} ) routing convention to enfore standards and simplify the discovery of the routes.

1. Parts

Part Example Note
AREA manage Represents a logical grouping of 1 or more APIs
API movies Represents an actual API associated with a Kotlin class
ACTION createSample Represents an action/endpoint on an API, and maps to a Kotlin method


2. Example

     
     POST localhost:5000/manage/movies/createSample 
     {
        "title": "Dark Knight",
        "playing": true,
        "cost" : 12.50,
        "released": "2018-07-18T00:00:00Z"
     }


3. Discovery

Because the routes are standardized on a 3 part heirarchical format, this makes discovery of all Areas, APIS, Actions, Inputs(for actions) incredibly straight-forward for both Web and CLI hosted APIs. You can drill down from areas into actions using an approach that resembles navigating a folder and showing the items.

Discover CLI Web Note
Areas ? /help Lists Areas available.
E.g. manage, discover
APIs in Area manage? /manage/help Lists APIs available in the manage area
E.g. movies, concerts
Actions on API manage.movies? /manage/movies/help Lists Actions available in manage/movies API
E.g. create, update, delete, disable
Inputs on Action manage.movies.createSample? /manage/movies/createSample/help Lists Inputs for manage / movies /createSample Action
E.g. title=string, cost=double

Back to features Back to top


Config

There are several annotation properties available to configure APIs and Actions.

1. API

These are all the properties for the @Api annotation to put on a class to indicate it should be accessable as an Api.

Type Name Required Default Purpose Example
@Api area Required n/a Represents the logical area of the API manage
- name Required n/a Represents the name of this API movies
- desc Optional Empty Description of the API Manages movies
- auth Optional None Specifies how the authentication should work AuthMode.kt
- roles Optional Empty List of roles allowed to access this API Roles.kt
- access Optional Public Desribes visibility of the API Access.kt
- verb Optional Auto Desribes how the Verbs should be handled Verbs.kt
- sources Optional All Indicates where this API can handle requests from Sources.kt

2. Action

These are all the properties for the @Action annotation to be put on methods to indicate it should be available as an action/endpoint

@Action area Required Default Represents the logical area of the API Example
- name Optional method name Represents the name of this Action createSample
- desc Optional Empty Description of the Action Create sample movie
- roles Optional Empty List of roles allowed to access this API Roles.kt
- access Optional Public Desribes visibility of the API Access.kt
- verb Optional Auto Desribes how the Verbs should be handled Verbs.kt
- sources Optional All Indicates where this API can handle requests from Sources.kt
Back to features Back to top


Requests

Requests in Slate Kit are abstracted out as Request.kt. They are implementations for a Web Request and CLI request.

     
    val request:Request = CommonRequest(
                path = "app.users.activate",
                parts = listOf("app", "users", "activate"),
                source = Source.CLI,
                verb = Verbs.POST,
                meta = InputArgs(mapOf("api-key" to "ABC-123")),
                data = InputArgs(mapOf("userId" to 5001)),
                raw = "the raw HTTP SparkJava send or CLI ShellCommand",
                tag = Random.uuid()
        )

        // NOTES: ResultSupport trait builds results that simulate Http Status codes
        // This allows easy construction of results/status from a server layer
        // instead of a controller/api layer

        // CASE 1: Get path of the request, parts, and associated 3 part routes
        println( request.path )
        println( request.parts )
        println( request.area   )
        println( request.name   )
        println( request.action )

        // CASE 2: Get the verb (For the CLI - the verb will be "cli")
        println( request.verb )

        // CASE 3: Get the tag
        // The request is immutable and the tag field is populated with
        // a random guid, so if an error occurs this tag links the request to the error.
        println( request.tag )

        // CASE 4: Get metadata named "api-key"
        println( request.meta.getString("api-key") )
        println( request.meta.getInt("sample-id") )
        println( request.meta.getIntOrNull("sample-id") )
        println( request.meta.getIntOrElse("sample-id", -1) )

        // CASE 5: Get a parameter named "userId" as integer
        // IF its not there, this will return a 0 as getInt
        // returns a non-nullable value.  
        // The value is obtained from query string if get, otherwise, 
        // if the request is a post, the value is
        // first checked in the body ( json data ) before checking
        // the query params
        println( request.data.getInt("userId") )
        println( request.data.getIntOrNull("userId") )
        println( request.data.getIntOrElse("userId", -1) )
     
Back to features Back to top


Responses

There are 2 ways to returns responses/values from methods. The first is to simply return the exact value. The second is to wrap the value into a Slate Kit Result.

1. Result model

      
    // Case 1: Return explicit value( will return an HTTP 200)
    return Movie.of(title, playing, cost, released)

    // Case 2. Return value wrapped as Outcome<Movie> = Result<Movie, Err>
    val movie = Movie.of(title, playing, cost, released)
    return Outcomes.success(movie)
      

2. Response JSON

JSON Responses from APIs always the following fields. The value field will represent the payload returned from your method. The success, code, msg, err fields are representing by the Result component for modeling successes/failures.

    {
        "success": true,
        "code": 200001,
        "meta": null,
        "value": {
            "version": "1.0.0",
            "date": "2019-08-10"
        },
        "msg": "Success",
        "err": null,
        "tag": null
    }
Back to features Back to top


Errors

You can accurately model successes and failures using Result. Result ( and the Outcome typealias ) help to model errors accurately which can then be easily converted to Http status codes and responses.

     
    @Action()
    fun createSampleOutcome(req:Request, title:String, playing:Boolean, cost:Int, released: DateTime):Outcome<Movie> {
        val result:Outcome<Movie> = when {
            !canCreate(req)       -> Outcomes.denied ("Not allowed to create")
            title.isNullOrEmpty() -> Outcomes.invalid("Title missing")
            !playing              -> Outcomes.ignored("Movies must be playing")
            cost > 20             -> Outcomes.errored("Prices must be reasonable")
            else                  -> {
                // Simple simulation of creation
                Outcomes.success( Movie.of(title, playing, cost, released) )
            }
        }
        return result 
    }
     
Back to features Back to top


Middleware

Back to features Back to top


Web

You can host Slate Kit APIs as Web APIs using the default Http Engine which is Ktor.

     
    fun runServer() {
        // ====================================
        // Slate Kit Setup
        // 1. Settings ( defaults: port = 5000, prefix = /api/)
        val settings = ServerSettings(docs = true, docKey = "abc123")

        // 2. APIs ( defaults applied )
        val apis = listOf(
                slatekit.apis.core.Api(
                    klass = SampleApi::class, 
                    singleton = SampleApi(ctx)
                )
        )

        // 3. API host
        val apiHost = ApiServer.of( ctx, apis, auth = null)

        // 4. Ktor handler: Delegates Ktor requests to Slate Kit
        val handler = KtorHandler(ctx, settings, apiHost)

        // ====================================
        // Jet Brains Ktor setup
        val server = embeddedServer(Netty, settings.port) {
            routing {

                // Root
                get("/") { ping(call) }

                // Your own custom path
                get(settings.prefix + "/ping") { ping(call) }

                // Your own multi-path route
                get("module1/feature1/action1") {
                    KtorResponse.json(call, Success("action 1 : " + DateTime.now().toString()).toResponse())
                }

                // Remaining outes beginning with /api/ to be handled by Slate Kit API Server
                handler.register(this)
            }
        }

        // Start Ktor server
        server.start(wait = true)
    }
Back to features Back to top


CLI

You can host Slate Kit Universal APIs on the CLI using the Slate Kit CLI

CLI Setup

      
    // 1. The API keys( DocApi, SetupApi are authenticated using an sample API key )
    val keys = listOf(ApiKey( name ="cli", key = "abc", roles = "dev,qa,ops,admin"))

    // 2. Authentication
    val auth = Authenticator(keys)

    // 3. Load all the Slate Kit Universal APIs
    val apis = listOf(
            slatekit.apis.core.Api(
                klass = SampleApi::class, 
                singleton = SampleApi(ctx)
            )
    )

    // 4. Makes the APIs accessible on the CLI runner
    val cli = CliApi(
            ctx = ctx,
            auth = auth,
            settings = CliSettings(enableLogging = true, enableOutput = true),
            apiItems = apis,
            metaTransform = {
                listOf("api-key" to keys.first().key)
            },
            serializer = {item, type -> Content.csv(slatekit.meta.Serialization.csv().serialize(item))}
    )

    // 5. Run interactive mode
    return cli.run()
       

CLI Example

You can then access this API on the CLI by hosting it in the Slate Kit CLI component. The request would like this:

     
     :> app.movies.createSample -title="Dark Knight" -playing=true -cost=12.50 -released="2018-07-18T00:00:00Z"
      
Back to features Back to top


File

You can then call this API from a request saved to a file

     // Assume file is ~/dev/slatekit/samples/apis/file-sample.json
     {
        "path": "/app/movies/createSample",
        "source": "file",
        "version": "1.0",
        "tag" : "",
        "timestamp": null,
        "meta": { }
        "data": {
            "title": "Dark Knight",
            "playing": true,
            "cost" : 12.50,
            "released": "2018-07-18T00:00:00Z"
         }
     }
      

Back to top