The number of Slack bots, Slack plug-ins and other integrations available recently seems to have skyrocketed. There are plug-ins to help facilitate live blogging, bots that can book rooms for your team (hopefully better than your company’s travel department), there’s even a Slack bot that will listen to all of your frustrations.
At work we switched to Slack recently and there are many things I’ve been wanting to try. One being to create a bot that will give API documentation and example responses.
In order to do this we’ll take a simple JSON schema, parse it a bit, then provide a sample response based on the schema. To handle this I’m leaning heavily on both Prmd, a tool for managing JSON schemas and generating documentation from them and for the Slack integration, the excellent slack-ruby-bot.
Creating a JSON Schema
I just recently started diving into JSON schema, so I’m still fairly new myself. I found the online book Understanding JSON Schema to be a great resource. For the purposes of this blog post I’m going to use a generic “person” schema. This person object will have a first name, last name, and an email address.
We’ll use Prmd to combine a meta.yml
and a person.yml
file into our
schema.json
file.
id: "person"
title: "Person"
properties:
id:
"$ref": "#/definitions/id"
first_name:
description: "The person's first name."
example: "Jean-Luc"
type: "string"
last_name:
description: "The person's last name."
example: "Picard"
type: "string"
email_address:
description: "The person's email address."
example: "locutus@borg.hive"
format: "email"
type: "string"
definitions:
person:
description: "A single person"
properties:
id:
"$ref": "#/id"
first_name:
"$ref": "#/first_name"
last_name:
"$ref": "#/last_name"
email_address:
"$ref": "#/email_address"
type: "object"
links:
- title: "Person details"
description: "Get the details of a person"
method: GET
href: "/person/{#/definitions/identity}"
targetSchema:
"$ref": "#/person"
Combining these into a single schema file is easy:
prmd combine --meta meta.yml person.yml > schema.json
This produces:
{
"$schema": "http://interagent.github.io/interagent-hyper-schema",
"type": [
"object"
],
"definitions": {
"identity": {
"$ref": "#/definitions/id"
},
"id": {
"description": "Unique identifier of a resource.",
"example": "1dc3567e-acd4-4819-afd5-21d0ef677dcd",
"readOnly": true,
"format": "uuid",
"type": [
"string"
]
},
"person": {
"title": "Person",
"properties": {
"id": {
"$ref": "#/definitions/id"
},
"first_name": {
"description": "The person's first name.",
"example": "Jean-Luc",
"type": [
"string"
]
},
"last_name": {
"description": "The person's last name.",
"example": "Picard",
"type": [
"string"
]
},
"email_address": {
"description": "The person's email address.",
"example": "locutus@borg.hive",
"format": "email",
"type": [
"string"
]
}
},
"definitions": {
"person": {
"description": "A single person",
"properties": {
"id": {
"$ref": "#/definitions/id"
},
"first_name": {
"$ref": "#/definitions/first_name"
},
"last_name": {
"$ref": "#/definitions/last_name"
},
"email_address": {
"$ref": "#/definitions/email_address"
}
},
"type": [
"object"
]
}
},
"links": [
{
"title": "Person details",
"description": "Get the details of a person",
"method": "GET",
"href": "/person/{#/definitions/identity}",
"targetSchema": {
"$ref": "#/definitions/person"
}
}
]
}
},
"properties": {
"person": {
"$ref": "#/definitions/person"
}
},
"id": "person-api",
"description": "Person Example API",
"title": "Person Example API",
"links": [
{
"href": "https://api.example.com",
"rel": "self"
}
]
}
Creating a basic Slack bot
We will need the following files:
docutron/
response.rb
docutron.rb
Gemfile
schema.json # the generated output from above
Gemfile
source 'http://rubygems.org'
gem 'slack-ruby-bot'
gem 'prmd'
docutron.rb
This will be the main entry point into the bot when a webhook payload is received.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
require 'slack-ruby-bot'
require_relative 'docutron/response'
module Docutron
class App < SlackRubyBot::App
end
class SlackBot < SlackRubyBot::Commands::Base
DOC_REQUEST = /^(?<request_method>\w*) (?<resource>\w*)$/
match DOC_REQUEST do |client, data, match|
method, resource = match[:request_method], match[:resource]
response = Docutron::Response.new(method, resource)
response.send(client, data.channel)
end
end
end
Docutron::App.instance.run
We match against the incoming message using the SlackRubyBot’s .match
method.
We’re looking for a message in the form of [request method] [resource name]
,
For instance:
GET person
When a message is received, we create a new Docutron::Response
instance and pass
it the request method and the resource. We then call #send
to respond in the
Slack channel the message was sent from.
docutron/response.rb
This is where we’ll do the bulk of the work of loading and parsing the schema, choosing the correct schema link for the requested resource, then returning the appropriate response.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
require 'prmd'
module Docutron
class Response
UnknownResponse = "Sorry, I don't know about that resource.".freeze
def initialize(method, resource)
@method = method.upcase
@resource = resource
@schemata = "#/definitions/#{@resource}"
@schema = Prmd::Schema.new(JSON.parse(File.read('schema.json')))
end
def link
@schema['definitions'][@resource]['links'].detect do |link|
link['method'] == @method
end or raise UnknownResponse
end
def json_example
if link['rel'] == 'empty'
elsif link.has_key?('targetSchema')
JSON.pretty_generate(@schema.schema_example(link['targetSchema']))
elsif link['rel'] == 'instances'
JSON.pretty_generate([@schema.schemata_example(@schemata)])
else
JSON.pretty_generate(@schema.schemata_example(@schemata))
end
end
def message
"```#{json_example}```"
end
def send(client, channel)
client.message text: message, channel: channel
end
end
end
The initializer of the Docutron::Response
class sets up some instance variables
and creates a new Prmd::Schema
instance using the schema.json data.
The #link
method finds the schema’s link definition for the resource and the request
method of the incoming Slack message. Our basic person schema defines one link:
{
"title": "Person details",
"description": "Get the details of a person",
"method": "GET",
"href": "/person/{#/definitions/identity}",
"targetSchema": {
"$ref": "#/definitions/person"
}
}
The #json_example
method uses the link to generate a JSON example either using
the targetSchema
of the link if it exists, or by using a default json
reference for the resource, in this case #/definitions/person
. If the link has
a rel of “instances”, it wraps the response in an array. This
method is adapted from Prmd’s link.md.erb
template.
The important bit here is the @schema.schemata_example(@schemata)
which
returns a JSON object based on the properties defined for a given “schemata” and
the example values defined in the schema. For person it looks like this:
{
"id": "1dc3567e-acd4-4819-afd5-21d0ef677dcd",
"first_name": "Jean-Luc",
"last_name": "Picard",
"email_address": "locutus@borg.hive"
}
Finally, the #message
method wraps the JSON example in triple back ticks so
that Slack will format the message as preformatted text. The #send
method as
you recall is what our Slack bot actually calls to send the message.
Configure Slack
The final step to run this basic Slack bot is to configure your team’s integrations. First create a new bot by going to the Add Bot page. Create a new bot and obtain the bot’s API token. You’ll need this to start the bot. Next, from the Add Outgoing Webhooks page create a new outgoing webhook and choose a specific channel for your bot to monitor.
To run the bot use the command:
SLACK_API_TOKEN=bot_api_token_here ruby docutron.rb
Just the beginning
For the documentation to be truly useful, you’ll of course want more information. Maybe some details about each property, for example. Prmd has templates to handle generating that which could be adapted for docutron, but I leave that as an exercise for the reader.
Happy Slacking!