Talk delivered at the Kotliners 2020 conference. Covering how Instil used the Kotlin Space DSL to automatically populate Space instances used in coding workshops.
12. – Multiple Instructors
– Specialized Tools
– Flexible Hours
The Three Key Components
To Make Virtual Training Work
13. – The instructor works with the group
– By teaching, live coding etc…
– They proceed at the average pace
– The coach responds to queries
– Working with students at the extremes
– They ensure that no one is left behind
Multiple Trainers
Acting as Instructor and Coach
14. – We partitioned our material into 60 min blocks
– Allowing us to deliver a course to your timeline
– A delivery could take place over:
– A week's worth of mornings
– One day a week for six weeks
– Afternoon and evening sessions
Flexible Hours
Fitting Material Into Your Day
15. – Distributed teams already build software
– We can take advantage of their tooling
– We already do:
– Video-conferencing
– Messaging and Chats
– Collaborative Content
– Distributed Versioning
Specialized Tools
We Have The Technology
32. – Create a specific instance for the course
– Add admin accounts for the 3 principal trainers
– Add a welcome blog post with setup instructions
– Create a project for examples / exercises (with repos)
– Create a private project for solutions (with repos)
– Create accounts for students with TODO lists
Making the Code Useful
What we need to do on each delivery…
33.
34. package com.instil.maurice.dsl
fun instance(title: String = "An Instil Delivery",
action: SpaceInstance.() -> Unit)
= SpaceInstance(title).apply(action)
class SpaceInstance(val title: String) {
override fun toString() = title
}
Building the DSL – Iteration 1
35. val dsl = instance("Kotlin 101 for Megacorp") {
}
println(dsl)
Kotlin 101 for Megacorp
Building the DSL – Iteration 1
36. class SpaceInstance(val title: String) {
private lateinit var projects: Projects
private lateinit var profiles: Profiles
private lateinit var blogs: Blogs
override fun toString() = "$titlen$profilesn$projectsn$blogsn"
fun profiles(action: Profiles.() -> Unit)
= Profiles().apply(action).also { this.profiles = it }
fun projects(action: Projects.() -> Unit)
= Projects().apply(action).also { this.projects = it }
fun blogs(action: Blogs.() -> Unit)
= Blogs().apply(action).also { this.blogs = it }
}
Building the DSL – Iteration 2
37. class Profiles {
private val profiles = mutableListOf<Profile>()
fun profile(action: Profile.() -> Unit)
= Profile().apply(action).also { profiles.add(it) }
override fun toString()
= profiles.fold("Current profiles:") { state, profile ->
"$statent$profile"
}
}
Building the DSL – Iteration 2
38. class Profile {
lateinit var forename: String
lateinit var surname: String
lateinit var email: String
override fun toString() = "$forename $surname at $email"
}
Building the DSL – Iteration 2
39. val dsl = instance("Kotlin 101 for Megacorp") {
profiles {
profile {
forename = "Jane"
surname = "Smith"
email = "Jane.Smith@megacorp.com"
}
}
projects { }
blogs { }
}
println(dsl)
Kotlin 101 for Megacorp
Current profiles:
Jane Smith at
Jane.Smith@megacorp.com
com.instil.maurice.dsl.Projects@350b3a17
com.instil.maurice.dsl.Blogs@38600b
Building the DSL – Iteration 2
40. class Repo(private val location: URI) {
override fun toString() = "t$location"
}
Building the DSL – Iteration 3
41. class Project(val title: String) {
private val repos = mutableListOf<Repo>()
fun repo(location: URI, action: Repo.() -> Unit)
= Repo(location).apply(action).also { repos.add(it) }
override fun toString()
= repos.fold("Project $title with repos:") { state, repo ->
"$statent$repo"
}
}
Building the DSL – Iteration 3
42. class Projects {
private val projects = mutableListOf<Project>()
fun project(title: String, action: Project.() -> Unit)
= Project(title).apply(action).also { projects.add(it) }
override fun toString()
= projects.fold("Current projects:") { state, project ->
"$statent$project"
}
}
Building the DSL – Iteration 3
43. class Blogs {
private val blogs = mutableListOf<Blog>()
fun blog(title: String,
location: URI,
action: Blog.() -> Unit)
= Blog(title, location).apply(action).also { blogs.add(it) }
override fun toString()
= blogs.fold("Current blogs:") { state, blog ->
"$statent$blog"
}
}
Building the DSL – Iteration 3
44. class Blog(val title: String, val location: URI) {
private val additionalContent = mutableListOf<String>()
operator fun String.unaryPlus() = additionalContent.add(this)
override fun toString()
= additionalContent.fold("$titlentt$location") {
state, text ->
"$statentt$text"
}
}
Building the DSL – Iteration 3
46. Kotlin 101 for Megacorp
Current profiles:
Jane Smith at Jane.Smith@megacorp.com
Current projects:
Project Kotlin Examples with repos:
http://somewhere.com
Current blogs:
Welcome and Setup
http://elsewhere.com
Some additional client-specific content
Building the DSL – Iteration 3
47. fun <T> foldOverChildren(start: String,
children: List<T>,
indent: String ="t")
= children.fold(start) { state, child -> "$staten$indent$child” }
Building the DSL – Refactoring 1
48. class Blog(private val title: String, private val location: URI) {
private val additionalContent = mutableListOf<String>()
operator fun String.unaryPlus() = additionalContent.add(this)
override fun toString()
= foldOverChildren("$titlentt$location",
additionalContent,
"tt")
}
Building the DSL – Refactoring 1
49. class Blogs {
private val blogs = mutableListOf<Blog>()
fun blog(title: String, location: URI, action: Blog.() -> Unit)
= Blog(title, location).apply(action).also { blogs.add(it) }
override fun toString() = foldOverChildren("Current blogs:", blogs)
}
Building the DSL – Refactoring 1
52. – We have a stable structure describing data
– We do want to add support for many operations
– The operations will have overlapping functionality
– We don’t want to pollute the DSL code with IO
Integrating the DSL with IO
Sounds like a familiar problem...
53.
54. Applying the Visitor Pattern
interface DslVisitor {
fun visitBlog(blog: Blog)
fun visitRepo(repo: Repo)
fun visitProfile(profile: Profile)
fun visitProject(project: Project)
fun visitInstance(instance:SpaceInstance)
}
interface Visited {
fun accept(visitor: DslVisitor)
}
55. Applying the Visitor Pattern
@SpaceEntityMarker
class SpaceInstance(val title: String): Visited {
private lateinit var projects: Projects
private lateinit var profiles: Profiles
private lateinit var blogs: Blogs
override fun toString() = "$titlen$profilesn$projectsn$blogsn"
fun profiles(action: Profiles.() -> Unit) = ...
fun projects(action: Projects.() -> Unit) = ...
fun blogs(action: Blogs.() -> Unit) = ...
override fun accept(visitor: DslVisitor) {
visitor.visitInstance(this)
listOf(projects, profiles, blogs).forEach { it.accept(visitor) }
}
}
56. class PrintVisitor: DslVisitor {
override fun visitBlog(blog: Blog) {
println("tBlog entitled ${blog.title}")
}
override fun visitRepo(repo: Repo) {
println("ttRepo at ${repo.location}")
}
override fun visitProfile(profile: Profile) {
with(profile) {
println("tProfile for $forename $surname at $email")
}
}
override fun visitProject(project: Project) {
println("tProject ${project.name} with key ${project.key}")
}
override fun visitInstance(instance: SpaceInstance) {
println("Visiting instance ${instance.title}")
}
}
Applying the Visitor Pattern
57. – We can leverage our existing WebClient code
– We put it behind an interface for abstraction
– Our component uses two WebClient objects
– One running as a Service Account for reading
– The other using a token to create entities
Creating the Space Client
Returning to the WebFlux WebClient
58. interface SpaceClient {
fun findProfiles(): Flux<Profile>
fun findProjects(): Flux<Project>
fun findBlogs(): Flux<Article>
fun createProfile(forename: String,
surname: String,
username: String): Mono<Boolean>
fun createProject(name: String, key: String): Mono<Boolean>
fun createBlog(title: String, content: String): Mono<Boolean>
}
SpaceClient.kt
Creating the Space Client
59. @Component("WebFluxSpaceClient")
class WebFluxSpaceClient(
@Qualifier("OAuthWebClient") val oauthClient: WebClient,
@Qualifier("TokenWebClient") val tokenClient: WebClient): SpaceClient {
override fun findProfiles(): Flux<Profile> {
val url = "/team-directory/profiles"
return retrieveData<AllProfilesResponse, Profile>(url) {
it.data ?: emptyList()
}
}
Creating the Space Client
60. – We can now create a Visitor for populating the instance
– Our ‘WebFluxSpaceClient’ will be injected into it
Bringing Everything Together
Using the Visitor Pattern
61. @Component
class SpaceCreationVisitor(val client: WebFluxSpaceClient): DslVisitor {
override fun visitBlog(blog: Blog) {
println("tCreating blog entitled ${blog.title}")
val content = blog.additionalContent.joinToString()
waitOnMono(client.createBlog(blog.title, content))
}
override fun visitRepo(repo: Repo) {
println("ttRepo at ${repo.location}")
}
SpaceCreationVisitor.kt
Bringing Everything Together
62. override fun visitProfile(profile: Profile) {
with(profile) {
println("tCreating profile for $forename $surname at $email")
val username = "$forename.$surname"
waitOnMono(client.createProfile(forename, surname, username))
}
}
override fun visitProject(project: Project) {
with(project) {
println("tCreating project ${project.name} with key ${project.key}")
waitOnMono(client.createProject(name, key))
}
}
SpaceCreationVisitor.kt
Bringing Everything Together
63. override fun visitInstance(instance: SpaceInstance) {
println("Trying to initialise ${instance.title}")
}
fun waitOnMono(mono: Mono<Boolean>) {
val result = mono.block() ?: false
println(if(result) "Success" else "Failure")
}
}
SpaceCreationVisitor.kt
Bringing Everything Together
65. – Space is intuitive and works well
– The Space API works well from Spring 5
– Automation saves time and reduces stress
Conclusions
The Good Things
66. – Space and its API are still in EAP
– Reactive coding remains tough going
– Bridging the ‘reactive divide’ is ugly
Conclusions
The Bad Things