Over a million developers have joined DZone.

Let's Build a Bitbucket Add-on in Clojure! - Part 2: Serving Our Connect Descriptor

Check out this tutorial on building a Bitbucket add-on in Clojure, focusing on connecting descriptors​.

· Java Zone

Navigate the Maze of the End-User Experience and pick up this APM Essential guide, brought to you in partnership with CA Technologies

In part 1 of this series, we did the fundamental work of building a Twelve Factor HTTP-stack from the ground using LeiningenRingCompojure, and Immutant. However, that was just the foundations, and now we're ready to start adding the necessary tooling to produce a full Atlassian Connect for Bitbucket application. This will include templating and introduce how to specify and authenticate our Connect add-on via its descriptor.

Connect Essentials; Serving a Descriptor

All Connect add-ons need to serve up a descriptor. This JSON file provides the add-on's location and identifying information along with the permissions it requires, its API, and other metadata such as a name and description.

As we want our add-on to insert run-time information into this descriptor before it goes out we're going to need a templating tool. In the Clojure world templating tools generally fall into two groups: those that take Clojure s-expressions and output a given format (usually a tree-oriented one such as HTML or JSON), or those that operate on marked-up files/data. The latter is what most people think of when talking about templating, and it's what makes the most sense for generating our descriptor, which is largely static. However Ring and Clojure HTTP clients can be configured to automatically convert between JSON and Clojure data structures, which will come in useful later.

There are quite a few markup-style templating engines for Clojure. Most of them aim to be compatible with de facto standards from other ecosystems, such as Handlebars (Javascript) or ERB (Ruby). In this case, I've chosen to use Selmer, which is closely related to Django's templating system, but you can use an alternative one if you prefer.

The descriptor we need for this project looks like this (template injections are delimited by {{/}}):

{
    "key": "hello-connect",
    "name": "Hello Connect",
    "description": "An example Clojure add-on for Bitbucket",
    "vendor": {
        "name": "Angry Nerds",
        "url": "https://www.atlassian.com/angrynerds"
    },
    "baseUrl": "{{base-url}}",
    "authentication": {
        "type": "jwt"
    },
    "lifecycle": {
        "installed": "/installed",
        "uninstalled": "/uninstalled"
    },
    "modules": {
        "oauthConsumer": {
            "clientId": "{{oauth-key}}"
        },
        "webhooks": [
            {
                "event": "*",
                "url": "/webhook"
            }
        ],
        "webPanel": [
            {
                "url": "/connect-example?repoPath={repo_path}",
                "name": {
                    "value": "Example Web Panel"
                },
                "location": "org.bitbucket.repository.overview.informationPanel",
                "key": "example-web-panel"
            }
        ]
    },
    "scopes": ["account", "repository"],
    "contexts": ["account"]
}

As you can see we need we're going to inject two variables: base-url and oauth-key. The base-url is where the add-on will be running (e.g. your ngrok tunnel if running it locally). The oauth-key is the key that uniquely identifies who controls this add-on. This needs to be generated within Bitbucket viaManage account > OAuth > Add consumer; see the Bitbucket Connect getting started guide or the later installments in this series for more details on using ngrok and OAuth to develop Connect add-ons.

Setting Our Variables

As mentioned previously, we're building this as a Twelve Factor application. This means we want to define our runtime information in the environment and then extract them with the environ library we used earlier to configure the HTTP server. In production, we would set the variables using the system environment via export or similar. However in development, this can be a pain. Luckily environ also supplies some methods to supply these during development. The simplest is to add the file .lein-envwith a dictionary of your variables; in our case it would look like:

{:base-url "OVERRIDE",
 :oauth-key "OVERRIDE"}

If you don't want this checked into to git just add it to your .gitignore. For a more flexible system,environ supplies a Leiningen plugin; add the following to your project.clj :plugins list:

  [lein-environ "1.0.1"]

This allows us to create an :env entry the same as the file above but in the Leiningen :dev profile which will set the environment. You can also add this to your profiles.clj; see the profiles documentation for more information.

Rendering Our Descriptor

Before we can render the descriptor we need to make it available to the runtime. In the JVM world, this usually means adding it to the resources path. Create a new directory in the base of our project calledresources, and another under that called views. Place the above descriptor template into a file calledatlassian-connect.json.selmer. We tell Leiningen about this resources directory by adding the following entry to our project.clj:

:resource-paths ["resources"]            

Leiningen will then place that directory on the JVM classpath.

Now we can have Selmer render the content and Ring/Immutant serve it. Add the following functions to our handler.clj:

(defn gen-descriptor []
  ;; Fetch configuration from the environment (see `environ` docs)
  (let [ctx {:base-url (env :base-url)
             :oauth-key (env :oauth-key)}]
    (render-file "views/atlassian-connect.json.selmer" ctx)))

(defn gen-descriptor-reply []
  (log/info "Received descriptor request")
  {:status 200
   :headers {"Content-Type" "application/json; charset=utf-8"}
   :body (gen-descriptor)})

This first function extracts the necessary variables from the environment and uses them to render the template we created (you'll need to add [selmer.parser :as selmer] to your :require list). The second function wraps the resulting output in Ring response map. This is what we'll pass back to the HTTP server to return.

Serving Our Descriptor

The last step is to add a route to retrieve the file. We'll have both / and /atlassian-connect.jsonserve this up by default. To do this return to the defroutes section of handler.clj and replace the existing "Hello Connect" route with the following:

(GET  "/" [] (response/redirect "/atlassian-connect.json"))
(GET  "/atlassian-connect.json" []
      (gen-descriptor-reply))

Now we can start up our server with lein run and go to http://localhost:3000/. If everything is working OK you should be redirected to http://localhost:3000/atlassian-connect.json and see the rendered version of our Connect descriptor.

The Code

The code for this part of the tutorial series is available in this tag in the accompanying Bitbucket repository. There will also code appearing there for the later parts as I work on them if you want to skip ahead.

Next Time...

Now that we have a descriptor we can start adding actual functionality to our add-on. You may notice that our descriptor above contains references to a lifecycle. This API allows us to Bitbucket to initiate a relationship between a user and our add-on by providing information such as user metadata and a unique identifying key. Next time we'll look more closely at how to respond to these calls, and after that, we'll look into communicating with Bitbucket using a client-side Javascript channel or server-to-server calls.

Originally written by Steve Smith

Thrive in the application economy with an APM model that is strategic. Be E.P.I.C. with CA APM.  Brought to you in partnership with CA Technologies.

Topics:
clojure ,bitbucket

Opinions expressed by DZone contributors are their own.

The best of DZone straight to your inbox.

SEE AN EXAMPLE
Please provide a valid email address.

Thanks for subscribing!

Awesome! Check your inbox to verify your email so you can start receiving the latest in tech news and resources.
Subscribe

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}